62 lines
3.3 KiB
JavaScript
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('无效的沙箱通信模式');
|