Files
wecom_it_smart_desk/docs/01-产品文档/04-坐席工作台/PRD-REQ-坐席-010-AI回复三态开关.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

182 lines
15 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 - AI 回复三态开关(坐席工作台)
> **REQ编号**: REQ-坐席-010
> **版本**: v1.0
> **优先级**: P0(含现状行为缺口修复)
> **日期**: 2026-08-04
> **负责人**: 产品经理 许清楚(Xu)
> **范围**: 简单 PRD(无竞品/市场分析)
---
## 一、需求概述
坐席工作台(`src/frontend-agent`Vue3 + Element Plus + Vite)需要新增一个 **「AI 回复三态开关」**,让坐席在**单会话**内控制 AI 是否继续自动回复、以及自动回复谁可见。
> ⚠️ **正交性约束**:本开关控制的是「AI **自动回复**」的总开关,与工具栏已有的 4 个 **AI 主动触发按钮**(💡自动补齐 / 🎚️语气 / 🖌️润色 / 🔄改写)**完全正交**,后者是对坐席输入框内文字的主动加工,不在此开关作用范围内,二者不可混淆。
---
## 二、三态定义
| 状态值(枚举) | 名称 | 行为 |
|------|------|------|
| `employee_only` | 仅回复员工(**默认**) | AI 只回复员工(等同现状) |
| `employee_and_agent` | 回复员工 + 推送坐席 | AI 回复员工,**同时**把 AI 回复推送给坐席端,让坐席也看到 AI 在说什么,便于介入 |
| `off` | 关闭自动回复 | 停止**所有** AI 自动回复 |
- **作用域****单会话**——每个 Conversation 独立保存自己的模式,不全局、不跨会话继承。
- **持久化**:切换即时生效并落库,刷新/切换会话后保持。
---
## 三、产品目标(Product Goals
| # | 目标 | 衡量标准 |
|---|------|---------|
| G1 | **补齐模式控制缺口**:让坐席能显式、即时地控制 AI 自动回复的开关与可见范围 | 三态可切换、即时生效、单会话独立持久化,100% 覆盖两条接入路径 |
| G2 | **修复接单后 AI 仍自动回复的异常**:坐席接管(serving)或手动关闭后,H5/WebSocket 路径不得再自动回复员工 | serving 态 + off 态下,路径 B 触发率为 0(现状为 100% 误触发) |
| G3 | **低认知负担的坐席操作**:用 1 个图标按钮 + 1 个 Popover 完成全部控制,与既有工具栏风格一致 | 坐席 1 次点击即可看到/切换当前态,无需进入设置页 |
---
## 四、用户故事(坐席视角)
| # | 角色 | 故事 | 价值 |
|---|------|------|------|
| US1 | 坐席 | 作为坐席,我希望在工具栏一键查看并切换「AI 自动回复」的当前模式,这样我能立刻知道 AI 现在是怎么回的 | 状态可见、可控,减少意外 |
| US2 | 坐席 | 作为坐席,当我接单(serving)后,我希望 AI **停止**自动回复员工,这样不会和我抢话、避免重复/矛盾答复 | 避免人机混答的混乱 |
| US3 | 坐席 | 作为坐席,当我还不想亲自回复、又想盯着 AI 时,我希望切到「回复员工+推送坐席」,这样 AI 回复员工的同时我也能在工作台看到,随时介入 | 人机协作、留痕可见 |
| US4 | 坐席 | 作为坐席,我希望切换只对**当前会话**生效、且立即保存,这样我处理不同会话时各自独立、刷新不丢失 | 单会话独立、可靠持久化 |
| US5 | 坐席 | 作为坐席,当我需要完全静默(如转人工/安抚/敏感场景)时,我希望一键「关闭自动回复」,这样 AI 绝不自动发声 | 可控的完全静默 |
---
## 五、技术规范
### 5.1 数据模型(约束)
- `src/backend/app/models/conversation.py``Conversation` 模型**当前无** `ai_reply_mode` / `reply_scope` 类字段。
- **必须新增正交枚举字段**(不得复用 `status` 状态机):
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `ai_reply_mode` | enum(`employee_only` / `employee_and_agent` / `off`) | `employee_only` | AI 自动回复模式,与 status 正交 |
- 存量会话迁移:默认值 `employee_only`(等同现状),不影响既有业务。
### 5.2 关键代码事实(已查证,作为约束纳入,无需重查)
AI 自动回复存在**两条完全独立的路径**,开关必须**同时作用两条**:
| 路径 | 入口 | 当前门控 | 回复去向 |
|------|------|---------|---------|
| **路径 A(企微 App 接入)** | `src/backend/app/services/message_router.py:176` | 仅当 `conversation.status == "ai_handling"` 触发 `_try_ai_reply` | `wecom_service.send_text_message(user_id=from_user_id)` **只推员工** |
| **路径 BH5 / WebSocket 接入)** | `src/backend/app/api/h5.py:939``src/backend/app/api/ws.py:472` | **零 status 门控**(直接 `asyncio.create_task(process_h5_ai_reply(...))`;已 grep 确认 `h5_ai_task.py` 内 status 仅用于写回 queued / WS 展示,从不读 status 拦截) | 把 AI 回复推给员工(`ai_reply`+ **广播 `new_message(sender=ai)` 给坐席端** |
> **🔴 现状缺口(P0 必修复)**:坐席接单(status→serving)后,路径 A 的 AI 已停(受 status 门控),但**路径 B 的 AI 仍在自动回复员工**——这是未预期行为。本次需求必须修复:**路径 B 在 `status == serving`(已接管)或 `ai_reply_mode == off` 时不得触发 AI 自动回复**。
### 5.3 需求池(按优先级)
#### P0Must have
| ID | 需求 | 验收标准 |
|----|------|---------|
| P0-1 | **新增正交枚举字段** `ai_reply_mode`employee_only / employee_and_agent / off,默认 employee_only),不触碰 status 状态机 | 模型迁移落地;存量会话默认值=employee_only;字段可被 ORM 读写 |
| P0-2 | **API 设置单会话模式**:提供接口设置指定 Conversation 的 `ai_reply_mode`,即时生效并落库,返回更新后值 | 调用后 DB 立即更新;返回新值;并发切换以最后一次为准 |
| P0-3 | **路径 A 门控**`message_router.py:176``_try_ai_reply``status==ai_handling` 基础上,**额外**增加 `ai_reply_mode != off` 判断;当 `off` 时即使 `ai_handling` 也不触发 | off 态下路径 A 触发率=0employee_only / employee_and_agent 下路径 A 行为同现状(仅推员工) |
| P0-4 | **路径 B 门控 + 接单后停 AI(修复缺口)**`h5.py:939``ws.py:472` 触发 `process_h5_ai_reply` 前,增加拦截——当 `ai_reply_mode == off` **或** `conversation.status == serving` 时**禁止触发** | serving 态 与 off 态下路径 B 触发率=0;修复「接单后 AI 仍自动回复员工」异常 |
| P0-5 | **工具栏 🤖 按钮 + Popover**`ReplyBox.vue` 工具栏新增 🤖 图标按钮,点击 `el-popover` 弹出三态单选(显示当前态、可切换),切换即时调 API 持久化 | 按钮可见、Popover 三态可点选;切换后当前态实时刷新;失败有提示/回滚 |
| P0-6 | **单会话持久化**:每个 Conversation 独立保存 `ai_reply_mode`,切换仅作用于当前会话、立即写后端落库;切到另一会话读取该会话自己的模式 | 多会话间互不影响;刷新/重进会话后保持已选模式 |
| P0-7 | **employee_and_agent 的坐席可见性**:当模式为 `employee_and_agent` 时,AI 自动回复在推员工的同时,需**同步让坐席端可见**(确保 AI 回复消息持久化并广播 `new_message(sender=ai)` 给坐席) | 坐席工作台能看到 AI 的自动回复内容,便于介入(如路径 A 当前仅推员工,需确认坐席侧展示链路) |
#### P1Should have
| ID | 需求 | 验收标准 |
|----|------|---------|
| P1-1 | **乐观更新 + 失败回滚**:切换时 UI 先即时反映,API 失败时回滚到原值并 `ElMessage` 报错 | 网络异常下状态不漂移 |
| P1-2 | **AI 回复来源标识**:在坐席端 AI 自动回复消息旁展示「AI 自动回复」标识/来源标签,尤其 `employee_and_agent` 时让坐席明确这是 AI 发的、便于接管 | 坐席可区分 AI 自动消息与人工消息 |
| P1-3 | **切换轻提示**:切换成功后 `ElMessage.success("已切换为:AI回复-仅员工/员工+坐席/关闭")` | 坐席明确当前生效模式 |
| P1-4 | **在途任务边界**:切换 `off`/`serving` 后,对**已提交但尚未完成**的 H5 AI 回复 task 不强制中断,仅阻止**新触发**(明确边界,避免打断在途回复造成半截消息) | 不出现半截/重复消息 |
#### P2Nice to have
| ID | 需求 | 验收标准 |
|----|------|---------|
| P2-1 | **按钮当前态可视化**:🤖 按钮用色点/图标变化表达当前态(如 off 置灰、employee_and_agent 高亮),hover tooltip 显示当前态文字 | 无需打开 Popover 即可一眼识别模式 |
| P2-2 | **快捷键切换**:支持快捷键在三种模式间循环切换 | 坐席可无鼠标操作 |
| P2-3 | **操作审计留痕**:记录「谁、何时、从 X 切到 Y」的切换日志 | 管理/复盘可追溯 |
| P2-4 | **会话详情/设置可查看**:在会话详情或设置区也可只读查看当前 `ai_reply_mode` | 多入口可见 |
---
## 六、UI 设计稿
### 6.1 工具栏按钮位置(基于既有 `ReplyBox.vue` 工具栏结构)
既有工具栏结构:`[常规工具组 toolbar-left] | tb-sep | [AI 工具组 AiAssistToolbar: 💡自动补齐 🎚️语气 🖌️润色 🔄改写]`
🤖 开关是「状态控制」而非「文字加工动作」,建议作为**最左侧独立段**(与常规工具组之间用 `tb-sep` 分隔),使其在一眼可见的位置且不与 4 个主动 AI 按钮混淆:
```
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 回复框 ReplyBox │
│ ┌── 拖拽手柄 ────────────────────────────────────────────────────────────────┐ │
│ │ [🤖 AI自动回复] │sep│ ✂️截图 📷拍照 │sep│ 😊表情 📎文件 │sep│ 👥邀请 │sep│ 💡 🎚️ 🖌️ 🔄 │ ← 工具栏 chat-toolbar
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ textarea …… 🎤 发 送 │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
说明:
- 🤖 按钮使用既有 .tb-btn 样式(32×28hover 变 --accent),单独成组,左侧以 tb-sep 与常规工具组隔开。
- 当前态表达(P2-1,P0 至少保证 Popover 内清晰显示):
employee_only → 🤖 正常色
employee_and_agent → 🤖 高亮(如蓝色描边/小蓝点)
off → 🤖 置灰(如灰色 + 斜杠感)
- 点击 🤖 触发 el-popoverreference=🤖 按钮,placement=bottom-start)。
```
### 6.2 Popover 三态布局(ASCII 草图)
```
┌───────────────────────────────────────┐
│ 🤖 AI 自动回复模式 │
│ ───────────────────────────────────── │
│ (●) 仅回复员工 (默认) │
│ AI 只回复员工,与现状一致 │
│ │
│ (○) 回复员工 + 推送坐席 │
│ AI 回复员工,坐席端同步可见 │
│ │
│ (○) 关闭自动回复 │
│ AI 不再自动回复任何消息 │
│ ───────────────────────────────────── │
│ 当前:仅回复员工 │
└───────────────────────────────────────┘
交互:
- 三态为单选(el-radio-group),默认选中当前会话的 ai_reply_mode。
- 选中即触发:调用 API 写库 → 成功 → 关闭 Popover + ElMessage 提示 + 按钮当前态刷新。
- 失败 → 回滚选中项 + ElMessage 报错,Popover 保持打开。
```
### 6.3 状态机上下文(仅作背景,不在本需求改动)
`ai_handling → queued → serving → pending_close → resolved`,其中 `serving` = 坐席已接管。本开关为**正交**的 `ai_reply_mode`,不修改该状态机;但路径 B 的拦截条件包含 `status == serving`(见 P0-4)。
---
## 七、待确认问题清单(Open Questions
| # | 问题 | 影响 | 建议/所需决策方 |
|---|------|------|----------------|
| Q1 | `employee_and_agent` 的「推送给坐席」在**路径 A(企微 App 接入)**下如何保证坐席可见?路径 A 当前仅 `wecom send` 给员工,坐席侧是否已存在该 AI 消息的持久化+广播链路? | 影响 P0-7 实现方式 | 架构师确认广播/持久化机制 |
| Q2 | 路径 B 在 `status == ai_handling` / `queued` 时是否**仍应**触发 AI(本期是否仅新增 `off` + `serving` 两个拦截,其余 status 保持零门控现状)? | 影响 P0-4 拦截条件边界 | 产品经理+架构师确认 |
| Q3 | 切换 `off`/`serving` 后,对**已提交未完成**的 H5 AI 回复 task 是否中断?(建议不中断在途,仅阻止新触发) | 影响 P1-4 边界 | 架构师确认 |
| Q4 | 字段/枚举命名确认:`ai_reply_mode` vs `reply_scope`?枚举值 `employee_and_agent` 是否易被误解为「员工和坐席都来对话」?是否需要更直白命名(如 `agent_visible`)? | 影响数据模型与前端文案 | 产品经理+架构师 |
| Q5 | API 形态:扩展现有 `PATCH /api/conversations/{id}`,还是新增专用 `PUT /api/conversations/{id}/ai-reply-mode`? | 影响 P0-2 前后端约定 | 架构师/后端 |
| Q6 | 存量会话迁移后,路径 B 在 `serving` 下从「会回复」变为「不回复」属行为变更,是否可接受为预期修复(非 breaking)? | 影响 P0-4 / 迁移说明 | 产品经理确认 |
| Q7 | 🤖 按钮最终放置位置(最左独立段 / AI 工具组内 / 最右)与当前态的可视化方案(色点/图标/斜杠)? | 影响 6.1/6.2 UI 实现 | 产品经理+前端 |