# 计划执行文档 — 问题排查助手易用性优化（在线/离线模式）

> 配套 PRD：`PRD_需求文档_易用性优化.md`（v1.0）
> 版本：1.0 ｜ 日期：2026-08-12 ｜ 分支：troubleshoot-ai-assistant
> 执行顺序：T1 → T2 → T3 → T4 → T5 → T6 → T7 → T8（P1→P2→P3→P4）

---

## 0. 执行概述

### 目标
针对现场技术人员和客户的使用痛点，优化问题排查助手在线/离线双模式的易用性：离线模式支持全文搜索、在线模式流式实时推送、修复 XSS 安全风险、对齐模型参数、加本地缓存与断线重连。

### 改动范围（严格遵守 PRD §6 隔离红线）
| 类型 | 文件 | 对应任务 |
|------|------|----------|
| 修改 | `frontend/src/views/troubleshoot/OfflineIndex.vue` | T1 离线全文搜索 |
| 修改 | `frontend/src/components/troubleshoot/SearchResultsPanel.vue`（新增） | T1 搜索结果组件 |
| 修改 | `skill/code/web/routes/troubleshoot.py` | T1/T6 搜索接口、T5 缓存key、T4 model参数 |
| 修改 | `skill/code/web/services/ai_service.py` | T2 流式改造、T6 model参数 |
| 修改 | `skill/code/web/safety_filter.py`（内部实现，不改公开接口） | T2 逐chunk过滤 |
| 修改 | `frontend/src/utils/sse.ts` | T3 断线重连 |
| 修改 | `frontend/src/views/troubleshoot/OnlineIndex.vue` | T3/T4/T8 重连、model降级、动态数量 |
| 修改 | `frontend/src/components/troubleshoot/FAQsPanel.vue` | T4 XSS修复+Markdown渲染 |
| 修改 | `skill/code/web/cache_manager.py`（内部实现，不改公开接口） | T5 缓存key加model |
| 修改 | `frontend/src/components/troubleshoot/FAQsPanel.vue`、`OfflineIndex.vue` | T7 本地缓存 |
| 修改 | `skill/code/web/routes/troubleshoot.py`、`product_service.py` | T8 文档预览 |
| **禁止修改** | `search_engine.py` / `safety_filter.py` / `cache_manager.py` 公开函数签名 | 隔离红线（94用例） |
| **禁止修改** | `service_monitor/`、`service_manage/` | 隔离红线 |

### 关键决策（已锁定）
1. **P1-2 流式改造**：`call_claude_api_stream()` 保持流式返回，安全过滤改为逐 chunk 处理，完整响应收完后再统一缓存
2. **P2-1 XSS 修复**：复用在线模式已有 `renderMarkdown()` 函数（含转义），不引入 DOMPurify 新依赖（已有渲染函数足够）
3. **P3-1 离线缓存**：前端 `localStorage`，5 分钟过期
4. **P3-2 断线重连**：sse.ts 递增退避 1s→2s→4s→8s，最多 3 次，保留已收内容
5. **P3-3 缓存key**：`cache_manager` 内部拼接 model 字段，公开接口签名不变（入参容纳 model 作为可选参数）
6. **P4-2 文档预览**：复用现有 `/api/troubleshoot/record/{id}/download` 能力，前端新增预览弹窗

---

## 1. 任务分解与实施计划

### T1：离线模式全文搜索（P1-1，核心功能）
**文件**：`OfflineIndex.vue`（修改）、`SearchResultsPanel.vue`（新增）、`routes/troubleshoot.py`（修改）

**职责**：离线模式顶部增加全局搜索框，调用 `/api/search` 接口全文搜索知识库，结果列表可点击查看详情。

