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-*/
179 lines
12 KiB
Markdown
179 lines
12 KiB
Markdown
# 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. 非目标
|
||
|
||
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 验收范围 | 许清楚、宋献 | 修复选项选择不落库导致的数据完整性问题 |
|