143 lines
12 KiB
Markdown
143 lines
12 KiB
Markdown
|
|
# 技术方案(设计)— 选项选择持久化 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 个问题逐一给出根因 + 修复路径;不写完整方案,待用户决策 | 高见远、宋献 |
|