Pārlūkot izejas kodu

AI 助手改为 SSE 流式输出

- mock 接口改为 rawResponse,支持 SSE 流式输出
- 前端使用 EventSource 接收流式数据
- 实现打字机效果,逐字显示 AI 回复

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tanlie 3 nedēļas atpakaļ
vecāks
revīzija
ee24789ef5
2 mainītis faili ar 100 papildinājumiem un 34 dzēšanām
  1. 36 16
      src/mock/index.ts
  2. 64 18
      src/views/ai-chat/index.vue

+ 36 - 16
src/mock/index.ts

@@ -153,12 +153,18 @@ export default [
     },
   },
 
-  // AI 对话接口
+  // AI 对话接口 (SSE 流式输出)
   {
     url: "/api/upms/ai/chat",
     method: "post",
-    response: ({ body }) => {
-      const { question } = body;
+    rawResponse: async (req, res) => {
+      const body = await new Promise<string>((resolve) => {
+        let data = '';
+        req.on('data', (chunk) => data += chunk);
+        req.on('end', () => resolve(data));
+      });
+
+      const { question } = JSON.parse(body);
 
       // AI 回复知识库
       const knowledgeBase: Record<string, string> = {
@@ -171,25 +177,39 @@ export default [
         "帮助": "我可以帮您:\n- 解答系统使用问题\n- 提供操作指导\n- 解释功能说明\n请直接输入您的问题!"
       };
 
-      // 模糊匹配
+      // 查找回复
+      let reply = '';
       for (const key in knowledgeBase) {
         if (question.includes(key) || key.includes(question)) {
-          return {
-            code: 200,
-            message: "success",
-            data: { reply: knowledgeBase[key] }
-          };
+          reply = knowledgeBase[key];
+          break;
         }
       }
 
       // 默认回复
-      return {
-        code: 200,
-        message: "success",
-        data: {
-          reply: `感谢您的提问!关于"${question}",我建议您:\n\n1. 查看相关功能模块的说明文档\n2. 联系系统管理员获取帮助\n3. 或者尝试在快捷问题中寻找类似问题\n\n如果您需要更详细的帮助,请联系技术支持。`
-        }
-      };
+      if (!reply) {
+        reply = `感谢您的提问!关于"${question}",我建议您:\n\n1. 查看相关功能模块的说明文档\n2. 联系系统管理员获取帮助\n3. 或者尝试在快捷问题中寻找类似问题\n\n如果您需要更详细的帮助,请联系技术支持。`;
+      }
+
+      // 设置 SSE 响应头
+      res.setHeader('Content-Type', 'text/event-stream');
+      res.setHeader('Cache-Control', 'no-cache');
+      res.setHeader('Connection', 'keep-alive');
+
+      // 流式发送回复,每个字延迟 50ms
+      const chars = reply.split('');
+      for (let i = 0; i < chars.length; i++) {
+        const data = {
+          content: chars[i],
+          index: i,
+          total: chars.length,
+          done: i === chars.length - 1
+        };
+        res.write(`data: ${JSON.stringify(data)}\n\n`);
+        await new Promise(resolve => setTimeout(resolve, 50));
+      }
+
+      res.end();
     },
   },
 ] as MockMethod[];

+ 64 - 18
src/views/ai-chat/index.vue

@@ -171,29 +171,75 @@ const sendMessage = async () => {
   isTyping.value = true
   scrollToBottom()
 
-  // 调用 AI 接口获取回复
-  const reply = await sendChatMessage(content)
-  isTyping.value = false
-  messages.value.push({
-    role: 'assistant',
-    content: reply,
-    time: new Date().toLocaleString()
-  })
-  isSending.value = false
-  scrollToBottom()
-}
-
-// AI 对话接口
-const sendChatMessage = async (question: string): Promise<string> => {
+  // 调用 AI 接口获取流式回复
   try {
-    const res = await http.post<{ reply: string }>('/upms/ai/chat', { question })
-    return res.reply
+    await sendChatMessage(content)
   } catch (error) {
-    console.error('AI 接口调用失败:', error)
-    return '抱歉,AI 服务暂时不可用,请稍后再试。'
+    console.error('AI 对话失败:', error)
+  } finally {
+    isTyping.value = false
+    isSending.value = false
+    scrollToBottom()
   }
 }
 
+// AI 对话接口 (SSE 流式输出)
+const sendChatMessage = async (question: string): Promise<void> => {
+  return new Promise((resolve, reject) => {
+    try {
+      // 先添加一个空的 AI 消息
+      const aiMessageIndex = messages.value.length
+      messages.value.push({
+        role: 'assistant',
+        content: '',
+        time: new Date().toLocaleString()
+      })
+
+      // 创建 SSE 连接
+      const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
+
+      eventSource.onmessage = (event) => {
+        try {
+          const data = JSON.parse(event.data)
+
+          // 追加内容到 AI 消息
+          if (messages.value[aiMessageIndex]) {
+            messages.value[aiMessageIndex].content += data.content
+          }
+
+          // 滚动到底部
+          scrollToBottom()
+
+          // 如果完成了,关闭连接
+          if (data.done) {
+            eventSource.close()
+            resolve()
+          }
+        } catch (error) {
+          console.error('解析 SSE 数据失败:', error)
+        }
+      }
+
+      eventSource.onerror = (error) => {
+        console.error('SSE 连接错误:', error)
+        eventSource.close()
+
+        // 如果有内容,则认为是正常结束
+        if (messages.value[aiMessageIndex]?.content) {
+          resolve()
+        } else {
+          // 如果没有内容,显示错误信息
+          messages.value[aiMessageIndex].content = '抱歉,AI 服务暂时不可用,请稍后再试。'
+          reject(error)
+        }
+      }
+    } catch (error) {
+      console.error('创建 SSE 连接失败:', error)
+      reject(error)
+    }
+  })
+}
+
 // 选择快捷问题
 const selectQuestion = (question: string) => {
   inputMessage.value = question