# PRD(增量草案) — 选项选择持久化 v1.1 > **REQ 编号**: REQ-通用-005 > **版本**: v1.1-**DRAFT**(草案,待用户确认范围,不替代 v1.0) > **日期**: 2026-08-02 > **作者**: 许清楚(PM) > **状态**: 🟡 草案(v1.0 已批准,v1.1 仅增不删) > **基线**: v1.0 PRD(179 行,9 章节,已批准)+ v1.0 技术方案(523 行,11 章节)+ v1.0 测试用例(35 条)+ Dify v3 DRAFT > **关联**: > - 基线 PRD:`docs/01-产品文档/00-产品规划/PRD-REQ-通用-005-选项选择持久化-v1.0.md` > - 基线技术方案:`docs/02-技术文档/技术架构/技术方案-REQ-通用-005-选项选择持久化-v1.0.md` > - 基线测试用例:`docs/03-测试文档/03-功能测试用例/TC-REQ-通用-005-选项选择持久化-v1.0.md` > - Dify v3 DRAFT:`docs/02-技术文档/实现配置/dify_dsl/itdesk_main_v3_feedback-vars_DRAFT.yml` > - 原型:无(v1.0 暂无独立原型;UX 调整需重开) --- ## 1. 增量背景(Why v1.1) v1.0 已部署并通过 35 条测试用例验收,但 2026-08-02 用户实测暴露 3 个持续 Bug + 1 个新需求,**Dify v3 工作流(用户尚未导入)** 仍未落地,导致"AI 跟进选项"语义无法端到端验证。 | 触发事件 | 观察 | 业务影响 | |---|---|---| | 2026-08-02 17:45 用户反馈 | **Bug 4**:员工提问后看到 AI 回答,坐席端实时收单,但**员工端需刷新才显示** | 员工体感卡顿,怀疑"卡死" | | 2026-08-02 17:45 用户反馈 | **Bug 5**:选选项后,**先看到 AI 思考占位 → 后同时出现"答案 + 上一个选择"** | 时序错位,破坏确定性信任 | | 2026-08-02 17:45 用户问询 | **Bug 6**:曾发生"**页面刷新后选择消失**",询问是否真解决 | 会话连续性 + 审计可追溯 | | 2026-08-02 17:45 用户强调 | **Req 7**:员工选选项后,**效果实时同步到坐席端(< 100ms)** | 紧急报修场景坐席分诊效率 | **v1.1 目标**:在不动 v1.0 6 项产品决策(前缀:撤回/重选、幂等键、跨卡归属、敏感词、Dify 语义、转人工快照)的前提下,修复时序与持久化体感问题,并验证 v1.0 的"刷新保留"是否真达成。 --- ## 2. 与 v1.0 的差异(What Changed) | 编号 | v1.0 内容 | v1.1 增量 | 优先级 | |---|---|---|---| | **Bug 4** | (v1.0 无) | 员工端 AI 回答延迟显示:消息已落库 + WS 已广播坐席端,但**员工端需刷新才显示** | **P0** | | **Bug 5** | (v1.0 无) | 选选项时序错位:先 AI 思考占位 → 后同时出现"答案 + 上一个选择" | **P1** | | **Bug 6** | v1.0 §4.1 / AC-01 已要求"落库 option_select + REST 仍可见" | 显式写入"刷新后必须保留选择历史"作为**P0 验收**;消除 v1.0 隐含歧义 | **P0** | | **Req 7** | v1.0 §4.2 + §3.1 #2 仅要求"坐席可实时可见" | **新增显式时延指标**:坐席端 < 100ms 同步;坐席不在线时恢复后立即可见 | **P0**(用户强调) | > v1.0 的 6 项产品决策(撤回/重选、5 秒 UUID 幂等、跨卡归属、敏感词 4 位中间 mask、Dify 5 字段 inputs、转人工 selected_options 快照)**全部不变**。 --- ## 3. 增量 PRD:详细功能需求 ### 3.1 Bug 4 — 员工端 AI 回答延迟显示(**P0**) | 字段 | 内容 | |---|---| | **现象** | 员工提问 → AI 思考 → 答案已落库 + WS 已广播到坐席(坐席端实时可见)→ **员工端界面不更新**,刷新后才行 | | **现状根因候选** | 员工端 `message-store` 内 `processedMessageIds` 已包含该 message_id(之前 WS 收到过 chunk 阶段 ID),导致 `new_message` 完整事件被 `if (processedMessageIds.has(data.message_id)) return` 静默丢弃(`src/frontend-h5/src/stores/conversation.ts:474-477`) | | **期望** | 员工端 AI 回答 < 1 秒内显示(不需要刷新) | | **验收** | 员工端提问 → 2 秒内看到 AI 回答气泡(不强求同步打字机,但气泡必须出现) | | **建议修复方向**(待架构师确认) | 区分"中间 chunk 的 message_id"与"最终 message_id";或把最终 `new_message` 事件直接接收,不去重 | | **关联基线** | v1.0 §4.2 WS 事件;`conversation.ts:474-477`、`conversation.ts:1404-1407` | ### 3.2 Bug 5 — 选选项时序错位(**P1**) | 字段 | 内容 | |---|---| | **现象** | 员工选选项 → UI 立即显示:AI 思考占位 + 上一个选择气泡 → 几秒后**同时出现"新答案 + 上一个选择"**(时序错位) | | **现状根因候选** | H5 `sendOptionSelect` 触发 `process_h5_ai_reply` → 思考占位立刻 push 到 `messages.value`(`h5_ai_task.py:1574` WS 推送 `ai_thinking`)→ 但**上一个 option_select 消息尚未回流**到 store → 答案到达时**上一条 option_select 一起渲染** | | **期望** | 选选项后 UI 顺序:① 已选气泡(✓)→ ② AI 思考占位 → ③ AI 答案气泡 | | **验收** | 选选项后 3 个 UI 元素**按时间顺序独立出现**,不同时弹出 | | **建议修复方向**(待架构师确认) | `sendOptionSelect` 同步把已选消息 push 到本地 store(不依赖 WS 回流),或后端先 ack 再触发 Dify | | **关联基线** | v1.0 §4.4 H5 字段补全;`conversation.ts:1640-1710` | ### 3.3 Bug 6 — 刷新后选择消失(**P0**,需确认 v1.0 是否真解决) | 字段 | 内容 | |---|---| | **现象** | 员工选选项后能看到 ✓ 气泡 → 刷新页面 → ✓ 气泡**消失** | | **现状调研** | 见 §7 本节根因分析 | | **调研结论** | H5 REST 端点 `h5.py:964-1029` **未实现 v1.0 §4.8 mask 要求**(存安全漏洞),但**未发现"消失"的代码缺陷**。理论上:DB 存原值 → REST 返回原值 → `mapMessage` 保留 `msg_type` → `MessageBubble.vue:174` 渲染 ✓ 模板 → 选项应可见 | | **可能的"消失"根因** | ① 默认 `limit=50`,长会话(>50 条)历史选项被分页(P1 修复);② 浏览器缓存被清除(H5 应有 fallback);③ DB 该行因 WS 关闭或事务回滚未落库 | | **v1.1 期望** | 显式写入"刷新后**所有历史选项气泡按时间顺序显示**"作为 P0 验收;不再依赖隐含理解 | | **验收** | ① 员工选选项 → 刷新 → ✓ 气泡依然可见;② 选项按 server_timestamp 升序;③ 选项时间戳对应的 AI 题目卡片也应可见 | | **建议修复方向** | ① 在 AC-01 加 P0 子项 AC-01-2"刷新后仍可见";② H5 端点在 `limit=50` 不够时支持 `before` 翻页验证;③ 紧急 P0 验证步骤必须包含实际操作 | | **关联基线** | v1.0 §4.1 + AC-01;`h5.py:964-1029` | ### 3.4 Req 7 — 实时同步到坐席端(**P0**,用户强调) | 字段 | 内容 | |---|---| | **现象** | 员工选选项后,**坐席端是否立即看到**?用户担心存在 < 1s 延迟 | | **现状** | v1.0 §4.2 已实现 `ws_manager.broadcast({"type": "new_message", "data": ...})`(坐席端有新事件后 MessageBubble 渲染 ✓),链路已稳定 | | **v1.1 期望** | **显式时延指标**:员工点击选项 → 坐席端 < 100ms 内看到"已选:xxx ✓" | | **验收** | ① 在线坐席端 < 1s 看到新消息事件;② 坐席不在线 → 重新加载时 REST 历史含同样 message_id;③ 弱网或 502 时降级为 3s 轮询可见 | | **建议实施** | **无需新代码**——v1.0 §4.2 已实现。建议架构师出具 `E2E` 验收日志(WS 广播时间戳 + 坐席端 MessageBubble 渲染时间戳差值)证明 < 100ms | | **关联基线** | v1.0 §4.2、AC-03、AC-10;`ws.py:407-421` | --- ## 4. 与 v1.0 兼容性(What Stays) | 维度 | v1.0 决策 | v1.1 是否变动 | |---|---|---| | 撤回/重选语义 | 追加新行、不更新旧行(§4.1) | ❌ 不变 | | 5 秒 UUID 幂等 | `(conversation_id, client_msg_id)` + 5s 窗口(§4.5) | ❌ 不变 | | 跨卡归属 | `question_id + option_id` 联合,label 不作主键(§6) | ❌ 不变 | | 敏感词 mask | 16 位数字中间 4 位 `****`(§4.8) | ❌ 不变 | | Dify 5 字段 inputs | `feedback_type/question_id/option_id/option_value/option_label`(§4.6) | ❌ 不变 | | 转人工快照 | `selected_options` 每题最新(§4.7) | ❌ 不变 | | 5 重 UUID 守卫(V0-C 修复) | WS 在线 + 5 题内 + 5s 内 + 同题 + 未陈旧(`conversation.ts:1662-1675`) | ❌ 不变,**作为 v1.1 Bug 5 修复的"前提"** | --- ## 5. 风险与依赖 | 风险 | 等级 | 缓解/验证 | |---|---|---| | **Dify v3 未导入** → 修复 Bug 4/5/6 后,**用户感知不到 Bug 真实修复**(因 AI 回答本身没变) | 高 | v1.1 落地**前提**:先确认 Dify v3 导入时间表;建议 2026-08-09 前完成 | | 实时同步依赖后端 `ws_manager` 单进程 | 中 | v1.0 已固定 `--workers 1`;v1.1 沿用 | | 坐席端 before 翻页未断言 | 中 | v1.0 TC-004 已覆盖;v1.1 沿用 | | 长会话 limit=50 可能漏显 | 中 | Bug 6 验证;若发现,v1.1 引入翻页 | | H5 端点 mask 缺失(`h5.py:1025`) | 高 | v1.0 §4.8 已要求,v1.1 修复(顺手) | | 浏览器缓存丢失 | 低 | 现有 fallback `getMessages` API | --- ## 6. 待用户确认(5 个问题) 1. **Bug 4 / 5 / 6 的优先级排序**是否合理?(当前:Bug 4=Bug 6=P0 / Bug 5=P1) 2. **Req 7 是否属于 v1.1 范围**?(用户强调"如果没有技术问题,希望实现"——倾向属于 v1.1) 3. 是否同意新增"**刷新后必须保留选择历史**"作为 v1.1 P0 验收(AC-01-2 增项)? 4. **Dify v3 何时导入**?(影响 AI 能否"基于选项的跟进",进而影响 Bug 4/5 是否真验收) 5. 是否需要拉原型?如需 UI 调整(坐席侧徽标位置、员工端选项视觉等),需 UX 重新设计 --- ## 7. Bug 6 根因分析(基于代码 + Git 现状) > 用户问"之前发生过页面刷新后选择消失问题,是否真解决"——下面给出**基于代码证据**的判断。 ### 7.1 代码调研(5 个关键点) | # | 调研点 | 代码位置 | 结论 | |---|---|---|---| | 1 | 后端持久化 | `ws.py:374-385` `Message(msg_type="option_select", content=option_label, ...)` + `db.add()` + `commit` | ✅ 落库 | | 2 | H5 REST 端点是否返回 option_select | `h5.py:964-1029` `select(Message).where(conversation_id==...)`(**无 msg_type 过滤**) + `MessageResponse.model_validate(m).model_dump()` | ✅ 应返回 | | 3 | H5 端点是否 mask option_select | `h5.py:1025` **直接 dump,无 mask**(违反 v1.0 §4.8) | ❌ **存安全漏洞**,但**与"消失"无关** | | 4 | 前端字段映射 | `api/conversation.ts:234-249` `mapMessage`:`msg_type: raw.msg_type` 直接透传 | ✅ 保留 | | 5 | UI 渲染分支 | `MessageBubble.vue:174-179` `` | ✅ 渲染分支存在 | | 6 | 缓存 + 合并 | `conversation.ts:62-82` + `85-97` + `117-119` `mergeMessages` 按 `message_id` 去重 | ✅ 应保留 | | 7 | 限分页 | `h5.py:999` `limit(limit)` 默认 50 | ⚠️ 长会话会被分页 | ### 7.2 根因判定(确定性) **基于代码,`Bug 6 "刷新后选择消失" 在当前 v1.0 应不应发生**?——**不应发生**。但有以下 3 个潜在触发场景: | 场景 | 当前是否根因 | 概率 | |---|:---:|:---:| | **A. H5 端点未 mask**(`h5.py:1025`) | 否(是安全 bug,不是"消失") | 高 | | **B. 默认 limit=50,长会话历史选项被分页** | **是**(超过 50 条之后,刷新只显示最新 50 条) | 中 | | **C. 浏览器缓存被清除 + step 3 异步 fetch 失败** | **是**(无网络时,UI 不会显示选项气泡) | 中 | | **D. 消息真正未落库**(DB 事务回滚) | 否(v1.0 §3.1 #1 已 P0 验收,PG advisory lock 已避免) | 极低 | | **E. 字段映射丢失**(`msg_type` 被过滤) | 否(`mapMessage` 直接透传) | 极低 | ### 7.3 结论 **Bug 6 在 v1.0 当前实现下大概率已被解决**——代码层面: - 后端 ✅ 落库 - API ✅ 返回 - 字段映射 ✅ 保留 - UI ✅ 渲染分支存在 **但有 2 个**潜在根因未被 v1.0 显式覆盖: 1. **H5 端点 limit=50 未分页验证**(场景 B) 2. **H5 端点 mask 缺失**(场景 A,与"消失"无关但是安全漏洞) **v1.1 建议**: - 把"刷新后保留"显式写入 PR v1.1 验收(AC-01-2) - 顺手修复 H5 端点 mask 漏洞(§3.1 修复的同时) - 增加 `E2E` 验收步骤:选选项 → 刷新 → 截图选项气泡 --- ## 8. 附录:v1.1 增量范围 vs 完整 PRD | 范围 | 是否在 v1.1 增量草案 | 备注 | |---|:---:|---| | Bug 4 / 5 / 6 修复 | ✅ 草案 | 待用户确认优先级 | | Req 7 实时同步验收 | ✅ 草案 | 显式时延指标 | | v1.0 6 项决策 | ❌ 不再重复 | 见 v1.0 原 PRD | | Dify v3 变更 | ❌ 不再重复 | 见 Dify v3 CHANGELOG | | 测试用例增量 | ⏳ 下一步 | 待 QA 在 v1.1 范围确认后增量 | | 完整 PRD(含组件图、时序图、API 变更) | ❌ 不出 | 用户明确"先写草案" | --- ## 9. 变更记录 | 版本 | 日期 | 变更内容 | 变更人 | 变更原因 | |---|---|---|---|---| | v1.0 | 2026-07-29 | 创建 PRD,固化方案 A、6 项产品决策与 MVP 验收范围 | 许清楚、宋献 | 修复选项选择不落库 | | **v1.1-DRAFT** | **2026-08-02** | **增量草案:3 Bug + 1 Req + Bug 6 根因分析 + 5 待确认问题** | **许清楚** | **用户实测反馈 + 询问刷新保留是否真解决** | --- > **下一步**:等待用户对 §6 五个问题的回复 → 确认 v1.1 范围 → 由架构师出 v1.1 技术方案 → 由 QA 补 v1.1 测试用例增量。