Spaces:
Sleeping
Sleeping
| import express from 'express'; | |
| import http from 'http'; | |
| import { WebSocketServer } from 'ws'; | |
| import puppeteer from 'puppeteer-core'; | |
| import path from 'path'; | |
| // ========================================== | |
| // 1. CONFIGURATION LAYER | |
| // ========================================== | |
| const PORT = process.env.PORT || 3000; | |
| const CHROME_PATH = process.env.CHROME_PATH || '/usr/bin/google-chrome'; | |
| const EXTENSION_PATH = process.env.EXTENSION_PATH || '/var/www/my-custom-extension'; | |
| const app = express(); | |
| const server = http.createServer(app); | |
| // Simple health check endpoint | |
| app.get('/health', (req, res) => { | |
| res.status(200).json({ status: 'healthy', timestamp: new Date() }); | |
| }); | |
| // Serve static interface if running standard setup | |
| app.use(express.static(path.join(process.cwd(), 'public'))); | |
| // Create the WebSocket server wrapper tied to the HTTP server | |
| const wss = new WebSocketServer({ server }); | |
| console.log(`[SYSTEM]: Configuration active. PORT: ${PORT}`); | |
| console.log(`[SYSTEM]: Target Chrome Binary: ${CHROME_PATH}`); | |
| console.log(`[SYSTEM]: Custom Chrome Extension: ${EXTENSION_PATH}`); | |
| // ========================================== | |
| // 2. WEBSOCKET CONTROLLER & LIFECYCLE | |
| // ========================================== | |
| wss.on('connection', (ws, req) => { | |
| const clientIp = req.socket.remoteAddress; | |
| console.log(`[WS]: New client connection established from IP: ${clientIp}`); | |
| // client-specific isolated browser variables | |
| let clientBrowser = null; | |
| let clientPage = null; | |
| let isScreencastActive = false; | |
| // Utility to safely dispatch JSON messages down the socket connection | |
| const sendToClient = (type, payload) => { | |
| if (ws.readyState === ws.OPEN) { | |
| ws.send(JSON.stringify({ type, ...payload })); | |
| } | |
| }; | |
| const logToClient = (message) => { | |
| console.log(`[CLIENT-LOG]: ${message}`); | |
| sendToClient('log', { message }); | |
| }; | |
| const logErrorToClient = (message, err) => { | |
| const errorDetails = err ? `: ${err.message || err}` : ''; | |
| console.error(`[CLIENT-ERR]: ${message}${errorDetails}`); | |
| sendToClient('log', { message: `[ERROR]: ${message}${errorDetails}`, isError: true }); | |
| sendToClient('error', { error: `${message}${errorDetails}` }); | |
| }; | |
| // Safe browser cleanup method | |
| const cleanupBrowser = async () => { | |
| isScreencastActive = false; | |
| if (clientBrowser) { | |
| try { | |
| logToClient('[SYSTEM]: Cleaning up client browser context...'); | |
| await clientBrowser.close(); | |
| logToClient('[SYSTEM]: Chrome context successfully closed.'); | |
| } catch (err) { | |
| console.error('[SYSTEM]: Failed to close browser context gracefully:', err.message); | |
| } finally { | |
| clientBrowser = null; | |
| clientPage = null; | |
| } | |
| } | |
| }; | |
| // Event listener for incoming client automation payloads | |
| ws.on('message', async (message) => { | |
| try { | |
| const data = JSON.parse(message); | |
| const { action } = data; | |
| if (!action) { | |
| logErrorToClient('No automation action specified in payload.'); | |
| return; | |
| } | |
| if (action === 'launch') { | |
| const { token, projectId, prompt } = data; | |
| if (!token) { | |
| logErrorToClient('Action "launch" requires a valid authentication token.'); | |
| return; | |
| } | |
| if (!projectId) { | |
| logErrorToClient('Action "launch" requires a target projectId.'); | |
| return; | |
| } | |
| if (!prompt) { | |
| logErrorToClient('Action "launch" requires an automation input prompt.'); | |
| return; | |
| } | |
| // Avoid double launching browser contexts | |
| if (clientBrowser) { | |
| logToClient('[SYSTEM]: Cleaning up existing browser session before launching new context...'); | |
| await cleanupBrowser(); | |
| } | |
| logToClient('[SYSTEM]: Initiating Puppeteer launch controller...'); | |
| // ========================================== | |
| // 3. PUPPETEER LOGIC BLOCK | |
| // ========================================== | |
| try { | |
| clientBrowser = await puppeteer.launch({ | |
| executablePath: CHROME_PATH, | |
| headless: false, // Chrome extensions are only loaded in non-headless or headless=new (modern Chrome) | |
| args: [ | |
| `--disable-extensions-except=${EXTENSION_PATH}`, | |
| `--load-extension=${EXTENSION_PATH}`, | |
| '--no-sandbox', | |
| '--disable-setuid-sandbox', | |
| '--headless=new', // modern headless argument | |
| '--disable-dev-shm-usage', | |
| '--disable-gpu', | |
| '--disable-software-rasterizer', | |
| '--disable-web-security' | |
| ], | |
| defaultViewport: { | |
| width: 1280, | |
| height: 720 | |
| } | |
| }); | |
| logToClient('[CHROME]: Browser instance spawned with unpacked extensions successfully.'); | |
| // Open target page context | |
| const pages = await clientBrowser.pages(); | |
| clientPage = pages.length > 0 ? pages[0] : await clientBrowser.newPage(); | |
| // Catch any unexpected browser crashes | |
| clientBrowser.on('disconnected', () => { | |
| logToClient('[SYSTEM]: Remote Chrome process disconnected / closed.'); | |
| cleanupBrowser(); | |
| }); | |
| // ========================================== | |
| // 4. CREDENTIAL INJECTION & AUTOMATION | |
| // ========================================== | |
| logToClient('[SYSTEM]: Navigating to Lovable.dev root domain to set context...'); | |
| await clientPage.goto('https://lovable.dev', { waitUntil: 'domcontentloaded', timeout: 30000 }); | |
| logToClient('[AUTH]: Injecting user authentication token into LocalStorage...'); | |
| await clientPage.evaluate((authToken) => { | |
| localStorage.setItem('sb-auth-token', authToken); | |
| }, token); | |
| const targetUrl = `https://lovable.dev/projects/${projectId}`; | |
| logToClient(`[SYSTEM]: Navigating to target workspace: ${targetUrl}`); | |
| await clientPage.goto(targetUrl, { waitUntil: 'load', timeout: 45000 }); | |
| // Wait for editor or workspace text input area to render | |
| logToClient('[AUTOMATION]: Waiting for prompt textarea input element...'); | |
| const textSelector = 'textarea, div[contenteditable="true"]'; | |
| await clientPage.waitForSelector(textSelector, { visible: true, timeout: 25000 }); | |
| logToClient('[AUTOMATION]: Typing user prompt payload...'); | |
| await clientPage.focus(textSelector); | |
| await clientPage.keyboard.type(prompt, { delay: 30 }); | |
| logToClient('[AUTOMATION]: Dispatching user prompt payload to Lovable engine...'); | |
| await clientPage.keyboard.press('Control+Enter'); | |
| logToClient('[SYSTEM]: Prompt submission dispatched successfully.'); | |
| // ========================================== | |
| // 5. GRAPHICAL SCREENCAST PIPELINE | |
| // ========================================== | |
| logToClient('[STREAM]: Starting graphical screencast captures (JPEG quality 50%)...'); | |
| const clientDevTools = await clientPage.target().createCDPSession(); | |
| await clientDevTools.send('Page.startScreencast', { | |
| format: 'jpeg', | |
| quality: 50, | |
| everyNthFrame: 1 | |
| }); | |
| isScreencastActive = true; | |
| clientDevTools.on('Page.screencastFrame', async (frame) => { | |
| if (!isScreencastActive) return; | |
| try { | |
| // Dispatch base64 frame back down the WebSocket client pipeline | |
| sendToClient('screen', { data: frame.data }); | |
| // Acknowledge the frame to prevent rendering backlog bottlenecks | |
| await clientDevTools.send('Page.ackScreencastFrame', { | |
| sessionId: frame.sessionId | |
| }); | |
| } catch (frameErr) { | |
| console.error('[STREAM]: Error processing screencast frame:', frameErr.message); | |
| } | |
| }); | |
| } catch (automationErr) { | |
| logErrorToClient('Automation sequence interrupted by exception', automationErr); | |
| await cleanupBrowser(); | |
| } | |
| } else if (action === 'stop') { | |
| logToClient('[SYSTEM]: Stop automation session requested by client.'); | |
| await cleanupBrowser(); | |
| } else { | |
| logErrorToClient(`Unknown action type: "${action}"`); | |
| } | |
| } catch (parseErr) { | |
| logErrorToClient('Failed to parse incoming WebSocket text message', parseErr); | |
| } | |
| }); | |
| // ========================================== | |
| // 6. TEARDOWN & LIFECYCLE MANAGEMENT | |
| // ========================================== | |
| ws.on('close', async () => { | |
| console.log(`[WS]: Client connection closed for IP: ${clientIp}`); | |
| await cleanupBrowser(); | |
| }); | |
| ws.on('error', async (socketErr) => { | |
| console.error(`[WS]: Error encountered for IP: ${clientIp}:`, socketErr.message); | |
| await cleanupBrowser(); | |
| }); | |
| }); | |
| // Bind server to port | |
| server.listen(PORT, () => { | |
| console.log(`\n======================================================`); | |
| console.log(`🚀 Server successfully launched on port ${PORT}`); | |
| console.log(`🏥 Health Check endpoint: http://localhost:${PORT}/health`); | |
| console.log(`======================================================\n`); | |
| }); | |