UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

851 lines (850 loc) 31.6 kB
/** * AI API 路由 * AI 提供商管理、摘要、翻译、对话、.env LLM 配置 */ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import express from 'express'; import { AgentMessage, Channel } from '../../agent/AgentMessage.js'; import { ConversationStore } from '../../agent/ConversationStore.js'; import { buildProjectBriefing } from '../../agent/core/ChatAgentPrompts.js'; import { taskCheckAndSubmit, taskDiscoverAllRelations, taskFullEnrich, taskGuardFullScan, taskQualityAudit, } from '../../agent/domain/ChatAgentTasks.js'; import { PRESETS } from '../../agent/presets.js'; import { createProvider } from '../../external/ai/AiFactory.js'; import Logger from '../../infrastructure/logging/Logger.js'; import { getRealtimeService } from '../../infrastructure/realtime/RealtimeService.js'; import { getServiceContainer } from '../../injection/ServiceContainer.js'; import { ValidationError } from '../../shared/errors/index.js'; import { resolveProjectRoot } from '../../shared/resolveProjectRoot.js'; import { AiChatBody, AiConfigBody, AiEnvConfigBody, AiFormatUsageGuideBody, AiLangBody, AiStreamBody, AiSummarizeBody, AiTaskBody, AiToolBody, AiTranslateBody, } from '../../shared/schemas/http-requests.js'; import { validate } from '../middleware/validate.js'; import { createStreamSession, getStreamSession } from '../utils/sse-sessions.js'; const router = express.Router(); const logger = Logger.getInstance(); /** 获取 DI 容器 */ function getContainer() { return getServiceContainer(); } /** 检查 AI Provider 是否可用(非 mock),不可用则抛 ValidationError */ function requireAiReady() { const container = getContainer(); const manager = container.singletons?._aiProviderManager; if (manager?.isMock) { throw new ValidationError('AI Provider 未配置,当前为 Mock 模式。请先在 .env 中配置 API Key。'); } return container; } // ═══════════════════════════════════════════════════════ // UI 语言偏好 — 前端 ↔ 服务端同步 // ═══════════════════════════════════════════════════════ /** * GET /api/v1/ai/lang * 获取当前默认 UI 语言(由系统环境变量初始化,前端可覆盖) */ router.get('/lang', async (req, res) => { const container = getContainer(); res.json({ success: true, data: { lang: container.getLang() || 'zh' } }); }); /** * POST /api/v1/ai/lang * 更新默认 UI 语言(前端切语言时同步到服务端) */ router.post('/lang', validate(AiLangBody), async (req, res) => { const { lang } = req.body; const container = getContainer(); container.setLang(lang); logger.info(`UI language preference updated to "${lang}"`); res.json({ success: true, data: { lang } }); }); /** * GET /api/v1/ai/providers * 获取可用的 AI 提供商列表 */ router.get('/providers', async (req, res) => { // API Key 环境变量映射(与 AiFactory.autoDetectProvider 保持一致) const KEY_ENVS = { google: 'ASD_GOOGLE_API_KEY', openai: 'ASD_OPENAI_API_KEY', deepseek: 'ASD_DEEPSEEK_API_KEY', claude: 'ASD_CLAUDE_API_KEY', }; const providers = [ { id: 'google', label: 'Google Gemini', defaultModel: 'gemini-3-flash-preview' }, { id: 'openai', label: 'OpenAI', defaultModel: 'gpt-5.4' }, { id: 'deepseek', label: 'DeepSeek', defaultModel: 'deepseek-chat' }, { id: 'claude', label: 'Claude', defaultModel: 'claude-sonnet-4-20250514' }, { id: 'ollama', label: 'Ollama', defaultModel: 'llama3' }, { id: 'mock', label: 'Mock (测试)', defaultModel: 'mock-l3' }, ].map((p) => ({ ...p, hasKey: KEY_ENVS[p.id] ? !!process.env[KEY_ENVS[p.id]] : true, // ollama / mock 不需要 key,始终可用 })); res.json({ success: true, data: providers }); }); /** * GET /api/v1/ai/config * 获取当前 AI 配置(优先从 AiProviderManager 读取) */ router.get('/config', async (req, res) => { const container = getServiceContainer(); const manager = container.singletons?._aiProviderManager; res.json({ success: true, data: { provider: manager.name, model: manager.model, isMock: manager.isMock }, }); }); /** * POST /api/v1/ai/config * 更新 AI 配置(切换提供商/模型)— 通过 AiProviderManager 统一热切换 */ router.post('/config', validate(AiConfigBody), async (req, res) => { const { provider, model } = req.body; // 创建新的 provider 实例验证配置有效 let newProvider; try { newProvider = createProvider({ provider: provider.toLowerCase(), model: model || undefined, }); } catch (error) { throw new ValidationError(`Invalid provider: ${error.message}`); } // 通过 reloadAiProvider → AiProviderManager.switchProvider() 统一热切换 const container = getServiceContainer(); container.reloadAiProvider(newProvider); logger.info('AI provider switched via AiProviderManager', { provider: provider.toLowerCase(), model: newProvider.model, }); res.json({ success: true, data: { provider: provider.toLowerCase(), model: newProvider.model, name: newProvider.name, }, }); }); /** * POST /api/v1/ai/mock/cleanup * 清理 Mock 模式产生的候选数据 */ router.post('/mock/cleanup', async (_req, res) => { const container = getContainer(); const knowledgeService = container.get('knowledgeService'); const knowledgeRepo = container.get('knowledgeRepository'); // 查找所有 mock 来源的候选 const mockSources = ['mock-bootstrap', 'mock-pipeline']; let totalDeleted = 0; for (const source of mockSources) { const ids = await knowledgeRepo.findIdsBySource(source); for (const id of ids) { try { await knowledgeService.delete(id, { userId: 'system:mock-cleanup' }); totalDeleted++; } catch { logger.debug(`Mock cleanup: failed to delete ${id}`); } } } // 清理 bootstrap 来源的 semantic_memories try { const memoryRepo = container.get('memoryRepository'); if (memoryRepo) { await memoryRepo.clearBootstrapMemories(); } } catch { // memoryRepository 可能未注册 } logger.info(`Mock cleanup completed: ${totalDeleted} entries deleted`); const rt = getRealtimeService(); if (rt) { rt.broadcastEvent('mock-cleanup-completed', { deleted: totalDeleted }); } res.json({ success: true, data: { deleted: totalDeleted }, }); }); /** * POST /api/v1/ai/summarize * AI 摘要生成 */ router.post('/summarize', validate(AiSummarizeBody), async (req, res) => { const { code, language } = req.body; const container = requireAiReady(); const factory = container.get('agentFactory'); const result = await factory.scanKnowledge({ label: 'code', files: [{ name: 'code', content: code, language }], task: 'summarize', }); if (result?.error) { throw new ValidationError(result.error); } res.json({ success: true, data: result }); }); /** * POST /api/v1/ai/translate * AI 翻译(中文 → 英文) */ router.post('/translate', validate(AiTranslateBody), async (req, res) => { const { summary, usageGuide } = req.body; if (!summary && !usageGuide) { return void res.json({ success: true, data: { summaryEn: '', usageGuideEn: '' }, }); } try { const container = requireAiReady(); const factory = container.get('agentFactory'); const result = await factory.translateToEnglish(summary, usageGuide); if (result?.error) { // AI 不可用,降级返回原文 logger.warn('AI translate tool returned error', { error: result.error }); return void res.json({ success: true, data: { summaryEn: summary || '', usageGuideEn: usageGuide || '' }, warning: result.error, }); } res.json({ success: true, data: result }); } catch (err) { logger.warn('AI translate failed, returning original text', { error: err.message, }); res.json({ success: true, data: { summaryEn: summary || '', usageGuideEn: usageGuide || '' }, warning: `Translation failed: ${err.message}`, }); } }); /** * POST /api/v1/ai/chat * AI 对话(RAG 模式,结合项目知识库) * * 增强特性 (Engine Migration): * - 对话持久化 (ConversationStore) * - ContextWindow 上下文窗口管理 * - Token 用量持久化 * - 项目概况注入 (buildProjectBriefing) * - SSE 流式最终回答 (text:start/delta/end) * - MemoryCoordinator 记忆提取 */ router.post('/chat', validate(AiChatBody), async (req, res) => { const { prompt, history, lang, conversationId } = req.body; const container = requireAiReady(); const factory = container.get('agentFactory'); // ── 对话持久化: 从 ConversationStore 加载历史 ── let convStore = null; let effectiveHistory = history; let effectiveConvId = conversationId || null; try { const projectRoot = resolveProjectRoot(container); convStore = new ConversationStore(projectRoot); if (effectiveConvId) { effectiveHistory = convStore.load(effectiveConvId); convStore.append(effectiveConvId, { role: 'user', content: prompt }); } else { effectiveConvId = convStore.create({ category: 'user', title: prompt.slice(0, 50) }); convStore.append(effectiveConvId, { role: 'user', content: prompt }); } } catch { /* ConversationStore 不可用时静默降级 */ } // ── 项目概况刷新 ── let _projectBriefing = ''; try { _projectBriefing = await buildProjectBriefing({ container }); } catch { /* 静默降级 */ } // ── 创建 ContextWindow ── const _contextWindow = factory.createContextWindow({ isSystem: false }); // ── 创建 Runtime 并注入 onProgress ── const message = AgentMessage.fromHttp(req); // 加载持久化历史到 message if (effectiveHistory.length > 0) { message.session.history = effectiveHistory; } const runtime = factory.createChat({ lang, onProgress: (event) => { // SSE 流式进度 (如果前端通过 SSE 建立了连接) try { const sessionId = req.body.sseSessionId; if (sessionId) { const session = getStreamSession(sessionId); if (session) { session.send(event); } } } catch { /* SSE 不可用时静默 */ } }, }); const result = await runtime.execute(message); // ── 持久化 assistant 回复 ── if (convStore && effectiveConvId && result.reply) { try { convStore.append(effectiveConvId, { role: 'assistant', content: result.reply }); } catch { /* 静默降级 */ } } // ── Token 用量持久化 ── try { const tokenStore = container.get('tokenUsageStore'); if (tokenStore && result.tokenUsage) { const aiProvider = container.singletons?.aiProvider; tokenStore.record({ source: 'user', dimension: undefined, provider: aiProvider?.name ?? undefined, model: aiProvider?.model ?? undefined, inputTokens: result.tokenUsage.input || 0, outputTokens: result.tokenUsage.output || 0, durationMs: result.durationMs || 0, toolCalls: result.toolCalls?.length || 0, sessionId: effectiveConvId, }); // 通知前端 token 用量变化 try { const realtime = getRealtimeService(); realtime?.broadcastTokenUsageUpdated?.(); } catch { /* optional */ } } } catch { /* token logging should never break execution */ } res.json({ success: true, data: { reply: result.reply, toolCalls: result.toolCalls, iterations: result.iterations || null, conversationId: effectiveConvId, tokenUsage: result.tokenUsage || null, }, }); }); /** * POST /api/v1/ai/agent/tool * 程序化直接调用 Agent 工具(跳过 ReAct 循环) * Body: { tool: string, params: object } */ router.post('/agent/tool', validate(AiToolBody), async (req, res) => { const { tool, params } = req.body; const container = requireAiReady(); const factory = container.get('agentFactory'); const result = await factory.invokeAgent(tool, params); res.json({ success: true, data: result }); }); /** * POST /api/v1/ai/agent/task * 执行预定义任务流(查重提交 / 批量关系发现 / 批量补全) * Body: { task: string, params: object } * * 支持两种任务类型: * 1. ToolRegistry 注册的工具 (直接通过 toolName 调用) * 2. ChatAgentTasks 的 5 个预定义 DAG 任务 */ const DAG_TASK_HANDLERS = { check_and_submit: taskCheckAndSubmit, discover_all_relations: taskDiscoverAllRelations, full_enrich: taskFullEnrich, quality_audit: taskQualityAudit, guard_full_scan: taskGuardFullScan, }; router.post('/agent/task', validate(AiTaskBody), async (req, res) => { const { task, params } = req.body; const container = requireAiReady(); const factory = container.get('agentFactory'); // 优先尝试 DAG 任务 const dagHandler = DAG_TASK_HANDLERS[task]; if (dagHandler) { const aiProvider = container.singletons?.aiProvider; const taskContext = { invokeAgent: (name, p) => factory.invokeAgent(name, p), aiProvider, container, logger, }; const result = await dagHandler(taskContext, params); return void res.json({ success: true, data: result }); } // 回退到 Agent 工具执行 const result = await factory.invokeAgent(task, params); res.json({ success: true, data: result }); }); /** * GET /api/v1/ai/agent/capabilities * 获取 Agent 能力清单(工具列表 + 任务列表) */ router.get('/agent/capabilities', async (req, res) => { const container = getContainer(); const toolRegistry = container.get('toolRegistry'); const tools = toolRegistry.getToolSchemas(); const presets = Object.entries(PRESETS).map(([name, p]) => ({ name, description: p.description, capabilities: p.capabilities, strategy: p.strategy?.type || 'single', })); res.json({ success: true, data: { tools, presets, tasks: [ { name: 'check_and_submit', description: '提交候选前自动查重 + 质量预评' }, { name: 'discover_all_relations', description: '批量发现 Recipe 之间的知识图谱关系' }, { name: 'full_enrich', description: '批量 AI 语义补全候选字段' }, { name: 'quality_audit', description: '批量质量审计全部 Recipe,标记低分项' }, { name: 'guard_full_scan', description: '用全部 Guard 规则扫描指定代码,生成完整报告' }, ], }, }); }); /** * POST /api/v1/ai/format-usage-guide * 格式化 usageGuide 文本(纯文本处理,不涉及 AI 调用) * 注:虽非 AI 功能,但前端从 /ai/ 路径调用,保留以维持 API 兼容 */ router.post('/format-usage-guide', validate(AiFormatUsageGuideBody), async (req, res) => { const { text } = req.body; if (!text) { return void res.json({ success: true, data: { formatted: '' } }); } // 简单文本格式化处理 let formatted = text.trim(); // 确保段落间有空行 formatted = formatted.replace(/\n{3,}/g, '\n\n'); // 确保代码块格式 formatted = formatted.replace(/```(\w+)?\n/g, '\n```$1\n'); res.json({ success: true, data: { formatted } }); }); // ═══════════════════════════════════════════════════════ // .env LLM 配置读写 // ═══════════════════════════════════════════════════════ /** 获取用户项目目录下 .env 的路径 */ function _getProjectEnvPath() { const container = getServiceContainer(); const projectRoot = container.singletons?._projectRoot || process.env.ASD_PROJECT_DIR || process.cwd(); return join(projectRoot, '.env'); } /** LLM 相关的 env 变量名 → 标签映射 */ const LLM_ENV_KEYS = [ 'ASD_AI_PROVIDER', 'ASD_AI_MODEL', 'ASD_GOOGLE_API_KEY', 'ASD_OPENAI_API_KEY', 'ASD_CLAUDE_API_KEY', 'ASD_DEEPSEEK_API_KEY', 'ASD_AI_PROXY', 'ASD_EMBED_PROVIDER', 'ASD_EMBED_MODEL', 'ASD_EMBED_BASE_URL', 'ASD_EMBED_API_KEY', ]; /** * 解析 .env 内容为 key-value(仅提取 LLM 相关变量) * 返回 { vars, hasEnvFile, llmReady } * llmReady: provider + 至少一个对应 API Key 已配置 */ function parseLlmEnv(envPath) { if (!existsSync(envPath)) { return { vars: {}, hasEnvFile: false, llmReady: false }; } const raw = readFileSync(envPath, 'utf8'); const vars = {}; for (const line of raw.split('\n')) { const trimmed = line.trim(); // 跳过注释和空行 if (!trimmed || trimmed.startsWith('#')) { continue; } const eqIdx = trimmed.indexOf('='); if (eqIdx === -1) { continue; } const key = trimmed.slice(0, eqIdx).trim(); const val = trimmed .slice(eqIdx + 1) .trim() .replace(/^["']|["']$/g, ''); if (LLM_ENV_KEYS.includes(key)) { vars[key] = val; } } // 判断 LLM 是否可用:有 provider + 对应的 API Key const provider = vars.ASD_AI_PROVIDER || ''; const keyMap = { google: 'ASD_GOOGLE_API_KEY', openai: 'ASD_OPENAI_API_KEY', claude: 'ASD_CLAUDE_API_KEY', deepseek: 'ASD_DEEPSEEK_API_KEY', ollama: '', // ollama 不需要 key mock: '', // mock 不需要 key }; const neededKey = keyMap[provider] || ''; const llmReady = !!provider && (!neededKey || !!vars[neededKey]); return { vars, hasEnvFile: true, llmReady }; } /** * GET /api/v1/ai/env-config * 读取用户项目 .env 中的 LLM 配置 */ router.get('/env-config', async (req, res) => { const envPath = _getProjectEnvPath(); const result = parseLlmEnv(envPath); res.json({ success: true, data: result }); }); /** * POST /api/v1/ai/env-config * 写入 / 更新用户项目 .env 中的 LLM 配置 * * Body: { provider, model, apiKey, proxy? } */ router.post('/env-config', validate(AiEnvConfigBody), async (req, res) => { const { provider, model, apiKey, proxy, embedProvider, embedModel, embedBaseUrl, embedApiKey } = req.body; const envPath = _getProjectEnvPath(); let content = existsSync(envPath) ? readFileSync(envPath, 'utf8') : ''; // 构建 key-value 更新列表 const updates = { ASD_AI_PROVIDER: provider, }; if (model) { updates.ASD_AI_MODEL = model; } if (proxy) { updates.ASD_AI_PROXY = proxy; } // 根据 provider 决定写入哪个 API Key 变量 const providerKeyMap = { google: 'ASD_GOOGLE_API_KEY', openai: 'ASD_OPENAI_API_KEY', claude: 'ASD_CLAUDE_API_KEY', deepseek: 'ASD_DEEPSEEK_API_KEY', }; const keyName = providerKeyMap[provider]; if (keyName && apiKey) { updates[keyName] = apiKey; } // Embedding 独立配置 if (embedProvider) { updates.ASD_EMBED_PROVIDER = embedProvider; if (embedModel) { updates.ASD_EMBED_MODEL = embedModel; } if (embedBaseUrl) { updates.ASD_EMBED_BASE_URL = embedBaseUrl; } if (embedApiKey) { updates.ASD_EMBED_API_KEY = embedApiKey; } } // 逐条合并到 .env 内容 for (const [k, v] of Object.entries(updates)) { // 匹配已有行(包括被注释的行) const activeRe = new RegExp(`^${k}\\s*=.*$`, 'm'); const commentedRe = new RegExp(`^#\\s*${k}\\s*=.*$`, 'm'); if (activeRe.test(content)) { // 替换已有活动行 content = content.replace(activeRe, `${k}=${v}`); } else if (commentedRe.test(content)) { // 取消注释并赋值 content = content.replace(commentedRe, `${k}=${v}`); } else { // 追加到末尾 if (!content.endsWith('\n')) { content += '\n'; } content += `${k}=${v}\n`; } } writeFileSync(envPath, content); logger.info('LLM env config updated', { provider, model }); // 同步到当前进程环境变量(热生效) for (const [k, v] of Object.entries(updates)) { process.env[k] = String(v); } // 尝试热切换 AI Provider(通过 AiProviderManager 统一处理) try { const newProvider = createProvider({ provider: provider.toLowerCase(), model: model || undefined, }); const container = getServiceContainer(); container.reloadAiProvider(newProvider); logger.info('AI provider hot-swapped via AiProviderManager after env update', { provider, model: newProvider.model, }); } catch (err) { logger.debug('Hot-swap AI provider failed (will take effect on restart)', { error: err.message, }); } const result = parseLlmEnv(envPath); res.json({ success: true, data: result }); }); // ═══════════════════════════════════════════════════════ // SSE Streaming — 流式对话(Session + EventSource 架构) // ═══════════════════════════════════════════════════════ /** * POST /api/v1/ai/chat/stream * 启动 AI 对话流 — 创建 session,后台执行 AgentRuntime,立即返回 sessionId * * 客户端拿到 sessionId 后通过 GET /chat/events/:sessionId (EventSource) 消费事件 * * 协议事件(通过 session 缓冲 + EventSource 交付): * step:start — 新推理步骤开始 {step, maxSteps, phase} * step:end — 推理步骤结束 {step} * tool:start — 工具调用开始 {id, tool, args} * tool:end — 工具调用结束 {tool, status, resultSize?, duration?, error?} * text:start — 文本流开始 {id, role} * text:delta — 文本分块 {id, delta} * text:end — 文本流结束 {id} * stream:done — 会话完成 {text, toolCalls, hasContext} * stream:error — 会话错误 {message} * * Body: { prompt: string, history?: Array<{role,content}> } * Response: { success: true, sessionId: string } */ router.post('/chat/stream', validate(AiStreamBody), async (req, res) => { const { prompt, history, lang } = req.body; const container = requireAiReady(); const factory = container.get('agentFactory'); const session = createStreamSession('chat'); logger.debug('SSE session created', { sessionId: session.sessionId }); // 立即返回 sessionId(不等待 Agent 执行) res.json({ success: true, sessionId: session.sessionId }); // AgentMessage 构建 const message = new AgentMessage({ content: prompt, channel: Channel.HTTP, session: { id: session.sessionId, history }, sender: { id: req.ip || 'http-user', type: 'user' }, metadata: { lang, stream: true }, }); // 创建 Runtime — 挂载 onProgress 回调映射到 SSE 事件 const runtime = factory.createChat({ lang, onProgress: (event) => { // 将 AgentRuntime 内部事件映射到前端 SSE 协议 switch (event.type) { case 'thinking': session.send({ type: 'step:start', step: event.iteration, maxSteps: event.maxIterations, phase: 'thinking', }); break; case 'tool_call': session.send({ type: 'tool:start', tool: event.tool, args: event.args }); break; case 'tool_end': session.send({ type: 'tool:end', tool: event.tool, status: event.status, resultSize: event.resultSize, duration: event.duration, error: event.error, }); break; default: session.send(event); } }, }); // 后台执行 AgentRuntime runtime .execute(message) .then((result) => { const replyText = result.reply || ''; // 发送最终文本 if (replyText) { const textId = `text_${Date.now()}`; session.send({ type: 'text:start', id: textId, role: 'assistant' }); session.send({ type: 'text:delta', id: textId, delta: replyText }); session.send({ type: 'text:end', id: textId }); } else { logger.warn('SSE session: empty reply from AgentRuntime', { sessionId: session.sessionId, iterations: result.iterations, toolCalls: result.toolCalls?.length || 0, }); } session.end({ text: replyText || '抱歉,AI 未能生成有效回复。请重试或换个问题。', toolCalls: result.toolCalls || [], iterations: result.iterations || 0, }); // ── Token 用量持久化(streaming) ── try { const tokenUsage = result.tokenUsage; if (tokenUsage) { const tokenStore = container.get('tokenUsageStore'); const aiProvider = container.singletons?.aiProvider; tokenStore.record({ source: 'user', provider: aiProvider?.name ?? undefined, model: aiProvider?.model ?? undefined, inputTokens: tokenUsage.input || 0, outputTokens: tokenUsage.output || 0, durationMs: result.durationMs || 0, toolCalls: result.toolCalls?.length || 0, }); try { const realtime = getRealtimeService(); realtime?.broadcastTokenUsageUpdated?.(); } catch { /* ignore */ } } } catch { /* token tracking should never break streaming */ } logger.debug('SSE session completed', { sessionId: session.sessionId, events: session.buffer.length, }); }) .catch((err) => { logger.warn('SSE session error', { sessionId: session.sessionId, error: err.message, }); session.error(err.message, 'RUNTIME_ERROR'); }); }); /** * GET /api/v1/ai/chat/events/:sessionId * EventSource SSE 端点 — 消费指定 session 的实时事件 * * 流程: * 1. 回放 session 缓冲区中已积累的所有事件 * 2. 如果 session 已完成 → 直接结束流 * 3. 否则订阅实时事件,直到 stream:done / stream:error * * 使用原生 EventSource API 消费(浏览器内置 SSE 支持,无缓冲问题) */ router.get('/chat/events/:sessionId', (req, res) => { const session = getStreamSession(req.params.sessionId); if (!session) { res.status(404).json({ success: false, error: 'Session not found or expired' }); return; } // ─── SSE Headers ─── res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); res.flushHeaders(); if (res.socket) { res.socket.setNoDelay(true); res.socket.setTimeout(0); } /** 写入一个 SSE data 行 */ function writeEvent(event) { if (res.writableEnded) { return; } const line = `data: ${JSON.stringify(event)}\n\n`; res.write(line); } // 1) 回放缓冲区 let isDone = false; for (const event of session.buffer) { writeEvent(event); if (event.type === 'stream:done' || event.type === 'stream:error') { isDone = true; } } // 2) 如果已完成,直接关闭 if (isDone || session.completed) { res.end(); return; } // 3) 订阅实时事件 const unsubscribe = session.on((event) => { writeEvent(event); if (event.type === 'stream:done' || event.type === 'stream:error') { unsubscribe(); clearInterval(heartbeat); res.end(); } }); // 心跳保活 (每 15 秒) const heartbeat = setInterval(() => { if (res.writableEnded) { clearInterval(heartbeat); return; } res.write(`: ping ${Date.now()}\n\n`); }, 15_000); // 客户端断开连接时清理 res.on('close', () => { unsubscribe(); clearInterval(heartbeat); }); }); /** * GET /api/v1/ai/token-usage * 近 7 日 Token 消耗报告(按日 + 按来源 + 总计) */ router.get('/token-usage', async (req, res) => { const container = getServiceContainer(); let tokenStore; try { tokenStore = container.get('tokenUsageStore'); } catch { return void res.json({ success: true, data: { daily: [], bySource: [], summary: { input_tokens: 0, output_tokens: 0, total_tokens: 0, call_count: 0, avg_per_call: 0, }, }, }); } const report = tokenStore.getLast7DaysReport(); res.json({ success: true, data: report }); }); export default router;