/** * Phase II — 统一对话 API 路由 * * POST /sessions/:id/chat — 统一对话入口(SSE 流式) * GET /sessions/:id/chat/history — 获取对话历史 * GET /sessions/:id/chat/conversation — 获取 conversation 元信息 * * SSE 格式:OpenAI Compatible(与前端 useAIStream 兼容) * 心跳:5 秒(H1) * 竞态保护:placeholder 占位(H3) */ import { FastifyInstance, FastifyRequest } from 'fastify'; import { logger } from '../../../common/logging/index.js'; import { prisma } from '../../../config/database.js'; import { conversationService } from '../services/ConversationService.js'; import { intentRouterService } from '../services/IntentRouterService.js'; import { chatHandlerService } from '../services/ChatHandlerService.js'; import { askUserService } from '../services/AskUserService.js'; function getUserId(request: FastifyRequest): string { const userId = (request as any).user?.userId; if (!userId) throw new Error('User not authenticated'); return userId; } export default async function chatRoutes(app: FastifyInstance) { /** * POST /sessions/:id/chat * 统一对话入口 — SSE 流式响应 */ app.post('/:id/chat', async (req, reply) => { const { id: sessionId } = req.params as { id: string }; const userId = getUserId(req); const { content, enableDeepThinking, metadata } = req.body as { content: string; enableDeepThinking?: boolean; metadata?: Record; }; if (!content?.trim()) { return reply.status(400).send({ error: '消息内容不能为空' }); } // SSE 响应头 reply.raw.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', 'X-Accel-Buffering': 'no', }); const writer = { write: (data: string) => { try { return reply.raw.write(data); } catch { return false; } }, end: () => { try { reply.raw.end(); } catch { /* ignore */ } }, on: (event: string, handler: () => void) => { req.raw.on(event, handler); }, }; try { // 1. 获取或创建 Conversation(延迟创建) const conversationId = await conversationService.getOrCreateConversation(sessionId, userId); // 2. 保存用户消息 await conversationService.saveUserMessage(conversationId, content.trim()); // ── H1 全局打断判定 ── const pending = await askUserService.getPending(sessionId); if (pending) { const askUserResponse = metadata?.askUserResponse ? askUserService.parseResponse(metadata) : null; if (askUserResponse) { // 正常回答问题(含 skip) const placeholderMsgId = await conversationService.createAssistantPlaceholder( conversationId, 'chat', ); const metaEvent = JSON.stringify({ type: 'intent_classified', intent: 'chat', confidence: 1, source: 'ask_user_response', guardTriggered: false, }); writer.write(`data: ${metaEvent}\n\n`); const result = await chatHandlerService.handleAskUserResponse( sessionId, conversationId, askUserResponse, writer, placeholderMsgId, ); logger.info('[SSA:Chat] AskUser response handled', { sessionId, action: askUserResponse.action, success: result.success, }); writer.end(); return; } else { // 用户无视卡片,强行打字转移话题 await askUserService.clearPending(sessionId); logger.info('[SSA:Chat] 用户转移话题,已取消挂起的 ask_user 状态', { sessionId }); } } // ── H1 结束 ── // 3. 执行模式:统一使用 Agent 通道(QPER 已废弃 UI 入口) const executionMode = 'agent'; // ── Agent 通道分流 ── if (executionMode === 'agent') { const placeholderMsgId = await conversationService.createAssistantPlaceholder( conversationId, 'chat', ); const metaEvent = JSON.stringify({ type: 'intent_classified', intent: 'analyze', confidence: 1, source: 'agent_mode', guardTriggered: false, }); writer.write(`data: ${metaEvent}\n\n`); const result = await chatHandlerService.handleAgentMode( sessionId, conversationId, content.trim(), writer, placeholderMsgId, metadata, ); logger.info('[SSA:Chat] Agent mode request completed', { sessionId, success: result.success, }); writer.end(); return; } // ── QPER 通道(现有逻辑) ── // 3. 意图分类 const intentResult = await intentRouterService.classify(content.trim(), sessionId); // 发送意图元数据事件(前端可用于 UI 切换) const metaEvent = JSON.stringify({ type: 'intent_classified', intent: intentResult.intent, confidence: intentResult.confidence, source: intentResult.source, guardTriggered: intentResult.guardTriggered || false, guardMessage: intentResult.guardMessage, }); writer.write(`data: ${metaEvent}\n\n`); // 4. 创建 assistant placeholder(H3 竞态保护) const placeholderMsgId = await conversationService.createAssistantPlaceholder( conversationId, intentResult.intent, ); // 5. 分发到意图处理器 const result = await chatHandlerService.handle( sessionId, conversationId, content.trim(), intentResult, writer, placeholderMsgId, ); logger.info('[SSA:Chat] Request completed', { sessionId, intent: result.intent, success: result.success, }); } catch (error: any) { logger.error('[SSA:Chat] Unhandled error', { sessionId, error: error.message, }); const errorEvent = JSON.stringify({ type: 'error', code: 'CHAT_ERROR', message: error.message || '处理消息时发生错误', }); try { writer.write(`data: ${errorEvent}\n\n`); } catch { /* ignore */ } } finally { writer.end(); } }); /** * GET /sessions/:id/chat/history * 获取对话历史消息 */ app.get('/:id/chat/history', async (req, reply) => { const { id: sessionId } = req.params as { id: string }; const conversation = await conversationService.getConversationBySession(sessionId); if (!conversation) { return reply.send({ messages: [], conversationId: null }); } const messages = await conversationService.getMessages(conversation.id); return reply.send({ conversationId: conversation.id, messages: messages.map(m => ({ id: m.id, role: m.role, content: m.content, thinkingContent: m.thinkingContent, intent: (m.metadata as any)?.intent, status: (m.metadata as any)?.status, createdAt: m.createdAt, })), }); }); /** * GET /sessions/:id/chat/conversation * 获取 conversation 元信息 */ app.get('/:id/chat/conversation', async (req, reply) => { const { id: sessionId } = req.params as { id: string }; const conversation = await conversationService.getConversationBySession(sessionId); if (!conversation) { return reply.send({ conversation: null }); } return reply.send({ conversation: { id: conversation.id, title: conversation.title, messageCount: conversation.messageCount, createdAt: conversation.createdAt, updatedAt: conversation.updatedAt, }, }); }); }