assistant/electron/control-server.ts

88 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
});
}