Files
wecom_it_smart_desk/docs/01-产品文档/00-产品规划/PRD-REQ-通用-005-选项选择持久化-v1.0.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .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-*/
2026-08-07 22:31:32 +08:00

179 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRD - 选项选择持久化
> **REQ 编号**: REQ-通用-005
> **版本**: v1.0
> **日期**: 2026-07-29
> **作者**: 许清楚(PM+ 宋献(Simon
> **状态**: ✅ 已批准
> **关联**:
> - 技术方案:docs/02-技术文档/技术架构/技术方案-REQ-通用-005-选项选择持久化-v1.0.md(待架构师出)
> - 测试用例:docs/03-测试文档/03-功能测试用例/TC-REQ-通用-005-选项选择持久化-v1.0.md(待 QA 出)
> - 代码真相:src/backend/app/api/ws.py + src/backend/app/services/h5_ai_task.py
> - 相关 PRDPRD-REQ-用户-006-智能推荐重构-v1.0.md(衍生关系)
---
## 1. 问题陈述
Dify 工作流可输出 quick reply / 选择题形式的“选项消息”。AI 选项消息已以 `msg_type="ai_structured"``extra_data.options` 持久化,但用户点选后,后端 `_handle_option_select` 当前故意不写 `messages` 表,仅进行瞬时 WebSocket 广播并将 `option_label` 传给 Dify`src/backend/app/api/ws.py:277-281`)。坐席端的 `selectedOptionLabels` 又是纯内存集合,重连即失效,导致选择动作无法追溯。
| 优先级 | 业务影响 | 真实场景 | 可量化后果 |
|---|---|---|---|
| P0 | 合规与审计留痕缺失 | 金融/政府客户复盘“员工选择了哪一项”时,管理端无记录 | 操作链不完整,无法满足可追溯要求 |
| P0 | 坐席分诊效率下降 | 紧急报修员工已选“网络中断”,坐席进会话后看不到,只能重问 | 会话时长预计增加约 20%,存在 SLA 风险 |
| P1 | AI 推荐反馈闭环断裂 | 智能推荐卡 A/B/C 中用户选 C,但系统无结构化选择数据 | A/B/C 效果不可归因,AI 投入 ROI 难评估 |
| P1 | 会话连续性中断 | 员工刷新或退出 H5 后,历史中缺少已选内容 | 断点续聊上下文不完整,Dify 将选择误识别为普通发言 |
| P2 | 员工体验不一致 | 员工看不到“我之前选了什么” | 重复操作、降低信任感 |
**目标**:以零数据库迁移方式,将每次选择作为 `employee` 消息持久化,并在 H5、坐席、管理审计、Dify 反馈链和转人工上下文中形成一致、可追溯的数据闭环。
---
## 2. 用户故事
| 角色 | 现状 | 期望用户故事 |
|---|---|---|
| 员工 H5 | 刷新/退出后已选内容消失 | 作为员工,我希望每次选择都进入消息历史,以便刷新或续聊时仍能确认我选过什么 |
| 坐席 PC | 仅实时内存可见,进入晚或重连后不可见 | 作为坐席,我希望实时及历史消息中看到“✓ 选项”,并识别最新选择,以便无需重复询问即可分诊 |
| 管理端 | 无选择记录可供审计、复盘 | 作为管理员,我希望按会话追溯选择时间、题目和选项,以便满足合规审计和投诉复盘 |
| AI 训练/运营 | 选择被当作普通自然语言,且缺少选项归属 | 作为 AI 训练人员,我希望获得带 `question_id``option_id``feedback_type` 的反馈,以便准确评估推荐效果 |
---
## 3. 范围
### 3.1 In-scopev1.0 MVP
| # | 范围项 | 优先级 |
|---|---|---|
| 1 | `_handle_option_select` 插入 `msg_type="option_select"` 的员工消息(`src/backend/app/api/ws.py:270-281` | P0 |
| 2 | 选择成功后使用标准 `new_message` 事件广播(`src/backend/app/api/ws.py:284-293` | P0 |
| 3 | 坐席 `MessageBubble.vue` 渲染“✓ {content}”灰色徽标,最新选择高亮(`src/frontend-agent/src/components/chat/MessageBubble.vue:52-77` | P0 |
| 4 | H5 `sendOptionSelect` 补齐来源消息、客户端幂等及题目/选项标识(`src/frontend-h5/src/stores/conversation.ts:1486` | P0 |
| 5 | 客户端生成 UUID,服务端按会话与客户端消息 ID 执行 5 秒去重 | P0 |
| 6 | 回归“发送→点选→坐席实时→坐席重连→REST 历史仍可见”完整链路 | P0 |
| 7 | Dify inputs 注入 `feedback_type=option_select` | P1 |
| 8 | 转人工时注入 `selected_options` 快照 | P1 |
| 9 | 选项展示遵循敏感信息中间四位脱敏 | P0 |
### 3.2 Out-of-scope
- 方案 B:新增 `conversation_selections` 表及数据库迁移。
- 方案 C:仅向 `ai_structured.extra_data` 追加 `selected_options`
- 多卡嵌套、父子题、条件题等复杂语义编排。
- 跨会话选择聚合、BI 看板与推荐效果报表。
---
## 4. 功能需求
### 4.1 后端落库 Schema
服务端收到合法选择后,必须先完成幂等判断,再向现有 `messages` 表追加一行;不得覆盖原选项消息或历史选择。实现位置为 `_handle_option_select``src/backend/app/api/ws.py:270-281`)。
| 字段 | 类型/示例 | 必填 | 规则 |
|---|---|---:|---|
| `msg_type` | `"option_select"` | 是 | 新增文档化取值;现有字段 `String(20)`,零迁移 |
| `sender_type` | `"employee"` | 是 | 表示员工操作 |
| `content` | `"网络中断"` | 是 | 保存原始 `option_label`,展示时再脱敏 |
| `extra_data.option_value` | `"network_down"` | 是 | 传给业务/Dify 的选项值 |
| `extra_data.selected_from_message_id` | 消息 ID | 是 | 关联产生选项的 `ai_structured` 消息 |
| `extra_data.client_msg_id` | UUID | 是 | 幂等键 |
| `extra_data.question_id` | `"fault_type"` | 是 | 题目稳定标识,支持跨卡归属 |
| `extra_data.option_id` | `"network_down"` | 是 | 选项稳定标识,禁止仅靠 label 归属 |
持久化成功后,REST 历史接口必须按现有消息排序规则返回该记录;失败时不得向 Dify提交“已成功选择”的假状态。
### 4.2 WS 事件改造
当前瞬时专用广播必须改为标准 `new_message` 事件(`src/backend/app/api/ws.py:284-293`),事件 payload 应复用持久化后的消息对象,至少包含消息 ID、会话 ID、发送方、消息类型、内容、`extra_data`、创建时间。坐席实时态与重连后的 REST 历史态必须使用同一数据模型。
### 4.3 坐席端渲染规范
- `src/frontend-agent/src/components/chat/MessageBubble.vue:52-77` 必须增加 `msg_type === "option_select"` 分支。
- 徽标位于员工消息流原选择发生的时间位置,文案为 `✓ {mask(content)}`,采用灰色次要信息样式,不渲染为普通气泡。
- 同一 `question_id` 多次选择全部保留;当前最新一条使用主色描边或浅色背景高亮,旧选择降级为灰色。
- 最新判定按服务端消息时间与消息 ID 稳定排序,不依赖 `selectedOptionLabels` 内存集合。
### 4.4 H5 端字段补全
`sendOptionSelect``src/frontend-h5/src/stores/conversation.ts:1486-1518`)必须发送:`option_label``option_value``selected_from_message_id``client_msg_id``question_id``option_id``client_msg_id` 在首次点击时生成 UUID;同一次请求重试必须复用,用户主动重选必须生成新 UUID。
### 4.5 幂等与去重规则
- 服务端幂等键:`(conversation_id, client_msg_id)`
- 去重窗口:首次受理后 5 秒;窗口内重复请求只返回首次成功结果,不新增消息、不重复广播、不重复调用 Dify。
- 5 秒后相同 ID 仍不得被客户端主动复用;服务端可记录告警并拒绝,以避免历史重复。
- 不同 `client_msg_id` 即视为撤回后的重选/再次选择,追加新行。
### 4.6 Dify 集成
传入 Dify Workflow 的 inputs 必须新增 `feedback_type="option_select"`,并同时传递 `question_id``option_id``option_value`、脱敏后的 `option_label`;接入点由技术方案基于现有 Dify 调用链定位(当前调用见 `src/backend/app/tasks/h5_ai_task.py:1505-1514`,任务入口见 `src/backend/app/tasks/h5_ai_task.py:1617-1624`)。Dify 必须据此区分“用户选择了 X”与“用户自然语言说了 X”,且不破坏现有普通文本消息链路。
### 4.7 转人工快照
触发转人工时,系统必须按每个 `question_id` 取最新一条有效选择,组成 `selected_options` 注入坐席上下文;每项至少包含 `question_id``option_id`、脱敏 label、选择消息 ID、选择时间。快照仅用于快速接续,审计真相仍以 `messages` 表全部追加记录为准。
### 4.8 敏感词 Mask
选项含账号、身份证号等敏感数字串时,数据库保存原始值以满足审计权限场景;H5、坐席普通视图、WS 普通 payload、Dify inputs 和转人工快照必须将数字串中间连续四位替换为 `****`。不足 4 位的敏感值全部掩码;脱敏不得改变 `question_id``option_id` 的匹配与最新选择判定。
---
## 5. 验收标准
| 编号 | 对应需求 | 验收用例与通过标准 |
|---|---|---|
| AC-01 | 4.1 | 点选后 `messages` 新增 1 行:`msg_type=option_select``sender_type=employee`5 个 `extra_data` 字段完整;REST 重拉仍存在 |
| AC-02 | 4.1 | 连续重选两次形成两行,不覆盖首次记录,均可按时间追溯 |
| AC-03 | 4.2 | 坐席在线时收到标准 `new_message`;断线重连后从 REST 得到相同消息 ID 与内容 |
| AC-04 | 4.3 | 坐席显示“✓ 选项”灰色徽标;同一 `question_id` 仅最新一条高亮,旧记录仍可见 |
| AC-05 | 4.4 | H5 每次主动选择生成合法 UUID,并携带来源消息、题目、选项标识;重试复用 UUID |
| AC-06 | 4.5 | 5 秒内用相同 `(conversation_id, client_msg_id)` 重发 3 次,仅落库、广播、调用 Dify 各 1 次 |
| AC-07 | 4.6 | Dify 收到 `feedback_type=option_select` 及题目/选项字段;普通文本仍沿用原语义 |
| AC-08 | 4.7 | 员工选择后立即转人工,坐席上下文包含各 `question_id` 最新选择;坐席无需重问 |
| AC-09 | 4.8 | 选项包含账号/身份证示例时,各普通展示及 Dify 输入中间四位为 `****`,数据库审计原值不变 |
| AC-10 | 全链路 | 完成“AI 发选项→员工点选→坐席实时 ✓→坐席重连→REST 历史仍可见”,全程无重复记录 |
---
## 6. 边界场景
| 场景 | 产品规则 | 预期结果 |
|---|---|---|
| 撤回/重选 | 不改旧行,使用新 `client_msg_id` 追加记录 | 全历史可见;同题最新一条高亮并进入快照 |
| 弱网重发 | 同一请求复用 `client_msg_id`,5 秒窗口去重 | 只落库、广播、调用 Dify 一次 |
| 跨卡归属 | 必须联合 `question_id + option_id`,label 不作为唯一键 | 多卡存在同名 label 时仍准确归属 |
| 敏感词 | 存储原值,展示与外发链路 mask 中间四位 | 审计可追溯,普通使用方不暴露敏感值 |
| 转人工 | 每题取最新选择生成 `selected_options` | 人工坐席获取完整断点上下文,历史行不丢失 |
---
## 7. 非目标
1. 不新增选择专表、不执行 Alembic 迁移。
2. 不把选择状态回写到原 `ai_structured.extra_data`,避免撤回/重选语义丢失。
3. 不建设选项编辑、撤销按钮;v1.0 的“撤回”通过再次选择表达。
4. 不定义多卡嵌套题、跨题依赖和选择有效期。
5. 不建设跨会话 BI、推荐转化率报表或模型自动训练流水线。
6. 不改造所有历史 `recommend_event` 数据,仅保证新链路兼容。
---
## 8. 风险
| 风险点 | 等级 | 说明 | 缓解/验证 |
|---|---|---|---|
| `ws_manager` 单例状态依赖 | 高 | 单进程内存去重或广播状态在重启后丢失 | 幂等以消息持久化查询为准,内存仅作加速;补充重启回归 |
| 多 worker 并发竞态 | 高 | 两个 worker 同时处理同一 UUID,5 秒内可能双写 | 技术方案必须定义原子去重策略;并发压测验证仅生成一条消息 |
| Dify 反馈链语义变化 | 中 | 新增 `feedback_type` 后,旧工作流节点可能忽略或误用字段 | 字段向后兼容、灰度开启;验证普通文本和选项两条链 |
| `recommend_event` 兼容性 | 中 | 现有智能推荐事件仍可能依赖旧 payload 或 label | 保留旧必要字段,新增字段只增不删;覆盖单卡、多卡与转人工回归 |
---
## 9. 变更记录
| 版本 | 日期 | 变更内容 | 变更人 | 变更原因 |
|---|---|---|---|---|
| v1.0 | 2026-07-29 | 创建 PRD,固化方案 A、6 项产品决策与 MVP 验收范围 | 许清楚、宋献 | 修复选项选择不落库导致的数据完整性问题 |