FanHong преди 2 седмици
родител
ревизия
8b19b8c975
променени са 2 файла, в които са добавени 526 реда и са изтрити 12 реда
  1. 372 0
      docs/ai-qa-api-call-chain.md
  2. 154 12
      shudao-vue-frontend/src/views/Index.vue

+ 372 - 0
docs/ai-qa-api-call-chain.md

@@ -0,0 +1,372 @@
+# AI问答模块 API 调用链路(详细版)
+
+本文档从“前端发起一次 AI 问答”开始,梳理本项目 AI 问答模块涉及的 **入站接口(前端 → shudao-chat-py)** 与 **出站接口(shudao-chat-py → 外部服务)**,并按不同交互场景给出完整调用链路与时序图。
+
+## 0. 名词与范围
+
+- **前端**:`shudao-vue-frontend`
+- **后端**:`shudao-chat-py`(FastAPI)
+- **aichat**:独立服务 `shudao-aichat`(本项目通过代理/兼容路由调用)
+- **搜索服务**:RAG 检索服务(本项目通过 HTTP 调用)
+- **Dify 工作流**:在线搜索链路使用
+- **Qwen3**:主模型接口(OpenAI 风格 `/v1/chat/completions`)
+- **DeepSeek**:备用模型(Qwen3 异常时回退)
+- **4A Auth**:外部 token 校验接口
+
+## 1. 入站接口清单(前端 → shudao-chat-py)
+
+下表只列 AI 问答相关;其他业务接口(考试工坊/PPT/隐患识别等)不在本文范围内。
+
+| 场景 | 方法&路径(前缀 `/apiv1`) | 作用 | 前端调用点(示例) | 后端实现 |
+|---|---|---|---|---|
+| 主流式问答(写 DB) | `POST /stream/chat-with-db` | SSE 流式输出 + 落库 | [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L2398-L2498) | [stream_chat_with_db](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1491-L1804) |
+| 非流式问答 | `POST /send_deepseek_message` | 一次性返回完整回答 | [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L2512-L2630)、[apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L17-L19) | [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183) |
+| 历史(列表/详情) | `GET /get_history_record` | `ai_conversation_id=0` 返回会话列表;`>0` 返回消息列表 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L38-L40) | [get_history_record](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1185-L1266) |
+| 删除对话/消息 | `POST /delete_conversation` | 软删除会话或某条消息对 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L71-L76) | [delete_conversation](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1268-L1333) |
+| 删除历史(会话级) | `POST /delete_history_record` | 软删除会话主记录 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L74-L76) | [delete_history_record](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1335-L1353) |
+| 猜你想问 | `POST /guess_you_want` | 基于某条 AI 消息生成 3 个关联问题并落库 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L116-L118) | [guess_you_want](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1811-L1885) |
+| 在线搜索 | `GET /online_search?question=...` | Qwen 提炼关键词 → Dify 工作流 → 返回摘要 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L101-L105) | [online_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1891-L1942) |
+| 保存在线搜索结果 | `POST /save_online_search_result` | 保存到 `AIMessage.search_source` | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L104-L106) | [save_online_search_result](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1944-L1970) |
+| 意图识别(独立) | `POST /intent_recognition` | 只做意图识别(可选写 DB) | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L107-L109) | [intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1976-L2072) |
+| 点赞/点踩 | `POST /like_and_dislike` | 保存 `AIMessage.user_feedback` 并处理积分 | [apis.js](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L58-L60) | [like_and_dislike](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/total.py#L226-L275) |
+| 报告兼容 SSE(Go 对齐) | `POST /report/complete-flow` | SSE:可能代理到 aichat;失败则降级本地 | [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L3744-L3830) | [complete_flow](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L187-L234) |
+| 停止报告 SSE | `POST /sse/stop` | 停止 SSE(外部 token 代理到 aichat) | [stopSSEStream](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L82-L140) | [stop_sse](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L302-L337) |
+| 回写 AI 消息 | `POST /report/update-ai-message` | 前端将整理后的内容回写到 DB(外部 token 代理到 aichat) | [updateAIMessageContent](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L147-L205) | [update_ai_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L246-L299) |
+| 附件解析 | `POST /attachments/parse` | 文件上传解析(代理到 aichat) | [parseAttachment](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/request/apis.js#L27-L29) | [parse_attachment](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L236-L244) |
+
+## 2. 出站接口清单(shudao-chat-py → 外部服务)
+
+| 目标服务 | 调用点 | 典型接口 | 用途 |
+|---|---|---|---|
+| 4A Auth | [verify_external_token](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/token.py#L175-L228) | `POST settings.auth.api_url` | 校验外部 token,解析 account 信息 |
+| 搜索服务(RAG) | [_rag_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L529-L553) | `POST settings.search.api_url` | 向量检索/知识库检索,拼接上下文 |
+| Qwen3(主模型) | [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207)、[QwenService.stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L208-L256) | `POST {settings.qwen3.api_url}/v1/chat/completions` | 主问答生成与流式输出 |
+| 意图识别模型 | [intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116) | `POST {settings.intent.api_url}/v1/chat/completions` | 识别 greeting/faq/知识库查询 等 |
+| DeepSeek(备用模型) | [DeepSeekService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/deepseek_service.py#L24-L48)、[stream_chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/deepseek_service.py#L50-L89) | `POST {settings.deepseek.api_url}/v1/chat/completions` | Qwen3 失败时回退 |
+| Dify 工作流(在线搜索) | [online_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1891-L1942) | `POST settings.dify.workflow_url` | 关键词 → 工作流 → 摘要 |
+| aichat 服务(代理) | [AIChatProxy](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/aichat_proxy.py#L13-L207) | `POST {settings.aichat.api_url}/*` | 兼容 Go 版报告 SSE、附件解析、停止、回写等 |
+
+## 3. 认证与用户注入(所有链路的共同前置)
+
+### 3.1 中间件入口
+
+后端在 [combined_middleware](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/main.py#L33-L107) 内做:
+
+1. 从 `Authorization` / `token` / `Token` 读取 token
+2. 调用 [verify_token](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/token.py#L230-L243)
+3. 校验成功后写入 `request.state.user`
+4. 路由层统一通过 `request.state.user` 获取当前用户
+
+### 3.2 外部 token 的校验链路
+
+外部 token:
+
+1. `POST settings.auth.api_url` 验证 token 合法性
+2. 从返回数据取 `accountID / name / role / exp`
+3. 调用 [_resolve_external_user_id](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/token.py#L149-L173) 映射到本地 `UserData.id`(用于复用 `AIConversation/AIMessage`)
+
+## 4. 链路 A:主流式问答(`POST /stream/chat-with-db`)
+
+这是“边生成边显示 + 保留历史”的标准链路。
+
+### 4.1 请求形态
+
+请求体(核心字段):
+
+```json
+{
+  "message": "用户问题",
+  "ai_conversation_id": 0,
+  "business_type": 0,
+  "online_search_content": ""
+}
+```
+
+响应:`text/event-stream`,每条事件形态为:
+
+- 首次:`data: {"type":"initial","ai_conversation_id":123,"ai_message_id":456}\n\n`
+- 中间:`data: <文本chunk>\n\n`(chunk 内换行会被转义为 `\\n`)
+- 结束:`data: [DONE]\n\n`
+
+实现见 [stream_chat_with_db](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1491-L1804)。
+
+### 4.2 详细调用链路(含 DB 与外部 API)
+
+1. 前端 → `POST /apiv1/stream/chat-with-db`
+2. 中间件:
+   - 校验 token(本地 / 外部 4A)
+   - 写入 `request.state.user`
+3. 路由进入 `stream_chat_with_db`:
+   - 创建/复用 `AIConversation`
+   - 写入 `AIMessage(type=user)`
+   - 写入 `AIMessage(type=ai, content='')` 占位
+   - SSE 推送 `initial`(返回会话/消息 ID)
+4. 触发 RAG:
+   - `POST settings.search.api_url`,请求体 `{"query": message, "n_results": top_k}`
+   - 将结果拼成 `rag_context`
+5. 拼装 Prompt:
+   - `final_answer`(含 userMessage、rag_context、historyContext、可选 online_search_content)
+6. 调用主模型流式生成:
+   - `POST {settings.qwen3.api_url}/v1/chat/completions`(`stream=true`)
+   - 若 Qwen3 上游错误或网络错误,回退到 DeepSeek 流式接口
+7. 可选:思考过程摘要(二次模型调用)
+   - 若流中出现 `<think>...</think>` 且摘要开关启用,会调用 [summarize_thinking_content](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/thinking_summary.py#L321-L397)
+   - 该摘要本质上是一次 `qwen_service.chat()`(仍会触发 DeepSeek 回退逻辑)
+8. SSE 持续返回给前端
+9. 流结束后回写数据库:
+   - 更新 `AIMessage(type=ai).content = full_response`
+   - 更新 `AIConversation.updated_at / business_type / content 预览`
+10. SSE 返回 `[DONE]`
+
+### 4.3 时序图(主流式问答)
+
+```mermaid
+sequenceDiagram
+    participant FE as 前端
+    participant MW as 中间件
+    participant AU as 4A Auth
+    participant RT as chat.py:/stream/chat-with-db
+    participant DB as MySQL
+    participant SS as 搜索服务
+    participant QW as Qwen3
+    participant DS as DeepSeek(回退)
+
+    FE->>MW: POST /apiv1/stream/chat-with-db
+    MW->>AU: (可选) POST settings.auth.api_url
+    AU-->>MW: valid + account
+    MW-->>RT: request.state.user
+
+    RT->>DB: upsert AIConversation
+    RT->>DB: insert user AIMessage
+    RT->>DB: insert ai placeholder AIMessage
+    RT-->>FE: SSE data: {"type":"initial",...}
+
+    RT->>SS: POST settings.search.api_url
+    SS-->>RT: docs
+    RT->>QW: POST /v1/chat/completions (stream=true)
+    alt Qwen3 失败
+        RT->>DS: POST /v1/chat/completions (stream=true)
+        DS-->>RT: chunk...
+    else Qwen3 正常
+        QW-->>RT: chunk...
+    end
+    RT-->>FE: SSE chunk...
+    RT->>DB: update ai_message.content
+    RT->>DB: update conversation snapshot
+    RT-->>FE: SSE [DONE]
+```
+
+## 5. 链路 B:非流式问答(`POST /send_deepseek_message`,`business_type=0`)
+
+这条链路更像“同步 RPC”:一次请求直接拿到完整回答。
+
+### 5.1 调用链路(AI问答分支)
+
+实现入口见 [send_deepseek_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L893-L1183)。
+
+1. 前端 → `POST /apiv1/send_deepseek_message`
+2. 中间件鉴权,写入 `request.state.user`
+3. 创建/复用 `AIConversation`
+4. 意图识别:
+   - 调用 [QwenService.intent_recognition](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L73-L116)
+   - 出站:`POST {settings.intent.api_url}/v1/chat/completions`
+5. 若意图命中“知识库查询/技术咨询”,触发 RAG:
+   - `POST settings.search.api_url`
+6. 组装 `final_answer` Prompt
+7. 非流式生成:
+   - `POST {settings.qwen3.api_url}/v1/chat/completions`(`stream=false`)
+   - Qwen3 可回退 DeepSeek(见 [QwenService.chat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L117-L207))
+8. 可选:思考过程摘要(再次调用 `qwen_service.chat()`)
+9. 返回 `{statusCode: 200, data: { reply/content/... } }`
+
+### 5.2 时序图(非流式问答)
+
+```mermaid
+sequenceDiagram
+    participant FE as 前端
+    participant MW as 中间件
+    participant RT as chat.py:/send_deepseek_message
+    participant IM as Intent模型
+    participant SS as 搜索服务
+    participant QW as Qwen3
+    participant DS as DeepSeek(回退)
+    participant DB as MySQL
+
+    FE->>MW: POST /apiv1/send_deepseek_message
+    MW-->>RT: request.state.user
+    RT->>DB: upsert AIConversation
+    RT->>IM: POST intent /v1/chat/completions
+    IM-->>RT: intent_type
+    opt 命中知识库查询
+        RT->>SS: POST settings.search.api_url
+        SS-->>RT: docs
+    end
+    RT->>QW: POST qwen3 /v1/chat/completions (stream=false)
+    alt Qwen3 失败
+        RT->>DS: POST deepseek /v1/chat/completions
+        DS-->>RT: answer
+    else 正常
+        QW-->>RT: answer
+    end
+    RT-->>FE: JSON response(一次性)
+```
+
+## 6. 链路 C:报告兼容 SSE(`POST /report/complete-flow`)
+
+这条链路是为了对齐 Go 版本接口格式,前端在“报告生成模式”会调用它(见 [Chat.vue](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/views/Chat.vue#L3744-L3830))。
+
+后端入口: [complete_flow](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L187-L234)
+
+### 6.1 两种路由策略(关键)
+
+兼容路由会根据 token 类型决定“走 aichat 代理”还是“本地降级”:
+
+- **外部 token(非本地 token)**:优先代理到 aichat  
+  判断逻辑: [should_proxy_to_aichat](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L39-L46)
+- **本地 token** 或 **代理失败**:降级为本地流式 `POST /stream/chat-with-db` 再转换为兼容事件流  
+  降级逻辑: [fallback_to_local_stream](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L93-L185)
+
+### 6.2 外部 token:代理到 aichat
+
+链路:
+
+1. 前端 → `POST /apiv1/report/complete-flow`(SSE)
+2. `report_compat.py` 构造转发请求体,规范化 `ai_conversation_id`(新会话必须是 0)
+3. 调用代理: [aichat_proxy.proxy_sse](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/aichat_proxy.py#L32-L95)
+4. 后端将 aichat SSE 原样转发给前端(chunk 级别)
+
+其中 aichat 的 base URL 来自:
+
+- `settings.aichat.api_url`(见 [AIChatConfig](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/config.py#L66-L69))
+
+### 6.3 本地 token:降级走本地流式并“翻译事件”
+
+链路:
+
+1. 前端 → `POST /apiv1/report/complete-flow`(SSE)
+2. 进入 `fallback_to_local_stream`
+3. 后端内部再次发起 HTTP 调用:
+   - `POST http://127.0.0.1:{settings.app.port}/apiv1/stream/chat-with-db`
+4. 从本地 SSE 中解析:
+   - 读取 `initial` 获取 `ai_conversation_id / ai_message_id`
+   - 将后续 chunk 累积为 `full_response`
+5. 当收到 `[DONE]` 时,输出兼容事件:
+   - `data: {"type":"online_answer","content": full_response, ...}\n\n`
+   - `data: {"type":"completed"}\n\n`
+
+### 6.4 时序图(报告兼容 SSE)
+
+```mermaid
+sequenceDiagram
+    participant FE as 前端
+    participant RC as report_compat:/report/complete-flow
+    participant AP as aichat_proxy
+    participant AC as aichat 服务
+    participant LC as 本地 chat-with-db
+
+    FE->>RC: POST /apiv1/report/complete-flow (SSE)
+    alt 外部 token
+        RC->>AP: proxy_sse("/report/complete-flow")
+        AP->>AC: POST {settings.aichat.api_url}/report/complete-flow (SSE)
+        AC-->>AP: SSE chunk...
+        AP-->>FE: SSE chunk...(原样转发)
+    else 本地 token 或代理失败
+        RC->>LC: POST http://127.0.0.1:{port}/apiv1/stream/chat-with-db (SSE)
+        LC-->>RC: SSE initial + chunk... + [DONE]
+        RC-->>FE: data: {"type":"online_answer",...}
+        RC-->>FE: data: {"type":"completed"}
+    end
+```
+
+## 7. 在线搜索链路(`GET /online_search` + `POST /save_online_search_result`)
+
+在线搜索用于补充回答来源或做联网摘要。
+
+### 7.1 `GET /online_search`
+
+实现入口: [online_search](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1891-L1942)
+
+调用链路:
+
+1. 前端 → `GET /apiv1/online_search?question=...`
+2. 后端用 Qwen 提炼关键词:
+   - [extract_keywords](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/qwen_service.py#L45-L71)
+3. 后端调用 Dify workflow:
+   - `POST settings.dify.workflow_url`
+   - `Authorization: Bearer settings.dify.auth_token`
+4. 返回 `{keywords, result}` 给前端
+
+### 7.2 `POST /save_online_search_result`
+
+实现入口: [save_online_search_result](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1944-L1970)
+
+调用链路:
+
+1. 前端提交 `{ai_message_id, search_result}`
+2. 后端更新 `AIMessage.search_source`
+
+## 8. “猜你想问”链路(`POST /guess_you_want`)
+
+实现入口: [guess_you_want](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/chat.py#L1811-L1885)
+
+调用链路:
+
+1. 前端提交 `{ai_message_id}`
+2. 后端读取该条 AI 消息内容
+3. 后端加载 `guess_questions` prompt 并调用 `qwen_service.chat()`
+4. 将结果解析为 `questions[]`,写入 `AIMessage.guess_you_want`
+5. 返回 `{ai_message_id, questions}`
+
+## 9. 附件解析链路(`POST /attachments/parse`)
+
+实现入口: [parse_attachment](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L236-L244)
+
+调用链路:
+
+1. 前端上传文件(multipart/form-data)
+2. 后端校验 `request.state.user` 不为空
+3. 代理到 aichat:
+   - [proxy_upload](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/services/aichat_proxy.py#L147-L192)
+   - `POST {settings.aichat.api_url}/attachments/parse`
+4. 返回解析结果给前端(通常含抽取文本/attachment_id 等)
+
+## 10. 停止 SSE 与回写消息(报告模式常用)
+
+### 10.1 `POST /sse/stop`
+
+前端调用: [stopSSEStream](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L82-L140)
+
+后端实现: [stop_sse](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L302-L337)
+
+路由策略:
+
+- 外部 token:代理到 aichat 的 `/sse/stop`
+- 本地 token:本地直接返回成功(不做真实中断)
+
+### 10.2 `POST /report/update-ai-message`
+
+前端调用: [updateAIMessageContent](file:///Users/fanhong/UGIT/shudao-main/shudao-vue-frontend/src/utils/api.js#L147-L205)
+
+后端实现: [update_ai_message](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/routers/report_compat.py#L246-L299)
+
+路由策略:
+
+- 外部 token:代理到 aichat 的 `/report/update-ai-message`
+- 本地 token:直接更新 `AIMessage.id = ai_message_id` 的 `content`
+
+## 11. 相关配置入口(定位“外部服务地址”用)
+
+统一配置类定义在:
+
+- [config.py](file:///Users/fanhong/UGIT/shudao-main/shudao-chat-py/utils/config.py)
+
+通常运行时值来自 `shudao-chat-py/config.yaml`(以及环境变量覆盖),关键项包括:
+
+- `auth.api_url`
+- `search.api_url`
+- `qwen3.api_url` / `qwen3.model`
+- `intent.api_url` / `intent.model`
+- `deepseek.api_url`
+- `dify.workflow_url` / `dify.workflow_id` / `dify.auth_token`
+- `aichat.api_url`
+

+ 154 - 12
shudao-vue-frontend/src/views/Index.vue

@@ -1,5 +1,23 @@
 <template>
 <template>
   <div class="container">
   <div class="container">
+    <!-- 停机维护公告弹窗 (新版样式) -->
+    <div class="maintenance-overlay" v-if="showMaintenance">
+      <div class="maintenance-banner">
+        <div class="mb-top-line"></div>
+        <div class="mb-content">
+          <h2 class="mb-title">系统升级维护公告</h2>
+          <p class="mb-desc">为进一步提升系统服务质量与运行稳定性,我单位定于以下时段进行系统升级维护,<br>届时部分业务服务将暂时无法访问,敬请谅解。</p>
+          <p class="mb-time">预计恢复时间2026年5月4日12:00</p>
+          <div class="mb-status-wrapper">
+            <div class="mb-status">
+              <span class="status-dot"></span>
+              维护中 · 部分服务暂不可用
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
     <!-- 顶部logo -->
     <!-- 顶部logo -->
     <div class="header">
     <div class="header">
       <div class="logo">
       <div class="logo">
@@ -220,6 +238,7 @@ const {
 } = useSpeechRecognition()
 } = useSpeechRecognition()
 
 
 // 响应式数据
 // 响应式数据
+const showMaintenance = ref(true) // 控制停机维护公告显示
 const searchText = ref('')
 const searchText = ref('')
 const showFeedbackModal = ref(false)
 const showFeedbackModal = ref(false)
 const isSending = ref(false)
 const isSending = ref(false)
@@ -473,10 +492,7 @@ const goToHazardDetection = () => {
 }
 }
 
 
 const goToSafetyTraining = () => {
 const goToSafetyTraining = () => {
-  router.push({
-    path: '/chat',
-    query: { mode: 'safety-training' }
-  })
+  router.push('/safety-hazard')
 }
 }
 
 
 const goToAIChat = () => {
 const goToAIChat = () => {
@@ -484,17 +500,11 @@ const goToAIChat = () => {
 }
 }
 
 
 const goToExamWorkshop = () => {
 const goToExamWorkshop = () => {
-  router.push({
-    path: '/chat',
-    query: { mode: 'exam-workshop' }
-  })
+  router.push('/exam-workshop')
 }
 }
 
 
 const goToAIWriting = () => {
 const goToAIWriting = () => {
-  router.push({
-    path: '/chat',
-    query: { mode: 'ai-writing' }
-  })
+  router.push('/ai-writing')
 }
 }
 
 
 const goToPolicyDocument = () => {
 const goToPolicyDocument = () => {
@@ -550,6 +560,138 @@ onUnmounted(() => {
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
 
 
+/* 停机维护公告弹窗 (新版样式) */
+.maintenance-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.4);
+  backdrop-filter: blur(4px);
+  z-index: 9999;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  animation: fadeIn 0.3s ease-out;
+}
+
+.maintenance-banner {
+  background: linear-gradient(180deg, #183582 0%, #0d1e57 100%);
+  border-radius: 16px;
+  overflow: hidden;
+  box-shadow: 0 20px 50px rgba(13, 30, 87, 0.5);
+  position: relative;
+  width: 90%;
+  max-width: 680px;
+  animation: scaleUp 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
+}
+
+.mb-close-btn {
+  position: absolute;
+  top: 12px;
+  right: 16px;
+  font-size: 28px;
+  color: rgba(255, 255, 255, 0.6);
+  cursor: pointer;
+  line-height: 1;
+  transition: all 0.2s;
+  z-index: 10;
+}
+
+.mb-close-btn:hover {
+  color: #fff;
+  transform: scale(1.1);
+}
+
+.mb-top-line {
+  height: 6px;
+  background-color: #ef4444; /* 顶部红线 */
+  width: 100%;
+}
+
+.mb-content {
+  padding: 40px 30px 45px 30px;
+  text-align: center;
+  color: #ffffff;
+}
+
+.mb-dept {
+  font-size: 14px;
+  color: rgba(255, 255, 255, 0.7);
+  margin-bottom: 20px;
+  letter-spacing: 2px;
+}
+
+.mb-title {
+  font-size: 38px;
+  font-weight: bold;
+  margin: 0 0 28px 0;
+  letter-spacing: 2px;
+  font-family: "STZhongsong", "SimSun", "Songti SC", serif; /* 宋体/明朝体风格 */
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.mb-desc {
+  font-size: 15px;
+  color: rgba(255, 255, 255, 0.9);
+  line-height: 1.8;
+  margin: 0 0 16px 0;
+  letter-spacing: 0.5px;
+}
+
+.mb-time {
+  font-size: 15px;
+  color: rgba(255, 255, 255, 0.9);
+  margin: 0 0 36px 0;
+  letter-spacing: 0.5px;
+  font-weight: 500;
+}
+
+.mb-status-wrapper {
+  display: flex;
+  justify-content: center;
+}
+
+.mb-status {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #fdf8eb; /* 浅黄色背景 */
+  color: #b45309; /* 橙色文字 */
+  padding: 12px 32px;
+  border-radius: 30px;
+  font-size: 16px;
+  font-weight: bold;
+  gap: 10px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+  letter-spacing: 1px;
+}
+
+.status-dot {
+  width: 10px;
+  height: 10px;
+  background-color: #d1a374; /* 圆点颜色 */
+  border-radius: 50%;
+  animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+  0% { opacity: 1; transform: scale(1); }
+  50% { opacity: 0.5; transform: scale(0.8); }
+  100% { opacity: 1; transform: scale(1); }
+}
+
+@keyframes fadeIn {
+  from { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes scaleUp {
+  from { opacity: 0; transform: scale(0.9); }
+  to { opacity: 1; transform: scale(1); }
+}
+
 .container {
 .container {
   width: 100%;
   width: 100%;
   height: 100vh;
   height: 100vh;