88 lines
3.6 KiB
TypeScript
88 lines
3.6 KiB
TypeScript
import http from 'node:http';
|
||
import type { BrowserHost } from './browser-host';
|
||
|
||
/**
|
||
* 供 Python 后端调用的本地控制接口(只监听 127.0.0.1):
|
||
* GET /health
|
||
* POST /views {task_id, partition, allowed_domains} -> {target_id, cdp_url}
|
||
* DELETE /views/:id
|
||
* POST /views/:id/reveal | /hide 让渲染层展开 / 收起浏览器面板
|
||
* POST /views/:id/mode {mode: 'agent' | 'user'}
|
||
*/
|
||
export function startControlServer(port: number, cdpPort: number, host: BrowserHost): http.Server {
|
||
const server = http.createServer((req, res) => {
|
||
void handle(req, res).catch(error => {
|
||
respond(res, 500, { error: (error as Error).message });
|
||
});
|
||
});
|
||
|
||
async function handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
||
const method = req.method ?? 'GET';
|
||
const body = await readJson(req);
|
||
|
||
if (method === 'GET' && url.pathname === '/health') {
|
||
respond(res, 200, { ok: true, views: host.list() });
|
||
return;
|
||
}
|
||
if (method === 'POST' && url.pathname === '/views') {
|
||
const taskId = str(body.task_id);
|
||
if (!taskId) { respond(res, 400, { error: 'task_id 必填' }); return; }
|
||
const allowed = Array.isArray(body.allowed_domains) ? body.allowed_domains.filter((d): d is string => typeof d === 'string') : [];
|
||
const { targetId } = await host.create({ taskId, partition: str(body.partition) || `task-${taskId}`, allowedDomains: allowed });
|
||
respond(res, 200, { target_id: targetId, cdp_url: `http://127.0.0.1:${cdpPort}` });
|
||
return;
|
||
}
|
||
const match = url.pathname.match(/^\/views\/([^/]+)(?:\/(reveal|hide|mode))?$/);
|
||
if (match) {
|
||
const taskId = decodeURIComponent(match[1]);
|
||
const action = match[2];
|
||
if (method === 'DELETE' && !action) { host.dispose(taskId); respond(res, 200, { ok: true }); return; }
|
||
if (method === 'POST' && action === 'reveal') { host.reveal(taskId); respond(res, 200, { ok: true }); return; }
|
||
if (method === 'POST' && action === 'hide') { host.hide(taskId); respond(res, 200, { ok: true }); return; }
|
||
if (method === 'POST' && action === 'mode') {
|
||
const mode = str(body.mode) === 'user' ? 'user' : 'agent';
|
||
host.setMode(taskId, mode);
|
||
respond(res, 200, { ok: true, mode });
|
||
return;
|
||
}
|
||
}
|
||
respond(res, 404, { error: 'not found' });
|
||
}
|
||
|
||
server.listen(port, '127.0.0.1', () => console.log(`[electron] 控制接口 http://127.0.0.1:${port}`));
|
||
return server;
|
||
}
|
||
|
||
function str(value: unknown): string {
|
||
return typeof value === 'string' ? value : '';
|
||
}
|
||
|
||
function respond(res: http.ServerResponse, status: number, payload: unknown): void {
|
||
const text = JSON.stringify(payload);
|
||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Content-Length': Buffer.byteLength(text) });
|
||
res.end(text);
|
||
}
|
||
|
||
function readJson(req: http.IncomingMessage): Promise<Record<string, unknown>> {
|
||
return new Promise((resolve, reject) => {
|
||
const chunks: Buffer[] = [];
|
||
let size = 0;
|
||
req.on('data', (chunk: Buffer) => {
|
||
size += chunk.length;
|
||
if (size > 65536) { reject(new Error('请求体过大')); req.destroy(); return; }
|
||
chunks.push(chunk);
|
||
});
|
||
req.on('end', () => {
|
||
if (!chunks.length) { resolve({}); return; }
|
||
try {
|
||
const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;
|
||
resolve(parsed && typeof parsed === 'object' ? parsed as Record<string, unknown> : {});
|
||
} catch (error) {
|
||
reject(error as Error);
|
||
}
|
||
});
|
||
req.on('error', reject);
|
||
});
|
||
}
|