const express = require('express'); const path = require('path'); const cors = require('cors'); const axios = require('axios'); const { exec, spawn } = require('child_process'); const fs = require('fs'); const app = express(); const PORT = process.env.PORT || 3000; // Security / Iframe Fix app.use((req, res, next) => { res.setHeader('X-Frame-Options', 'SAMEORIGIN'); res.removeHeader('X-Frame-Options'); // Try to remove it completely first, if proxy allows next(); }); // Storage manager routes const storageManager = require('./storage-manager'); app.use('/api/clones', storageManager); // Security headers app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); next(); }); // Middleware app.use(cors()); app.use(express.json()); // Helper para parsear cookies function parseCookies(cookieHeader) { const list = {}; if (!cookieHeader) return list; cookieHeader.split(';').forEach(cookie => { let [name, ...rest] = cookie.split('='); name = name?.trim(); if (!name) return; const value = rest.join('=').trim(); if (!value) return; list[name] = decodeURIComponent(value); }); return list; } // Rota para ativar o Virtual Root Preview (define o cookie e redireciona para a raiz) app.get('/api/preview/:folder', (req, res) => { const folderName = req.params.folder; res.setHeader('Set-Cookie', `clone_preview_target=${encodeURIComponent(folderName)}; Path=/; HttpOnly; SameSite=Lax`); res.redirect('/'); }); // Rota para sair do Virtual Root Preview (limpa o cookie e volta ao dashboard) app.get('/api/exit-preview', (req, res) => { res.setHeader('Set-Cookie', 'clone_preview_target=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT'); res.redirect('/'); }); // Middleware de Virtual Root Proxy (serve o site clonado diretamente na raiz / quando o cookie está ativo) app.use((req, res, next) => { const cookies = parseCookies(req.headers.cookie); if (cookies.clone_preview_target && !req.path.startsWith('/api') && req.path !== '/old') { const folderName = cookies.clone_preview_target; const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName); if (fs.existsSync(folderPath)) { let targetFile = req.path; if (targetFile === '/') targetFile = '/index.html'; // Se o arquivo solicitado existir dentro da pasta do clone const fullPath = path.join(folderPath, targetFile); if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { // Se for HTML, injeta a barra de preview flutuante if (targetFile.endsWith('.html')) { let html = fs.readFileSync(fullPath, 'utf8'); const previewBar = `
Live Preview: ${folderName.split('_')[0]} Sair do Preview
`; if (html.includes('')) { html = html.replace('', previewBar + ''); } else { html += previewBar; } res.setHeader('Content-Type', 'text/html'); return res.send(html); } // Para outros arquivos (CSS, JS, imagens), serve diretamente com o MIME type correto if (targetFile.endsWith('.css')) res.setHeader('Content-Type', 'text/css'); if (targetFile.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript'); return res.sendFile(fullPath); } else { // Fallback de SPA: se a rota virtual não existir como arquivo no disco, serve o index.html do clone!!! const indexPath = path.join(folderPath, 'index.html'); if (fs.existsSync(indexPath)) { let html = fs.readFileSync(indexPath, 'utf8'); const previewBar = `
Live Preview: ${folderName.split('_')[0]} Sair do Preview
`; if (html.includes('')) html = html.replace('', previewBar + ''); else html += previewBar; res.setHeader('Content-Type', 'text/html'); return res.send(html); } } } } next(); }); // Serve static files with proper MIME types app.use(express.static(path.join(__dirname, 'public'), { setHeaders: (res, path) => { if (path.endsWith('.css')) res.setHeader('Content-Type', 'text/css'); if (path.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript'); if (path.endsWith('.html')) res.setHeader('Content-Type', 'text/html'); } })); // Middleware para injetar fix de roteador em sites clonados (SPAs como React, Vue, Angular, Vite) e reescrever JS app.use((req, res, next) => { if (!req.path.startsWith('/api') && req.path !== '/' && req.path !== '/old') { let urlPath = req.path; // Remover prefixo /cloned-sites se existir para buscar corretamente no disco if (urlPath.startsWith('/cloned-sites/')) { urlPath = urlPath.replace('/cloned-sites/', '/'); } if (!urlPath.match(/\.[a-zA-Z0-9]+$/)) { if (!urlPath.endsWith('/')) urlPath += '/'; urlPath += 'index.html'; } // Interceptação de arquivos HTML (Camada 1 - DOM Mock Universal) if (urlPath.endsWith('.html')) { const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath); if (fs.existsSync(filePath)) { try { let html = fs.readFileSync(filePath, 'utf8'); if (!html.includes('__CLONEWEB_ROUTER_FIX__')) { const injectScript = ` `; if (html.includes('')) { html = html.replace('', '' + injectScript); } else { html = injectScript + html; } } res.setHeader('Content-Type', 'text/html'); return res.send(html); } catch (err) { console.error('Erro ao injetar fix no HTML do clone:', err); } } } // Interceptação de arquivos JS de sites clonados (Camada 2 - JS Rewriter Supremo) if (urlPath.endsWith('.js') && (urlPath.includes('_202') || urlPath.includes('/cloned-sites/'))) { const filePath = path.join(__dirname, '..', 'cloned-sites', urlPath); if (fs.existsSync(filePath)) { try { let jsContent = fs.readFileSync(filePath, 'utf8'); // Substitui window.location.pathname de forma segura jsContent = jsContent.replace(/\bwindow\.location\.pathname\b/g, '(window.__mockLocationPathname || window.location.pathname)'); // Substitui location.pathname isolado (evita quebrar e.location.pathname ou history.location.pathname) jsContent = jsContent.replace(/(? { if (filePath.endsWith('.css')) res.setHeader('Content-Type', 'text/css'); if (filePath.endsWith('.js')) res.setHeader('Content-Type', 'application/javascript'); } })); // Proxy para o crawler service - SEM TIMEOUT app.post('/api/crawl', async (req, res) => { try { console.log('🚀 Proxying crawl request to crawler service...'); console.log('⏳ Clonagem pode demorar vários minutos para sites grandes...'); // SEM TIMEOUT - permite clonagens longas const response = await axios.post('http://localhost:3001/crawl', req.body, { timeout: 0 // Sem limite de tempo }); console.log('✅ Crawl completed successfully'); res.json(response.data); } catch (error) { console.error('❌ Crawl error:', error.message); res.status(500).json({ success: false, error: error.response?.data?.error || error.message }); } }); // Proxy para o wget cloner - SEM TIMEOUT para clonagens grandes app.post('/api/wget-clone', async (req, res) => { try { console.log('🔧 Proxying wget clone request...'); console.log('⏳ Clonagem pode demorar vários minutos para sites grandes...'); // SEM TIMEOUT - permite clonagens que demoram mais de 10 minutos const response = await axios.post('http://localhost:3002/wget-clone', req.body, { timeout: 0 // Sem limite de tempo }); console.log('✅ Wget clone completed successfully'); // Sync to host /root/Desktop/Clonados const directory = response.data.data?.directory || response.data.directory; if (response.data && response.data.success && directory) { const { exec } = require('child_process'); const containerId = require('os').hostname(); const cloneDir = directory.split('/').pop(); const hostDir = req.body.outputDir || '/root/Desktop/Clonados'; exec(`mkdir -p ${hostDir} && cp -r ${directory} ${hostDir}/ 2>/dev/null || docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => { if (err) console.log('⚠️ Sync to host failed:', err.message); else console.log('✅ Clone synced to host Desktop:', cloneDir); }); } res.json(response.data); } catch (error) { console.error('❌ Wget clone error:', error.message); res.status(500).json({ success: false, error: error.response?.data?.error || error.message }); } }); // Smart Clone Endpoint (Local Spawn) app.post('/api/smart-clone', (req, res) => { const { url, folderName } = req.body; if (!url) { return res.status(400).json({ error: 'URL is required' }); } console.log('🚀 Starting Smart Clone for:', url); console.log('⏳ This uses Puppeteer and may take a while...'); // Spawn the smart cloner process const scriptPath = path.join(__dirname, '..', 'simple-crawler', 'smart-cloner.js'); let spawnArgs = [scriptPath, `--url=${url}`]; if (folderName) spawnArgs.push(`--folderName=${folderName}`); console.log(`[SmartClone] Executing: node "${scriptPath}" --url="${url}" ${folderName ? `--folderName="${folderName}"` : ''}`); const child = spawn('node', spawnArgs, { cwd: path.join(__dirname, '..'), shell: true }); let output = ''; let errorOutput = ''; child.stdout.on('data', (data) => { const text = data.toString(); output += text; console.log(`[SmartClone STDOUT] ${text.trim()}`); }); child.stderr.on('data', (data) => { const text = data.toString(); errorOutput += text; console.error(`[SmartClone STDERR] ${text.trim()}`); }); child.on('error', (err) => { console.error('[SmartClone SPAWN ERROR]', err); if (!res.headersSent) { res.status(500).json({ success: false, error: 'Failed to start cloning process', details: err.message }); } }); child.on('close', (code) => { console.log(`[SmartClone] Process exited with code ${code}`); if (res.headersSent) return; try { // Find the last JSON object in output const matches = output.match(/\{[\s\S]*\}/g); if (matches && matches.length > 0) { const lastMatch = matches[matches.length - 1]; const result = JSON.parse(lastMatch); // Sync to host /root/Desktop/Clonados const directory = result.data?.directory || result.directory || result.path; if (result.success && directory) { const { exec } = require('child_process'); const containerId = require('os').hostname(); const cloneDir = directory.split('/').pop(); const hostDir = req.body.outputDir || '/root/Desktop/Clonados'; exec(`mkdir -p ${hostDir} && cp -r ${directory} ${hostDir}/ 2>/dev/null || docker cp ${containerId}:${directory} ${hostDir}/ 2>/dev/null`, (err) => { if (err) console.log('⚠️ SmartClone sync to host failed:', err.message); else console.log('✅ SmartClone synced to host Desktop:', cloneDir); }); } res.json({ success: result.success, data: result, error: result.error }); } else { throw new Error('No valid JSON output found from cloner'); } } catch (e) { console.error('[SmartClone PARSE ERROR]', e.message); if (code === 0) { res.json({ success: true, message: 'Process finished but output parsing failed', fullOutput: output }); } else { res.status(500).json({ success: false, error: 'Clone failed or output was invalid', details: errorOutput }); } } }); }); // Health check do wget cloner (porta 3002) app.get('/api/health', async (req, res) => { try { const response = await axios.get('http://localhost:3002/health', { timeout: 5000 }); res.json(response.data); } catch (error) { res.status(500).json({ status: 'error', message: 'WGET Cloner service unavailable' }); } }); // Rota de visualização de arquivos da pasta clonada (Gerenciador de Arquivos Web Embutido) app.get('/api/browse/:folder', (req, res) => { try { const folderName = req.params.folder; const folderPath = path.join(__dirname, '..', 'cloned-sites', folderName); if (!fs.existsSync(folderPath)) { return res.status(404).send('

