import http from 'http'; import { exec } from 'child_process'; import { chromium } from 'playwright'; const PORT = parseInt(process.env.TOOL_SERVER_PORT || '7700'); let browser = null; let context = null; let page = null; const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type', 'Content-Type': 'application/json', }; function parseBody(req) { return new Promise((resolve) => { let body = ''; req.on('data', c => body += c); req.on('end', () => { try { resolve(JSON.parse(body || '{}')); } catch { resolve({}); } }); }); } async function launchBrowser() { if (!browser) { browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'] }); } return browser; } async function getPage() { if (!page || page.isClosed()) { const b = await launchBrowser(); if (context) await context.close().catch(() => {}); context = await b.newContext({ viewport: { width: 1280, height: 800 } }); page = await context.newPage(); } return page; } async function snap() { const p = await getPage(); const buf = await p.screenshot({ type: 'jpeg', quality: 70, fullPage: false }); return { screenshot: buf.toString('base64'), url: p.url(), title: await p.title() }; } async function handleBash(body) { const { command, timeout = 30000 } = body; if (!command) return { error: 'No command provided' }; return new Promise((resolve) => { exec(command, { timeout, maxBuffer: 10 * 1024 * 1024, cwd: process.env.WORKSPACE || process.env.HOME || '/root', env: { ...process.env, TERM: 'dumb', COLUMNS: '200' }, }, (error, stdout, stderr) => { resolve({ stdout: stdout || '', stderr: stderr || '', exitCode: error ? (error.code ?? 1) : 0, output: (stdout || '') + (stderr ? '\nSTDERR: ' + stderr : ''), }); }); }); } async function handleNavigate(body) { const { url } = body; if (!url) return { error: 'No URL' }; const p = await getPage(); const target = url.startsWith('http') ? url : 'https://' + url; await p.goto(target, { timeout: 30000, waitUntil: 'domcontentloaded' }); return snap(); } async function handleClick(body) { const p = await getPage(); await p.mouse.click(body.x || 0, body.y || 0); await p.waitForTimeout(500); return snap(); } async function handleType(body) { const p = await getPage(); if (body.selector) await p.fill(body.selector, body.text || ''); else await p.keyboard.type(body.text || ''); await p.waitForTimeout(300); return snap(); } async function handleScroll(body) { const p = await getPage(); await p.mouse.wheel(0, body.dy || 300); await p.waitForTimeout(300); return snap(); } async function handleBack() { const p = await getPage(); await p.goBack({ timeout: 10000 }).catch(() => {}); return snap(); } async function handleText() { const p = await getPage(); const text = await p.evaluate(() => document.body.innerText); return { url: p.url(), title: await p.title(), text: text.slice(0, 8000) }; } async function handleEval(body) { const p = await getPage(); const result = await p.evaluate(body.script || 'null'); const ss = await snap(); return { ...ss, evalResult: String(result) }; } const routes = { '/api/bash': handleBash, '/api/browser/navigate': handleNavigate, '/api/browser/click': handleClick, '/api/browser/type': handleType, '/api/browser/scroll': handleScroll, '/api/browser/back': handleBack, '/api/browser/screenshot': () => snap(), '/api/browser/text': handleText, '/api/browser/eval': handleEval, }; http.createServer(async (req, res) => { if (req.method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return; } if (req.url === '/health') { res.writeHead(200, cors); res.end(JSON.stringify({ ok: true })); return; } const handler = routes[req.url]; if (req.method === 'POST' && handler) { try { const body = await parseBody(req); const result = await handler(body); res.writeHead(200, cors); res.end(JSON.stringify(result)); } catch (err) { res.writeHead(500, cors); res.end(JSON.stringify({ error: String(err) })); } return; } res.writeHead(404, cors); res.end(JSON.stringify({ error: 'Not found' })); }).listen(PORT, () => { console.log(`[tool-server] listening on :${'PORT'}`); });