# 技术方案(设计)— 选项选择持久化 v1.1 > **REQ 编号**: REQ-通用-005 > **版本**: v1.1 — 方案设计稿 > **日期**: 2026-08-02 > **作者**: 高见远(架构师 · Bob) > **状态**: ⏳ 待用户决策(不进开发,先评审修复路径) > **关联**: > - PRD v1.0:`docs/01-产品文档/00-产品规划/PRD-REQ-通用-005-选项选择持久化-v1.0.md`(已批准,不改) > - PRD v1.1 增量草案:待 PM(许清楚)出 > - 技术方案 v1.0:`docs/02-技术文档/技术架构/技术方案-REQ-通用-005-选项选择持久化-v1.0.md`(实施基础,已部署) > - 代码真相:`src/backend/app/api/ws.py` + `src/backend/app/api/h5.py` + `src/backend/app/tasks/h5_ai_task.py` + `src/frontend-h5/src/stores/conversation.ts` + `src/frontend-h5/src/composables/useH5WebSocket.ts` + `src/frontend-h5/src/components/chat/MessageBubble.vue` > - Dify v3 DRAFT:`docs/02-技术文档/实现配置/dify_dsl/itdesk_main_v3_feedback-vars_DRAFT.yml`(用户尚未导入) --- ## §1 技术现状评估 v1.0 已部署生产。整体架构稳定:后端 `_handle_option_select`(ws.py:254-448)走 advisory lock + 5 秒幂等 + COMMIT → 广播 → Dify 的 5 阶段管线;H5 `sendOptionSelect`(conversation.ts:1621-1737)补齐 6 字段、UUID 守卫、5 条复用判定;REST 历史接口(messages.py:158-173)已对 `option_select` 做 mask 并去除审计原值字段;坐席 `MessageBubble.vue` 徽标已生效。本次用户反馈的 4 个问题中: - **Bug 4(员工端 AI 回答延迟显示,P0)** — 后端 ws 广播与 Dify 返回两通道时序错位 + H5 `handleAiReply` 依赖 `currentConversation.value.conversation_id` 精确匹配,两者叠加在弱网下导致 AI 回复丢失。属于 v1.0 边界场景(弱网 / 会话切换竞态),未单测覆盖。 - **Bug 5(选选项后时序错位,P1)** — 与 Bug 4 同源,但用户视角是"思考占位 + AI 答案 + 上一选项同时出现"。根因是 v1.0 把"上一选项也保留在列表"作为 PRD §6 的撤回语义,但 UI 没有视觉分组,3 个气泡挤在一起显得错位。 - **Bug 6(刷新后选择消失,待确认)** — `selectedOptionLabels` 是 store 内 ref(conversation.ts:1612),无持久化;`ai_structured.options` 按钮的"已选"状态依赖该 ref 计算(MessageBubble.vue:332-343),刷新后 ref 重置为 `[]`,所有选项按钮均不再显示 ✓,但实际 `option_select` 消息仍在历史中可查(masked REST 已包含)。属实现缺陷,非 PRD 漏写。 - **Req 7(实时同步坐席端,用户强调)** — 已实现(ws.py:407 `await ws_manager.broadcast(...)`),仅缺 E2E 时延指标验证与监控埋点。 依赖关系:B4 / B5 同根(H5 收 ai_reply 的时序),建议同工单修复;B6 独立(H5 前端状态派生);Req 7 仅需观测与监控。 --- ## §2 4 个问题的修复路径 | 编号 | 真实根因(基于代码) | 修复位置 | 行数估算 | 阻塞 | |---|---|---|---|---| | Bug 4 | 后端 `_handle_option_select` 仅广播 `new_message` 给**坐席**(ws.py:407 `ws_manager.broadcast`),员工端要等 Dify 回包后的 `ai_reply`(h5_ai_task.py:443 `broadcast_to_employees`);若 Dify 超时或 WS 抖动,员工只看到 `ai_thinking` 占位。叠加 H5 `handleAiReply`(conversation.ts:1395)的 `conversation_id` 严格相等校验 — 任何切换 / 重复挂载场景下被静默丢弃 | (a) ws.py:407 增加 `broadcast_to_employees` 同步推送自己的 `option_select` 给员工;(b) conversation.ts:1395 改为软校验 + 一次性提示;(c) h5_ai_task.py:443 增加 `still_thinking` 时主动 `broadcast_to_employees` 一个 `option_select` 确认事件(兜底) | < 50 | 无 | | Bug 5 | UI 没有"同一 question 的 option_select + 后续 AI reply"视觉分组,3 个气泡线性排列;上一选项的 ✓ 气泡和新的 AI 答案同时显示在列表里。`ai_thinking` 占位气泡渲染时切到了"仍在思考",但前一次的 option_select 气泡没折叠 | (a) MessageBubble.vue:174 在 `option_select` 分支上加 `data-question-id` 属性;(b) ChatPanel.vue 在消息列表中加一个 computed:当连续 N 条 `option_select + ai_text` 来自同一 `question_id`,对 option_select 加 `collapsed` 灰底 class | < 50 | 无 | | Bug 6 | `selectedOptionLabels`(conversation.ts:1612)纯内存 ref,刷新即空;`isOptionSelected`(MessageBubble.vue:332)依赖它判定 ai_structured 按钮的 ✓ 样式。`option_select` 消息本身在 messages 里(masked REST 完整返回),但用户视觉上"按钮未选中" | (a) `isOptionSelected` 改为派生自 `messages.filter(m => m.msg_type==='option_select')`,不依赖内存 ref;(b) `selectedOptionLabels` 标记 deprecated,写注释保留仅作兼容 | < 30 | 无 | | Req 7 | **已实现**:ws.py:407 同步广播给所有坐席(commit 后立即触发,不等 Dify)。缺的是时延监控埋点与端到端可观测性 | (a) BackendObserver 增加 `option_select_broadcast_latency_ms` 指标(落库 commit 时间戳 vs 广播发送时间戳);(b) H5 `useH5WebSocket.ts` 上报端到端时延(WS 收到 `option_select` 时间 vs 点击时间) | < 30 | 无 | > **关于阻塞列**:经分析本次 4 个问题全部为局部修复,无跨模块强依赖;后端 ws 链路经 v1.0 实战验证稳定;Dify 未导入不影响修复(H5 视角独立)。 --- ## §3 实施建议(按优先级) **P0 必做(影响主链路 / 审计可追溯)** - **Bug 4** — 员工端 AI 回答延迟。涉及 ws.py:407、conversation.ts:1395、h5_ai_task.py:443 三处小改;建议同工单合入以保证时序回归一次到位。 - **Bug 6** — 刷新后选择消失。改 1 个文件(MessageBubble.vue:332)即可,技术风险极低;建议先于 Bug 4 完成以便坐席侧 + H5 侧 QA 同步测试。 **P1 建议(体验问题)** - **Bug 5** — 时序错位。需要 ChatPanel.vue 加计算 + MessageBubble.vue 加 attr,影响视觉但不动数据,建议作为视觉打磨任务排在下个 Sprint。 - **Req 7** — 实时同步。技术已实现,建议把"验证 + 监控"作为快速胜利任务(< 1 人日):写一个 5 分钟观测脚本(puppeteer 模拟点击 + ws 抓包)出时延报告。 **P2 可选** - 把 H5 store 的 `selectedOptionLabels` 全量替换为派生计算(v1.0 已落地 P0-C 的 UUID 复用守卫,逻辑可平滑迁移) --- ## §4 实施步骤(粗粒度,不写代码) ``` Step 1: 修复 Bug 6(前置 / 解锁 QA) Owner: 工程师(寇豆码) Files: src/frontend-h5/src/components/chat/MessageBubble.vue:332-343 思路: 把 isOptionSelected 改为从 store.messages 派生(过滤 msg_type==='option_select', 用 extra_data.question_id/option_id 匹配),不再依赖内存 ref 验证: H5 刷新页面 → ai_structured 按钮仍带 ✓ 样式(即使 selectedOptionLabels 为空) 强校验: dist 必须包含派生计算的特征字符串;旧 selectedOptionLabels 引用计数清零 Step 2: 修复 Bug 4(H5 收 ai_reply 的时序) Owner: 工程师(寇豆码) Files: src/backend/app/api/ws.py:407(加 broadcast_to_employees) src/frontend-h5/src/stores/conversation.ts:1395(软校验 + 一次性提示) src/backend/app/tasks/h5_ai_task.py:443(兜底 still_thinking 时补 option_select 确认) 思路: a) 员工选选项后立即看到自己的"✓ 选项"气泡(不等 Dify) b) AI 答案到达时即使 conversation_id 轻微偏差也能落入 UI(但记录 warning) 验证: 模拟弱网(用 puppeteer throttle 到 1Mbps + 100ms RTT)→ 选项后 100ms 内看到 ✓ 模拟会话切换竞态 → 切换前点选的选项在新会话仍可见 强校验: dist 包含 broadcast_to_employees 与软校验 fallback 关键字 Step 3: 修复 Bug 5(视觉分组) Owner: 工程师(寇豆码) Files: src/frontend-h5/src/components/chat/MessageBubble.vue:174 src/frontend-h5/src/components/chat/ChatPanel.vue:96-112 思路: option_select 加 data-question-id;ChatPanel 用 computed 把"同一 question 的 option_select + 紧随其后的 ai_text"折叠为一个视觉组;折叠展开可点击 验证: 选同一 question 不同 option → 上次选项灰底显示,新选项高亮,新 AI 答案紧随 强校验: dist 包含折叠 class 关键字 Step 4: 验证 Req 7(实时同步) Owner: 工程师 + QA(寇豆码 + QA) 工具: puppeteer 脚本(src/frontend-h5/scripts/measure-option-latency.mjs) + BackendObserver 埋点(option_select_broadcast_latency_ms) 步骤: a) 启动两个浏览器上下文(员工 + 坐席) b) 员工点击选项,记录 t0(点击)→ t1(员工端看到 ✓)→ t2(坐席端看到 ✓) c) 输出 p50/p95 时延 + 告警阈值(>1s 触发 BackendObserver warning) 验证: t2 - t0 < 100ms(坐席感知);< 50ms 为优 强校验: 抓 ws 帧确认 type=new_message 且 msg_type=option_select Step 5: 跨端回归 + 发布 Owner: 全员 范围: H5 dist + 后端 container 重启(仍 --workers 1) 验证: 5 场景 E2E(在线点选 / 重连 REST / 弱网重试 / 同题重选 / 跨会话切换) ``` --- ## §5 风险与依赖 | 风险 | 等级 | 说明 | 缓解 | |---|:--:|---|---| | **Dify v3 仍未导入** | 中 | 若 Dify 没导入 v3 feedback vars,员工端 AI 答案内容不变(B4/B5 修复后用户可能感觉不到差异)。但 Bug 6(刷新后 ✓)与 Dify 无关 | 强烈建议 PM 与用户同步 Dify 导入时间表;若 1 周内不导入,先修 Bug 6 + Req 7(无 Dify 依赖) | | **后端 ws 单 worker** | 低 | v1.0 已固定 `--workers 1`;本次修复不引入跨进程广播 | 保持单 worker;如未来扩容,先接 Redis Pub/Sub | | **H5 dist 强校验铁律** | 低 | 3 次踩坑经验(v0.5 缓存路径 / v1.0 P0-B 字符串匹配 / v1.0 P0-C UUID 守卫) | 每次修复后强校验 4 个证据链:dist 关键字 + 产物 hash + 部署 HTTP 200 + 浏览器实测 | | **H5 `processedMessageIds` 误伤** | 中 | 若 `handleAiReply` 因去重而丢弃,可能与 Bug 4 同症 | 修复 B4 时同步核对该集合的清空时机(特别在 conversation 切换时) | | **坐席端 regress** | 低 | 坐席 MessageBubble 已有徽标逻辑(v1.0 已部署),本次不动坐席代码 | 仅做回归测试覆盖 AC-03 / AC-04 | --- ## §6 待用户确认的 5 个问题 1. **修复顺序是否同意 P0 → P1 → 验证 Req 7?** 推荐先 Bug 6(最小风险、解锁 QA)→ Bug 4(最大价值)→ Bug 5(视觉打磨)→ Req 7 验证 + 监控。 2. **是否需要等 Dify v3 导入后再开始 Bug 4 修复?** 若用户能在 1 周内导入 Dify,可推迟 B4;若超过 1 周,建议先发 B6 + Req 7。 3. **Req 7 实时同步指标是否同意 < 100ms(端到端,员工→坐席)?** v1.0 后端 ws commit 后立即广播,单 worker 下 50ms 内可达;< 100ms 是合理预算。若需要 < 50ms 需引入 Redis Pub/Sub(不在本次范围)。 4. **是否需要拉 UX 重新设计"上一选项 + 新 AI 答案"的视觉分组?** Bug 5 修复方案 1 是程序计算分组(成本低、行为可解释),方案 2 是 UX 重新设计(成本高、效果更好)。建议先用方案 1 兜底,再决定是否启动 UX。 5. **是否需要扩展 BackendObserver 监控实时性(option_select_broadcast_latency_ms + option_select_end_to_end_latency_ms)?** 强烈推荐:缺失监控是本次 Bug 4 难以快速定位的主因。建议作为 Step 4 的强制交付物。 --- ## §7 交付物清单(待 PM / 工程师同步) - [ ] PRD v1.1 增量草案(PM 许清楚负责) - [ ] 本方案设计稿经用户确认后,转为正式技术方案 v1.1(包含完整任务分解) - [ ] Dify v3 导入时间表(用户决策) - [ ] 测试用例 v1.0 增量(QA 编制,覆盖 B4/B5/B6 + Req 7) - [ ] BackendObserver 指标名 + 告警阈值(运维确认) --- ## §8 变更记录 | 版本 | 日期 | 变更内容 | 变更人 | |---|---|---|---| | v1.1 设计稿 | 2026-08-02 | 基于已部署代码 + 用户 2026-08-02 17:45 反馈,对 4 个问题逐一给出根因 + 修复路径;不写完整方案,待用户决策 | 高见远、宋献 |