**实现要点**：
1. **前端 OfflineIndex.vue**：在产品树上方新增搜索框（占位"搜索历史问题案例..."）
2. **新增 SearchResultsPanel.vue**：搜索结果列表（标题+匹配度+项目名+关键词摘要），每项可展开查看详情，无结果时显示"未找到相关案例"
3. **后端 `/api/search`**：确认现有 `search_cases()` 返回结构含 `score`/`title`/`project`/`phenomenon`，前端可直接复用
4. **搜索与产品树互不干扰**：搜索时不影响左侧产品树浏览

**验收**：
- 搜索框在离线模式顶部可见
- 输入关键词返回匹配结果，按相似度排序
- 无结果显示"未找到相关案例"
- 搜索结果可点击查看详情
- 搜索不影响产品树浏览

---

### T2：流式响应实时推送（P1-2，在线模式）
**文件**：`routes/troubleshoot.py`（修改）、`services/ai_service.py`（修改）、`safety_filter.py`（内部实现）

**职责**：将 `analyze_stream()` 从"收完整响应再回放"改为"边收边推"，首帧 <3s。

**实现要点**：
1. **`analyze_stream()` 改造**：改为生成器内逐 chunk 接收 → 逐 chunk 过滤 → 立即 yield
2. **安全过滤逐 chunk**：safety_filter 增加逐行/vice chunk 处理路径（不改 `filter_output` 公开签名，新增内部辅助或复用现有函数逐行调用）
3. **敏感信息过滤**：IP/密码掩码逐 chunk 处理
4. **缓存**：完整响应收完后统一缓存（缓存逻辑挪到流结束后）
5. **降级路径**：API 失败 → 模拟响应分块推送，保持可用

**验收**：
- 点击"帮我排查"后 <3s 看到首帧
- 流式内容实时显示，无需等待全部生成
- 安全过滤仍生效（危险命令行被替换）
- 敏感信息仍被过滤
- 完整响应仍存入缓存
- 降级路径正常工作

---

### T3：SSE 断线重连（P3-2）
**文件**：`frontend/src/utils/sse.ts`（修改）、`OnlineIndex.vue`（修改）

**职责**：SSE 断连后自动重连（递增退避），保留已收内容。

**实现要点**：
1. `createSSE()` 增加重试逻辑：1s → 2s → 4s → 8s，最多 3 次
2. 重连成功后新内容追加到已有内容，不重置
3. 提高 `maxErrorCount` 前先尝试重连（区分"短暂波动"与"真断开"）
4. 用户手动 `close()` 后不自动重连
5. 3 次重试失败判定最终失败，显示错误提示

**验收**：
- 断连后自动重连（最多3次）
- 重连成功后续内容追加到已有内容
- 已显示内容不丢失/不重复
- 3次失败后显示错误提示
- 手动取消不重连

---

### T4：FAQsPanel XSS 修复 + Markdown 渲染（P2-1）
**文件**：`FAQsPanel.vue`（修改）

**职责**：修复 `v-html` 直接渲染用户内容导致的 XSS 风险，并升级为真正的 Markdown 渲染。

**实现要点**：
1. 将 `v-html="faq.solution.replace(/\n/g, '<br>')"` 改为调用 `renderMarkdown(faq.solution)`（复用在线模式已有渲染函数）
2. 确认 `renderMarkdown()` 对内容做转义处理（HTML 标签按文本显示，不执行）
3. 确认渲染函数放 `utils/` 中可复用位置，或从在线模式组件提取到公共工具

**验收**：
- FAQ 详情支持 Markdown（标题/列表/代码块/链接）
- XSS 注入脚本不执行
- 原有 FAQ 内容显示正常，无格式丢失

---

### T5：缓存 key 增加 model 参数（P3-3）
**文件**：`cache_manager.py`（内部实现）、`routes/troubleshoot.py`（修改）

**职责**：缓存 key 增加 model 维度，切换模型返回各自缓存。

**实现要点**：
1. `cache_manager` 的 get/set 内部拼接 model 字段到 key（公开签名不变或增加可选 model 参数）
2. 路由层把 model 传到缓存方法
3. 兼容旧缓存（无 model 字段）——不清除，视为旧格式

