yt-project / server.js
3v324v23's picture
feat: add production-ready server.js automation backend and update project dependencies
707375a
Raw
History Blame Contribute Delete
9.35 kB
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`);
});