瀏覽代碼

feat:顶层自动跳转判定逻辑采用显示规则判定和隐式意图分析判定

FanHong 1 周之前
父節點
當前提交
f7db3c4953
共有 42 個文件被更改,包括 1632 次插入335 次删除
  1. 1 1
      README.md
  2. 328 108
      api-docs.html
  3. 2 2
      docs/ai-qa-api-call-chain.md
  4. 4 4
      docs/superpowers/plans/2026-04-16-desktop-sidebar-navigation.md
  5. 2 2
      docs/superpowers/specs/2026-04-16-desktop-sidebar-navigation-design.md
  6. 6 0
      shudao-chat-py/config/prompt_config.yaml
  7. 1 1
      shudao-chat-py/models/chat.py
  8. 4 4
      shudao-chat-py/models/total.py
  9. 1 1
      shudao-chat-py/prompts/JSON.MD
  10. 1 1
      shudao-chat-py/prompts/RAG.md
  11. 1 1
      shudao-chat-py/prompts/backup/JSON.MD
  12. 1 1
      shudao-chat-py/prompts/backup/yitushibiefeiliu.md
  13. 1 1
      shudao-chat-py/prompts/final_answer_template.md
  14. 1 1
      shudao-chat-py/prompts/liushi.md
  15. 15 0
      shudao-chat-py/prompts/module_dispatch_template.md
  16. 1 1
      shudao-chat-py/prompts/yitushibie_template.md
  17. 1 1
      shudao-chat-py/prompts/yitushibiefeiliu.md
  18. 19 4
      shudao-chat-py/routers/chat.py
  19. 167 11
      shudao-chat-py/routers/exam.py
  20. 273 52
      shudao-chat-py/services/qwen_service.py
  21. 1 1
      shudao-chat-py/tests/test_chat.md
  22. 2 2
      shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-50-25-677Z.yml
  23. 2 2
      shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-52-11-624Z.yml
  24. 2 2
      shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-56-55-892Z.yml
  25. 2 2
      shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-57-58-344Z.yml
  26. 2 2
      shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-58-47-567Z.yml
  27. 2 2
      shudao-vue-frontend/.playwright-cli/page-2026-04-16T06-05-11-107Z.yml
  28. 174 0
      shudao-vue-frontend/src/components/DispatchFeedbackModal.vue
  29. 1 1
      shudao-vue-frontend/src/components/MobileHistoryDrawer.vue
  30. 1 1
      shudao-vue-frontend/src/components/MobileTabBar.vue
  31. 1 1
      shudao-vue-frontend/src/components/Sidebar.test.js
  32. 53 22
      shudao-vue-frontend/src/components/Toast.vue
  33. 1 1
      shudao-vue-frontend/src/request/ai.json
  34. 2 1
      shudao-vue-frontend/src/request/apis.js
  35. 172 24
      shudao-vue-frontend/src/views/Chat.vue
  36. 166 21
      shudao-vue-frontend/src/views/ExamWorkshop.vue
  37. 6 6
      shudao-vue-frontend/src/views/Index.vue
  38. 39 2
      shudao-vue-frontend/src/views/mobile/m-AIWriting.vue
  39. 124 38
      shudao-vue-frontend/src/views/mobile/m-Chat.vue
  40. 22 1
      shudao-vue-frontend/src/views/mobile/m-ExamWorkshop.vue
  41. 5 5
      shudao-vue-frontend/src/views/mobile/m-Index.vue
  42. 22 1
      shudao-vue-frontend/src/views/mobile/m-SafetyHazard.vue

+ 1 - 1
README.md

@@ -2,7 +2,7 @@
 
 ## 项目概述
 
-蜀道安全AI系统是为四川省蜀道投资集团打造的AI安全管理平台,提供AI问答、公文写作、培训PPT生成、试题生成、隐患检测等功能。
+蜀道安全AI系统是为四川省蜀道投资集团打造的AI安全管理平台,提供AI助手、公文写作、培训PPT生成、试题生成、隐患检测等功能。
 
 ## 技术栈
 

+ 328 - 108
api-docs.html

@@ -1,38 +1,176 @@
 <!DOCTYPE html>
 <html lang="zh-CN">
+
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>蜀道安全AI系统 - API接口文档</title>
     <style>
