62 lines
3.3 KiB
JavaScript

import { request as httpRequest, createServer } from 'node:http';
import { createInterface } from 'node:readline';
import { spawn } from 'node:child_process';
// Runs INSIDE the OS sandbox. Its only privileged connection is the run-scoped
// broker socket; it never receives database/storage/provider credentials.
function forward(path, body, headers = {}) {
return new Promise((resolve, reject) => {
const endpoint = process.env.CANVAS_AGENT_BROKER_URL;
const target = endpoint ? new URL(path, endpoint) : null;
const request = httpRequest({
...(target ? { hostname: target.hostname, port: target.port, path: target.pathname }
: { socketPath: '/run/broker.sock', path }),
method: 'POST', headers: { 'content-type': 'application/json', ...headers,
authorization: `Bearer ${process.env.CANVAS_AGENT_RUN_TOKEN}` },
}, resolve);
request.on('error', reject); request.setTimeout(20 * 60_000, () => request.destroy(new Error('任务网关超时')));
request.end(body);
});
}
if (process.argv[2] === 'mcp') {
const lines = createInterface({ input: process.stdin });
const outstanding = new Set();
for await (const line of lines) {
if (!line.trim()) continue;
const job = (async () => {
let message;
try {
message = JSON.parse(line);
const response = await forward('/mcp', JSON.stringify(message));
const chunks = []; for await (const chunk of response) chunks.push(chunk);
const result = JSON.parse(Buffer.concat(chunks).toString('utf8'));
if (message.id !== undefined) process.stdout.write(`${JSON.stringify(result)}\n`);
} catch {
if (message?.id !== undefined) process.stdout.write(`${JSON.stringify({ jsonrpc: '2.0', id: message.id,
error: { code: -32603, message: '画布工具调用中断;请核对任务记录,勿重复提交收费操作' } })}\n`);
}
})();
outstanding.add(job); job.finally(() => outstanding.delete(job));
}
await Promise.allSettled(outstanding);
} else if (process.argv[2] === 'launch') {
// Linux has a private network namespace: only this loopback adapter exists.
const server = createServer(async (request, response) => {
try {
if (!['/v1/chat/completions', '/v1/responses', '/v1/messages'].includes(request.url)) {
response.writeHead(404); response.end(); return;
}
const chunks = []; let size = 0;
for await (const chunk of request) { size += chunk.length; if (size > 32 * 1024 * 1024) throw new Error('oversize'); chunks.push(chunk); }
const upstream = await forward(request.url, Buffer.concat(chunks));
response.writeHead(upstream.statusCode || 502, { 'content-type': upstream.headers['content-type'] || 'application/json' });
upstream.pipe(response);
} catch { response.writeHead(502); response.end('{"error":{"message":"任务模型网关暂不可用"}}'); }
});
await new Promise(resolve => server.listen(32119, '127.0.0.1', resolve));
const child = spawn(process.execPath, process.argv.slice(3), { stdio: 'inherit', shell: false });
for (const signal of ['SIGINT', 'SIGTERM']) process.on(signal, () => child.kill(signal));
child.on('error', () => { server.close(); process.exitCode = 1; });
child.on('close', code => { server.close(); process.exitCode = code ?? 1; });
} else throw new Error('无效的沙箱通信模式');