**重构前**(旧编号 02-11): - docs/02-产品需求/ → 00 产品规划/PRD - docs/03-技术架构/ → 01-05 子目录散落 - docs/04-原型设计/ → 01-02 产品设计(HTML 原型) - docs/05-原型设计/ → screens/ - docs/06-测试素材/ → 02-E2E / 03-功能 / 04-版本测试 - docs/07-项目管理/ → 任务说明书/日报/计划 - docs/08-安全审计/ → 审计报告 - docs/09-堡垒运维/ → toolbox / deploy - docs/10-项目管理/ → 任务说明书(重复) - docs/11-历史归档/ → deploy-nas-archived **重构后**(新编号 00-07,语义化): - docs/00-产品开发流程与文档管理规范.md - docs/00-版本迭代总览.md - docs/01-产品文档/ (PRD/原型/认证/会话/AI 服务/坐席/集成) - docs/02-技术文档/ (技术方案/架构图/重构记录/前端改造/实现配置) - docs/03-测试文档/ (E2E/功能用例/版本报告/缺陷单) - docs/04-运维文档/ (部署运维/运维指南) - docs/05-运营文档/ (品牌推广/用户手册) - docs/06-安全审计/ (审计报告) - docs/07-项目管理/ (任务说明书/日报/计划/看板) **净收益**: - 目录编号与产品文档管理规范对齐(按文档阶段 01-07 编号) - 消除 02-产品需求 与 10-项目管理 的编号重叠 - 子目录按文档类型分组(如 01-产品文档/00-产品规划、01-产品文档/01-认证与登录) - 把运维/安全/项目管理从 0X 散落改为 04/06/07 合计 494 文件 + 78495 行 / - 14076 行
12 KiB
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
- 相关 PRD:PRD-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-scope(v1.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. 非目标
- 不新增选择专表、不执行 Alembic 迁移。
- 不把选择状态回写到原
ai_structured.extra_data,避免撤回/重选语义丢失。 - 不建设选项编辑、撤销按钮;v1.0 的“撤回”通过再次选择表达。
- 不定义多卡嵌套题、跨题依赖和选择有效期。
- 不建设跨会话 BI、推荐转化率报表或模型自动训练流水线。
- 不改造所有历史
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 验收范围 | 许清楚、宋献 | 修复选项选择不落库导致的数据完整性问题 |