-        * { margin: 0; padding: 0; box-sizing: border-box; }
-        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #fafafa; color: #3b4151; }
-        .header { background: linear-gradient(135deg, #1f8a70 0%, #2d5a4a 100%); color: white; padding: 20px 30px; }
-        .header h1 { font-size: 28px; margin-bottom: 5px; }
-        .header p { opacity: 0.9; font-size: 14px; }
-        .container { max-width: 1800px; margin: 0 auto; padding: 20px; }
-        .api-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; }
-        .api-card { background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.12); overflow: hidden; }
-        .api-card-header { padding: 12px 15px; display: flex; align-items: center; gap: 10px; border-bottom: 1px solid #eee; }
-        .method { padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 700; color: white; text-transform: uppercase; }
-        .method.get { background: #61affe; }
-        .method.post { background: #49cc90; }
-        .api-path { font-family: monospace; font-size: 13px; color: #3b4151; word-break: break-all; }
-        .api-card-body { padding: 12px 15px; }
-        .api-title { font-size: 14px; font-weight: 600; color: #3b4151; margin-bottom: 8px; }
-        .api-desc { font-size: 12px; color: #6b7280; line-height: 1.5; margin-bottom: 10px; }
-        .params-title { font-size: 11px; font-weight: 600; color: #1f8a70; margin-bottom: 5px; text-transform: uppercase; }
-        .param-item { font-size: 11px; color: #4b5563; padding: 3px 0; border-bottom: 1px dashed #e5e7eb; }
-        .param-item:last-child { border-bottom: none; }
-        .param-name { font-family: monospace; color: #1f8a70; font-weight: 500; }
-        .param-type { color: #9ca3af; font-size: 10px; }
-        .section-title { font-size: 18px; font-weight: 600; color: #1f8a70; margin: 25px 0 15px; padding-bottom: 8px; border-bottom: 2px solid #1f8a70; }
-        .tag { display: inline-block; padding: 2px 6px; background: #e0f2f1; color: #1f8a70; border-radius: 3px; font-size: 10px; margin-right: 5px; }
-        .response-tag { background: #fef3c7; color: #92400e; }
-        @media (max-width: 1200px) { .api-grid { grid-template-columns: repeat(2, 1fr); } }
-        @media (max-width: 768px) { .api-grid { grid-template-columns: 1fr; } }
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: #fafafa;
+            color: #3b4151;
+        }
+
+        .header {
+            background: linear-gradient(135deg, #1f8a70 0%, #2d5a4a 100%);
+            color: white;
+            padding: 20px 30px;
+        }
+
+        .header h1 {
+            font-size: 28px;
+            margin-bottom: 5px;
+        }
+
+        .header p {
+            opacity: 0.9;
+            font-size: 14px;
+        }
+
+        .container {
+            max-width: 1800px;
+            margin: 0 auto;
+            padding: 20px;
+        }
+
+        .api-grid {
+            display: grid;
+            grid-template-columns: repeat(3, 1fr);
+            gap: 15px;
+        }
+
+        .api-card {
+            background: white;
+            border-radius: 8px;
+            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
+            overflow: hidden;
+        }
+
+        .api-card-header {
+            padding: 12px 15px;
+            display: flex;
+            align-items: center;
+            gap: 10px;
+            border-bottom: 1px solid #eee;
+        }
+
+        .method {
+            padding: 4px 10px;
+            border-radius: 4px;
+            font-size: 11px;
+            font-weight: 700;
+            color: white;
+            text-transform: uppercase;
+        }
+
+        .method.get {
+            background: #61affe;
+        }
+
+        .method.post {
+            background: #49cc90;
+        }
+
+        .api-path {
+            font-family: monospace;
+            font-size: 13px;
+            color: #3b4151;
+            word-break: break-all;
+        }
+
+        .api-card-body {
+            padding: 12px 15px;
+        }
+
+        .api-title {
+            font-size: 14px;
+            font-weight: 600;
+            color: #3b4151;
+            margin-bottom: 8px;
+        }
+
+        .api-desc {
+            font-size: 12px;
+            color: #6b7280;
+            line-height: 1.5;
+            margin-bottom: 10px;
+        }
+
+        .params-title {
+            font-size: 11px;
+            font-weight: 600;
+            color: #1f8a70;
+            margin-bottom: 5px;
+            text-transform: uppercase;
+        }
+
+        .param-item {
+            font-size: 11px;
+            color: #4b5563;
+            padding: 3px 0;
+            border-bottom: 1px dashed #e5e7eb;
+        }
+
+        .param-item:last-child {
+            border-bottom: none;
+        }
+
+        .param-name {
+            font-family: monospace;
+            color: #1f8a70;
+            font-weight: 500;
+        }
+
+        .param-type {
+            color: #9ca3af;
+            font-size: 10px;
+        }
+
+        .section-title {
+            font-size: 18px;
+            font-weight: 600;
+            color: #1f8a70;
+            margin: 25px 0 15px;
+            padding-bottom: 8px;
+            border-bottom: 2px solid #1f8a70;
+        }
+
+        .tag {
+            display: inline-block;
+            padding: 2px 6px;
+            background: #e0f2f1;
+            color: #1f8a70;
+            border-radius: 3px;
+            font-size: 10px;
+            margin-right: 5px;
+        }
+
+        .response-tag {
+            background: #fef3c7;
+            color: #92400e;
+        }
+
+        @media (max-width: 1200px) {
+            .api-grid {
+                grid-template-columns: repeat(2, 1fr);
+            }
+        }
+
+        @media (max-width: 768px) {
+            .api-grid {
+                grid-template-columns: 1fr;
+            }
+        }
     </style>
 </head>
+
 <body>
     <div class="header">
         <h1>🛡️ 蜀道安全AI系统 API文档</h1>
@@ -52,8 +190,10 @@
                     <div class="api-title">本地登录</div>
                     <div class="api-desc">用户名密码登录,返回JWT Token(需配置enable_local_login=true)</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">username</span> <span class="param-type">string</span> - 用户名</div>
-                    <div class="param-item"><span class="param-name">password</span> <span class="param-type">string</span> - 密码</div>
+                    <div class="param-item"><span class="param-name">username</span> <span
+                            class="param-type">string</span> - 用户名</div>
+                    <div class="param-item"><span class="param-name">password</span> <span
+                            class="param-type">string</span> - 密码</div>
                     <div class="params-title" style="margin-top:8px">响应</div>
                     <div class="param-item"><span class="tag response-tag">200</span> token, userInfo</div>
                 </div>
@@ -72,10 +212,14 @@
                     <div class="api-title">发送DeepSeek消息</div>
                     <div class="api-desc">发送消息到AI模型,支持安全培训PPT生成、AI写作等业务类型</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">message</span> <span class="param-type">string</span> - 用户消息</div>
-                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span class="param-type">uint64</span> - 对话ID(可选)</div>
-                    <div class="param-item"><span class="param-name">business_type</span> <span class="param-type">int</span> - 业务类型(1:安全培训,2:AI写作)</div>
-                    <div class="param-item"><span class="param-name">exam_name</span> <span class="param-type">string</span> - 考试名称(可选)</div>
+                    <div class="param-item"><span class="param-name">message</span> <span
+                            class="param-type">string</span> - 用户消息</div>
+                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span
+                            class="param-type">uint64</span> - 对话ID(可选)</div>
+                    <div class="param-item"><span class="param-name">business_type</span> <span
+                            class="param-type">int</span> - 业务类型(1:安全培训,2:AI写作)</div>
+                    <div class="param-item"><span class="param-name">exam_name</span> <span
+                            class="param-type">string</span> - 考试名称(可选)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -87,8 +231,10 @@
                     <div class="api-title">流式聊天 (SSE)</div>
                     <div class="api-desc">流式输出AI回复,支持RAG检索增强生成</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">message</span> <span class="param-type">string</span> - 用户消息</div>
-                    <div class="param-item"><span class="param-name">model</span> <span class="param-type">string</span> - 模型名称(可选)</div>
+                    <div class="param-item"><span class="param-name">message</span> <span
+                            class="param-type">string</span> - 用户消息</div>
+                    <div class="param-item"><span class="param-name">model</span> <span class="param-type">string</span>
+                        - 模型名称(可选)</div>
                     <div class="params-title" style="margin-top:8px">响应</div>
                     <div class="param-item"><span class="tag">SSE</span> text/event-stream</div>
                 </div>
@@ -102,10 +248,14 @@
                     <div class="api-title">流式聊天(数据库集成)</div>
                     <div class="api-desc">流式聊天并自动保存对话记录到数据库</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">message</span> <span class="param-type">string</span> - 用户消息</div>
-                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span class="param-type">uint64</span> - 对话ID</div>
-                    <div class="param-item"><span class="param-name">business_type</span> <span class="param-type">int</span> - 业务类型</div>
-                    <div class="param-item"><span class="param-name">online_search_content</span> <span class="param-type">string</span> - 联网搜索内容</div>
+                    <div class="param-item"><span class="param-name">message</span> <span
+                            class="param-type">string</span> - 用户消息</div>
+                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span
+                            class="param-type">uint64</span> - 对话ID</div>
+                    <div class="param-item"><span class="param-name">business_type</span> <span
+                            class="param-type">int</span> - 业务类型</div>
+                    <div class="param-item"><span class="param-name">online_search_content</span> <span
+                            class="param-type">string</span> - 联网搜索内容</div>
                 </div>
             </div>
             <div class="api-card">
@@ -129,7 +279,8 @@
                     <div class="api-title">删除对话</div>
                     <div class="api-desc">删除指定的AI对话</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span class="param-type">uint64</span> - 对话ID</div>
+                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span
+                            class="param-type">uint64</span> - 对话ID</div>
                 </div>
             </div>
             <div class="api-card">
@@ -141,7 +292,8 @@
                     <div class="api-title">删除历史记录</div>
                     <div class="api-desc">删除指定的历史记录</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint64</span> - 记录ID</div>
+                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint64</span> -
+                        记录ID</div>
                 </div>
             </div>
             <div class="api-card">
@@ -153,7 +305,8 @@
                     <div class="api-title">意图识别</div>
                     <div class="api-desc">识别用户输入的意图类型</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">message</span> <span class="param-type">string</span> - 用户消息</div>
+                    <div class="param-item"><span class="param-name">message</span> <span
+                            class="param-type">string</span> - 用户消息</div>
                 </div>
             </div>
             <div class="api-card">
@@ -165,7 +318,8 @@
                     <div class="api-title">获取ChromaDB文档</div>
                     <div class="api-desc">从向量数据库检索相关文档并生成回答</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">query</span> <span class="param-type">string</span> - 查询内容</div>
+                    <div class="param-item"><span class="param-name">query</span> <span class="param-type">string</span>
+                        - 查询内容</div>
                 </div>
             </div>
             <div class="api-card">
@@ -177,7 +331,8 @@
                     <div class="api-title">猜你想问</div>
                     <div class="api-desc">根据上下文推荐相关问题</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">context</span> <span class="param-type">string</span> - 上下文内容</div>
+                    <div class="param-item"><span class="param-name">context</span> <span
+                            class="param-type">string</span> - 上下文内容</div>
                 </div>
             </div>
             <div class="api-card">
@@ -189,7 +344,8 @@
                     <div class="api-title">用户输入推荐问题</div>
                     <div class="api-desc">用户输入时实时返回推荐问题</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">input</span> <span class="param-type">string</span> - 用户输入内容</div>
+                    <div class="param-item"><span class="param-name">input</span> <span class="param-type">string</span>
+                        - 用户输入内容</div>
                 </div>
             </div>
             <div class="api-card">
@@ -201,7 +357,8 @@
                     <div class="api-title">联网搜索</div>
                     <div class="api-desc">执行联网搜索获取实时信息</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">query</span> <span class="param-type">string</span> - 搜索关键词</div>
+                    <div class="param-item"><span class="param-name">query</span> <span class="param-type">string</span>
+                        - 搜索关键词</div>
                 </div>
             </div>
             <div class="api-card">
@@ -213,8 +370,10 @@
                     <div class="api-title">保存联网搜索结果</div>
                     <div class="api-desc">将联网搜索结果存入AIMessage表</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">content</span> <span class="param-type">string</span> - 搜索结果内容</div>
-                    <div class="param-item"><span class="param-name">ai_message_id</span> <span class="param-type">uint64</span> - 消息ID</div>
+                    <div class="param-item"><span class="param-name">content</span> <span
+                            class="param-type">string</span> - 搜索结果内容</div>
+                    <div class="param-item"><span class="param-name">ai_message_id</span> <span
+                            class="param-type">uint64</span> - 消息ID</div>
                 </div>
             </div>
         </div>
@@ -231,7 +390,8 @@
                     <div class="api-title">生成考试提示词</div>
                     <div class="api-desc">根据考试配置生成AI提示词</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">exam_config</span> <span class="param-type">object</span> - 考试配置对象</div>
+                    <div class="param-item"><span class="param-name">exam_config</span> <span
+                            class="param-type">object</span> - 考试配置对象</div>
                 </div>
             </div>
             <div class="api-card">
@@ -243,8 +403,10 @@
                     <div class="api-title">单题生成提示词</div>
                     <div class="api-desc">为单个题目重新生成提示词</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">question_type</span> <span class="param-type">string</span> - 题目类型</div>
-                    <div class="param-item"><span class="param-name">context</span> <span class="param-type">string</span> - 上下文</div>
+                    <div class="param-item"><span class="param-name">question_type</span> <span
+                            class="param-type">string</span> - 题目类型</div>
+                    <div class="param-item"><span class="param-name">context</span> <span
+                            class="param-type">string</span> - 上下文</div>
                 </div>
             </div>
             <div class="api-card">
@@ -256,8 +418,10 @@
                     <div class="api-title">修改考试题目</div>
                     <div class="api-desc">修改已生成的考试题目内容</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span class="param-type">uint64</span> - 对话ID</div>
-                    <div class="param-item"><span class="param-name">content</span> <span class="param-type">string</span> - 新内容</div>
+                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span
+                            class="param-type">uint64</span> - 对话ID</div>
+                    <div class="param-item"><span class="param-name">content</span> <span
+                            class="param-type">string</span> - 新内容</div>
                 </div>
             </div>
             <div class="api-card">
@@ -269,7 +433,8 @@
                     <div class="api-title">重新生成单题</div>
                     <div class="api-desc">重新生成指定的单个考试题目</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">question_id</span> <span class="param-type">uint64</span> - 题目ID</div>
+                    <div class="param-item"><span class="param-name">question_id</span> <span
+                            class="param-type">uint64</span> - 题目ID</div>
                 </div>
             </div>
         </div>
@@ -286,9 +451,12 @@
                     <div class="api-title">隐患识别</div>
                     <div class="api-desc">使用YOLO模型识别图片中的安全隐患,返回标注后的图片</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">scene_name</span> <span class="param-type">string</span> - 场景名称(模型类型)</div>
-                    <div class="param-item"><span class="param-name">image</span> <span class="param-type">string</span> - 图片OSS链接</div>
-                    <div class="param-item"><span class="param-name">date</span> <span class="param-type">string</span> - 日期</div>
+                    <div class="param-item"><span class="param-name">scene_name</span> <span
+                            class="param-type">string</span> - 场景名称(模型类型)</div>
+                    <div class="param-item"><span class="param-name">image</span> <span class="param-type">string</span>
+                        - 图片OSS链接</div>
+                    <div class="param-item"><span class="param-name">date</span> <span class="param-type">string</span>
+                        - 日期</div>
                 </div>
             </div>
             <div class="api-card">
@@ -300,10 +468,14 @@
                     <div class="api-title">保存步骤</div>
                     <div class="api-desc">保存PPT生成步骤、JSON文件和封面图</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span class="param-type">uint64</span> - 对话ID</div>
-                    <div class="param-item"><span class="param-name">step</span> <span class="param-type">int</span> - 步骤编号</div>
-                    <div class="param-item"><span class="param-name">ppt_json_url</span> <span class="param-type">string</span> - PPT JSON URL</div>
-                    <div class="param-item"><span class="param-name">cover_image</span> <span class="param-type">string</span> - 封面图URL</div>
+                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span
+                            class="param-type">uint64</span> - 对话ID</div>
+                    <div class="param-item"><span class="param-name">step</span> <span class="param-type">int</span> -
+                        步骤编号</div>
+                    <div class="param-item"><span class="param-name">ppt_json_url</span> <span
+                            class="param-type">string</span> - PPT JSON URL</div>
+                    <div class="param-item"><span class="param-name">cover_image</span> <span
+                            class="param-type">string</span> - 封面图URL</div>
                 </div>
             </div>
             <div class="api-card">
@@ -327,7 +499,8 @@
                     <div class="api-title">获取识别记录详情</div>
                     <div class="api-desc">获取指定识别记录的详细信息</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">recognition_record_id</span> <span class="param-type">int64</span> - 记录ID</div>
+                    <div class="param-item"><span class="param-name">recognition_record_id</span> <span
+                            class="param-type">int64</span> - 记录ID</div>
                 </div>
             </div>
             <div class="api-card">
@@ -339,7 +512,8 @@
                     <div class="api-title">删除识别记录</div>
                     <div class="api-desc">删除指定的隐患识别历史记录</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint64</span> - 记录ID</div>
+                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint64</span> -
+                        记录ID</div>
                 </div>
             </div>
             <div class="api-card">
@@ -351,11 +525,16 @@
                     <div class="api-title">提交点评</div>
                     <div class="api-desc">用户对识别结果提交点评反馈</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint</span> - 记录ID</div>
-                    <div class="param-item"><span class="param-name">scene_match</span> <span class="param-type">int</span> - 场景匹配度</div>
-                    <div class="param-item"><span class="param-name">tip_accuracy</span> <span class="param-type">int</span> - 提示准确度</div>
-                    <div class="param-item"><span class="param-name">effect_evaluation</span> <span class="param-type">int</span> - 效果评价</div>
-                    <div class="param-item"><span class="param-name">user_remark</span> <span class="param-type">string</span> - 用户备注</div>
+                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint</span> -
+                        记录ID</div>
+                    <div class="param-item"><span class="param-name">scene_match</span> <span
+                            class="param-type">int</span> - 场景匹配度</div>
+                    <div class="param-item"><span class="param-name">tip_accuracy</span> <span
+                            class="param-type">int</span> - 提示准确度</div>
+                    <div class="param-item"><span class="param-name">effect_evaluation</span> <span
+                            class="param-type">int</span> - 效果评价</div>
+                    <div class="param-item"><span class="param-name">user_remark</span> <span
+                            class="param-type">string</span> - 用户备注</div>
                 </div>
             </div>
             <div class="api-card">
@@ -379,7 +558,8 @@
                     <div class="api-title">获取三级场景示例图</div>
                     <div class="api-desc">获取隐患识别三级场景的正确和错误示例图</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">third_scene_name</span> <span class="param-type">string</span> - 三级场景名称</div>
+                    <div class="param-item"><span class="param-name">third_scene_name</span> <span
+                            class="param-type">string</span> - 三级场景名称</div>
                 </div>
             </div>
         </div>
@@ -410,7 +590,8 @@
                     <div class="api-title">上传图片</div>
                     <div class="api-desc">上传图片到OSS,自动压缩到200KB以下</div>
                     <div class="params-title">请求参数 (FormData)</div>
-                    <div class="param-item"><span class="param-name">image</span> <span class="param-type">file</span> - 图片文件(≤10MB)</div>
+                    <div class="param-item"><span class="param-name">image</span> <span class="param-type">file</span> -
+                        图片文件(≤10MB)</div>
                     <div class="params-title" style="margin-top:8px">响应</div>
                     <div class="param-item"><span class="param-name">fileUrl</span> - 代理访问URL</div>
                     <div class="param-item"><span class="param-name">fileName</span> - 文件名</div>
@@ -426,7 +607,8 @@
                     <div class="api-title">上传PPT JSON文件</div>
                     <div class="api-desc">上传PPT配置JSON文件到OSS</div>
                     <div class="params-title">请求参数 (FormData)</div>
-                    <div class="param-item"><span class="param-name">json</span> <span class="param-type">file</span> - JSON文件(≤50MB)</div>
+                    <div class="param-item"><span class="param-name">json</span> <span class="param-type">file</span> -
+                        JSON文件(≤50MB)</div>
                     <div class="params-title" style="margin-top:8px">响应</div>
                     <div class="param-item"><span class="param-name">fileUrl</span> - 代理访问URL</div>
                 </div>
@@ -440,7 +622,8 @@
                     <div class="api-title">OSS代理解析</div>
                     <div class="api-desc">代理转发OSS URL请求,解密并获取文件内容</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">url</span> <span class="param-type">string</span> - 加密的OSS URL</div>
+                    <div class="param-item"><span class="param-name">url</span> <span class="param-type">string</span> -
+                        加密的OSS URL</div>
                 </div>
             </div>
         </div>
@@ -457,7 +640,8 @@
                     <div class="api-title">获取推荐问题</div>
                     <div class="api-desc">随机返回推荐问题列表</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">limit</span> <span class="param-type">int</span> - 数量(默认5)</div>
+                    <div class="param-item"><span class="param-name">limit</span> <span class="param-type">int</span> -
+                        数量(默认5)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -469,7 +653,8 @@
                     <div class="api-title">获取功能卡片</div>
                     <div class="api-desc">随机返回4条功能卡片</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">function_type</span> <span class="param-type">int</span> - 类型(0:AI问答,1:安全培训)</div>
+                    <div class="param-item"><span class="param-name">function_type</span> <span
+                            class="param-type">int</span> - 类型(0:AI助手,1:安全培训)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -481,7 +666,8 @@
                     <div class="api-title">获取热点问题</div>
                     <div class="api-desc">随机返回4条热点问题</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">question_type</span> <span class="param-type">int</span> - 类型(0:AI问答,1:安全培训)</div>
+                    <div class="param-item"><span class="param-name">question_type</span> <span
+                            class="param-type">int</span> - 类型(0:AI助手,1:安全培训)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -493,8 +679,10 @@
                     <div class="api-title">提交意见反馈</div>
                     <div class="api-desc">用户提交意见反馈信息</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">content</span> <span class="param-type">string</span> - 反馈内容</div>
-                    <div class="param-item"><span class="param-name">contact</span> <span class="param-type">string</span> - 联系方式</div>
+                    <div class="param-item"><span class="param-name">content</span> <span
+                            class="param-type">string</span> - 反馈内容</div>
+                    <div class="param-item"><span class="param-name">contact</span> <span
+                            class="param-type">string</span> - 联系方式</div>
                 </div>
             </div>
             <div class="api-card">
@@ -506,8 +694,10 @@
                     <div class="api-title">点赞/踩</div>
                     <div class="api-desc">对AI回复进行点赞或踩的反馈</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint</span> - 消息ID</div>
-                    <div class="param-item"><span class="param-name">user_feedback</span> <span class="param-type">int</span> - 反馈(1:赞,-1:踩)</div>
+                    <div class="param-item"><span class="param-name">id</span> <span class="param-type">uint</span> -
+                        消息ID</div>
+                    <div class="param-item"><span class="param-name">user_feedback</span> <span
+                            class="param-type">int</span> - 反馈(1:赞,-1:踩)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -536,10 +726,14 @@
                     <div class="api-title">获取政策文件列表</div>
                     <div class="api-desc">分页获取政策文件列表,支持类型筛选和搜索</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">policy_type</span> <span class="param-type">int</span> - 政策类型(0:全部)</div>
-                    <div class="param-item"><span class="param-name">search</span> <span class="param-type">string</span> - 搜索关键词</div>
-                    <div class="param-item"><span class="param-name">page</span> <span class="param-type">int</span> - 页码</div>
-                    <div class="param-item"><span class="param-name">pageSize</span> <span class="param-type">int</span> - 每页数量</div>
+                    <div class="param-item"><span class="param-name">policy_type</span> <span
+                            class="param-type">int</span> - 政策类型(0:全部)</div>
+                    <div class="param-item"><span class="param-name">search</span> <span
+                            class="param-type">string</span> - 搜索关键词</div>
+                    <div class="param-item"><span class="param-name">page</span> <span class="param-type">int</span> -
+                        页码</div>
+                    <div class="param-item"><span class="param-name">pageSize</span> <span class="param-type">int</span>
+                        - 每页数量</div>
                 </div>
             </div>
             <div class="api-card">
@@ -551,8 +745,10 @@
                     <div class="api-title">下载文件</div>
                     <div class="api-desc">从OSS链接下载文件并返回给前端</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">pdf_oss_download_link</span> <span class="param-type">string</span> - OSS下载链接</div>
-                    <div class="param-item"><span class="param-name">file_name</span> <span class="param-type">string</span> - 自定义文件名(可选)</div>
+                    <div class="param-item"><span class="param-name">pdf_oss_download_link</span> <span
+                            class="param-type">string</span> - OSS下载链接</div>
+                    <div class="param-item"><span class="param-name">file_name</span> <span
+                            class="param-type">string</span> - 自定义文件名(可选)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -564,8 +760,10 @@
                     <div class="api-title">政策文件统计</div>
                     <div class="api-desc">政策文件查看和下载次数+1</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">policy_file_id</span> <span class="param-type">int</span> - 政策文件ID</div>
-                    <div class="param-item"><span class="param-name">action_type</span> <span class="param-type">int</span> - 操作类型(1:查看,2:下载)</div>
+                    <div class="param-item"><span class="param-name">policy_file_id</span> <span
+                            class="param-type">int</span> - 政策文件ID</div>
+                    <div class="param-item"><span class="param-name">action_type</span> <span
+                            class="param-type">int</span> - 操作类型(1:查看,2:下载)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -577,7 +775,8 @@
                     <div class="api-title">获取文件链接</div>
                     <div class="api-desc">根据文件名从数据库查找对应的OSS链接</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">file_name</span> <span class="param-type">string</span> - 文件名</div>
+                    <div class="param-item"><span class="param-name">file_name</span> <span
+                            class="param-type">string</span> - 文件名</div>
                 </div>
             </div>
             <div class="api-card">
@@ -589,8 +788,10 @@
                     <div class="api-title">知识库高级搜索</div>
                     <div class="api-desc">从ChromaDB向量数据库进行高级文件搜索</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">query_str</span> <span class="param-type">string</span> - 查询字符串</div>
-                    <div class="param-item"><span class="param-name">n_results</span> <span class="param-type">int</span> - 结果数量(默认50)</div>
+                    <div class="param-item"><span class="param-name">query_str</span> <span
+                            class="param-type">string</span> - 查询字符串</div>
+                    <div class="param-item"><span class="param-name">n_results</span> <span
+                            class="param-type">int</span> - 结果数量(默认50)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -602,8 +803,10 @@
                     <div class="api-title">保存PPT大纲</div>
                     <div class="api-desc">保存AI生成的PPT大纲内容</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span class="param-type">uint64</span> - 对话ID</div>
-                    <div class="param-item"><span class="param-name">outline</span> <span class="param-type">string</span> - 大纲内容</div>
+                    <div class="param-item"><span class="param-name">ai_conversation_id</span> <span
+                            class="param-type">uint64</span> - 对话ID</div>
+                    <div class="param-item"><span class="param-name">outline</span> <span
+                            class="param-type">string</span> - 大纲内容</div>
                 </div>
             </div>
             <div class="api-card">
@@ -615,8 +818,10 @@
                     <div class="api-title">保存编辑文档</div>
                     <div class="api-desc">AI写作保存编辑后的文档内容</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">ai_message_id</span> <span class="param-type">uint64</span> - 消息ID</div>
-                    <div class="param-item"><span class="param-name">content</span> <span class="param-type">string</span> - 文档内容</div>
+                    <div class="param-item"><span class="param-name">ai_message_id</span> <span
+                            class="param-type">uint64</span> - 消息ID</div>
+                    <div class="param-item"><span class="param-name">content</span> <span
+                            class="param-type">string</span> - 文档内容</div>
                 </div>
             </div>
         </div>
@@ -647,8 +852,10 @@
                     <div class="api-title">消费积分</div>
                     <div class="api-desc">消费积分下载文件(每次10积分)</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">file_name</span> <span class="param-type">string</span> - 文件名</div>
-                    <div class="param-item"><span class="param-name">file_url</span> <span class="param-type">string</span> - 文件URL</div>
+                    <div class="param-item"><span class="param-name">file_name</span> <span
+                            class="param-type">string</span> - 文件名</div>
+                    <div class="param-item"><span class="param-name">file_url</span> <span
+                            class="param-type">string</span> - 文件URL</div>
                     <div class="params-title" style="margin-top:8px">响应</div>
                     <div class="param-item"><span class="param-name">new_balance</span> - 新余额</div>
                     <div class="param-item"><span class="param-name">points_consumed</span> - 消费积分</div>
@@ -663,8 +870,10 @@
                     <div class="api-title">获取消费记录</div>
                     <div class="api-desc">分页获取用户的积分消费历史记录</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">page</span> <span class="param-type">int</span> - 页码(默认1)</div>
-                    <div class="param-item"><span class="param-name">pageSize</span> <span class="param-type">int</span> - 每页数量(默认10)</div>
+                    <div class="param-item"><span class="param-name">page</span> <span class="param-type">int</span> -
+                        页码(默认1)</div>
+                    <div class="param-item"><span class="param-name">pageSize</span> <span class="param-type">int</span>
+                        - 每页数量(默认10)</div>
                 </div>
             </div>
         </div>
@@ -681,9 +890,12 @@
                     <div class="api-title">记录埋点数据</div>
                     <div class="api-desc">记录用户行为埋点数据</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">api_path</span> <span class="param-type">string</span> - 接口路径</div>
-                    <div class="param-item"><span class="param-name">method</span> <span class="param-type">string</span> - 请求方法</div>
-                    <div class="param-item"><span class="param-name">extra_data</span> <span class="param-type">string</span> - 额外数据(可选)</div>
+                    <div class="param-item"><span class="param-name">api_path</span> <span
+                            class="param-type">string</span> - 接口路径</div>
+                    <div class="param-item"><span class="param-name">method</span> <span
+                            class="param-type">string</span> - 请求方法</div>
+                    <div class="param-item"><span class="param-name">extra_data</span> <span
+                            class="param-type">string</span> - 额外数据(可选)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -695,10 +907,14 @@
                     <div class="api-title">获取埋点记录</div>
                     <div class="api-desc">分页获取埋点记录列表</div>
                     <div class="params-title">请求参数 (Query)</div>
-                    <div class="param-item"><span class="param-name">user_id</span> <span class="param-type">int</span> - 用户ID(可选)</div>
-                    <div class="param-item"><span class="param-name">api_path</span> <span class="param-type">string</span> - 接口路径(可选)</div>
-                    <div class="param-item"><span class="param-name">page</span> <span class="param-type">int</span> - 页码(默认1)</div>
-                    <div class="param-item"><span class="param-name">page_size</span> <span class="param-type">int</span> - 每页数量(默认20)</div>
+                    <div class="param-item"><span class="param-name">user_id</span> <span class="param-type">int</span>
+                        - 用户ID(可选)</div>
+                    <div class="param-item"><span class="param-name">api_path</span> <span
+                            class="param-type">string</span> - 接口路径(可选)</div>
+                    <div class="param-item"><span class="param-name">page</span> <span class="param-type">int</span> -
+                        页码(默认1)</div>
+                    <div class="param-item"><span class="param-name">page_size</span> <span
+                            class="param-type">int</span> - 每页数量(默认20)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -710,9 +926,12 @@
                     <div class="api-title">添加接口映射</div>
                     <div class="api-desc">添加接口路径到名称的映射关系</div>
                     <div class="params-title">请求参数 (Body JSON)</div>
-                    <div class="param-item"><span class="param-name">api_path</span> <span class="param-type">string</span> - 接口路径</div>
-                    <div class="param-item"><span class="param-name">api_name</span> <span class="param-type">string</span> - 接口名称</div>
-                    <div class="param-item"><span class="param-name">api_desc</span> <span class="param-type">string</span> - 接口描述(可选)</div>
+                    <div class="param-item"><span class="param-name">api_path</span> <span
+                            class="param-type">string</span> - 接口路径</div>
+                    <div class="param-item"><span class="param-name">api_name</span> <span
+                            class="param-type">string</span> - 接口名称</div>
+                    <div class="param-item"><span class="param-name">api_desc</span> <span
+                            class="param-type">string</span> - 接口描述(可选)</div>
                 </div>
             </div>
             <div class="api-card">
@@ -736,4 +955,5 @@
         </div>
     </div>
 </body>
-</html>
+
+</html>

+ 2 - 2
docs/ai-qa-api-call-chain.md

@@ -1,4 +1,4 @@
-# AI问答模块 API 调用链路(详细版)
+# AI助手模块 API 调用链路(详细版)
 
 本文档从“前端发起一次 AI 问答”开始,梳理本项目 AI 问答模块涉及的 **入站接口(前端 → shudao-chat-py)** 与 **出站接口(shudao-chat-py → 外部服务)**,并按不同交互场景给出完整调用链路与时序图。
 
@@ -160,7 +160,7 @@ sequenceDiagram
 
 这条链路更像“同步 RPC”:一次请求直接拿到完整回答。
 
-### 5.1 调用链路(AI问答分支)
+### 5.1 调用链路(AI助手分支)
 
 实现入口见 [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183)。
 

+ 4 - 4
docs/superpowers/plans/2026-04-16-desktop-sidebar-navigation.md

@@ -61,7 +61,7 @@ describe('Sidebar', () => {
   it('renders the desktop feature navigation buttons', () => {
     const wrapper = mount(Sidebar)
 
-    expect(wrapper.text()).toContain('AI问答')
+    expect(wrapper.text()).toContain('AI助手')
     expect(wrapper.text()).toContain('隐患提示')
     expect(wrapper.text()).toContain('AI写作')
     expect(wrapper.text()).toContain('安全培训')
@@ -69,7 +69,7 @@ describe('Sidebar', () => {
   })
 
   it.each([
-    ['AI问答', '/chat'],
+    ['AI助手', '/chat'],
     ['隐患提示', '/hazard-detection'],
     ['AI写作', '/ai-writing'],
     ['安全培训', '/safety-hazard'],
@@ -101,7 +101,7 @@ describe('Sidebar', () => {
 
 Run: `npm run test -- src/components/Sidebar.test.js`
 
-Expected: FAIL because the current sidebar only renders AI问答 and 隐患提示, and does not expose `data-testid` attributes for all five route buttons.
+Expected: FAIL because the current sidebar only renders AI助手 and 隐患提示, and does not expose `data-testid` attributes for all five route buttons.
 
 ### Task 2: Data-Driven Sidebar Implementation
 
@@ -115,7 +115,7 @@ Replace the hardcoded menu items with a `navItems` array:
 
 ```js
 const navItems = [
-  { routeName: 'Chat', path: '/chat', label: 'AI问答', icon: chatIcon },
+  { routeName: 'Chat', path: '/chat', label: 'AI助手', icon: chatIcon },
   { routeName: 'HazardDetection', path: '/hazard-detection', label: '隐患提示', icon: hazardIcon },
   { routeName: 'AIWriting', path: '/ai-writing', label: 'AI写作', icon: aiWritingIcon },
   { routeName: 'SafetyHazard', path: '/safety-hazard', label: '安全培训', icon: safetyIcon },

+ 2 - 2
docs/superpowers/specs/2026-04-16-desktop-sidebar-navigation-design.md

@@ -2,7 +2,7 @@
 
 ## Summary
 
-The desktop pages for AI问答, AI写作, 安全培训, 隐患提示, and 考试工坊 should share one left-side navigation sidebar. The AI写作, 安全培训, and 考试工坊 sidebar buttons must mirror the existing mode buttons below the Chat input, not route to the standalone legacy pages.
+The desktop pages for AI助手, AI写作, 安全培训, 隐患提示, and 考试工坊 should share one left-side navigation sidebar. The AI写作, 安全培训, and 考试工坊 sidebar buttons must mirror the existing mode buttons below the Chat input, not route to the standalone legacy pages.
 
 ## Selected Approach
 
@@ -16,7 +16,7 @@ The sidebar will expose these route buttons:
 
 | Label | Behavior | Route fallback |
 | --- | --- | --- |
-| AI问答 | Chat mode `ai-qa` when already on Chat | `/chat` |
+| AI助手 | Chat mode `ai-qa` when already on Chat | `/chat` |
 | 隐患提示 | Route to the independent 隐患提示 page | `/hazard-detection` |
 | AI写作 | Chat mode `ai-writing` when already on Chat | `/chat?mode=ai-writing` |
 | 安全培训 | Chat mode `safety-training` when already on Chat | `/chat?mode=safety-training` |

+ 6 - 0
shudao-chat-py/config/prompt_config.yaml

@@ -8,6 +8,12 @@ prompts:
     description: "用户意图识别prompt(精简版,适配小模型)"
     encoding: "utf-8"
     variables: ["userMessage"]
+
+  module_dispatch:
+    file: "prompts/module_dispatch_template.md"
+    description: "AI 助手顶层模块分发 prompt"
+    encoding: "utf-8"
+    variables: ["userMessage"]
     
   rag_query:
     file: "prompts/RAG.md"

+ 1 - 1
shudao-chat-py/models/chat.py

@@ -9,7 +9,7 @@ class AIConversation(Base):
     id = Column(BigInteger, primary_key=True, autoincrement=True)
     user_id = Column(BigInteger, nullable=False, comment="用户ID")
     content = Column(Text, nullable=False, comment="对话内容")
-    business_type = Column(SmallInteger, nullable=False, default=0, comment="业务类型(0、AI问答,1、安全培训,2、AI写作,3、考试工坊)")
+    business_type = Column(SmallInteger, nullable=False, default=0, comment="业务类型(0、AI助手,1、安全培训,2、AI写作,3、考试工坊)")
     exam_name = Column(String(255), default="", comment="考试名称")
     step = Column(Integer, default=0, comment="步骤")
     cover_image = Column(String(1000), default="", comment="封面图")

+ 4 - 4
shudao-chat-py/models/total.py

@@ -71,12 +71,12 @@ class PolicyFile(Base):
 
 
 class FunctionCard(Base):
-    """AI问答和安全培训功能卡片表"""
+    """AI助手和安全培训功能卡片表"""
     __tablename__ = "function_card"
     
     id = Column(Integer, primary_key=True, autoincrement=True)
     function_title = Column(String(255), nullable=False, comment="功能标题")
-    function_type = Column(SmallInteger, nullable=False, comment="功能类型(0、AI问答,1、安全培训)")
+    function_type = Column(SmallInteger, nullable=False, comment="功能类型(0、AI助手,1、安全培训)")
     function_content = Column(String(255), nullable=False, comment="功能内容")
     function_icon = Column(String(255), default="", comment="图标")
     created_at = Column(BigInteger, comment="创建时间")
@@ -86,12 +86,12 @@ class FunctionCard(Base):
 
 
 class HotQuestion(Base):
-    """AI问答和安全培训底部热点问题表"""
+    """AI助手和安全培训底部热点问题表"""
     __tablename__ = "hot_question"
     
     id = Column(Integer, primary_key=True, autoincrement=True)
     question = Column(String(255), nullable=False, comment="问题")
-    question_type = Column(SmallInteger, nullable=False, comment="问题类型(0、AI问答,1、安全培训)")
+    question_type = Column(SmallInteger, nullable=False, comment="问题类型(0、AI助手,1、安全培训)")
     question_icon = Column(String(255), default="", comment="图标")
     click_count = Column(Integer, default=0, comment="点击量")
     created_at = Column(BigInteger, comment="创建时间")

+ 1 - 1
shudao-chat-py/prompts/JSON.MD

@@ -1,6 +1,6 @@
 # Role
 
-你是名为"蜀安AI助手"的专业AI问答助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
+你是名为"蜀安AI助手"的专业AI助手助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
 
 # Overall Goal
 

+ 1 - 1
shudao-chat-py/prompts/RAG.md

@@ -40,7 +40,7 @@
 		
 		greeting: 问候、寒暄等。如"你好"、"在吗"、"谢谢"。
 		
-		faq: 主要关于围绕"蜀安AI助手"AI问答助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
+		faq: 主要关于围绕"蜀安AI助手"AI助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
 		
 		query_knowledge_base: 除了greeting、faq外,所有用户问题一律归为此类别处理。
 		

+ 1 - 1
shudao-chat-py/prompts/backup/JSON.MD

@@ -1,6 +1,6 @@
 # Role
 
-你是名为"蜀安AI助手"的专业AI问答助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
+你是名为"蜀安AI助手"的专业AI助手助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
 
 # Overall Goal
 

+ 1 - 1
shudao-chat-py/prompts/backup/yitushibiefeiliu.md

@@ -31,7 +31,7 @@
 
 greeting: 问候、寒暄等。如"你好"、"在吗"、"谢谢"。
 
-faq: 主要关于围绕"蜀安AI助手"AI问答助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
+faq: 主要关于围绕"蜀安AI助手"AI助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
 
 query_knowledge_base: 除了greeting、faq外,所有用户问题一律归为此类别处理。
 

+ 1 - 1
shudao-chat-py/prompts/final_answer_template.md

@@ -1,6 +1,6 @@
 # Role
 
-你是名为"蜀安AI助手"的专业AI问答助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
+你是名为"蜀安AI助手"的专业AI助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
 
 # Overall Goal
 

+ 1 - 1
shudao-chat-py/prompts/liushi.md

@@ -1,6 +1,6 @@
 # Role
 
-你是名为"蜀安AI助手"的专业AI问答助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
+你是名为"蜀安AI助手"的专业AI助手,专注于提供路桥隧轨等基建建筑施工技术相关的专业咨询服务。
 
 # Overall Goal
 

+ 15 - 0
shudao-chat-py/prompts/module_dispatch_template.md

@@ -0,0 +1,15 @@
+# Role
+你是意图分发器。
+
+# Rules
+1. ai_writing: 要求"写通知/方案/报告/总结/制度"或"给出一份...要点/方法/流程/指南"等文稿撰写。
+2. safety_training: 要求"培训/PPT/课件/大纲"。
+3. exam_workshop: 要求"试卷/题库/考试/出题"。
+4. ai_qa: 普通问答/法规查询/闲聊/无法归类的。
+
+# Output
+仅输出JSON格式,严禁其他解释:
+{"route_mode": "ai_qa|ai_writing|safety_training|exam_workshop"}
+
+# Input
+{userMessage}

+ 1 - 1
shudao-chat-py/prompts/yitushibie_template.md

@@ -38,7 +38,7 @@
 
 greeting: 问候、寒暄等。如"你好"、"在吗"、"谢谢"。
 
-faq: 主要关于围绕"蜀安AI助手"AI问答助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
+faq: 主要关于围绕"蜀安AI助手"AI助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
 
 query_knowledge_base: 除了greeting、faq外,所有用户问题一律归为此类别处理。
 

+ 1 - 1
shudao-chat-py/prompts/yitushibiefeiliu.md

@@ -31,7 +31,7 @@
 
 greeting: 问候、寒暄等。如"你好"、"在吗"、"谢谢"。
 
-faq: 主要关于围绕"蜀安AI助手"AI问答助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
+faq: 主要关于围绕"蜀安AI助手"AI助手展开的相关问题,比如身份、作用、使用技巧等。"你是谁?"、"你能做什么"。
 
 query_knowledge_base: 除了greeting、faq外,所有用户问题一律归为此类别处理。
 

+ 19 - 4
shudao-chat-py/routers/chat.py

@@ -928,7 +928,7 @@ class SendMessageRequest(BaseModel):
     message: str
     conversation_id: Optional[int] = None
     ai_conversation_id: Optional[int] = None
-    business_type: int = 0  # 0=AI问答, 1=PPT大纲, 2=AI写作, 3=考试工坊
+    business_type: int = 0  # 0=AI助手, 1=PPT大纲, 2=AI写作, 3=考试工坊
     exam_name: str = ""
     ai_message_id: int = 0
 
@@ -942,7 +942,7 @@ async def send_deepseek_message(
     """
     发送消息(非流式)
     支持多种业务类型:
-    - 0: AI问答(意图识别 + RAG)
+    - 0: AI助手(意图识别 + RAG)
     - 1: PPT大纲生成
     - 2: AI写作
     - 3: 考试工坊
@@ -991,7 +991,7 @@ async def send_deepseek_message(
         ai_message_id = 0
 
         if data.business_type == 0:
-            # AI问答:意图识别 + RAG
+            # AI助手:意图识别 + RAG
             try:
                 intent_result = await qwen_service.intent_recognition(message)
                 intent_type = ""
@@ -1051,7 +1051,7 @@ async def send_deepseek_message(
                 error_detail = str(e).strip() if str(
                     e).strip() else f"未知错误({type(e).__name__})"
                 logger.error(
-                    f"[send_deepseek_message] AI问答异常: {type(e).__name__}: {error_detail}")
+                    f"[send_deepseek_message] AI助手异常: {type(e).__name__}: {error_detail}")
                 response_text = f"处理失败: {error_detail}"
 
         elif data.business_type == 1:
@@ -2021,6 +2021,7 @@ class IntentRecognitionRequest(BaseModel):
     message: str
     save_to_db: bool = False
     ai_conversation_id: int = 0
+    scene: str = "default"
 
 
 @router.post("/intent_recognition")
@@ -2035,6 +2036,20 @@ async def intent_recognition(
         return {"statusCode": 401, "msg": "未授权"}
 
     try:
+        if (data.scene or "").strip().lower() == "module_dispatch":
+            dispatch_result = await qwen_service.module_dispatch_recognition(data.message)
+            return {
+                "statusCode": 200,
+                "msg": "success",
+                "data": {
+                    "route_mode": dispatch_result.get("route_mode", "ai-qa"),
+                    "business_type": dispatch_result.get("business_type", 0),
+                    "confidence": dispatch_result.get("confidence", 0.5),
+                    "reason": dispatch_result.get("reason", ""),
+                    "saved_to_db": False,
+                },
+            }
+
         intent_result = await qwen_service.intent_recognition(data.message)
         intent_type = ""
         response_text = ""

+ 167 - 11
shudao-chat-py/routers/exam.py

@@ -36,9 +36,15 @@ class BuildPromptRequest(BaseModel):
     totalScore: int = 0
     questionTypes: list[QuestionTypeItem] = Field(default_factory=list)
     pptContent: str = ""
+    basisContent: str = ""
     requireBasis: bool = False
 
 
+class GenerateTitleRequest(BaseModel):
+    projectType: str = ""
+    sourceContent: str = ""
+
+
 def _get_exam_section(payload: dict, question_type: str) -> Optional[dict]:
     if not isinstance(payload, dict):
         return None
@@ -72,6 +78,73 @@ def _get_section_question_count(section: Optional[dict]) -> int:
     return len(questions)
 
 
+def _is_placeholder_text(value: object) -> bool:
+    if value is None:
+        return True
+    text = str(value).strip()
+    if not text:
+        return True
+    placeholder_patterns = (
+        r"^\.\.\.$",
+        r"^…+$",
+        r"^题干$",
+        r"^题目$",
+        r"^题目内容$",
+        r"^解析$",
+        r"^解析内容$",
+        r"^答案解析$",
+        r"^答题要点$",
+        r"^参考答案$",
+        r"^未设置$",
+    )
+    return any(re.fullmatch(pattern, text) for pattern in placeholder_patterns)
+
+
+def _extract_short_outline_text(question: dict) -> str:
+    outline = question.get("outline") or question.get(
+        "answer_outline") or question.get("答题要点")
+    if isinstance(outline, dict):
+        key_factors = outline.get("keyFactors") or outline.get("key_factors")
+        if isinstance(key_factors, list):
+            return ";".join(str(item).strip() for item in key_factors if str(item).strip())
+        return str(key_factors or "").strip()
+    if isinstance(outline, list):
+        return ";".join(str(item).strip() for item in outline if str(item).strip())
+    if isinstance(outline, str):
+        return outline.strip()
+    return ""
+
+
+def _validate_section_questions(section: Optional[dict], question_type: str) -> tuple[bool, str]:
+    if not isinstance(section, dict):
+        return False, f"{question_type}缺少题型对象"
+    questions = section.get("questions")
+    if not isinstance(questions, list) or not questions:
+        return False, f"{question_type}缺少题目列表"
+
+    for index, question in enumerate(questions, start=1):
+        if not isinstance(question, dict):
+            return False, f"{question_type}第{index}题不是对象"
+
+        text = str(
+            question.get("text")
+            or question.get("question_text")
+            or question.get("question")
+            or question.get("title")
+            or question.get("content")
+            or ""
+        ).strip()
+        if _is_placeholder_text(text):
+            return False, f"{question_type}第{index}题题干是占位内容"
+
+        if question_type == "简答题":
+            outline_text = _extract_short_outline_text(question)
+            if _is_placeholder_text(outline_text):
+                return False, f"{question_type}第{index}题答题要点是占位内容"
+
+    return True, ""
+
+
 def _get_knowledge_search_api_url() -> str:
     aichat_config = getattr(settings, "aichat", None)
     aichat_base_url = getattr(aichat_config, "api_url", "").rstrip("/")
@@ -584,6 +657,47 @@ async def _resolve_exam_title(
     if not source_text:
         return "智能生成试卷"
 
+    model_source = source_text[:6000]
+    title_prompt = (
+        "你是考试命名助手。请基于用户提供的出题依据、培训材料或 PPT 提取内容,"
+        "提炼核心主题并生成一个简短、正式、适合试卷顶部展示的标题。\n"
+        "要求:\n"
+        "1. 只输出 JSON,不要输出 markdown 代码块或解释文字。\n"
+        "2. 标题必须简短,控制在 6-15 个汉字内,避免口语化。\n"
+        "3. 优先提炼正文主题,不要机械拼接“考试/考核/题库/试卷”等泛词,除非确有必要。\n"
+        "4. 如果内容里出现公司或组织名称,要么完全省略,要么保留完整全称,禁止擅自简写。\n"
+        "5. 如果同时包含用户输入依据和 PPT 内容,要综合两者,不要只取其一。\n"
+        f"项目类型:{project_type or '未指定'}\n"
+        f"出题依据内容:\n{model_source}\n\n"
+        '请严格返回:{"title":"简短试卷名称"}'
+    )
+
+    try:
+        model_response = await qwen_service.chat(
+            [{"role": "user", "content": title_prompt}],
+            disable_reasoning=True,
+        )
+        json_match = re.search(r"\{.*\}", model_response.strip(), re.DOTALL)
+        if json_match:
+            parsed = json.loads(json_match.group())
+            model_title = _refine_exam_title_candidate(
+                str(parsed.get("title") or "").strip()
+            )
+            if 2 <= len(model_title) <= 15:
+                logger.info(
+                    f"[exam/title] 模型生成试卷标题成功: source_len={len(source_text)}, title={model_title}"
+                )
+                return model_title
+        else:
+            plain_title = _refine_exam_title_candidate(model_response.strip())
+            if 2 <= len(plain_title) <= 15:
+                logger.info(
+                    f"[exam/title] 模型纯文本试卷标题成功: source_len={len(source_text)}, title={plain_title}"
+                )
+                return plain_title
+    except Exception as e:
+        logger.warning(f"[exam/title] 模型生成试卷标题失败,回退规则提取: {repr(e)}")
+
     resolved = _extract_exam_title_from_source(source_text, project_type)
     logger.info(
         f"[exam/title] 基于用户输入出题依据提取试卷标题: source_len={len(source_text)}, title={resolved}"
@@ -591,6 +705,28 @@ async def _resolve_exam_title(
     return resolved
 
 
+@router.post("/exam/generate_title")
+async def generate_exam_title(
+    request: Request,
+    data: GenerateTitleRequest,
+):
+    user = request.state.user
+    if not user:
+        return {"statusCode": 401, "msg": "未授权"}
+
+    resolved_title = await _resolve_exam_title(
+        user_title="",
+        title_source=data.sourceContent,
+        project_type=data.projectType,
+    )
+
+    return {
+        "statusCode": 200,
+        "msg": "success",
+        "data": {"title": resolved_title}
+    }
+
+
 async def _fetch_knowledge_docs(query_str: str, log_prefix: str) -> Optional[list[str]]:
     import httpx
 
@@ -688,7 +824,7 @@ async def build_exam_prompt(
     question_schema = "\n".join(
         question_schema_lines) if question_schema_lines else "- 未提供有效题型"
 
-    ppt_content = (data.pptContent or "").strip()
+    ppt_content = _get_basis_content(data)
     retrieval_query = _build_knowledge_search_query(
         ppt_content, data.projectType)
     combined_source_mode = "用户输入依据:" in ppt_content and "PPT提取内容:" in ppt_content
@@ -710,14 +846,14 @@ async def build_exam_prompt(
             # 覆盖原来的 ppt_content,改为:用户关键词 + 检索到的真实知识库内容
             ppt_content = (
                 f"用户指定的主题/关键词:{query_str}\n\n"
-                f"原始出题依据:\n{text[:2000] if (text := (data.pptContent or '').strip()) else '无'}\n\n"
+                f"原始出题依据:\n{text[:2000] if (text := _get_basis_content(data)) else '无'}\n\n"
                 "以下是从知识库中检索到的相关原文片段,请严格基于这些原文片段出题:\n\n"
                 f"{retrieved_text}"
             )
         elif retrieved_docs == []:
             logger.warning(
                 f"[exam/build_prompt] 知识库中未检索到与 '{query_str}' 相关的文档块")
-            ppt_content = f"(注:未能在知识库中检索到相关文档,请仅根据以下关键词及原始依据出题:{query_str}\n\n{data.pptContent or ''})"
+            ppt_content = f"(注:未能在知识库中检索到相关文档,请仅根据以下关键词及原始依据出题:{query_str}\n\n{_get_basis_content(data)})"
 
     if ppt_content:
         max_chars = 12000
@@ -730,7 +866,7 @@ async def build_exam_prompt(
                 + ppt_content[-tail_len:]
             )
             logger.info(
-                f"[exam/build_prompt] pptContent truncated: original_len={len(data.pptContent)} kept_len={len(ppt_content)}"
+                f"[exam/build_prompt] basis content truncated: original_len={len(_get_basis_content(data))} kept_len={len(ppt_content)}"
             )
 
     basis_field = ', "basis": "<文件名:...;章节条款:...;正文:...>"' if data.requireBasis else ''
@@ -810,10 +946,15 @@ class GenerateStreamRequest(BaseModel):
     totalScore: int = 0
     questionTypes: list[QuestionTypeItem] = Field(default_factory=list)
     pptContent: str = ""
+    basisContent: str = ""
     requireBasis: bool = False
     ai_conversation_id: Optional[int] = 0
 
 
+def _get_basis_content(data: BuildPromptRequest | GenerateStreamRequest) -> str:
+    return ((getattr(data, "basisContent", "") or getattr(data, "pptContent", "")) or "").strip()
+
+
 @router.post("/exam/generate_stream")
 async def generate_exam_stream(
     request: Request,
@@ -830,9 +971,10 @@ async def generate_exam_stream(
         db = None
         try:
             yield f"data: {json.dumps({'type': 'progress', 'message': '正在检索知识库...', 'percent': 5}, ensure_ascii=False)}\n\n"
+            yield f"data: {json.dumps({'type': 'progress', 'message': '正在分析试卷标题...', 'percent': 8}, ensure_ascii=False)}\n\n"
 
             # 2. 获取上下文
-            raw_basis_content = (data.pptContent or "").strip()
+            raw_basis_content = _get_basis_content(data)
             ppt_content = raw_basis_content
             retrieval_query = _build_knowledge_search_query(
                 raw_basis_content, data.projectType)
@@ -1010,12 +1152,19 @@ async def generate_exam_stream(
                         try:
                             parsed, actual_count = _parse_exam_section_payload(
                                 qwen_response, name)
-                            if actual_count == count:
+                            section = _get_exam_section(parsed, name)
+                            is_valid_content, invalid_reason = _validate_section_questions(
+                                section, name)
+                            if actual_count == count and is_valid_content:
                                 last_error = None
                                 break
 
-                            last_error = ValueError(
-                                f"{name}返回题量不完整,期望{count}道,实际{actual_count}道")
+                            if actual_count != count:
+                                last_error = ValueError(
+                                    f"{name}返回题量不完整,期望{count}道,实际{actual_count}道")
+                            else:
+                                last_error = ValueError(
+                                    invalid_reason or f"{name}存在占位内容")
                             logger.warning(
                                 f"[exam/generate_stream] {last_error}; attempt={attempt + 1}/2")
                         except Exception as inner_error:
@@ -1029,11 +1178,18 @@ async def generate_exam_stream(
                             )
                             if repaired_payload is not None:
                                 parsed, actual_count = repaired_payload
-                                if actual_count == count:
+                                section = _get_exam_section(parsed, name)
+                                is_valid_content, invalid_reason = _validate_section_questions(
+                                    section, name)
+                                if actual_count == count and is_valid_content:
                                     last_error = None
                                     break
-                                last_error = ValueError(
-                                    f"{name}轻量修复后题量仍不完整,期望{count}道,实际{actual_count}道")
+                                if actual_count != count:
+                                    last_error = ValueError(
+                                        f"{name}轻量修复后题量仍不完整,期望{count}道,实际{actual_count}道")
+                                else:
+                                    last_error = ValueError(
+                                        invalid_reason or f"{name}轻量修复后仍存在占位内容")
                                 logger.warning(
                                     f"[exam/generate_stream] {last_error}; attempt={attempt + 1}/2")
                                 continue

+ 273 - 52
shudao-chat-py/services/qwen_service.py

@@ -3,8 +3,9 @@ Qwen AI 服务
 """
 import httpx
 import json
+import re
 import time
-from typing import AsyncGenerator
+from typing import Any, AsyncGenerator, Optional
 from utils.config import settings
 from utils.logger import logger
 from utils.prompt_loader import load_prompt
@@ -17,15 +18,17 @@ class QwenService:
         base_url = settings.qwen3.api_url.rstrip('/')
         self.api_url = f"{base_url}/v1/chat/completions"
         self.model = settings.qwen3.model
-        
+
         # 意图识别使用专门的配置
         intent_base_url = settings.intent.api_url.rstrip('/')
         self.intent_api_url = f"{intent_base_url}/v1/chat/completions"
         self.intent_model = settings.intent.model
 
         self._timeout = httpx.Timeout(120.0, connect=10.0)
-        self._limits = httpx.Limits(max_connections=50, max_keepalive_connections=20)
-        self._client = httpx.AsyncClient(timeout=self._timeout, limits=self._limits)
+        self._limits = httpx.Limits(
+            max_connections=50, max_keepalive_connections=20)
+        self._client = httpx.AsyncClient(
+            timeout=self._timeout, limits=self._limits)
 
     async def aclose(self) -> None:
         await self._client.aclose()
@@ -35,33 +38,111 @@ class QwenService:
 
     async def _fallback_deepseek(self, messages: list) -> str:
         try:
-            logger.warning("[Qwen API] Falling back to DeepSeek due to upstream error")
+            logger.warning(
+                "[Qwen API] Falling back to DeepSeek due to upstream error")
             return await deepseek_service.chat(messages)
         except Exception as e:
             error_msg = str(e).strip() if str(e).strip() else type(e).__name__
-            logger.error(f"[Qwen API] DeepSeek fallback failed: {type(e).__name__}: {error_msg}")
-            raise RuntimeError(f"AI服务暂时不可用,主模型和备用模型均无法响应({type(e).__name__}),请稍后重试") from e
-    
+            logger.error(
+                f"[Qwen API] DeepSeek fallback failed: {type(e).__name__}: {error_msg}")
+            raise RuntimeError(
+                f"AI服务暂时不可用,主模型和备用模型均无法响应({type(e).__name__}),请稍后重试") from e
+
+    def _extract_first_json_object(self, response_text: str) -> Optional[dict[str, Any]]:
+        """从模型文本中提取首个合法 JSON 对象,避免贪婪正则误匹配多段内容。"""
+        if not response_text:
+            return None
+
+        cleaned = re.sub(r"```(?:json)?\s*", "", response_text).strip()
+        start_index = cleaned.find("{")
+        if start_index < 0:
+            return None
+
+        depth = 0
+        in_string = False
+        escape = False
+
+        for index in range(start_index, len(cleaned)):
+            char = cleaned[index]
+
+            if in_string:
+                if escape:
+                    escape = False
+                elif char == "\\":
+                    escape = True
+                elif char == '"':
+                    in_string = False
+                continue
+
+            if char == '"':
+                in_string = True
+                continue
+
+            if char == "{":
+                depth += 1
+            elif char == "}":
+                depth -= 1
+                if depth == 0:
+                    candidate = cleaned[start_index:index + 1]
+                    try:
+                        parsed = json.loads(candidate)
+                    except json.JSONDecodeError:
+                        return None
+                    return parsed if isinstance(parsed, dict) else None
+
+        return None
+
+    def _normalize_route_mode(self, raw_route_mode: object) -> str:
+        route_mode_mapping = {
+            "ai-qa": "ai-qa",
+            "ai_qa": "ai-qa",
+            "ai_qa_module": "ai-qa",
+            "qa": "ai-qa",
+            "general_chat": "ai-qa",
+            "ai-writing": "ai-writing",
+            "ai_writing": "ai-writing",
+            "writing": "ai-writing",
+            "document_writing": "ai-writing",
+            "safety-training": "safety-training",
+            "safety_training": "safety-training",
+            "training": "safety-training",
+            "ppt_outline": "safety-training",
+            "exam-workshop": "exam-workshop",
+            "exam_workshop": "exam-workshop",
+            "exam": "exam-workshop",
+            "question_bank": "exam-workshop",
+        }
+        normalized_value = str(raw_route_mode or "ai-qa").strip().lower()
+        return route_mode_mapping.get(normalized_value, "ai-qa")
+
+    def _route_mode_to_business_type(self, route_mode: str) -> int:
+        return {
+            "ai-qa": 0,
+            "safety-training": 1,
+            "ai-writing": 2,
+            "exam-workshop": 3,
+        }.get(route_mode, 0)
+
     async def extract_keywords(self, question: str) -> str:
         """从问题中提炼搜索关键词"""
         # 使用prompt加载器加载关键词提取prompt(如果配置了的话)
         # 这里暂时保留原有逻辑,可以后续添加到prompt配置中
         keyword_prompt = """你是一个关键词提取助手。请从用户的问题中提炼出最核心的搜索关键词。
-要求:
-1. 提取2-5个最关键的词语
-2. 去除语气词、助词等无意义词汇
-3. 保留专业术语和核心概念
-4. 以空格分隔多个关键词
+        要求:
+        1. 提取2-5个最关键的词语
+        2. 去除语气词、助词等无意义词汇
+        3. 保留专业术语和核心概念
+        4. 以空格分隔多个关键词
 
-直接返回关键词,不要其他说明。
+        直接返回关键词,不要其他说明。
+
+        用户问题:"""
 
-用户问题:"""
-        
         messages = [
             {"role": "system", "content": keyword_prompt},
             {"role": "user", "content": question}
         ]
-        
+
         try:
             keywords = await self.chat(messages)
             return keywords.strip()
@@ -69,51 +150,176 @@ class QwenService:
             logger.error(f"关键词提取失败: {e}")
             # 失败时返回原问题
             return question
-    
+
     async def intent_recognition(self, message: str) -> dict:
         """意图识别"""
         # 使用prompt加载器加载意图识别prompt
         intent_prompt = load_prompt("intent_recognition", userMessage=message)
-        
+
         messages = [
             {"role": "user", "content": intent_prompt}
         ]
-        
+
         try:
             # 使用专门的意图识别API和模型
             response = await self.chat(messages, model=self.intent_model, api_url=self.intent_api_url)
             logger.info(f"意图识别原始响应: {response[:500]}")
-            # 尝试解析JSON - 使用支持多行和嵌套的正则
-            import re
-            # 先去除 markdown 代码块标记
-            cleaned = re.sub(r'```(?:json)?\s*', '', response)
-            cleaned = cleaned.strip()
-            # 匹配最外层的 { ... }(支持多行和嵌套)
-            json_match = re.search(r'\{.*\}', cleaned, re.DOTALL)
-            if json_match:
-                result = json.loads(json_match.group())
+            result = self._extract_first_json_object(response)
+            if result:
                 # 兼容模板输出的 "intent" 和 "intent_type" 两种字段名
-                intent_type = (result.get("intent_type") or result.get("intent") or "").lower()
+                intent_type = (result.get("intent_type")
+                               or result.get("intent") or "").lower()
                 # 统一设置 intent_type 字段,确保下游一致
                 result["intent_type"] = intent_type
-                
+
                 # 优先使用模型返回的 direct_answer,否则使用预设回复
                 direct_answer = result.get("direct_answer", "")
-                
+
                 if intent_type in ("greeting", "问候"):
                     result["response"] = direct_answer if direct_answer else "您好!我是蜀道集团智能助手,很高兴为您服务。"
                 elif intent_type in ("faq", "常见问题"):
                     result["response"] = direct_answer if direct_answer else "我可以帮您解答常见问题,请告诉我您想了解什么。"
                 else:
                     result["response"] = direct_answer or ""
-                
+
                 return result
             logger.warning(f"意图识别JSON解析失败,原始响应: {response[:300]}")
             return {"intent_type": "general_chat", "confidence": 0.5, "reason": "无法解析JSON", "response": ""}
         except Exception as e:
             logger.error(f"意图识别失败: {e}")
             return {"intent_type": "general_chat", "confidence": 0.5, "reason": str(e), "response": ""}
-    
+
+    async def module_dispatch_recognition(self, message: str) -> dict:
+        """顶层模块分发识别"""
+        def build_dispatch_result(route_mode: str, confidence: float, reason: str) -> dict:
+            normalized_route_mode = self._normalize_route_mode(route_mode)
+            return {
+                "route_mode": normalized_route_mode,
+                "business_type": self._route_mode_to_business_type(normalized_route_mode),
+                "confidence": confidence,
+                "reason": reason,
+            }
+
+        def explicit_rule_route(user_message: str) -> Optional[dict]:
+            normalized_message = (user_message or "").strip().lower()
+            if not normalized_message:
+                return None
+
+            exam_keywords = (
+                "试卷", "题库", "题目", "考题", "考试", "考核", "出题", "组卷", "练习题"
+            )
+            strong_training_keywords = (
+                "培训课件", "培训大纲", "培训讲稿", "培训计划", "培训材料", "培训资料", "培训ppt"
+            )
+            training_keywords = (
+                "课件", "讲稿", "大纲", "ppt",
+            )
+            writing_action_keywords = (
+                "写", "写个", "写一份", "写一个", "起草", "草拟", "拟一份", "拟写", "撰写", "生成", "润色", "改写", "给我一份", "帮我出一份", "整理一份", "拟定", "编写",
+            )
+            writing_document_keywords = (
+                "通知", "方案", "报告", "制度", "纪要", "函", "总结", "公文", "申请", "发言稿", "倡议书", "要点", "方法", "流程", "预案", "指南", "手册", "细则",
+            )
+
+            if any(keyword in normalized_message for keyword in exam_keywords):
+                return build_dispatch_result("exam-workshop", 0.97, "显式命中考试工坊关键词")
+
+            if any(keyword in normalized_message for keyword in strong_training_keywords):
+                return build_dispatch_result("safety-training", 0.96, "显式要求生成核心培训物料")
+
+            has_training_keyword = any(
+                keyword in normalized_message for keyword in training_keywords) or "培训" in normalized_message
+            has_writing_action = any(
+                keyword in normalized_message for keyword in writing_action_keywords)
+            has_writing_document = any(
+                keyword in normalized_message for keyword in writing_document_keywords)
+
+            if has_training_keyword and has_writing_document:
+                return build_dispatch_result("ai-writing", 0.95, "培训场景下显式要求撰写文稿,优先归入AI写作")
+
+            if has_training_keyword:
+                return build_dispatch_result("safety-training", 0.93, "显式命中安全培训关键词")
+
+            if has_writing_document and has_writing_action:
+                return build_dispatch_result("ai-writing", 0.98, "显式要求撰写正式文稿,优先归入AI写作")
+
+            return None
+
+        def keyword_fallback_route(user_message: str) -> dict:
+            explicit_route = explicit_rule_route(user_message)
+            if explicit_route:
+                return explicit_route
+
+            normalized_message = (user_message or "").strip().lower()
+            if not normalized_message:
+                return build_dispatch_result("ai-qa", 0.3, "空消息回退到AI助手")
+
+            exam_keywords = (
+                "试卷", "题库", "题目", "考题", "考试", "考核", "出题", "组卷", "练习题"
+            )
+            training_keywords = (
+                "培训课件", "培训大纲", "培训讲稿", "培训计划", "培训材料", "培训ppt", "课件", "讲稿", "大纲", "ppt"
+            )
+            writing_keywords = (
+                "通知", "方案", "报告", "制度", "纪要", "函", "总结", "公文", "写一份", "写个", "起草", "润色", "改写"
+            )
+
+            if any(keyword in normalized_message for keyword in exam_keywords):
+                return build_dispatch_result("exam-workshop", 0.85, "关键词规则命中考试工坊")
+
+            if any(keyword in normalized_message for keyword in training_keywords):
+                return build_dispatch_result("safety-training", 0.8, "关键词规则命中安全培训")
+
+            if any(keyword in normalized_message for keyword in writing_keywords):
+                return build_dispatch_result("ai-writing", 0.8, "关键词规则命中AI写作")
+
+            if "培训" in normalized_message and ("通知" in normalized_message or "方案" in normalized_message):
+                return build_dispatch_result("ai-writing", 0.78, "培训类文稿回退到AI写作")
+
+            if "培训" in normalized_message:
+                return build_dispatch_result("safety-training", 0.72, "培训关键词回退到安全培训")
+
+            return build_dispatch_result("ai-qa", 0.5, "未命中规则,回退到AI助手")
+
+        explicit_route = explicit_rule_route(message)
+        if explicit_route:
+            logger.info(
+                f"模块分发快速命中显式规则: route={explicit_route['route_mode']}, "
+                f"message={str(message or '')[:120]}"
+            )
+            return explicit_route
+
+        dispatch_prompt = load_prompt("module_dispatch", userMessage=message)
+        messages = [
+            {"role": "user", "content": dispatch_prompt}
+        ]
+
+        try:
+            response = await self.chat(messages, model=self.intent_model, api_url=self.intent_api_url)
+            logger.info(f"模块分发原始响应: {response[:500]}")
+
+            result = self._extract_first_json_object(response)
+            if not result:
+                logger.warning(f"模块分发JSON解析失败,原始响应: {response[:300]}")
+                return keyword_fallback_route(message)
+
+            normalized_route_mode = self._normalize_route_mode(
+                result.get("route_mode")
+                or result.get("target_module")
+                or result.get("intent_type")
+                or result.get("intent")
+                or "ai_qa"
+            )
+
+            return build_dispatch_result(
+                normalized_route_mode,
+                float(result.get("confidence", 0.5) or 0.5),
+                result.get("reason", "")
+            )
+        except Exception as e:
+            logger.error(f"模块分发识别失败: {e}")
+            return keyword_fallback_route(message)
+
     async def chat(
         self,
         messages: list,
@@ -138,34 +344,41 @@ class QwenService:
             "messages": final_messages,
             "stream": False  # 明确指定非流式
         }
-        
+
+        if data["model"] == self.intent_model:
+            # 意图识别是确定性分类,降低随机性并严格限制生成长度以加速
+            data["temperature"] = 0.1
+            data["max_tokens"] = 30
+
         # 使用指定的API URL,默认使用qwen3的URL
         target_url = api_url or self.api_url
         normalized_target = target_url.rstrip("/")
         is_qwen3_target = normalized_target == self.api_url.rstrip("/")
-        
+
         start_at = time.monotonic()
         logger.info(
             f"[Qwen API] 请求: url={target_url} model={data['model']} "
             f"messages={len(final_messages)} disable_reasoning={disable_reasoning}"
         )
-        
+
         try:
             # 准备请求头
             headers = {
                 "Content-Type": "application/json"
             }
-            
+
             # 如果配置中有 token,添加到请求头(兼容需要认证的场景)
             if hasattr(settings, 'intent') and hasattr(settings.intent, 'token') and normalized_target == self.intent_api_url.rstrip("/"):
                 if settings.intent.token:
                     headers["Authorization"] = f"Bearer {settings.intent.token}"
-                    logger.info("[Qwen API] 已添加 Intent API Authorization header")
+                    logger.info(
+                        "[Qwen API] 已添加 Intent API Authorization header")
             elif hasattr(settings, 'qwen3') and hasattr(settings.qwen3, 'token') and normalized_target == self.api_url.rstrip("/"):
                 if settings.qwen3.token:
                     headers["Authorization"] = f"Bearer {settings.qwen3.token}"
-                    logger.info("[Qwen API] 已添加 Qwen3 API Authorization header")
-            
+                    logger.info(
+                        "[Qwen API] 已添加 Qwen3 API Authorization header")
+
             response = await self._client.post(
                 target_url,
                 json=data,
@@ -173,9 +386,11 @@ class QwenService:
             )
 
             elapsed_ms = int((time.monotonic() - start_at) * 1000)
-            logger.info(f"[Qwen API] 响应: status={response.status_code} elapsed_ms={elapsed_ms}")
+            logger.info(
+                f"[Qwen API] 响应: status={response.status_code} elapsed_ms={elapsed_ms}")
             logger.debug(f"[Qwen API] 响应头: {dict(response.headers)}")
-            logger.debug(f"[Qwen API] 响应预览: {(response.text[:500] if response.text else '(空响应)')}")
+            logger.debug(
+                f"[Qwen API] 响应预览: {(response.text[:500] if response.text else '(空响应)')}")
 
             response.raise_for_status()
 
@@ -192,7 +407,8 @@ class QwenService:
                         if data_str and data_str != "[DONE]":
                             try:
                                 data_json = json.loads(data_str)
-                                delta_content = data_json.get('choices', [{}])[0].get('delta', {}).get('content', '')
+                                delta_content = data_json.get('choices', [{}])[0].get(
+                                    'delta', {}).get('content', '')
                                 if delta_content:
                                     content_parts.append(delta_content)
                             except json.JSONDecodeError:
@@ -203,28 +419,32 @@ class QwenService:
 
             try:
                 result = response.json()
-                content = result.get('response', result.get('choices', [{}])[0].get('message', {}).get('content', ''))
+                content = result.get('response', result.get('choices', [{}])[
+                                     0].get('message', {}).get('content', ''))
                 logger.info(f"[Qwen API] JSON 解析成功,内容长度: {len(content)}")
                 return content
             except json.JSONDecodeError as je:
                 logger.error(f"[Qwen API] 响应不是有效的 JSON: {response.text[:200]}")
                 raise ValueError(f"无效的 JSON 响应: {str(je)}")
-                
+
         except httpx.HTTPStatusError as e:
-            logger.error(f"[Qwen API] HTTP 错误 - 状态码: {e.response.status_code}, URL: {target_url}")
+            logger.error(
+                f"[Qwen API] HTTP 错误 - 状态码: {e.response.status_code}, URL: {target_url}")
             logger.error(f"[Qwen API] HTTP 错误响应: {e.response.text[:500]}")
             if is_qwen3_target and self._should_fallback(e.response.status_code):
                 return await self._fallback_deepseek(final_messages)
             raise
         except httpx.RequestError as e:
-            logger.error(f"[Qwen API] 请求错误 - URL: {target_url}, 错误: {type(e).__name__}: {str(e)}")
+            logger.error(
+                f"[Qwen API] 请求错误 - URL: {target_url}, 错误: {type(e).__name__}: {str(e)}")
             if is_qwen3_target:
                 return await self._fallback_deepseek(final_messages)
             raise
         except Exception as e:
-            logger.error(f"[Qwen API] 未知错误 - URL: {target_url}, 模型: {data['model']}, 错误: {type(e).__name__}: {str(e)}")
+            logger.error(
+                f"[Qwen API] 未知错误 - URL: {target_url}, 模型: {data['model']}, 错误: {type(e).__name__}: {str(e)}")
             raise
-    
+
     async def stream_chat(self, messages: list) -> AsyncGenerator[str, None]:
         """流式聊天"""
         data = {
@@ -232,7 +452,7 @@ class QwenService:
             "messages": messages,
             "stream": True
         }
-        
+
         try:
             async with self._client.stream(
                 "POST",
@@ -249,7 +469,8 @@ class QwenService:
                             data_json = json.loads(data_str)
                             choices = data_json.get('choices', [])
                             if choices:
-                                content = choices[0].get('delta', {}).get('content', '') or choices[0].get('message', {}).get('content', '')
+                                content = choices[0].get('delta', {}).get(
+                                    'content', '') or choices[0].get('message', {}).get('content', '')
                             else:
                                 content = data_json.get('content', '')
                             if content:

+ 1 - 1
shudao-chat-py/tests/test_chat.md

@@ -41,7 +41,7 @@
 |------|------|------|--------|------|
 | message | string | 是 | - | 用户消息内容 |
 | conversation_id | int | 否 | null | 对话 ID,为空则新建对话 |
-| business_type | int | 否 | 0 | 业务类型:0=AI问答, 1=PPT大纲, 2=AI写作, 3=考试工坊 |
+| business_type | int | 否 | 0 | 业务类型:0=AI助手, 1=PPT大纲, 2=AI写作, 3=考试工坊 |
 | exam_name | string | 否 | "" | 考试名称(business_type=3 时有效) |
 | ai_message_id | int | 否 | 0 | AI 消息 ID |
 

+ 2 - 2
shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-50-25-677Z.yml

@@ -3,8 +3,8 @@
     - img "logo" [ref=e101] [cursor=pointer]
     - generic [ref=e102]:
       - generic [ref=e103] [cursor=pointer]:
-        - img "AI问答" [ref=e104]
-        - generic [ref=e105]: AI问答
+        - img "AI助手" [ref=e104]
+        - generic [ref=e105]: AI助手
       - generic [ref=e106] [cursor=pointer]:
         - img "隐患提示" [ref=e107]
         - generic [ref=e108]: 隐患提示

+ 2 - 2
shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-52-11-624Z.yml

@@ -3,8 +3,8 @@
     - img "logo" [ref=e101] [cursor=pointer]
     - generic [ref=e102]:
       - generic [ref=e103] [cursor=pointer]:
-        - img "AI问答" [ref=e104]
-        - generic [ref=e105]: AI问答
+        - img "AI助手" [ref=e104]
+        - generic [ref=e105]: AI助手
       - generic [ref=e106] [cursor=pointer]:
         - img "隐患提示" [ref=e107]
         - generic [ref=e108]: 隐患提示

+ 2 - 2
shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-56-55-892Z.yml

@@ -3,8 +3,8 @@
     - img "logo" [ref=e7] [cursor=pointer]
     - generic [ref=e8]:
       - generic [ref=e9] [cursor=pointer]:
-        - img "AI问答" [ref=e10]
-        - generic [ref=e11]: AI问答
+        - img "AI助手" [ref=e10]
+        - generic [ref=e11]: AI助手
       - generic [ref=e12] [cursor=pointer]:
         - img "隐患提示" [ref=e13]
         - generic [ref=e14]: 隐患提示

+ 2 - 2
shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-57-58-344Z.yml

@@ -3,8 +3,8 @@
     - img "logo" [ref=e7] [cursor=pointer]
     - generic [ref=e8]:
       - generic [ref=e9] [cursor=pointer]:
-        - img "AI问答" [ref=e10]
-        - generic [ref=e11]: AI问答
+        - img "AI助手" [ref=e10]
+        - generic [ref=e11]: AI助手
       - generic [ref=e12] [cursor=pointer]:
         - img "隐患提示" [ref=e13]
         - generic [ref=e14]: 隐患提示

+ 2 - 2
shudao-vue-frontend/.playwright-cli/page-2026-04-16T05-58-47-567Z.yml

@@ -3,8 +3,8 @@
     - img "logo" [ref=e7] [cursor=pointer]
     - generic [ref=e8]:
       - generic [ref=e9] [cursor=pointer]:
-        - img "AI问答" [ref=e10]
-        - generic [ref=e11]: AI问答
+        - img "AI助手" [ref=e10]
+        - generic [ref=e11]: AI助手
       - generic [ref=e12] [cursor=pointer]:
         - img "隐患提示" [ref=e13]
         - generic [ref=e14]: 隐患提示

+ 2 - 2
shudao-vue-frontend/.playwright-cli/page-2026-04-16T06-05-11-107Z.yml

@@ -3,8 +3,8 @@
     - img "logo" [ref=e7] [cursor=pointer]
     - generic [ref=e8]:
       - generic [ref=e9] [cursor=pointer]:
-        - img "AI问答" [ref=e10]
-        - generic [ref=e11]: AI问答
+        - img "AI助手" [ref=e10]
+        - generic [ref=e11]: AI助手
       - generic [ref=e12] [cursor=pointer]:
         - img "隐患提示" [ref=e13]
         - generic [ref=e14]: 隐患提示

+ 174 - 0
shudao-vue-frontend/src/components/DispatchFeedbackModal.vue

@@ -0,0 +1,174 @@
+<template>
+  <el-dialog
+    :model-value="visible"
+    width="420px"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    :show-close="false"
+    center
+    class="dispatch-feedback-dialog"
+  >
+    <div class="dispatch-feedback-dialog-content">
+      <div class="dispatch-feedback-modal-loader">
+        <span class="dispatch-feedback-modal-ring ring-outer"></span>
+        <span class="dispatch-feedback-modal-ring ring-middle"></span>
+        <span class="dispatch-feedback-modal-core"></span>
+      </div>
+      <div class="dispatch-feedback-modal-body">
+        <div class="dispatch-feedback-modal-badge">AI助手正在分析</div>
+        <div class="dispatch-feedback-modal-dots">
+          <span></span>
+          <span></span>
+          <span></span>
+        </div>
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+defineProps({
+  visible: {
+    type: Boolean,
+    default: false
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.dispatch-feedback-dialog-content {
+  text-align: center;
+  padding: 10px 0 20px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  gap: 30px;
+}
+
+.dispatch-feedback-modal-loader {
+  position: relative;
+  width: 92px;
+  height: 92px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.dispatch-feedback-modal-ring {
+  position: absolute;
+  border-radius: 50%;
+  border: 2px solid transparent;
+}
+
+.dispatch-feedback-modal-ring.ring-outer {
+  inset: 0;
+  border-top-color: #60a5fa;
+  border-right-color: rgba(96, 165, 250, 0.25);
+  animation: dispatch-feedback-modal-spin 1.8s linear infinite;
+}
+
+.dispatch-feedback-modal-ring.ring-middle {
+  inset: 13px;
+  border-bottom-color: #818cf8;
+  border-left-color: rgba(129, 140, 248, 0.22);
+  animation: dispatch-feedback-modal-spin-reverse 1.3s linear infinite;
+}
+
+.dispatch-feedback-modal-core {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  background: radial-gradient(circle at 35% 35%, #ffffff, #60a5fa 68%, #2563eb 100%);
+  box-shadow: 0 0 22px rgba(96, 165, 250, 0.55);
+  animation: dispatch-feedback-modal-pulse 1.4s ease-in-out infinite;
+}
+
+.dispatch-feedback-modal-body {
+  position: relative;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+  text-align: center;
+}
+
+.dispatch-feedback-modal-badge {
+  display: inline-flex;
+  align-items: center;
+  padding: 8px 16px;
+  border-radius: 999px;
+  font-size: 18px;
+  font-weight: 600;
+  line-height: 1;
+  color: #2563eb;
+  background: rgba(96, 165, 250, 0.16);
+  border: 1px solid rgba(147, 197, 253, 0.24);
+}
+
+.dispatch-feedback-modal-dots {
+  display: inline-flex;
+  align-items: center;
+  gap: 10px;
+  margin-top: 2px;
+}
+
+.dispatch-feedback-modal-dots span {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  background: #60a5fa;
+  animation: dispatch-feedback-modal-bounce 1.1s ease-in-out infinite;
+}
+
+.dispatch-feedback-modal-dots span:nth-child(2) {
+  animation-delay: 0.15s;
+}
+
+.dispatch-feedback-modal-dots span:nth-child(3) {
+  animation-delay: 0.3s;
+}
+
+
+
+@keyframes dispatch-feedback-modal-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@keyframes dispatch-feedback-modal-spin-reverse {
+  from {
+    transform: rotate(360deg);
+  }
+  to {
+    transform: rotate(0deg);
+  }
+}
+
+@keyframes dispatch-feedback-modal-pulse {
+  0%, 100% {
+    transform: scale(0.9);
+    opacity: 0.85;
+  }
+  50% {
+    transform: scale(1.08);
+    opacity: 1;
+  }
+}
+
+@keyframes dispatch-feedback-modal-bounce {
+  0%, 80%, 100% {
+    transform: translateY(0);
+    opacity: 0.45;
+  }
+  40% {
+    transform: translateY(-6px);
+    opacity: 1;
+  }
+}
+</style>

+ 1 - 1
shudao-vue-frontend/src/components/MobileHistoryDrawer.vue

@@ -102,7 +102,7 @@ const emit = defineEmits(['close', 'createNewTask', 'handleHistoryItem', 'delete
 
 // TabBar相关逻辑
 const tabs = computed(() => ([
-  { path: '/mobile/chat', label: 'AI问答', icon: chatIcon, iconActive: chatIconActive },
+  { path: '/mobile/chat', label: 'AI助手', icon: chatIcon, iconActive: chatIconActive },
   { path: '/mobile/safety-hazard', label: '安全培训', icon: homeIcon, iconActive: homeIconActive },
   { path: '/mobile/hazard-detection', label: '隐患提示', icon: hazardIcon, iconActive: hazardIconActive },
   { path: '/mobile/ai-writing', label: 'AI写作', icon: writeIcon, iconActive: writeIconActive },

+ 1 - 1
shudao-vue-frontend/src/components/MobileTabBar.vue

@@ -36,7 +36,7 @@ const router = useRouter()
 const route = useRoute()
 
 const tabs = computed(() => ([
-  { path: '/mobile/chat', label: 'AI问答', icon: chatIcon, iconActive: chatIconActive },
+  { path: '/mobile/chat', label: 'AI助手', icon: chatIcon, iconActive: chatIconActive },
   { path: '/mobile/safety-hazard', label: '安全培训', icon: homeIcon, iconActive: homeIconActive },
   { path: '/mobile/hazard-detection', label: '隐患提示', icon: hazardIcon, iconActive: hazardIconActive },
   { path: '/mobile/ai-writing', label: 'AI写作', icon: writeIcon, iconActive: writeIconActive },

+ 1 - 1
shudao-vue-frontend/src/components/Sidebar.test.js

@@ -27,7 +27,7 @@ describe('Sidebar', () => {
     const wrapper = mount(Sidebar)
 
     expect(wrapper.text()).toContain('AI助手')
-    expect(wrapper.text()).not.toContain('AI问答')
+    expect(wrapper.text()).not.toContain('AI助手')
     expect(wrapper.text()).toContain('隐患提示')
     expect(wrapper.text()).toContain('AI写作')
     expect(wrapper.text()).toContain('安全培训')

+ 53 - 22
shudao-vue-frontend/src/components/Toast.vue

@@ -1,10 +1,20 @@
 <template>
   <transition name="toast-fade">
     <div v-if="visible" class="toast-container">
-      <div class="toast-content">
+      <div :class="['toast-content', type]">
         <div class="toast-icon" v-if="icon">
           <img :src="icon" :alt="type" class="icon-img">
         </div>
+        <div class="toast-icon" v-else>
+          <!-- Success Icon -->
+          <svg v-if="type === 'success'" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm-55.808 536.384-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336L456.192 600.384z"></path></svg>
+          <!-- Error Icon -->
+          <svg v-else-if="type === 'error'" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336L512 457.664z"></path></svg>
+          <!-- Warning Icon -->
+          <svg v-else-if="type === 'warning'" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 192a58.432 58.432 0 0 0-58.24 63.744l23.36 256.384a35.072 35.072 0 0 0 69.76 0l23.296-256.384A58.432 58.432 0 0 0 512 256zm0 512a51.2 51.2 0 1 0 0-102.4 51.2 51.2 0 0 0 0 102.4z"></path></svg>
+          <!-- Info Icon -->
+          <svg v-else viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><path fill="currentColor" d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 192a51.2 51.2 0 1 0 0 102.4 51.2 51.2 0 0 0 0-102.4zm0 192a38.4 38.4 0 0 0-38.4 38.4v256a38.4 38.4 0 1 0 76.8 0V486.4A38.4 38.4 0 0 0 512 448z"></path></svg>
+        </div>
         <span class="toast-text">{{ message }}</span>
       </div>
     </div>
@@ -12,7 +22,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onBeforeUnmount } from 'vue'
 
 // Props
 const props = defineProps({
@@ -31,19 +41,25 @@ const props = defineProps({
   },
   icon: {
     type: String,
-    default: '/src/assets/AIWriting/17.png'
+    default: ''
   }
 })
 
 // 响应式数据
 const visible = ref(false)
+let hideTimer = null
 
 // 显示提示
 const show = () => {
   visible.value = true
+  if (hideTimer) {
+    clearTimeout(hideTimer)
+    hideTimer = null
+  }
   if (props.duration > 0) {
-    setTimeout(() => {
+    hideTimer = setTimeout(() => {
       visible.value = false
+      hideTimer = null
     }, props.duration)
   }
 }
@@ -51,6 +67,10 @@ const show = () => {
 // 隐藏提示
 const hide = () => {
   visible.value = false
+  if (hideTimer) {
+    clearTimeout(hideTimer)
+    hideTimer = null
+  }
 }
 
 // 暴露方法
@@ -59,10 +79,12 @@ defineExpose({
   hide
 })
 
-// 组件挂载后不自动显示,等待手动调用
-// onMounted(() => {
-//   show()
-// })
+onBeforeUnmount(() => {
+  if (hideTimer) {
+    clearTimeout(hideTimer)
+    hideTimer = null
+  }
+})
 </script>
 
 <style lang="less" scoped>
@@ -79,17 +101,21 @@ defineExpose({
   display: flex;
   align-items: center;
   gap: 0.5rem;
-  background: #FFFFFF;
-  color: #666666;
-  padding: 0.5rem;
+  padding: 0.75rem 1.25rem;
   border-radius: 0.5rem;
-  box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15);
-  backdrop-filter: blur(10px);
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  font-size: 1rem;
+  line-height: 1.4;
+  font-weight: 500;
+  pointer-events: auto;
   
   .toast-icon {
-    width: 1rem;
-    height: 1rem;
+    width: 1.25rem;
+    height: 1.25rem;
     flex-shrink: 0;
+    display: flex;
+    align-items: center;
+    justify-content: center;
     
     .icon-img {
       width: 100%;
@@ -99,31 +125,36 @@ defineExpose({
   }
   
   .toast-text {
-    font-size: 1rem;
-    line-height: 1.4;
-    text-align: center;
     flex: 1;
   }
 }
 
 // 成功类型
 .toast-content.success {
-  background: rgba(34, 197, 94, 0.9);
+  background: #f0f9eb;
+  border: 1px solid #e1f3d8;
+  color: #67c23a;
 }
 
 // 错误类型
 .toast-content.error {
-  background: rgba(239, 68, 68, 0.9);
+  background: #fef0f0;
+  border: 1px solid #fde2e2;
+  color: #f56c6c;
 }
 
 // 警告类型
 .toast-content.warning {
-  background: rgba(245, 158, 11, 0.9);
+  background: #fdf6ec;
+  border: 1px solid #faecd8;
+  color: #e6a23c;
 }
 
 // 信息类型
 .toast-content.info {
-  background: rgba(59, 130, 246, 0.9);
+  background: #f4f4f5;
+  border: 1px solid #e9e9eb;
+  color: #909399;
 }
 
 // 动画效果

+ 1 - 1
shudao-vue-frontend/src/request/ai.json

@@ -506,7 +506,7 @@
                 "url": "https://baike.baidu.com/item/%E6%89%BF%E8%BD%BD%E8%83%BD%E5%8A%9B/4970204"
             },
             {
-                "content": "* 你好,{{loginInfo.showname|truncate(12)}} * 注销 * 注册;) * 登录;) * 注册;) * 登录;) 国务院 省政府 市政府 网站支持IPV6 * 首页 * 政务公开  政府信息公开 重点领域信息公开 工作报告 人事信息 政策文件 财政资金 重大建设项目 统计信息  专题专栏 * 解读回应 * 办事服务  网上办事大厅 便民服务 闽政通APP 三明市网上公共服务平台 行政权力运行 政务地图 * 互动交流  咨询投诉 AI问答互动交流知识库 在线访谈 网上调查 意见征集 * 走进沙县 热门搜索: 历史搜索: 长者模式;) 退出长者模式;) 无障碍浏览;) 当前位置:首页 > 互动交流 > 互动交流知识库 > 水利局 ## 什么是水资源承载能力?日期:2024-08-14 来源:沙县区水利局 ***|* ***|* ***|* **  ** *|* ** * **微信 * **微博 * **QQ空间 水资源承载能力是指在一定流域或区域内,其自身的水资源能够持续支撑的经济社会发展规模并维系良好生态系统的能力。经济社会发展在水资源承载能力以内,就能实现可持续发展;超越了,发展就会失去物质基础,造成生态系统破坏,生存条件恶化。相关链接: 扫一扫在手机上查看当前页面 **关闭; \"关闭\") * 省级网站 + 北京市 + 天津市 + 河北省 + 山西省 + 内蒙古自治区 + 辽宁省 + 吉林省 + 黑龙江 + 上海市 + 江苏省 + 浙江省 + 福建省 + 安徽省 + 江西省 + 山东省 + 河南省 + 水利局 + 水利局",
+                "content": "* 你好,{{loginInfo.showname|truncate(12)}} * 注销 * 注册;) * 登录;) * 注册;) * 登录;) 国务院 省政府 市政府 网站支持IPV6 * 首页 * 政务公开  政府信息公开 重点领域信息公开 工作报告 人事信息 政策文件 财政资金 重大建设项目 统计信息  专题专栏 * 解读回应 * 办事服务  网上办事大厅 便民服务 闽政通APP 三明市网上公共服务平台 行政权力运行 政务地图 * 互动交流  咨询投诉 AI助手互动交流知识库 在线访谈 网上调查 意见征集 * 走进沙县 热门搜索: 历史搜索: 长者模式;) 退出长者模式;) 无障碍浏览;) 当前位置:首页 > 互动交流 > 互动交流知识库 > 水利局 ## 什么是水资源承载能力?日期:2024-08-14 来源:沙县区水利局 ***|* ***|* ***|* **  ** *|* ** * **微信 * **微博 * **QQ空间 水资源承载能力是指在一定流域或区域内,其自身的水资源能够持续支撑的经济社会发展规模并维系良好生态系统的能力。经济社会发展在水资源承载能力以内,就能实现可持续发展;超越了,发展就会失去物质基础,造成生态系统破坏,生存条件恶化。相关链接: 扫一扫在手机上查看当前页面 **关闭; \"关闭\") * 省级网站 + 北京市 + 天津市 + 河北省 + 山西省 + 内蒙古自治区 + 辽宁省 + 吉林省 + 黑龙江 + 上海市 + 江苏省 + 浙江省 + 福建省 + 安徽省 + 江西省 + 山东省 + 河南省 + 水利局 + 水利局",
                 "raw_content": null,
                 "score": 0.6612456,
                 "title": "什么是水资源承载能力? _ 水利局 - 沙县区",

+ 2 - 1
shudao-vue-frontend/src/request/apis.js

@@ -17,6 +17,7 @@ export const apis = {
   //发送deepseek消息
   sendDeepseekMessage: (data) => request.post('/send_deepseek_message', data),
   buildExamPrompt: (data) => request.post('/exam/build_prompt', data),
+  generateExamTitle: (data) => request.post('/exam/generate_title', data),
   saveExam: (data) => request.post('/save_exam', data),
   getExamHistory: () => request.get('/get_exam_history'),
   getExamById: (id) => request.get(`/get_exam/${id}`),
@@ -24,7 +25,7 @@ export const apis = {
   //上传oss
   uploadOss: (data) => request.post('/oss/upload', data),
 
-  // 解析AI问答上传附件
+  // 解析AI助手上传附件
   parseAttachment: (data) => request.post('/attachments/parse', data),
   
   // 获取功能卡片

+ 172 - 24
shudao-vue-frontend/src/views/Chat.vue

@@ -60,7 +60,7 @@
       class="chat-main-area"
       :class="{ 'is-ai-writing-resizing': aiWritingSidebarResizing }"
     >
-      <!-- 右侧AI问答区域 -->
+      <!-- 右侧AI助手区域 -->
       <div class="main-chat" :class="{ 'sidebar-open': webSearchSidebarVisible }">
       <!-- 聊天头部 -->
       <div class="chat-header" v-if="currentMode !== 'exam-workshop'">
@@ -79,7 +79,13 @@
 
       <!-- 考试工坊内容区域 -->
       <div v-if="currentMode === 'exam-workshop'" class="exam-workshop-wrapper" style="height: 100%; flex: 1;">
-        <ExamWorkshop :hideSidebar="true" @return-to-ai="setMode('ai-qa')" />
+        <ExamWorkshop
+          :hideSidebar="true"
+          :auto-task="examWorkshopAutoTask"
+          :history-id="examWorkshopHistoryId"
+          @auto-task-consumed="handleExamWorkshopAutoTaskConsumed"
+          @return-to-ai="setMode('ai-qa')"
+        />
       </div>
 
       <!-- 聊天内容区域 -->
@@ -702,6 +708,10 @@
       style="display: none"
       @change="handleFileSelect"
     />
+
+    <DispatchFeedbackModal
+      :visible="dispatchFeedbackVisible"
+    />
     
     <!-- Toast提示组件 -->
     <Toast
@@ -746,6 +756,7 @@ import { useRoute, useRouter } from 'vue-router'
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import '@wangeditor/editor/dist/css/style.css'
 import Sidebar from '@/components/Sidebar.vue'
+import DispatchFeedbackModal from '@/components/DispatchFeedbackModal.vue'
 import ExamWorkshop from '@/views/ExamWorkshop.vue'
 
 // 导入Element Plus组件
@@ -870,6 +881,41 @@ const toggleModelType = () => {
 
 const currentMode = ref('ai-qa')
 const hasUserSelectedMode = ref(false)
+const examWorkshopAutoTask = ref('')
+const examWorkshopHistoryId = ref('')
+
+const ROUTE_MODE_LABELS = {
+  'ai-qa': 'AI助手',
+  'ai-writing': 'AI写作',
+  'safety-training': '安全培训',
+  'exam-workshop': '考试工坊'
+}
+
+const normalizeDispatchedRouteMode = (rawMode) => {
+  const normalized = String(rawMode || '').trim().toLowerCase()
+  const modeMap = {
+    'ai-qa': 'ai-qa',
+    ai_qa: 'ai-qa',
+    qa: 'ai-qa',
+    general_chat: 'ai-qa',
+    'ai-writing': 'ai-writing',
+    ai_writing: 'ai-writing',
+    writing: 'ai-writing',
+    'safety-training': 'safety-training',
+    safety_training: 'safety-training',
+    training: 'safety-training',
+    ppt_outline: 'safety-training',
+    'exam-workshop': 'exam-workshop',
+    exam_workshop: 'exam-workshop',
+    exam: 'exam-workshop'
+  }
+
+  return modeMap[normalized] || 'ai-qa'
+}
+
+const handleExamWorkshopAutoTaskConsumed = () => {
+  examWorkshopAutoTask.value = ''
+}
 
 const defaultHotQuestionsByMode = {
   'ai-qa': [
@@ -995,16 +1041,20 @@ const defaultFunctionCards = computed(
 
 const setMode = (mode, options = {}) => {
   const { allowToggle = true, source = 'user' } = options
-  if (source === 'user') {
-    hasUserSelectedMode.value = true
-  }
+  let nextMode = mode
   if (allowToggle && currentMode.value === mode) {
-    currentMode.value = 'ai-qa' // 再次点击取消选中,回到默认问答
-  } else {
-    currentMode.value = mode
+    nextMode = 'ai-qa' // 再次点击取消选中,回到默认问答
+  }
+  currentMode.value = nextMode
+
+  if (currentMode.value === 'ai-qa') {
+    // 回到默认 AI 助手时重新启用自动分发
+    hasUserSelectedMode.value = false
+  } else if (source === 'user') {
+    hasUserSelectedMode.value = true
   }
   
-  // 切换回AI问答时,确保输入框和聊天区域正常显示
+  // 切换回AI助手时,确保输入框和聊天区域正常显示
   if (currentMode.value === 'ai-qa' && !chatMessages.value.length) {
     showChat.value = false
   } else if (currentMode.value === 'ai-qa' && chatMessages.value.length > 0) {
@@ -1038,6 +1088,49 @@ const toastRef = ref(null)
 const toastMessage = ref('')
 const toastType = ref('success')
 const toastDuration = ref(2000)
+const dispatchFeedbackVisible = ref(false)
+const currentDispatchFeedbackMessage = ref('正在判断最适合处理该问题的模块...')
+let stopGenerationMessageInstance = null
+
+const dispatchFeedbackMessages = [
+  '正在判断最适合处理该问题的模块...',
+  '正在分析您的表达意图,请稍候...',
+  '如果命中考试工坊、AI写作或安全培训,将自动跳转...'
+]
+
+let dispatchFeedbackRevealTimer = null
+let dispatchFeedbackRotateTimer = null
+
+const stopDispatchFeedback = () => {
+  dispatchFeedbackVisible.value = false
+  currentDispatchFeedbackMessage.value = dispatchFeedbackMessages[0]
+
+  if (dispatchFeedbackRevealTimer) {
+    clearTimeout(dispatchFeedbackRevealTimer)
+    dispatchFeedbackRevealTimer = null
+  }
+
+  if (dispatchFeedbackRotateTimer) {
+    clearInterval(dispatchFeedbackRotateTimer)
+    dispatchFeedbackRotateTimer = null
+  }
+}
+
+const startDispatchFeedback = () => {
+  stopDispatchFeedback()
+
+  let currentIndex = 0
+  currentDispatchFeedbackMessage.value = dispatchFeedbackMessages[currentIndex]
+
+  dispatchFeedbackRevealTimer = setTimeout(() => {
+    dispatchFeedbackVisible.value = true
+  }, 250)
+
+  dispatchFeedbackRotateTimer = setInterval(() => {
+    currentIndex = (currentIndex + 1) % dispatchFeedbackMessages.length
+    currentDispatchFeedbackMessage.value = dispatchFeedbackMessages[currentIndex]
+  }, 1600)
+}
 
 // 显示Toast提示
 const showToast = (message, type = 'success', duration = 2000) => {
@@ -1051,6 +1144,25 @@ const showToast = (message, type = 'success', duration = 2000) => {
   }
 }
 
+const closeStopGenerationMessage = () => {
+  if (stopGenerationMessageInstance?.close) {
+    stopGenerationMessageInstance.close()
+  }
+  stopGenerationMessageInstance = null
+}
+
+const showStopGenerationMessage = () => {
+  closeStopGenerationMessage()
+  stopGenerationMessageInstance = ElMessage({
+    message: '已停止生成',
+    type: 'success',
+    duration: 2000,
+    onClose: () => {
+      stopGenerationMessageInstance = null
+    }
+  })
+}
+
 // 语音功能
 const {
   isSupported: speechSupported,
@@ -2306,14 +2418,43 @@ const clearNewConversationState = () => {
 
 // 根据当前模式分发发送请求
 const submitQuestionByCurrentMode = async (question, options = {}) => {
-  const { windowSize = 3, nResults = 10 } = options
+  const { windowSize = 3, nResults = 10, skipTopLevelRouting = false } = options
   const normalizedQuestion = typeof question === 'string' ? question.trim() : ''
   if (!normalizedQuestion) return
 
-  if (currentMode.value === 'ai-writing' || currentMode.value === 'safety-training') {
+  let targetMode = currentMode.value
+
+  if (!skipTopLevelRouting && !hasUserSelectedMode.value) {
+    startDispatchFeedback()
+    try {
+      const response = await apis.intentRecognition({
+        message: normalizedQuestion,
+        scene: 'module_dispatch'
+      })
+      const dispatchedMode = normalizeDispatchedRouteMode(response?.data?.route_mode)
+      if (dispatchedMode !== currentMode.value) {
+        targetMode = dispatchedMode
+        setMode(dispatchedMode, { allowToggle: false, source: 'system' })
+        showToast(`已自动切换到${ROUTE_MODE_LABELS[dispatchedMode] || 'AI助手'}`, 'success')
+      }
+    } catch (error) {
+      console.error('顶层模块分发失败,回退到AI助手:', error)
+    } finally {
+      stopDispatchFeedback()
+    }
+  }
+
+  if (targetMode === 'exam-workshop') {
+    examWorkshopHistoryId.value = ''
+    examWorkshopAutoTask.value = normalizedQuestion
+    isSending.value = false
+    return
+  }
+
+  if (targetMode === 'ai-writing' || targetMode === 'safety-training') {
     await handleNonStreamingSubmit({
       question: normalizedQuestion,
-      businessType: currentMode.value === 'ai-writing' ? 2 : 1
+      businessType: targetMode === 'ai-writing' ? 2 : 1
     })
     return
   }
@@ -3754,7 +3895,7 @@ const handleStopGeneration = async () => {
   })
   
   isAIReplyProcessComplete.value = true
-  ElMessage.success('已停止生成')
+  showStopGenerationMessage()
   
   // ===== 已删除:getUserId() - stopSSEStream 函数需要更新 =====
   stopSSEStream(null, ai_conversation_id.value)
@@ -3911,6 +4052,14 @@ const handleHistoryItem = async (historyItem) => {
   historyData.value.forEach(item => {
     item.isActive = item.id === historyItem.id
   })
+
+  const bType = Number(historyItem.businessType)
+  if (bType === 3) {
+    examWorkshopAutoTask.value = ''
+    examWorkshopHistoryId.value = String(historyItem.id)
+    setMode('exam-workshop', { allowToggle: false, source: 'system' })
+    return
+  }
   
   showChat.value = true
   
@@ -3931,11 +4080,7 @@ const handleHistoryItem = async (historyItem) => {
       chatMessages.value = chatMessages.value.filter(msg => msg.id !== loadingMessage.id)
       
       // 根据历史记录的业务类型进行跳转或模式切换
-      const bType = Number(historyItem.businessType)
-      if (bType === 3) {
-        router.push({ path: '/exam-workshop', query: { historyId: historyItem.id } })
-        return
-      } else if (bType === 2) {
+      if (bType === 2) {
         currentMode.value = 'ai-writing'
       } else if (bType === 1) {
         currentMode.value = 'safety-training'
@@ -4593,7 +4738,7 @@ const getModeBusinessType = (mode) => {
 // 根据当前模式过滤历史记录
 const filteredHistoryData = computed(() => {
   if (currentMode.value === 'ai-qa') {
-    // AI问答模式显示所有记录(business_type 为 0 或未设置的)
+    // AI助手模式显示所有记录(business_type 为 0 或未设置的)
     return historyData.value.filter(item => !item.businessType || Number(item.businessType) === 0)
   }
   const targetType = getModeBusinessType(currentMode.value)
@@ -5645,7 +5790,7 @@ const handleBrowserResize = () => {
 
 onMounted(async () => {
   try {
-    console.log('🚀 AI问答页面初始化开始,优先加载历史记录...')
+    console.log('🚀 AI助手页面初始化开始,优先加载历史记录...')
     
     // 设置初始加载状态
     isLoadingHistory.value = true
@@ -5702,7 +5847,7 @@ onMounted(async () => {
     
     // 1. 首先获取历史记录列表(最高优先级)
     await getHistoryRecordList()
-    console.log('✅ AI问答历史记录加载完成')
+    console.log('✅ AI助手历史记录加载完成')
 
     await fetchPointsBalance()
     
@@ -5727,12 +5872,12 @@ onMounted(async () => {
     // 3. 等待其他数据加载完成(后台进行)
     try {
       await otherDataPromise
-      console.log('✅ AI问答其他数据加载完成')
+      console.log('✅ AI助手其他数据加载完成')
     } catch (error) {
-      console.warn('⚠️ AI问答其他数据加载失败,但不影响主要功能:', error)
+      console.warn('⚠️ AI助手其他数据加载失败,但不影响主要功能:', error)
     }
     
-    console.log('🎉 AI问答页面初始化完成')
+    console.log('🎉 AI助手页面初始化完成')
     
     // 检查是否带有指定的模式
     const targetMode = Array.isArray(route.query.mode) ? route.query.mode[0] : route.query.mode
@@ -5773,6 +5918,9 @@ onMounted(async () => {
 
 // 组件销毁前,强制停止任何朗读
 onBeforeUnmount(() => {
+  stopDispatchFeedback()
+  closeStopGenerationMessage()
+
   if (speakingMessageId.value) {
     stopAllAudio()
     speakingMessageId.value = null

+ 166 - 21
shudao-vue-frontend/src/views/ExamWorkshop.vue

@@ -71,13 +71,13 @@
         <div v-if="!showExamDetail" class="exam-workshop-card app-container">
                 <!-- 中间主操作区 -->
             <main class="main-content" style="padding-top: 36px; position: relative;">
-                <!-- 返回AI问答按钮 -->
+                <!-- 返回AI助手按钮 -->
                 <button v-if="!showExamDetail" class="return-ai-btn" @click="handleReturnToAI">
                   <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
                     <circle cx="12" cy="12" r="12" fill="white" stroke="#E5E7EB" stroke-width="1"/>
                     <path d="M14 7L9 12L14 17" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
                   </svg>
-                  返回AI问答
+                  返回AI助手
                 </button>
                 
                 <div class="form-group" style="position: relative;">
@@ -673,7 +673,7 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted, onUnmounted, reactive, watch, defineProps, defineEmits } from "vue";
+import { ref, computed, onMounted, onUnmounted, reactive, watch, defineProps, defineEmits, nextTick } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import Sidebar from "@/components/Sidebar.vue";
 import DeleteConfirmModal from "@/components/DeleteConfirmModal.vue";
@@ -683,10 +683,18 @@ const props = defineProps({
   hideSidebar: {
     type: Boolean,
     default: false
+  },
+  autoTask: {
+    type: String,
+    default: ''
+  },
+  historyId: {
+    type: [String, Number],
+    default: ''
   }
 });
 
-const emit = defineEmits(['return-to-ai']);
+const emit = defineEmits(['return-to-ai', 'auto-task-consumed']);
 
 const route = useRoute();
 const router = useRouter();
@@ -742,6 +750,7 @@ const isLoadingHistory = ref(false); // 是否正在加载历史记录
 const isLoadingHistoryItem = ref(false); // 是否正在加载历史记录详情
 const showDeleteModal = ref(false); // 控制删除确认弹窗显示
 const deleteTargetItem = ref(null); // 要删除的目标项
+let autoTaskMessageInstance = null;
 // 编辑功能已禁用
 /*
 const showEditModal = ref(false); // 控制编辑选项弹窗显示
@@ -808,6 +817,33 @@ const buildCombinedBasisContent = () => {
   return sections.join('\n\n');
 };
 
+const ensureAutoGeneratedExamTitle = async () => {
+  if (!shouldUseAutoGeneratedExamTitle(examName.value, uploadedFiles.value.length > 0)) {
+    return examName.value.trim();
+  }
+
+  const combinedBasisContent = buildCombinedBasisContent();
+  if (!combinedBasisContent.trim()) {
+    return '';
+  }
+
+  try {
+    const response = await apis.generateExamTitle({
+      projectType: projectTypes[selectedProjectType.value]?.name || '',
+      sourceContent: combinedBasisContent
+    });
+    const generatedTitle = String(response?.data?.title || '').trim();
+    if (response?.statusCode === 200 && generatedTitle) {
+      examName.value = generatedTitle;
+      return generatedTitle;
+    }
+  } catch (error) {
+    console.error('自动生成试卷名称失败:', error);
+  }
+
+  return examName.value.trim();
+};
+
 
 // 移除原有的Toast相关变量,使用ElMessage替代
 // const showToast = ref(false); // 控制轻提示显示
@@ -1153,6 +1189,51 @@ const clearSettings = () => {
   console.log("清除设置");
 };
 
+const closeAutoTaskMessage = () => {
+  if (autoTaskMessageInstance?.close) {
+    autoTaskMessageInstance.close();
+  }
+  autoTaskMessageInstance = null;
+};
+
+const showAutoTaskMessage = () => {
+  closeAutoTaskMessage();
+  autoTaskMessageInstance = ElMessage({
+    message: '已将您的问题填入出题依据,请确认题型配置后再生成试卷',
+    type: 'info',
+    duration: 2500,
+    onClose: () => {
+      autoTaskMessageInstance = null;
+    }
+  });
+};
+
+const applyAutoTask = async (task) => {
+  const normalizedTask = typeof task === 'string' ? task.trim() : '';
+  if (!normalizedTask || isGenerating.value) return;
+
+  const currentBasis = String(questionBasis.value || '').trim();
+  if (
+    normalizedTask === currentBasis &&
+    selectedFunction.value === 'ai' &&
+    !showExamDetail.value
+  ) {
+    emit('auto-task-consumed', normalizedTask);
+    return;
+  }
+
+  try {
+    selectedFunction.value = 'ai';
+    showExamDetail.value = false;
+    questionBasis.value = normalizedTask;
+
+    await nextTick();
+    showAutoTaskMessage();
+  } finally {
+    emit('auto-task-consumed', normalizedTask);
+  }
+};
+
 const validateExamName = () => {
   if (examName.value.length > 32) {
     examName.value = examName.value.slice(0, 20);
@@ -1260,10 +1341,44 @@ const generateExam = async () => {
   }
 };
 
+watch(
+  () => props.autoTask,
+  (newTask) => {
+    if (!newTask || !newTask.trim()) return;
+    applyAutoTask(newTask);
+  },
+  { immediate: true }
+);
+
+const loadHistoryItemById = async (historyId) => {
+  const normalizedHistoryId = String(historyId ?? '').trim();
+  if (!normalizedHistoryId || historyData.value.length === 0) return;
+
+  const targetItem = historyData.value.find(item => String(item.id) === normalizedHistoryId);
+  if (targetItem) {
+    await handleHistoryItem(targetItem);
+  }
+};
+
+watch(
+  [() => props.historyId, () => historyData.value.length],
+  async ([newHistoryId, historyCount], [oldHistoryId, oldHistoryCount]) => {
+    const normalizedHistoryId = String(newHistoryId ?? '').trim();
+    if (!normalizedHistoryId || historyCount === 0) return;
+
+    const normalizedOldHistoryId = String(oldHistoryId ?? '').trim();
+    if (normalizedHistoryId === normalizedOldHistoryId && historyCount === oldHistoryCount) return;
+
+    await loadHistoryItemById(normalizedHistoryId);
+  }
+);
+
 // PPT生成试卷
 const generatePPTExam = async () => {
   try {
     isGenerating.value = true;
+
+    const resolvedExamTitle = await ensureAutoGeneratedExamTitle();
     
     // 构建PPT生成提示词(服务端构建)
     const prompt = await fetchExamPrompt('ppt');
@@ -1275,7 +1390,7 @@ const generatePPTExam = async () => {
       // ===== 已删除:user_id - 后端从token解析 =====
       business_type: 3,
       message: prompt,
-      exam_name: examName.value,
+      exam_name: resolvedExamTitle,
       ai_conversation_id: ai_conversation_id.value
     });
     
@@ -1329,9 +1444,9 @@ const generateAIExam = async () => {
   try {
     isGenerating.value = true;
     showProgressModal.value = true;
-    generateProgress.value = 0;
-    displayProgress.value = 0;
-    generateStatusText.value = "准备生成环境...";
+    generateProgress.value = 1;
+    displayProgress.value = 1;
+    generateStatusText.value = "正在分析试卷标题...";
 
     // 启动平滑进度推进定时器
     if (progressTimer) clearInterval(progressTimer);
@@ -1349,10 +1464,14 @@ const generateAIExam = async () => {
         }
       }
     }, 200);
+
+    const resolvedExamTitle = await ensureAutoGeneratedExamTitle();
+    generateProgress.value = Math.max(generateProgress.value, 3);
+    generateStatusText.value = "准备生成环境...";
     
     // 初始化试卷结构
     currentExam.value = {
-      title: examName.value,
+      title: resolvedExamTitle || examName.value,
       totalScore: totalScore.value,
       totalQuestions: 0,
       singleChoice: { scorePerQuestion: 0, totalScore: 0, count: 0, questions: [] },
@@ -1369,14 +1488,13 @@ const generateAIExam = async () => {
       scorePerQuestion: Number(type.scorePerQuestion) || 0,
     }));
 
-    const pptContents = uploadedFiles.value.map(file => file.content).join('\n\n');
-    const finalContentBasis = pptContents || questionBasis.value || '';
+    const finalContentBasis = buildCombinedBasisContent();
 
     const payload = {
       mode: 'ai',
       client: 'pc',
       projectType: projectTypes[selectedProjectType.value]?.name || '',
-      examTitle: shouldUseAutoGeneratedExamTitle(examName.value, uploadedFiles.value.length > 0) ? '' : examName.value,
+      examTitle: resolvedExamTitle || examName.value,
       totalScore: totalScore.value,
       questionTypes: normalizedQuestionTypes,
       pptContent: finalContentBasis,
@@ -1535,7 +1653,7 @@ const fetchExamPrompt = async (mode = 'ai') => {
     mode,
     client: 'pc',
     projectType: projectTypes[selectedProjectType.value]?.name || '',
-    examTitle: shouldUseAutoGeneratedExamTitle(examName.value, uploadedFiles.value.length > 0) ? '' : examName.value,
+    examTitle: examName.value.trim(),
     totalScore: totalScore.value,
     questionTypes: normalizedQuestionTypes,
     pptContent: finalContentBasis,
@@ -1692,10 +1810,12 @@ const parseAIExamResponse = (aiReply) => {
     if (typeof value === 'string') {
       const text = value.trim();
       return [
+        /^\.\.\.$/,
         /^题目内容$/,
         /^解析内容$/,
         /^参考措施$/,
         /^答题要点$/,
+        /^未设置$/,
         /^具体选项内容$/,
         /^选项[ABCD]$/,
         /^.+工程相关(?:单选题|多选题|判断题|简答题)\d+$/,
@@ -1756,6 +1876,33 @@ const normalizeQuestions = (questions = [], sectionKey) => {
     return [];
   }
 
+  const normalizeOutline = (outlineValue = undefined, question = {}) => {
+    if (typeof outlineValue === 'string') {
+      const text = outlineValue.trim();
+      return {
+        keyFactors: text || question.answer || question['答案'] || "答题要点、关键因素、示例答案"
+      };
+    }
+    if (Array.isArray(outlineValue)) {
+      const text = outlineValue.map(item => String(item || '').trim()).filter(Boolean).join(';');
+      return {
+        keyFactors: text || question.answer || question['答案'] || "答题要点、关键因素、示例答案"
+      };
+    }
+    if (outlineValue && typeof outlineValue === 'object') {
+      const keyFactors = Array.isArray(outlineValue.keyFactors)
+        ? outlineValue.keyFactors.map(item => String(item || '').trim()).filter(Boolean).join(';')
+        : String(outlineValue.keyFactors || '').trim();
+      return {
+        ...outlineValue,
+        keyFactors: keyFactors || question.answer || question['答案'] || "答题要点、关键因素、示例答案"
+      };
+    }
+    return {
+      keyFactors: question.answer || question['答案'] || "答题要点、关键因素、示例答案"
+    };
+  };
+
   return questions.map((question = {}) => {
     if (sectionKey === 'singleChoice') {
       const selectedAnswer = question.selectedAnswer || question.correct_answer || question.answer || question['正确答案'] || question['答案'] || "";
@@ -1792,7 +1939,10 @@ const normalizeQuestions = (questions = [], sectionKey) => {
 
     return {
       text: question.text || question.question_text || question.question || question.title || question.content || question['题干'] || question['题目'] || "",
-      outline: question.outline || question.answer_outline || question['答题要点'] || { keyFactors: question.answer || question['答案'] || "答题要点、关键因素、示例答案" },
+      outline: normalizeOutline(
+        question.outline || question.answer_outline || question['答题要点'],
+        question
+      ),
       analysis: question.analysis || question.explanation || question['解析'] || "",
       basis: question.basis || question['出题依据'] || "",
     };
@@ -3887,13 +4037,7 @@ onMounted(async () => {
   await getHistoryRecordList()
   
   // 检查URL参数是否有historyId需要加载
-  const historyId = route.query.historyId
-  if (historyId) {
-    const targetItem = historyData.value.find(item => String(item.id) === String(historyId))
-    if (targetItem) {
-      await handleHistoryItem(targetItem)
-    }
-  }
+  await loadHistoryItemById(props.historyId || route.query.historyId)
   
   // 添加全局点击事件监听器
   document.addEventListener('click', handleClickOutside);
@@ -3903,6 +4047,7 @@ onMounted(async () => {
 onUnmounted(() => {
   // 清理事件监听器
   document.removeEventListener('click', handleClickOutside);
+  closeAutoTaskMessage();
 })
 </script>
 

+ 6 - 6
shudao-vue-frontend/src/views/Index.vue

@@ -86,7 +86,7 @@
           </div>
         </div>
 
-        <!-- 第二列:隐患提示和AI问答 -->
+        <!-- 第二列:隐患提示和AI助手 -->
         <div class="card-column">
           <div class="hazard-card" @click="goToHazardDetection">
             <div class="card-title">隐患提示</div>
@@ -94,13 +94,13 @@
           </div>
           <div class="ai-chat-card" @click="goToAIChat">
             <div class="ai-chat-icon">
-              <img src="@/assets/new_index/chat-icon.png" alt="AI问答" class="chat-icon-img">
+              <img src="@/assets/new_index/chat-icon.png" alt="AI助手" class="chat-icon-img">
             </div>
             <div class="ai-chat-images">
               <img src="@/assets/new_index/chat-img-1.png" alt="对话1" class="chat-img chat-img-back">
               <img src="@/assets/new_index/chat-img-2.png" alt="对话2" class="chat-img chat-img-front">
             </div>
-            <div class="card-title">AI问答</div>
+            <div class="card-title">AI助手</div>
             <div class="card-description">AI对话助手,智能解答您的问题</div>
             
             <!-- 波浪效果 -->
@@ -389,7 +389,7 @@ const handleSearch = async () => {
   
   try {
     console.log('搜索内容:', searchText.value)
-    // 跳转到AI问答页面,并传递搜索内容
+    // 跳转到AI助手页面,并传递搜索内容
     router.push({
       path: '/chat',
       query: {
@@ -458,7 +458,7 @@ const getFeedbackType = (type) => {
 // 推荐问题点击跳转
 const goToAIQuestion = (question) => {
   console.log('点击问题:', question)
-  // 跳转到AI问答页面,并传递问题内容
+  // 跳转到AI助手页面,并传递问题内容
   router.push({
     path: '/chat',
     query: {
@@ -1005,7 +1005,7 @@ onUnmounted(() => {
   }
 }
 
-/* AI问答卡片 */
+/* AI助手卡片 */
 .ai-chat-card {
   background: #428EFE;
   border-radius: 16px;

+ 39 - 2
shudao-vue-frontend/src/views/mobile/m-AIWriting.vue

@@ -160,7 +160,7 @@
 
 <script setup>
 import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick, computed } from 'vue'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import MobileHeader from '@/components/MobileHeader.vue'
 import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
 import MobileToast from '@/components/MobileToast.vue'
@@ -182,6 +182,7 @@ import meetingIcon from '@/assets/AIWriting/20.png'
 import speechIcon from '@/assets/AIWriting/21.png'
 
 const router = useRouter()
+const route = useRoute()
 
 // 响应式数据 - 复用PC端逻辑
 const currentView = ref('main') // 'main' | 'editor'
@@ -423,6 +424,16 @@ const handleCopy = (event) => {
   }
 }
 
+const setTemplateInputContent = (content) => {
+  const normalizedContent = String(content || '').trim()
+  const inputElement = document.querySelector('.template-input-container')
+  if (!inputElement) return false
+
+  inputElement.textContent = normalizedContent
+  templateContent.value = normalizedContent
+  return true
+}
+
 // 处理文件上传
 const triggerFileUpload = () => {
   const input = document.createElement('input')
@@ -934,6 +945,20 @@ const createNewTask = () => {
   console.log('✅ 新建任务完成,已重置对话ID为0,准备创建新对话')
 }
 
+const consumeAutoMessage = async (message) => {
+  const normalizedMessage = String(message || '').trim()
+  if (!normalizedMessage) return
+
+  createNewTask()
+  await nextTick()
+
+  if (!setTemplateInputContent(normalizedMessage)) {
+    templateContent.value = normalizedMessage
+  }
+
+  await sendAIWritingRequest()
+}
+
 // 获取指定对话的详细消息 - 复用PC端的实现
 const getConversationMessages = async (conversationId) => {
   try {
@@ -1110,6 +1135,18 @@ onMounted(() => {
       })
     }
   })
+
+  const autoMessage = route.query.autoMessage
+  if (autoMessage) {
+    router.replace({
+      path: route.path,
+      query: { ...route.query, autoMessage: undefined }
+    })
+
+    nextTick(() => {
+      consumeAutoMessage(autoMessage)
+    })
+  }
 })
 </script>
 
@@ -1669,4 +1706,4 @@ onMounted(() => {
   50% { opacity: 0.5; }
 }
 
-</style>
+</style>

+ 124 - 38
shudao-vue-frontend/src/views/mobile/m-Chat.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="mobile-chat">
-    <!-- 移动端AI问答页面 -->
-    <MobileHeader title="AI问答" @back="goBack" @menu="showHistoryDrawer" />
+    <!-- 移动端AI助手页面 -->
+    <MobileHeader title="AI助手" @back="goBack" @menu="showHistoryDrawer" />
     
     <div class="mobile-content">
       
@@ -539,6 +539,57 @@ import networkSearchIconOff from '@/assets/Chat/25.png'
 const router = useRouter()
 const route = useRoute()
 
+const MOBILE_ROUTE_MODE_LABELS = {
+  'ai-qa': 'AI助手',
+  'ai-writing': 'AI写作',
+  'safety-training': '安全培训',
+  'exam-workshop': '考试工坊'
+}
+
+const normalizeDispatchedRouteMode = (rawMode) => {
+  const normalized = String(rawMode || '').trim().toLowerCase()
+  const modeMap = {
+    'ai-qa': 'ai-qa',
+    ai_qa: 'ai-qa',
+    qa: 'ai-qa',
+    general_chat: 'ai-qa',
+    'ai-writing': 'ai-writing',
+    ai_writing: 'ai-writing',
+    writing: 'ai-writing',
+    'safety-training': 'safety-training',
+    safety_training: 'safety-training',
+    training: 'safety-training',
+    ppt_outline: 'safety-training',
+    'exam-workshop': 'exam-workshop',
+    exam_workshop: 'exam-workshop',
+    exam: 'exam-workshop'
+  }
+
+  return modeMap[normalized] || 'ai-qa'
+}
+
+const routeToMobileModule = async (routeMode, message) => {
+  const normalizedMessage = String(message || '').trim()
+  if (!normalizedMessage) return false
+
+  if (routeMode === 'ai-writing') {
+    await router.push({ path: '/mobile/ai-writing', query: { autoMessage: normalizedMessage } })
+    return true
+  }
+
+  if (routeMode === 'safety-training') {
+    await router.push({ path: '/mobile/safety-hazard', query: { autoMessage: normalizedMessage } })
+    return true
+  }
+
+  if (routeMode === 'exam-workshop') {
+    await router.push({ path: '/mobile/exam-workshop', query: { autoTask: normalizedMessage } })
+    return true
+  }
+
+  return false
+}
+
 const goBack = () => {
   router.go(-1)
 }
@@ -1120,18 +1171,18 @@ const formatHistoryTime = (timestamp) => {
 // 获取历史记录列表
 const getHistoryRecordList = async () => {
   try {
-    console.log('📋 开始获取移动端AI问答历史记录列表...')
+    console.log('📋 开始获取移动端AI助手历史记录列表...')
     isLoadingHistory.value = true
     const startTime = performance.now()
     
     const response = await apis.getHistoryRecord({ 
       // ===== 已删除:user_id - 后端从token解析 =====
       ai_conversation_id: 0, // 0表示获取对话列表
-      business_type: 0 // AI问答类型
+      business_type: 0 // AI助手类型
     })
     
     const endTime = performance.now()
-    console.log(`📋 移动端AI问答历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
+    console.log(`📋 移动端AI助手历史记录API调用耗时: ${(endTime - startTime).toFixed(2)}ms`)
     console.log('📋 移动端历史记录列表响应:', response)
     
     if (response.statusCode === 200) {
@@ -1152,7 +1203,7 @@ const getHistoryRecordList = async () => {
       if (ai_conversation_id.value) {
         historyData.value.forEach(item => { item.isActive = item.id === ai_conversation_id.value })
       }
-      console.log(`✅ 移动端AI问答历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
+      console.log(`✅ 移动端AI助手历史记录列表已设置: ${historyData.value.length}条记录,总数: ${historyTotal.value}`)
     } else {
       console.error('❌ 获取移动端历史记录列表失败:', response.statusCode)
     }
@@ -3487,7 +3538,7 @@ const bindStandardReferenceEvents = () => {
 // 页面加载时不再自动加载历史记录,改为点击菜单时加载
 onMounted(async () => {
   try {
-    console.log('🚀 移动端AI问答页面初始化,加载功能卡片...')
+    console.log('🚀 移动端AI助手页面初始化,加载功能卡片...')
     
     // 初始化原生导航栏(子页面模式:返回按钮执行路由后退)
     initNativeNavForSubPage(() => router.back())
@@ -3538,9 +3589,9 @@ onMounted(async () => {
       autoSendMessage(autoMessage)
     }
     
-    console.log('✅ 移动端AI问答页面初始化完成')
+    console.log('✅ 移动端AI助手页面初始化完成')
   } catch (error) {
-    console.error('❌ 移动端AI问答页面初始化失败:', error)
+    console.error('❌ 移动端AI助手页面初始化失败:', error)
   }
 })
 
@@ -3594,6 +3645,19 @@ const handleFunctionCard = (cardType) => {
   sendMessage()
 }
 
+const dispatchMobileRouteByMessage = async (message) => {
+  const normalizedMessage = String(message || '').trim()
+  if (!normalizedMessage) {
+    return 'ai-qa'
+  }
+
+  const response = await apis.intentRecognition({
+    message: normalizedMessage,
+    scene: 'module_dispatch'
+  })
+  return normalizeDispatchedRouteMode(response?.data?.route_mode)
+}
+
 // 调用流式聊天接口
 const callStreamChatWithDB = async (messageToAI, aiMessage, userMessage, currentMessage, responseContent, onlineSearchContent = null) => {
   try {
@@ -3620,7 +3684,7 @@ const callStreamChatWithDB = async (messageToAI, aiMessage, userMessage, current
         message: messageToAI,
         // ===== 已删除:user_id - 后端从token解析 =====
         ai_conversation_id: ai_conversation_id.value,
-        business_type: 0, // AI问答类型
+        business_type: 0, // AI助手类型
         ai_message_id: aiMessage.id,
         online_search_content: onlineSearchContent // 添加联网搜索内容
       })
@@ -3907,36 +3971,58 @@ const sendMessage = async () => {
   if (!messageText.value.trim() || isSending.value) return
   
   console.log('📤 移动端发送消息:', messageText.value)
-  
-  // 清除推荐问题
-  aiRelatedQuestions.value = []
-  relatedQuestionsMessageId.value = null
+  const normalizedMessage = messageText.value.trim()
   
   isSending.value = true
-  showChat.value = true
-  
-  // 如果是新对话,清除所有历史记录的选中状态
-  if (chatMessages.value.length === 0) {
-    historyData.value.forEach((item) => {
-      item.isActive = false
+  try {
+    const dispatchedMode = await dispatchMobileRouteByMessage(normalizedMessage)
+    if (dispatchedMode !== 'ai-qa') {
+      messageText.value = ''
+      isSending.value = false
+      showToastMessage(`已自动跳转到${MOBILE_ROUTE_MODE_LABELS[dispatchedMode] || '对应模块'}`, 2000)
+      await routeToMobileModule(dispatchedMode, normalizedMessage)
+      return
+    }
+
+    // 清除推荐问题
+    aiRelatedQuestions.value = []
+    relatedQuestionsMessageId.value = null
+    showChat.value = true
+    
+    // 如果是新对话,清除所有历史记录的选中状态
+    if (chatMessages.value.length === 0) {
+      historyData.value.forEach((item) => {
+        item.isActive = false
+      })
+      expandedSearchSources.value = {}
+      expandedOnlineSearchResults.value = {}
+      onlineSearchResults.value = {}
+    }
+    
+    // 保存当前消息并清空输入框
+    const currentMessage = normalizedMessage
+    messageText.value = ''
+    
+    // 调用ReportGenerator的SSE接口
+    await handleReportGeneratorSubmit({
+      question: currentMessage,
+      windowSize: 3,
+      nResults: 10
     })
-    expandedSearchSources.value = {}
-    expandedOnlineSearchResults.value = {}
-    onlineSearchResults.value = {}
+    
+    scrollToBottom()
+  } catch (error) {
+    console.error('移动端顶层分发失败,回退到AI助手:', error)
+    showToastMessage('模块分发失败,已回退到AI助手', 2000)
+    showChat.value = true
+    await handleReportGeneratorSubmit({
+      question: normalizedMessage,
+      windowSize: 3,
+      nResults: 10
+    })
+    messageText.value = ''
+    scrollToBottom()
   }
-  
-  // 保存当前消息并清空输入框
-  const currentMessage = messageText.value
-  messageText.value = ''
-  
-  // 调用ReportGenerator的SSE接口
-  await handleReportGeneratorSubmit({
-    question: currentMessage,
-    windowSize: 3,
-    nResults: 10
-  })
-  
-  scrollToBottom()
 }
 
 
@@ -4065,7 +4151,7 @@ const scrollToBottom = () => {
 const getFunctionCards = async () => {
   try {
     console.log('开始获取功能卡片...')
-    const response = await apis.getFunctionCard({ function_type: 0 }) // 0为AI问答类型
+    const response = await apis.getFunctionCard({ function_type: 0 }) // 0为AI助手类型
     console.log('功能卡片响应:', response)
     
     if (response.statusCode === 200) {
@@ -4083,7 +4169,7 @@ const getFunctionCards = async () => {
 const getHotQuestions = async () => {
   try {
     console.log('开始获取热点问题...')
-    const response = await apis.getHotQuestion({ question_type: 0 }) // 0为AI问答类型
+    const response = await apis.getHotQuestion({ question_type: 0 }) // 0为AI助手类型
     console.log('热点问题响应:', response)
     
     if (response.statusCode === 200) {

+ 22 - 1
shudao-vue-frontend/src/views/mobile/m-ExamWorkshop.vue

@@ -751,6 +751,16 @@ const createNewTask = () => {
   })
 }
 
+const consumeAutoTask = async (task) => {
+  const normalizedTask = String(task || '').trim()
+  if (!normalizedTask) return
+
+  createNewTask()
+  await Promise.resolve()
+  pptContentDescription.value = normalizedTask
+  await generateExam()
+}
+
 // 处理历史记录点击
 const handleHistoryItem = async (historyItem) => {
   if (isGenerating.value || isLoadingHistoryItem.value) return
@@ -1063,6 +1073,7 @@ const generateExam = async () => {
       totalScore: totalScore.value,
       questionTypes: normalizedQuestionTypes,
       pptContent: finalContentBasis,
+      basisContent: finalContentBasis,
       requireBasis: false,
       ai_conversation_id: ai_conversation_id.value
     };
@@ -1208,7 +1219,8 @@ const fetchMobileExamPrompt = async (mode = 'ai') => {
     examTitle: shouldUseAutoGeneratedExamTitle(examName.value, !!pptContentDescription.value) ? '' : examName.value,
     totalScore: totalScore.value,
     questionTypes: normalizedQuestionTypes,
-    pptContent: pptContentDescription.value || ''
+      pptContent: pptContentDescription.value || '',
+      basisContent: pptContentDescription.value || ''
   };
 
   try {
@@ -2469,6 +2481,15 @@ onMounted(async () => {
     // 初始化原生导航栏(子页面模式:返回按钮执行路由后退)
     initNativeNavForSubPage(() => router.back())
     
+    const autoTask = route.query.autoTask
+    if (autoTask) {
+      router.replace({
+        path: route.path,
+        query: { ...route.query, autoTask: undefined }
+      })
+      await consumeAutoTask(autoTask)
+    }
+
     // 检查URL参数是否有historyId需要加载
     const historyId = route.query.historyId
     if (historyId) {

+ 5 - 5
shudao-vue-frontend/src/views/mobile/m-Index.vue

@@ -88,14 +88,14 @@
 
           <div class="mobile-service-item mobile-ai-chat-item" @click="goToAIChat">
             <div class="mobile-ai-chat-icon">
-              <img src="@/assets/new_index/chat-icon.png" alt="AI问答" class="mobile-chat-icon-img">
+              <img src="@/assets/new_index/chat-icon.png" alt="AI助手" class="mobile-chat-icon-img">
             </div>
             <div class="mobile-ai-chat-images">
               <img src="@/assets/new_index/chat-img-1.png" alt="对话1" class="mobile-chat-img mobile-chat-img-back">
               <img src="@/assets/new_index/chat-img-2.png" alt="对话2" class="mobile-chat-img mobile-chat-img-front">
             </div>
             <div class="mobile-service-info mobile-service-info-large">
-              <div class="mobile-service-title mobile-service-title-large">AI问答</div>
+              <div class="mobile-service-title mobile-service-title-large">AI助手</div>
               <div class="mobile-service-desc mobile-service-desc-large">AI对话助手,智能解答您的问题</div>
             </div>
             
@@ -281,7 +281,7 @@ const handleSearch = async () => {
   isSending.value = true
   try {
     console.log('搜索内容:', searchText.value)
-    // 跳转到AI问答页面,并传递搜索内容
+    // 跳转到AI助手页面,并传递搜索内容
     router.push({
       path: '/mobile/chat',
       query: {
@@ -350,7 +350,7 @@ const getFeedbackType = (type) => {
 // 推荐问题点击跳转
 const goToAIQuestion = (question) => {
   console.log('点击问题:', question)
-  // 跳转到AI问答页面,并传递问题内容
+  // 跳转到AI助手页面,并传递问题内容
   router.push({
     path: '/mobile/chat',
     query: {
@@ -890,7 +890,7 @@ onBeforeUnmount(() => {
     }
   }
   
-  // AI问答卡片(第2个)- 使用蓝色渐变背景,与Web端一致
+  // AI助手卡片(第2个)- 使用蓝色渐变背景,与Web端一致
   .mobile-ai-chat-item {
     background: #428EFE !important;
     background-image: none !important;

+ 22 - 1
shudao-vue-frontend/src/views/mobile/m-SafetyHazard.vue

@@ -402,7 +402,7 @@
 </template>
 
 <script setup>
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import MobileHeader from '@/components/MobileHeader.vue'
 import MobileHistoryDrawer from '@/components/MobileHistoryDrawer.vue'
 import DeleteConfirmModal from '@/components/DeleteConfirmModal.vue'
@@ -442,6 +442,7 @@ import pptIcon from '@/assets/Safety/13.png'
 import voiceInputIcon from '@/assets/Chat/18.png'
 
 const router = useRouter()
+const route = useRoute()
 
 const goBack = () => {
   router.go(-1)
@@ -2358,6 +2359,16 @@ const createNewTask = () => {
   })
 }
 
+const consumeAutoMessage = async (message) => {
+  const normalizedMessage = String(message || '').trim()
+  if (!normalizedMessage) return
+
+  createNewTask()
+  await nextTick()
+  messageText.value = normalizedMessage
+  await sendMessage()
+}
+
 // 处理历史记录点击
 const handleHistoryItem = async (historyItem) => {
   if (historyItem.isActive) return
@@ -2597,6 +2608,16 @@ onMounted(async () => {
     initNativeNavForSubPage(() => router.back())
     
     await getFunctionCards()
+
+    const autoMessage = route.query.autoMessage
+    if (autoMessage) {
+      router.replace({
+        path: route.path,
+        query: { ...route.query, autoMessage: undefined }
+      })
+      await consumeAutoMessage(autoMessage)
+    }
+
     console.log('✅ 移动端安全培训页面初始化完成')
   } catch (error) {
     console.error('❌ 移动端安全培训页面初始化失败:', error)