first commit
This commit is contained in:
121
async-agent-lab/server/main.ts
Normal file
121
async-agent-lab/server/main.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
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<string, LiveRun>();
|
||||
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<Record<string, unknown>> {
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user