Pasta não encontrada

A pasta solicitada não existe no servidor.

'); } const items = fs.readdirSync(folderPath); let filesHtml = ''; items.forEach(item => { const itemPath = path.join(folderPath, item); const stat = fs.statSync(itemPath); const isDir = stat.isDirectory(); const size = isDir ? '-' : (stat.size / 1024).toFixed(2) + ' KB'; const date = new Date(stat.mtime).toLocaleString('pt-BR'); const icon = isDir ? 'fa-folder' : (item.endsWith('.html') ? 'fa-file-code' : (item.endsWith('.json') ? 'fa-file-alt' : 'fa-file')); const link = isDir ? `#` : `/${folderName}/${item}`; filesHtml += ` ${item} ${size} ${date} ${!isDir ? `Visualizar` : ''} ${!isDir ? `Baixar` : ''} `; }); const html = ` Arquivos Clonados - ${folderName}

${folderName}

${filesHtml}
Nome do Ficheiro Tamanho Modificado em Ações
`; res.send(html); } catch (error) { console.error('Erro ao listar pasta:', error); res.status(500).send('

Erro interno do servidor

'); } }); // Abrir pasta do clone (mantém compatibilidade legada e retorna a URL do Web Browse) app.post('/api/open-folder', (req, res) => { try { const { path: folderPath } = req.body; if (!folderPath) { return res.status(400).json({ error: 'Caminho da pasta é obrigatório' }); } const folderName = path.basename(folderPath); const browseUrl = `/api/browse/${folderName}`; res.json({ success: true, message: 'Pasta acessível via Web', url: browseUrl }); } catch (error) { console.error('Erro ao abrir pasta:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // Abrir site clonado via HTTP para evitar problemas de origem/CORS app.post('/api/open-site', (req, res) => { try { const { path: sitePath } = req.body; if (!sitePath) { return res.status(400).json({ error: 'Caminho do site é obrigatório' }); } const folderName = path.basename(sitePath); const siteUrl = `/${folderName}/index.html`; res.json({ success: true, message: 'Site aberto com sucesso', url: siteUrl }); } catch (error) { console.error('Erro ao abrir site:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); // Abrir página de navegação via HTTP app.post('/api/open-navigation', (req, res) => { try { const { path: sitePath } = req.body; if (!sitePath) { return res.status(400).json({ error: 'Caminho do site é obrigatório' }); } const folderName = path.basename(sitePath); const navUrl = `http://localhost:${PORT}/${folderName}/navegacao.html`; console.log(`🌍 Opening navigation via HTTP: ${navUrl}`); // Abrir navegacao.html no navegador padrão exec(`start "" "${navUrl}"`, (error) => { if (error) { console.error('Erro ao abrir navegação:', error); return res.status(500).json({ error: 'Não foi possível abrir a navegação via HTTP' }); } res.json({ success: true, message: 'Navegação aberta via HTTP com sucesso', url: navUrl }); }); } catch (error) { console.error('Erro ao abrir navegação:', error); res.status(500).json({ error: 'Erro interno do servidor' }); } }); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index-new.html')); }); // Serve old interface app.get('/old', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Serve favicon requests app.get('/favicon.ico', (req, res) => { res.redirect('/favicon.svg'); }); // Handle common missing resources app.get('/index', (req, res) => { res.redirect('/'); }); // List optimized pages app.get('/api/optimized-pages', (req, res) => { try { const clonesDir = path.join(__dirname, '..', 'cloned-sites'); if (!fs.existsSync(clonesDir)) { return res.json({ success: true, pages: [] }); } const items = fs.readdirSync(clonesDir); const optimized = items .filter(item => { const fullPath = path.join(clonesDir, item); return fs.statSync(fullPath).isDirectory() && item.endsWith('_designer'); }) .map(item => ({ name: item, directory: item })); res.json({ success: true, pages: optimized }); } catch (error) { console.error('Error listing optimized pages:', error); res.status(500).json({ success: false, error: error.message }); } }); // AI Agent Route (MiniMax) app.post('/api/ai/agent', async (req, res) => { const { systemPrompt, userMessage } = req.body; const apiKey = process.env.MINIMAX_API_KEY || 'sk-cp-iWzSsdzjVy0tT5dnvv_sGrAlv3Nw7kkrZ8hPK0KjbNeSRQxIY1O6AhkQAq_yrYV1XPJY1TH6ZQeOlJZd-Zi_elkJhef9PcTSzn2m9bcKIxezarM19BREcVM'; if (!apiKey) { return res.status(500).json({ error: 'MINIMAX_API_KEY is missing' }); } try { const response = await axios.post('https://api.minimax.io/v1/chat/completions', { model: "MiniMax-M2.7", messages: [ { role: "system", content: systemPrompt || "Você é um assistente de inteligência artificial útil." }, { role: "user", content: userMessage } ], max_tokens: 2048 }, { headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, timeout: 90000 // 90s }); let reply = response.data.choices?.[0]?.message?.content || 'Processamento concluído, porém sem retorno.'; // Strip reasoning tags if present reply = reply.replace(/[\s\S]*?<\/think>/gi, '').trim(); res.json({ success: true, data: reply }); } catch (error) { console.error('MiniMax AI Error:', error.response?.data || error.message); res.status(500).json({ error: error.message, details: error.response?.data }); } }); // AI Inject Redesign Route (Pilar 1 - Agente Designer Real) app.post('/api/ai/inject-redesign', (req, res) => { const { cloneDir, appliedStyles, elementsRedesigned, outputDir, agentName } = req.body; if (!cloneDir) return res.status(400).json({ error: 'cloneDir is required' }); const originalFolderName = cloneDir.split('/').pop(); const folderPath = path.join(__dirname, '..', 'cloned-sites', originalFolderName); if (!fs.existsSync(folderPath)) { return res.status(404).json({ error: 'Cloned site folder not found' }); } try { // 1. Criar o arquivo CSS de Redesign Ultra-Moderno (__ai_redesign.css) const cssContent = ` /* ========================================================================== BRAINSTEEL WEB - AI ULTRA-MODERN REDESIGN (AGENTE DESIGNER) ========================================================================== */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@400;600;800&display=swap'); :root { --ai-bg-dark: #0b0f19; --ai-bg-card: rgba(18, 24, 38, 0.7); --ai-border-glass: rgba(255, 255, 255, 0.08); --ai-accent-glow: #3b82f6; --ai-accent-gradient: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%); --ai-text-main: #f3f4f6; --ai-text-muted: #9ca3af; } /* Força Dark Mode elegante e tipografia moderna no body e containers principais */ body, main, #root, #app, .app, .main, .container, .wrapper, header, footer, section { background-color: var(--ai-bg-dark) !important; color: var(--ai-text-main) !important; font-family: 'Inter', sans-serif !important; } /* Glassmorphism e Glow em Cards, Painéis, Cabeçalhos e Rodapés */ div[class*="card"], div[class*="panel"], div[class*="box"], div[class*="container"], div[class*="item"], header, footer, nav, .navbar, .header, .footer { background: var(--ai-bg-card) !important; backdrop-filter: blur(16px) !important; -webkit-backdrop-filter: blur(16px) !important; border: 1px solid var(--ai-border-glass) !important; box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.5) !important; border-radius: 16px !important; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important; } div[class*="card"]:hover, div[class*="item"]:hover { transform: translateY(-5px) !important; border-color: rgba(59, 130, 246, 0.4) !important; box-shadow: 0 20px 40px -15px rgba(59, 130, 246, 0.3) !important; } /* Títulos com Gradiente Vibrante (Outfit Font) */ h1, h2, h3, h4, h5, .title, .heading, .header-title { font-family: 'Outfit', sans-serif !important; background: var(--ai-accent-gradient) !important; -webkit-background-clip: text !important; -webkit-text-fill-color: transparent !important; background-clip: text !important; font-weight: 700 !important; letter-spacing: -0.03em !important; } /* Botões Ultra-Modernos com Gradiente e Animação de Pulso */ button, .btn, a[class*="btn"], input[type="submit"], input[type="button"] { background: var(--ai-accent-gradient) !important; color: #ffffff !important; border: none !important; border-radius: 50px !important; padding: 12px 28px !important; font-family: 'Inter', sans-serif !important; font-weight: 600 !important; letter-spacing: 0.02em !important; box-shadow: 0 10px 25px -5px rgba(139, 92, 246, 0.5) !important; cursor: pointer !important; transition: all 0.3s ease !important; text-transform: none !important; -webkit-text-fill-color: #ffffff !important; } button:hover, .btn:hover, a[class*="btn"]:hover { transform: scale(1.05) translateY(-2px) !important; box-shadow: 0 15px 35px -5px rgba(236, 72, 153, 0.6) !important; } /* Inputs e Textareas com Efeito Glass */ input, select, textarea, .input, .form-control { background: rgba(255, 255, 255, 0.05) !important; border: 1px solid var(--ai-border-glass) !important; color: var(--ai-text-main) !important; border-radius: 12px !important; padding: 12px 16px !important; font-family: 'Inter', sans-serif !important; } input:focus, select:focus, textarea:focus { outline: none !important; border-color: var(--ai-accent-glow) !important; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.3) !important; } /* Links e Textos Secundários */ a { color: var(--ai-accent-glow) !important; text-decoration: none !important; transition: color 0.2s !important; } a:hover { color: #8b5cf6 !important; } p, span, li, label, td, th { color: var(--ai-text-muted) !important; line-height: 1.6 !important; } `; const newFolderName = `${originalFolderName}_${agentName || 'designer'}`; const newFolderPath = path.join(__dirname, '..', 'cloned-sites', newFolderName); // Copy the original folder to the new folder exec(`cp -r "${folderPath}" "${newFolderPath}"`, (copyErr) => { if (copyErr) { console.error('Error copying folder for agent redesign:', copyErr); return res.status(500).json({ error: 'Failed to create new agent folder' }); } // 1. Criar o arquivo CSS de Redesign Ultra-Moderno (__ai_redesign.css) no novo diretório const cssPath = path.join(newFolderPath, '__ai_redesign.css'); fs.writeFileSync(cssPath, cssContent, 'utf8'); // 2. Injetar a tag no index.html do novo clone const indexPath = path.join(newFolderPath, 'index.html'); if (fs.existsSync(indexPath)) { let html = fs.readFileSync(indexPath, 'utf8'); const linkTag = `\n\n`; if (!html.includes('__ai_redesign.css')) { if (html.includes('')) { html = html.replace('', linkTag + ''); } else { html = linkTag + html; } fs.writeFileSync(indexPath, html, 'utf8'); } } // 3. Sincronizar o novo diretório com o host const hostDir = outputDir || '/root/Desktop/Clonados'; const containerId = require('os').hostname(); exec(`mkdir -p "${hostDir}" && cp -r "${newFolderPath}" "${hostDir}/" 2>/dev/null || docker cp ${containerId}:"${newFolderPath}" "${hostDir}/" 2>/dev/null`, (syncErr) => { if (syncErr) console.log('⚠️ Redesign sync to host failed:', syncErr.message); else console.log('✅ Redesign synced to host output dir:', newFolderName); }); res.json({ success: true, message: 'AI Redesign CSS injected successfully', newFolderName }); }); } catch (err) { console.error('Error injecting AI redesign:', err); res.status(500).json({ error: err.message }); } }); // Handle 404s app.use((req, res) => { res.status(404).json({ error: 'Not Found', message: `Route ${req.path} not found` }); }); // Error handler app.use((err, req, res, next) => { console.error('Server error:', err); res.status(500).json({ error: 'Internal Server Error', message: err.message }); }); app.listen(PORT, () => { console.log(`🌐 CloneWeb Interface running on http://localhost:${PORT}`); console.log(`🔗 Make sure crawler service is running on http://localhost:3001`); console.log(`📁 Serving static files from: ${path.join(__dirname, 'public')}`); });