import type { IncomingMessage, ServerResponse } from 'node:http'; import { CLOUD_AGENTS_PATH } from '../../../shared/cloud-agents'; import { CloudAgentsError, CloudAgentsModule } from '../../services/cloud-agents'; import { parseJsonBody, sendJson, flushStreamingHeaders, writeStreamingChunk } from '../route-utils'; const cloudAgents = new CloudAgentsModule(); export async function handleCloudAgentsRoutes(req: IncomingMessage, res: ServerResponse, url: URL): Promise { if (!url.pathname.startsWith(CLOUD_AGENTS_PATH + '/')) return false; res.setHeader('Cache-Control', 'no-store'); const path = url.pathname.slice(CLOUD_AGENTS_PATH.length); try { let data: unknown; const stream = /^\/runs\/([a-zA-Z0-9_-]{1,64})\/events$/.exec(path); if (stream && req.method === 'GET') { const controller = new AbortController(); const decoder = new TextDecoder(); const close = () => controller.abort(); res.once('close', close); try { const after = (typeof req.headers['last-event-id'] === 'string' ? req.headers['last-event-id'] : null) ?? url.searchParams.get('after_seq') ?? '0-0'; for await (const chunk of cloudAgents.events(stream[1], after, controller.signal)) { if (!res.headersSent) { res.setHeader('Content-Type', 'text/event-stream; charset=utf-8'); res.setHeader('Connection', 'keep-alive'); flushStreamingHeaders(res); } if (!await writeStreamingChunk(res, decoder.decode(chunk, { stream: true }))) break; } } finally { controller.abort(); res.off('close', close); } res.end(); return true; } if (path === '/recovery' && req.method === 'GET') { data = await cloudAgents.recovery(); } else if (path === '/recovery/recent' && req.method === 'PUT') { data = await cloudAgents.rememberRecent(await parseJsonBody(req)); } else if (path === '/recovery/pending' && req.method === 'POST') { data = await cloudAgents.resolvePending(await parseJsonBody(req)); } else if (path === '/attachments/pick' && req.method === 'POST') { data = await cloudAgents.upload(); } else if (path === '/knowledge/pick' && req.method === 'POST') { data = await cloudAgents.uploadKnowledge(await parseJsonBody(req)); } else if (path === '/skills/pick' && req.method === 'POST') { data = await cloudAgents.uploadSkill(); } else if (path === '/files/save' && req.method === 'POST') { data = await cloudAgents.download(await parseJsonBody(req)); } else if (path === '/actions' && req.method === 'POST') { data = await cloudAgents.execute(await parseJsonBody(req)); } else if ((path === '/bootstrap' || path === '/agents') && req.method === 'GET') { data = await cloudAgents.list(url.searchParams.get('cursor'), url.searchParams.get('archived') === 'true'); } else if (path === '/agents' && req.method === 'POST') { data = await cloudAgents.create(await parseJsonBody(req)); } else { const match = /^\/agents\/(ml-[a-f0-9]{32})(\/draft)?$/.exec(path); if (match && !match[2] && req.method === 'GET') data = await cloudAgents.get(match[1]); else if (match && match[2] && req.method === 'PATCH') data = await cloudAgents.save(match[1], await parseJsonBody(req)); else { sendJson(res, 404, { error: '智能体接口不存在' }); return true; } } sendJson(res, 200, data); } catch (error) { const failure = error instanceof CloudAgentsError ? error : new CloudAgentsError(error instanceof SyntaxError ? 400 : 502, error instanceof SyntaxError ? 'invalid_input' : 'cloud_service_unavailable'); if (res.headersSent) res.end(); else sendJson(res, failure.status, { success: false, error: failure.message, code: failure.code }); } return true; }