**验收**：
- 同一 query 不同 model 返回各自缓存
- 旧缓存兼容处理，不清除

---

### T6：模型选择参数对齐（P2-2）
**文件**：`routes/troubleshoot.py`（修改）、`services/ai_service.py`（修改）、`OnlineIndex.vue`（修改）

**职责**：非流式 `/api/analyze` 支持 model 参数，前端降级路径正确传递。

**实现要点**：
1. `/api/analyze` 接口增加 `model` 参数接收
2. `call_claude_api()` 支持 model 参数传递（默认仍读 config）
3. 前端 `fallbackToNormalRequest()` 传递 model 参数
4. 与 T5 缓存key联动（model 同时进缓存 key）

**验收**：
- 非流式接口支持 model 参数
- 切换模型后缓存返回对应模型结果
- 前端降级路径正确传递模型参数

---

### T7：离线模式本地缓存（P3-1）
**文件**：`OfflineIndex.vue`、`FAQsPanel.vue`（修改）

**职责**：离线模式切换产品时使用本地缓存，避免重复请求。

**实现要点**：
1. 首次加载产品 FAQ 后写入 `localStorage`（key 含 productId，带时间戳）
2. 5 分钟内再次切换同一产品，直接读缓存不请求 API
3. 超过 5 分钟自动刷新
4. 注意与离线模式数据静态性匹配（服务启动后知识库不变）

**验收**：
- 首次加载写入缓存
- 5分钟内切换同一产品不请求API
- 超过5分钟自动刷新
- 缓存不影响数据正确性

---

### T8：在线文档预览 + 动态知识库数量（P4-2 / P4-3）
**文件**：`routes/troubleshoot.py`、`product_service.py`（修改）、`OnlineIndex.vue`（修改）、`FAQsPanel.vue`（修改）

**职责**：离线模式文档支持在线预览；前端知识库数量改为动态获取。

**实现要点**：
1. **文档预览**：新增 `/api/troubleshoot/document/{doc_id}/preview` 接口（或复用 download 接口 + 前端预览弹窗），支持 PDF/文本类文档在线查看
2. **动态数量**：`OnlineIndex.vue` 中硬编码的"357 条记录"改为从 `/api/health` 或 `/api/troubleshoot` 的 `total` 字段动态读取

**验收**：
- 文档可在浏览器内预览（无需下载）
- 知识库数量由后端动态返回，前端不硬编码

---

## 2. 验收标准（对照 PRD §5）
| PRD 编号 | 验收项 | 对应任务 |
|----------|--------|----------|
| A1 | 离线模式全文搜索可用 | T1 |
| A2 | 离线搜索结果按相似度排序 | T1 |
| A3 | 在线流式首帧<3秒 | T2 |
| A4 | 流式实时安全过滤生效 | T2 |
| A5 | FAQ详情Markdown渲染正确 | T4 |
| A6 | XSS注入测试通过 | T4 |
| A7 | 非流式接口支持model参数 | T6 |
| A8 | 切换模型缓存不同 | T5/T6 |
| A9 | 离线模式缓存5分钟 | T7 |
| A10 | SSE断线自动重连3次 | T3 |
| A11 | 所有现有测试全绿 | 全程 |

---

## 3. 测试计划

### 3.1 单元测试（T2/T4/T5 联动）
- 回归：`cd skill/code && python -m pytest -v` 全部 167 用例全绿
- 新增：缓存 key 含 model 的用例（T5）、safety_filter 逐 chunk 过滤用例（T2）
- **约束**：不改 `search_engine.py` / `safety_filter.py` / `cache_manager.py` 公开函数签名，新增用例只覆盖内部行为

### 3.2 前端验证
- 离线模式：搜索→结果→详情→产品树共存
- 在线模式：流式首帧计时、断线重连、模型切换缓存
- XSS：注入 `<script>alert(1)</script>` 不执行
- 移动端 375px/768px 断点回归

