facc04aa65
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
12 KiB
12 KiB
技术方案(设计)— 选项选择持久化 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 个问题
- 修复顺序是否同意 P0 → P1 → 验证 Req 7? 推荐先 Bug 6(最小风险、解锁 QA)→ Bug 4(最大价值)→ Bug 5(视觉打磨)→ Req 7 验证 + 监控。
- 是否需要等 Dify v3 导入后再开始 Bug 4 修复? 若用户能在 1 周内导入 Dify,可推迟 B4;若超过 1 周,建议先发 B6 + Req 7。
- Req 7 实时同步指标是否同意 < 100ms(端到端,员工→坐席)? v1.0 后端 ws commit 后立即广播,单 worker 下 50ms 内可达;< 100ms 是合理预算。若需要 < 50ms 需引入 Redis Pub/Sub(不在本次范围)。
- 是否需要拉 UX 重新设计"上一选项 + 新 AI 答案"的视觉分组? Bug 5 修复方案 1 是程序计算分组(成本低、行为可解释),方案 2 是 UX 重新设计(成本高、效果更好)。建议先用方案 1 兜底,再决定是否启动 UX。
- 是否需要扩展 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 个问题逐一给出根因 + 修复路径;不写完整方案,待用户决策 | 高见远、宋献 |