Spaces:
Sleeping
Sleeping
File size: 9,353 Bytes
707375a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | 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`);
});
|