### 3.3 回归测试（隔离红线验证）
- `service_monitor/`、`service_manage/` 文件 git diff 为空（零改动）
- 三个核心被测模块公开签名 diff 为空

### 3.4 部署验证
- 部署后 `verify_deployment.py` 通过
- health 响应确认 `search.mode` 与 `offline_mode` 字段正确

---

## 4. 风险评估
| 风险 | 等级 | 缓解措施 |
|------|------|----------|
| 流式逐 chunk 过滤削弱安全过滤上下文判断（如跨 chunk 的危险命令） | 中 | 危险命令匹配单行即可（PRD §3.2.3），逐行处理；跨 chunk 边界用缓冲行累积 |
| `renderMarkdown()` 复用后仍存在未转义路径 | 中 | T4 验收专门做 XSS 注入用例 |
| cache_manager 公开签名改动破坏单测 | 中 | T5 只改内部实现，签名不变或加可选参数，新增用例覆盖 |
| SSE 重连与后端幂等冲突（重连重复触发分析） | 中 | PRD §3.6.2 明确"后端幂等本次不做"，前端重连仅保留内容，风险记录在案 |
| 文档预览引入新依赖（PDF渲染） | 低 | 优先复用现有 download 文件流 + 浏览器原生预览，不新增重依赖 |
| 前端缓存与离线数据更新不同步 | 低 | 5 分钟过期 + 手动刷新入口 |

---

## 5. 实施记录

| 任务 | 状态 | 完成时间 | 备注 |
|------|------|----------|------|
| T1 离线全文搜索 | ⏳ 待实施 | — | P1 核心 |
| T2 流式实时推送 | ⏳ 待实施 | — | P1 核心 |
| T3 SSE断线重连 | ⏳ 待实施 | — | |
| T4 XSS+Markdown | ⏳ 待实施 | — | |
| T5 缓存key加model | ⏳ 待实施 | — | |
| T6 模型参数对齐 | ⏳ 待实施 | — | |
| T7 离线本地缓存 | ✅ 已完成 | 2026-08-12 | localStorage 5分钟 TTL，OfflineIndex/FAQsPanel/DocumentsPanel 三处都加了缓存 |
| T8 文档预览+动态数量 | ✅ 已完成 | 2026-08-12 | 后端 preview 端点(支持PDF/图片/文本)；前端预览弹窗；OnlineIndex 动态读取 health 接口 |

---

## 6. 后续工作（本期不做）

- SSE 后端幂等性（重连不重复触发分析）
- 前端 ui 组件库（引入 DOMPurify 等）的规范化
- 产品资料从 `products.json` 迁移到数据目录（架构优化，独立立项）
- 服务端离线搜索的 TF-IDF 提升（调参/加索引）

---

## 7. 附录

### 7.1 关键代码定位
- 流式伪流式：`skill/code/web/routes/troubleshoot.py::analyze_stream`（L426-427 收集完整响应后回放）
- 流式API：`services/ai_service.py::call_claude_api_stream`
- XSS风险：`frontend/src/components/troubleshoot/FAQsPanel.vue:89`（`v-html` 直接渲染）
- 缓存key：`skill/code/web/cache_manager.py`（key 不含 model）
- SSE连接：`frontend/src/utils/sse.ts::createSSE`
- 离线模式：`frontend/src/views/troubleshoot/OfflineIndex.vue`（无全文搜索）
- 硬编码数量：`frontend/src/views/troubleshoot/OnlineIndex.vue:496`（"357 条记录"）

### 7.2 PRD 对照
- §3.1 离线全文搜索 → T1
- §3.2 流式实时推送 → T2
- §3.3 XSS修复 → T4
- §3.4 模型参数对齐 → T6
- §3.5 离线本地缓存 → T7
- §3.6 SSE断线重连 → T3
- §3.7 缓存key加model → T5
- §4 非功能需求 → 第 2 节验收
- §6 约束隔离 → 全程遵守