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-*/
182 lines
15 KiB
Markdown
182 lines
15 KiB
Markdown
# 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)` **只推员工** |
|
||
| **路径 B(H5 / 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 需求池(按优先级)
|
||
|
||
#### P0(Must 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 触发率=0;employee_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 当前仅推员工,需确认坐席侧展示链路) |
|
||
|
||
#### P1(Should 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 不强制中断,仅阻止**新触发**(明确边界,避免打断在途回复造成半截消息) | 不出现半截/重复消息 |
|
||
|
||
#### P2(Nice 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×28,hover 变 --accent),单独成组,左侧以 tb-sep 与常规工具组隔开。
|
||
- 当前态表达(P2-1,P0 至少保证 Popover 内清晰显示):
|
||
employee_only → 🤖 正常色
|
||
employee_and_agent → 🤖 高亮(如蓝色描边/小蓝点)
|
||
off → 🤖 置灰(如灰色 + 斜杠感)
|
||
- 点击 🤖 触发 el-popover(reference=🤖 按钮,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 实现 | 产品经理+前端 |
|