import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; import { mkdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { loadProvider, PROJECT_ROOT } from '../agent/config.ts'; import { createLLM } from '../agent/llm.ts'; import { RUNS_DIR } from '../agent/workspace.ts'; import { LiveRun, scenarios } from '../live/run.ts'; import type { LiveEvent } from '../live/types.ts'; const port = Number(process.env.PORT ?? 8791); const host = '127.0.0.1'; const index = join(PROJECT_ROOT, 'web/dist/index.html'); const provider = { ...loadProvider(), baseURL: 'https://dd-ai-api.eastmoney.com/coding/v1', model: 'DeepSeek-V4.1-Flash-1017', contextWindow: 1_000_000, maxOutputTokens: 384_000, inputModalities: ['text', 'image'], }; const llm = createLLM(provider); const runs = new Map(); mkdirSync(RUNS_DIR, { recursive: true }); function json(res: ServerResponse, status: number, body: unknown) { res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }); res.end(JSON.stringify(body)); } async function readBody(req: IncomingMessage): Promise> { let raw = ''; for await (const chunk of req) { raw += chunk; if (raw.length > 12_000) throw new Error('请求过长。'); } const data = JSON.parse(raw || '{}') as unknown; if (!data || typeof data !== 'object' || Array.isArray(data)) throw new Error('请求体必须是对象。'); return data as Record; } function events(req: IncomingMessage, res: ServerResponse, run: LiveRun) { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-store', Connection: 'keep-alive' }); res.flushHeaders(); const lastId = Number(req.headers['last-event-id'] ?? 0); const send = (event: LiveEvent) => res.write(`id: ${event.seq}\ndata: ${JSON.stringify(event)}\n\n`); for (const event of run.events.slice(Number.isSafeInteger(lastId) && lastId > 0 ? lastId : 0)) send(event); run.listeners.add(send); const ping = setInterval(() => res.write(': ping\n\n'), 15_000); res.on('close', () => { clearInterval(ping); run.listeners.delete(send); }); } async function handle(req: IncomingMessage, res: ServerResponse) { const origin = `http://${req.headers.host}`; if (!/^(127\.0\.0\.1|localhost)(?::\d+)?$/.test(req.headers.host ?? '') || (req.method === 'POST' && req.headers.origin && req.headers.origin !== origin)) { return json(res, 403, { error: '请从本机主讲稿发起请求。' }); } const path = new URL(req.url ?? '/', origin).pathname; if (req.method === 'GET' && path === '/') { let html: Buffer; try { html = readFileSync(index); } catch { return json(res, 503, { error: '请先运行 pnpm build 或 pnpm start。' }); } res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' }); return res.end(html); } if (req.method === 'GET' && path === '/api/config') { const { displayName, baseURL, model, contextWindow, maxOutputTokens, inputModalities } = provider; return json(res, 200, { provider: { displayName, baseURL, model, contextWindow, maxOutputTokens, inputModalities }, scenarios: scenarios.map(({ execute, ...info }) => info), }); } if (req.method === 'POST' && path === '/api/runs') { const { scenario: id } = await readBody(req); const scenario = scenarios.find((candidate) => candidate.id === id); if (!scenario) return json(res, 400, { error: '请选择演示场景。' }); const run = new LiveRun(scenario, llm); runs.set(run.id, run); void run.start(); return json(res, 201, { id: run.id }); } const route = /^\/api\/runs\/([\w-]+)\/(events|answer|stop)$/.exec(path); const run = route && runs.get(route[1]); if (!run) return json(res, 404, { error: '运行不存在。' }); if (req.method === 'GET' && route[2] === 'events') return events(req, res, run); if (req.method === 'POST' && route[2] === 'stop') { run.stop(); return json(res, 200, { ok: true }); } if (req.method === 'POST' && route[2] === 'answer') { const { questionId, answer } = await readBody(req); if (typeof questionId !== 'string') throw new Error('缺少问题 ID。'); run.runtime.answer(questionId, answer); return json(res, 200, { ok: true }); } return json(res, 404, { error: '接口不存在。' }); } createServer((req, res) => { void handle(req, res).catch((error: Error) => { if (!res.headersSent) { const message = error.message.replaceAll(provider.apiKey, '[已隐藏]'); json(res, 400, { error: message }); } }); }).listen(port, host, () => { console.log(`主讲稿 http://${host}:${port} · ${provider.model}`); }); for (const signal of ['SIGINT', 'SIGTERM'] as const) { process.on(signal, () => { for (const run of runs.values()) run.stop(); process.exit(0); }); }