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-*/
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# PRD - 摇人功能(坐席呼叫坐席)
|
||||
|
||||
> **REQ编号**: REQ-会话-003
|
||||
> **版本**: v1.0
|
||||
> **优先级**: P1
|
||||
> **日期**: 2026-07-19
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
坐席在处理会话时,可呼叫其他坐席进入群聊协助处理,实现多人协作。
|
||||
|
||||
---
|
||||
|
||||
## 二、需求详情
|
||||
|
||||
### 2.1 核心概念
|
||||
|
||||
| 术语 | 定义 | 区别 |
|
||||
|------|------|------|
|
||||
| **人工** | 用户(员工)呼叫坐席 | H5用户端触发 |
|
||||
| **摇人** | 坐席呼叫其他坐席 | 仅坐席端可用 |
|
||||
|
||||
### 2.2 功能设计
|
||||
|
||||
**触发方式**:
|
||||
- 坐席点击「摇人」按钮 → 选择坐席 → 发送邀请
|
||||
- 被邀请坐席收到通知 → 点击加入群聊
|
||||
|
||||
**群聊模式**:
|
||||
- 主责坐席 + 被邀请坐席 + 员工
|
||||
- 消息实时同步所有参与者
|
||||
- 支持转让主责
|
||||
|
||||
### 2.3 坐席端摇人话术体系
|
||||
|
||||
| 触发场景 | 话术 | 语气 |
|
||||
|---------|------|------|
|
||||
| 点击「摇人」按钮 | 大哥,俺这就去摇人,稍等... | 亲切 |
|
||||
| 排队等待(30秒) | 人还在路上,别急别急~ | 安抚 |
|
||||
| 被邀请坐席接入 | 坐席已加入群聊 | 明确交接 |
|
||||
| 等待超时(2分钟) | 坐席都在忙,不过AI还在呢... | 降级安抚 |
|
||||
|
||||
> 话术存配置表,支持后台动态修改
|
||||
|
||||
---
|
||||
|
||||
## 三、关联文档
|
||||
|
||||
- 原型: `原型-REQ-集成-002-邀请流程-v1.0.html`
|
||||
@@ -0,0 +1,191 @@
|
||||
# 坐席端截图 + 拍照功能 PRD
|
||||
|
||||
> **子系统**: 04-坐席工作台
|
||||
> **模块**: 截图拍照
|
||||
|
||||
## 项目信息
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| **语言** | 中文 |
|
||||
| **技术栈** | Vue3 + Element Plus + Tailwind CSS |
|
||||
| **项目名称** | agent_screenshot_camera |
|
||||
| **前端目录** | `D:\资料\03-项目开发\wecom_it_smart_desk\frontend-agent` |
|
||||
| **涉及组件** | `ReplyBox.vue` / `ScreenshotEditor.vue` / `ScreenCapture.vue`(改造)+ 新增 `CameraCapture.vue` |
|
||||
|
||||
### 原始需求复述
|
||||
|
||||
IT智能服务台坐席端(PC Web 应用)需要增加截图和相机拍照功能。客服在处理工单时经常需要截取屏幕内容或拍照来辅助沟通。截图需支持全屏/窗口/标签页选择(含跨屏),截图时屏幕变暗出现矩形选框 + 工具栏(马赛克、文字标注、箭头、撤销),样式参考微信/企微截图工具。拍照需用 `getUserMedia` + `<video>` 实现内嵌摄像头预览,支持重拍。截图和拍照结果均插入到聊天输入区作为图片消息等待发送(非立即发送)。
|
||||
|
||||
---
|
||||
|
||||
## 产品定义
|
||||
|
||||
### 产品目标
|
||||
|
||||
1. **提升沟通效率**:客服无需切换到外部截图工具或手机拍照,直接在工作台内完成截图/拍照→编辑→发送的全流程,减少上下文切换。
|
||||
2. **保证信息准确性**:通过截图标注(马赛克打码、箭头指引、文字说明)和实时拍照,让客服能够精准传递屏幕画面和实物状况,减少口头描述的歧义。
|
||||
3. **统一图片消息发送体验**:截图和拍照结果统一进入输入区待发送队列,客服可在发送前预览、补充文字说明或删除,与现有粘贴图片/文件上传的交互保持一致。
|
||||
|
||||
### 用户故事
|
||||
|
||||
1. **作为客服坐席**,我想直接在工作台点击截图按钮截取屏幕画面,这样我就能把用户报障的屏幕现象快速发给用户确认,而不需要切换到微信截图或系统截图工具。
|
||||
|
||||
2. **作为客服坐席**,我想在截图时用马赛克遮挡敏感信息(如密码、个人信息),这样在发送截图时不会泄露用户隐私。
|
||||
|
||||
3. **作为客服坐席**,我想在截图选区内用箭头和文字标注重点位置,这样用户能快速理解我指的具体是哪里。
|
||||
|
||||
4. **作为客服坐席**,我想通过摄像头拍照来记录实物设备状态(如硬件故障、线缆连接),这样比文字描述更直观。
|
||||
|
||||
5. **作为客服坐席**,我想截图/拍照后的图片先进入输入区等我自己确认后再发送,这样我能在发送前检查标注是否正确、是否打码到位。
|
||||
|
||||
---
|
||||
|
||||
## 技术规范
|
||||
|
||||
### 现有代码分析
|
||||
|
||||
通过对 `frontend-agent/src/components/chat/` 目录的代码审查,发现以下现状:
|
||||
|
||||
| 组件 | 现状 | 差距 |
|
||||
|---|---|---|
|
||||
| `ReplyBox.vue` | 工具栏有表情、文件、邀请、快速回复按钮;已有 `handleScreenshot()`(用 `html2canvas`)和 `handleScreenCapture()`(用 `getDisplayMedia`,但未接入任何按钮) | 缺少截图/拍照入口按钮;截图结果直接上传发送而非插入输入区 |
|
||||
| `ScreenshotEditor.vue` | 裁剪 + 编辑两步模式,工具有画笔/高亮/矩形/箭头/文字,支持撤销/重做 | 缺少马赛克工具 |
|
||||
| `ScreenCapture.vue` | 全屏遮罩框选模式,工具有画笔/矩形/箭头/文字 | 缺少马赛克工具;结果直接发送 |
|
||||
| `upload.ts` | 文件上传 API,支持重试,返回 `{ url, filename, file_size, msg_type }` | 可复用 |
|
||||
| 拍照功能 | **完全不存在** | 需新建 `CameraCapture.vue` 组件 |
|
||||
|
||||
**关键发现**:`ReplyBox.vue` 中已有 `handleScreenCapture()` 方法调用了 `navigator.mediaDevices.getDisplayMedia()`,但该方法未绑定到任何 UI 按钮。当前截图按钮实际走的是 `handleScreenshot()` → `html2canvas`(仅能截取当前页面 DOM,无法截取其他窗口/屏幕)。
|
||||
|
||||
### 需求池
|
||||
|
||||
#### P0 — 必须实现(Must Have)
|
||||
|
||||
| 编号 | 需求 | 验收标准 |
|
||||
|---|---|---|
|
||||
| P0-1 | **截图入口按钮**:在 `ReplyBox.vue` 工具栏新增截图按钮(图标裁剪刀或相机图标),与表情、文件等按钮并列 | 点击后触发 `getDisplayMedia()` 屏幕选择流程 |
|
||||
| P0-2 | **`getDisplayMedia` 屏幕捕获**:调用 `navigator.mediaDevices.getDisplayMedia()` 获取屏幕流,捕获一帧画面传入截图编辑器 | 浏览器弹出屏幕/窗口/标签页选择器;支持选择跨屏显示器;用户取消时静默退出不报错 |
|
||||
| P0-3 | **截图选区编辑**:全屏遮罩 + 矩形选框拖拽,选区内可编辑,选区外半透明遮罩(对标微信截图) | 屏幕变暗;拖拽出现选框;选框可拖动调整大小;8 个方向手柄 |
|
||||
| P0-4 | **马赛克工具**:截图编辑工具栏新增马赛克工具,拖拽涂抹区域进行像素化打码 | 选择马赛克工具后在选区内拖拽,拖过区域被马赛克覆盖;支持撤销 |
|
||||
| P0-5 | **文字标注工具**:点击选区内位置弹出文本输入,输入后渲染为画布上的文字标注 | 点击出现输入框(非 `prompt()`,用内联 input);回车确认渲染到 canvas |
|
||||
| P0-6 | **箭头工具**:拖拽绘制带箭头的线段 | 起点到终点绘制箭头;箭头头部自动计算角度 |
|
||||
| P0-7 | **撤销功能**:工具栏撤销按钮,逐步撤销编辑操作 | 每次操作入栈,撤销弹栈;撤销到空时禁用按钮 |
|
||||
| P0-8 | **截图结果插入输入区**:截图确认后,图片以缩略图形式显示在输入框上方(textarea 之上),作为待发送图片,不自动发送 | 输入区上方出现图片缩略图 + 删除按钮;可继续输入文字;点击发送时图片和文字一起发送 |
|
||||
| P0-9 | **拍照入口按钮**:在 `ReplyBox.vue` 工具栏新增拍照按钮(摄像头图标) | 点击后弹出摄像头预览弹窗 |
|
||||
| P0-10 | **`getUserMedia` 摄像头预览**:用 `navigator.mediaDevices.getUserMedia({ video: true })` 获取摄像头流,渲染到 `<video>` 元素实时预览 | 弹窗内嵌视频预览;自动选择前置摄像头;摄像头不可用时显示错误提示 |
|
||||
| P0-11 | **拍照捕获**:点击拍照按钮,从 `<video>` 当前帧 capture 到 canvas,转为图片 | 拍照后预览切换为静态图片;显示"重拍"和"确认"按钮 |
|
||||
| P0-12 | **重拍功能**:拍照后点击"重拍"返回实时预览状态 | 重拍后清除上一张照片,恢复 `<video>` 实时预览 |
|
||||
| P0-13 | **拍照结果插入输入区**:拍照确认后,图片以缩略图形式显示在输入框上方,作为待发送图片 | 与 P0-8 行为一致 |
|
||||
| P0-14 | **待发送图片发送流程**:发送消息时,如果有待发送图片,先上传图片再发送图片消息(`msg_type: 'image'`),如有文字则额外发送文字消息 | 图片上传复用 `uploadFile()` API;发送成功后清空待发送图片和文字 |
|
||||
| P0-15 | **待发送图片删除**:输入区待发送图片缩略图上有删除按钮,点击移除 | 点击 × 后缩略图消失,不影响文字内容 |
|
||||
|
||||
#### P1 — 应该实现(Should Have)
|
||||
|
||||
| 编号 | 需求 | 验收标准 |
|
||||
|---|---|---|
|
||||
| P1-1 | **多张待发送图片**:输入区可同时插入多张待发送图片(如先截一张再拍一张) | 支持多张缩略图横向排列;发送时按顺序上传发送;可单独删除某一张 |
|
||||
| P1-2 | **颜色选择**:截图编辑工具栏提供颜色选择(红/黄/蓝/绿/黑等常用色) | 点击颜色切换标注颜色;当前选中颜色高亮 |
|
||||
| P1-3 | **矩形框工具**:拖拽绘制矩形边框(用于圈选区域) | 与微信截图矩形工具一致 |
|
||||
| P1-4 | **截图保存到本地**:截图编辑器提供"保存"按钮,将截图下载到本地 | 下载为 PNG 文件,文件名 `screenshot-{timestamp}.png` |
|
||||
| P1-5 | **摄像头切换**:如果有多个摄像头,支持切换前后摄像头 | 弹窗内显示切换按钮;切换后预览更新 |
|
||||
| P1-6 | **ESC 退出**:截图模式和拍照弹窗均支持 ESC 键退出 | 按 ESC 等同于取消 |
|
||||
| P1-7 | **截图选区尺寸显示**:拖拽选区时实时显示选区宽×高像素值 | 选区右下角或工具栏旁显示 `W×H` 像素值 |
|
||||
|
||||
#### P2 — 优先级较低(Nice to Have)
|
||||
|
||||
| 编号 | 需求 | 验收标准 |
|
||||
|---|---|---|
|
||||
| P2-1 | **画笔自由绘制**:截图编辑器提供自由画笔工具 | 拖拽绘制自由曲线;线宽可调 |
|
||||
| P2-2 | **高亮工具**:半透明黄色高亮笔 | 拖拽覆盖区域呈半透明高亮 |
|
||||
| P2-3 | **快捷键截图**:支持 `Ctrl+Shift+A` 快捷键触发截图 | 全局快捷键,焦点不在输入框也能触发 |
|
||||
| P2-4 | **拍照镜像翻转**:前置摄像头预览水平镜像(符合自拍习惯),但保存的图片不镜像 | 预览镜像、输出正向 |
|
||||
| P2-5 | **截图延时捕获**:支持 3 秒延时截图(用于捕获鼠标悬停菜单等动态画面) | 选择延时后倒计时 3-2-1 再捕获 |
|
||||
|
||||
---
|
||||
|
||||
## UI 设计要点
|
||||
|
||||
### 1. 工具栏布局(ReplyBox.vue)
|
||||
|
||||
现有工具栏顺序:`表情 | 文件 | 分隔线 | 邀请 | 快速回复`
|
||||
|
||||
**改造后**:`截图 | 拍照 | 分隔线 | 表情 | 文件 | 分隔线 | 邀请 | 快速回复`
|
||||
|
||||
- 截图按钮放在最左侧(最高频使用),图标用裁剪刀 SVG 或 ✂️
|
||||
- 拍照按钮紧跟截图按钮,图标用摄像头 SVG 或 📷
|
||||
- 与现有按钮样式保持一致(`.tb-btn` 类,32×28px,hover 高亮 + tooltip)
|
||||
|
||||
### 2. 截图模式(全屏覆盖层)
|
||||
|
||||
- **触发**:点击截图按钮 → 浏览器弹出 `getDisplayMedia` 选择器(选屏幕/窗口/标签页)
|
||||
- **选区**:选中后屏幕变暗(`rgba(0,0,0,0.5)` 遮罩),鼠标变为十字光标
|
||||
- **拖拽框选**:按住鼠标拖拽出现矩形选框(绿色边框 `#07C160`,对标微信),选区内画面清晰,选区外半透明
|
||||
- **8 方向手柄**:选框四角 + 四边中点各一个手柄,可拖拽调整大小
|
||||
- **工具栏**:选区下方居中浮动工具栏,从左到右:
|
||||
- 工具组:`马赛克 | 文字 | 箭头 | 矩形`(P1)
|
||||
- 颜色组:`🔴 🟡 🔵 🟢 ⚫`(P1)
|
||||
- 操作组:`撤销 | 保存(P1) | 取消 | 确认✓`
|
||||
- **尺寸提示**:选区右下角实时显示 `宽×高` 像素值(P1)
|
||||
- **ESC 退出**:按 ESC 取消截图
|
||||
|
||||
### 3. 拍照弹窗(CameraCapture.vue)
|
||||
|
||||
- **弹出方式**:Element Plus `el-dialog` 居中弹窗,宽度约 640px
|
||||
- **预览区**:`<video>` 元素填满弹窗主体,自适应摄像头分辨率
|
||||
- **控制区**:弹窗底部居中放置拍照按钮(圆形大按钮,图标 📷)
|
||||
- **拍照后状态**:
|
||||
- `<video>` 隐藏,显示 `<canvas>` 或 `<img>` 静态画面
|
||||
- 底部按钮变为:`重拍` (左)| `确认` (右,绿色主按钮)
|
||||
- **错误状态**:摄像头不可用时显示"无法访问摄像头,请检查权限设置"提示 + 关闭按钮
|
||||
- **关闭**:右上角 ✕ 或 ESC 关闭弹窗,停止摄像头流
|
||||
|
||||
### 4. 待发送图片预览区(输入框上方)
|
||||
|
||||
- **位置**:在 `ReplyBox.vue` 的 `textarea` 上方、工具栏下方,新增一个待发送图片预览条
|
||||
- **布局**:横向排列缩略图,每张缩略图约 80×80px,圆角,右上角有 × 删除按钮
|
||||
- **样式**:浅色背景容器,与现有引用回复预览条(`.reply-preview`)风格一致
|
||||
- **多张图片**:横向排列,超出宽度时可横向滚动
|
||||
- **交互**:
|
||||
- 鼠标悬停缩略图可放大预览(可选 P2)
|
||||
- 点击 × 删除该张待发送图片
|
||||
- 发送后清空所有待发送图片
|
||||
|
||||
### 5. 视觉风格
|
||||
|
||||
- 主色调:与现有坐席端一致,使用 CSS 变量 `var(--accent)`、`var(--bg-secondary)` 等
|
||||
- 截图选框颜色:`#07C160`(微信绿,与现有 `ScreenCapture.vue` 一致)
|
||||
- 工具栏:白色背景 + 阴影,与现有 `ScreenshotEditor.vue` 工具栏风格一致
|
||||
- 弹窗:Element Plus 默认主题,圆角 8px
|
||||
|
||||
---
|
||||
|
||||
## 待确认问题
|
||||
|
||||
| 编号 | 问题 | 影响范围 | 建议 |
|
||||
|---|---|---|---|
|
||||
| Q1 | **企微内嵌浏览器兼容性**:`getDisplayMedia()` 和 `getUserMedia()` 在企微内置浏览器中是否可用?现有代码注释提到"Screen Capture API 在企微桌面端被限制,不稳定"。 | 截图功能核心可行性 | 建议在企微环境下做一次可用性测试;如不可用,保留 `html2canvas` 作为 fallback(仅能截当前页面) |
|
||||
| Q2 | **HTTPS 要求**:`getDisplayMedia()` 和 `getUserMedia()` 要求 HTTPS 环境(或 localhost)。坐席端部署是否为 HTTPS? | 截图 + 拍照功能可行性 | 确认生产环境域名是否为 HTTPS;如非 HTTPS 则两个功能都无法使用 |
|
||||
| Q3 | **待发送图片与文字的组合发送**:用户在输入区有待发送图片 + 文字时,发送方式是:(a) 先发图片消息再发文字消息(两条消息)?还是 (b) 合并为一条富文本消息? | 消息发送逻辑 | 建议方案 (a):先发图片再发文字,与现有粘贴图片行为一致。后端如支持图文混排消息可改为 (b) |
|
||||
| Q4 | **截图工具栏工具优先级**:需求明确提到"马赛克、文字标注、箭头、撤销"四个工具。是否也需要矩形框工具(P1)和高亮/画笔(P2)? | 编辑器功能范围 | 建议 P0 只做需求明确的 4 个工具,矩形框放入 P1 |
|
||||
| Q5 | **摄像头权限提示**:首次使用拍照功能时浏览器会请求摄像头权限。是否需要自定义权限引导提示? | 用户体验 | 建议在调用 `getUserMedia` 前先显示一个简短提示"即将请求摄像头权限,请在弹窗中允许" |
|
||||
| Q6 | **InputBox.vue 是否同步改造**:代码库中存在 `InputBox.vue`(旧版输入组件)和 `ReplyBox.vue`(当前使用)。是否需要同步改造 `InputBox.vue`? | 改造范围 | 建议只改 `ReplyBox.vue`,`InputBox.vue` 如已废弃则不动 |
|
||||
| Q7 | **截图后的编辑器复用**:现有 `ScreenshotEditor.vue`(裁剪+编辑两步)和 `ScreenCapture.vue`(全屏框选+编辑)两个组件功能重叠。新截图功能基于哪个改造? | 架构设计 | 建议基于 `ScreenCapture.vue` 改造(全屏遮罩框选更符合微信截图体验),废弃或保留 `ScreenshotEditor.vue` 供 `html2canvas` fallback 使用 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:现有代码关键路径
|
||||
|
||||
```
|
||||
frontend-agent/src/
|
||||
├── components/chat/
|
||||
│ ├── ReplyBox.vue ← 主输入组件(改造:加按钮 + 待发送图片区)
|
||||
│ ├── InputBox.vue ← 旧版输入组件(待确认是否改造)
|
||||
│ ├── ScreenshotEditor.vue ← 截图编辑器(改造:加马赛克工具)
|
||||
│ ├── ScreenCapture.vue ← 全屏框选截图(改造:接入 getDisplayMedia + 加马赛克)
|
||||
│ └── CameraCapture.vue ← 新建:摄像头拍照组件
|
||||
├── api/
|
||||
│ ├── upload.ts ← 文件上传 API(复用)
|
||||
│ └── message.ts ← 消息发送 API(复用)
|
||||
└── stores/
|
||||
└── conversation.ts ← 会话状态管理(可能需扩展:待发送图片队列)
|
||||
```
|
||||
@@ -0,0 +1,622 @@
|
||||
# 坐席端 AI 辅助消息框 — 产品需求文档 (PRD)
|
||||
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-07-11
|
||||
> **作者**: 宋献
|
||||
> **状态**: 需求已确认,待开发
|
||||
> **子系统**: 04-坐席工作台
|
||||
> **模块**: AI辅助
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状分析
|
||||
|
||||
坐席端已具备较完善的 AI 辅助基础设施:
|
||||
|
||||
| 已有功能 | 模式 | 状态 |
|
||||
|----------|------|------|
|
||||
| AI 草稿生成 | 点击右栏按钮 → 生成完整回复 → 采纳 | 已对接 Dify API |
|
||||
| AI 草稿气泡 | 消息流内联气泡 → 采纳/编辑/忽略 | 已对接后端 |
|
||||
| 快速回复模板 | 三层渐进导航 + 变量替换 | 本地静态数据 |
|
||||
| AI 推荐回复(内联) | 3 张卡片 + Ctrl+1/2/3 | Mock 数据,未对接 API |
|
||||
| 会话摘要 + 智能标注 | 结单时自动生成 | 已对接 Dify API |
|
||||
|
||||
**核心差距**:现有功能均为"生成完整草稿 → 坐席采纳"模式,缺失"输入过程中实时辅助 + 发送前精修"模式。
|
||||
|
||||
### 1.2 行业标杆
|
||||
|
||||
- **Freshdesk Freddy AI**:语气增强(更正式/更随意/专业/友好/随性),选中文字后一键改写
|
||||
- **智齿科技**:AI 扩写润色 + 风格设置(友好口吻/专业口吻)
|
||||
- **Yellow.ai**:Tone Changer + Sentiment Detection + AI Copilot
|
||||
|
||||
### 1.3 目标
|
||||
|
||||
在现有 ReplyBox 输入框中新增 4 项 AI 辅助功能,补齐"输入过程中实时辅助 + 发送前精修"能力:
|
||||
|
||||
1. **实时自动补齐** — 输入停顿时显示灰色幽灵文字,Tab 键接受
|
||||
2. **语气调整** — 选中文字后选择专业/友好/简洁风格,一键改写
|
||||
3. **文字润色** — 扩写/压缩/纠错,弹出精修面板确认后替换
|
||||
4. **智能改写** — 基于对话上下文 + 知识库,生成 3 个备选版本
|
||||
|
||||
### 1.4 设计原则
|
||||
|
||||
- **共存不替代**:新增功能与现有草稿生成互补,不替代
|
||||
- **渐进增强**:坐席可选择使用,不强制改变现有工作流
|
||||
- **低延迟感知**:补齐建议 < 1.5s,语气/润色/改写 < 3s
|
||||
- **可降级**:AI 服务不可用时不影响正常输入和发送
|
||||
|
||||
---
|
||||
|
||||
## 2. 功能详细需求
|
||||
|
||||
### 2.1 实时自动补齐
|
||||
|
||||
#### 2.1.1 交互流程
|
||||
|
||||
```
|
||||
坐席输入文字 → 停顿 > 0.8s → 调用 AI 补齐 API → 光标位置显示灰色幽灵文字
|
||||
├─ Tab 键 → 接受补齐,文字变为正常颜色
|
||||
├─ 继续输入 → 忽略补齐,重新触发
|
||||
└─ Esc 键 → 清除补齐建议
|
||||
```
|
||||
|
||||
#### 2.1.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 输入停顿 > 800ms,且输入框内容 > 5 字符 |
|
||||
| 请求防抖 | debounce 800ms,输入中取消上一个请求 |
|
||||
| 显示方式 | 光标后方灰色斜体文字(ghost text) |
|
||||
| 补齐长度 | 1-2 个短句(不超过 80 字符) |
|
||||
| 接受方式 | Tab 键接受全部;Shift+Tab 接受一个词 |
|
||||
| 忽略方式 | 继续输入自动忽略;Esc 清除 |
|
||||
| 上下文 | 携带最近 5 条对话消息 + 当前输入内容 |
|
||||
| 并发控制 | 同一时刻只保留最新的补齐请求 |
|
||||
| 加载状态 | 幽灵文字位置显示 3 个点动画(typing indicator) |
|
||||
| 超时处理 | 1.5s 未返回则取消,不显示 |
|
||||
|
||||
#### 2.1.3 前端实现要点
|
||||
|
||||
- 在 `<textarea>` 上方覆盖一层 `<div>` 渲染幽灵文字,通过计算光标位置定位
|
||||
- 或使用 `contenteditable` div 替换 textarea(更灵活但改动较大)
|
||||
- **推荐方案**:保持 `<textarea>` 不变,在下方独立区域显示补齐建议条(简化实现,降低风险)
|
||||
|
||||
> **决策点**:纯内联幽灵文字需要精确计算光标坐标,实现复杂度高。若需降低首版复杂度,可采用"输入框下方建议条"作为过渡方案。当前需求确认为内联幽灵文字。
|
||||
|
||||
#### 2.1.4 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/autocomplete
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"current_text": "张工您好,您反馈的VPN连接问题我已经",
|
||||
"cursor_position": 22,
|
||||
"max_length": 80
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"completion": "正在查看相关工单记录,请稍候。",
|
||||
"confidence": 0.85
|
||||
}
|
||||
```
|
||||
|
||||
**Dify System Prompt**:
|
||||
```
|
||||
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
|
||||
要求:
|
||||
1. 补齐内容自然衔接当前文字,不要重复已有内容
|
||||
2. 长度控制在1-2个短句,不超过80字
|
||||
3. 语气专业、简洁,符合IT服务规范
|
||||
4. 只返回补齐的文字,不要加引号或其他标记
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 语气调整
|
||||
|
||||
#### 2.2.1 交互流程
|
||||
|
||||
```
|
||||
坐席选中已输入文字 → 点击工具栏"语气"按钮 → 弹出语气选择菜单
|
||||
├─ 专业 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
|
||||
├─ 友好 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
|
||||
└─ 简洁 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
|
||||
```
|
||||
|
||||
#### 2.2.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 选中输入框中的文字(≥ 5 字符),点击工具栏"语气"按钮 |
|
||||
| 语气选项 | 专业 / 友好 / 简洁(3 种) |
|
||||
| 显示方式 | 弹出浮层菜单,3 个选项纵向排列 |
|
||||
| 改写显示 | 浮层中显示"原文 → 改写文"对比 |
|
||||
| 确认方式 | 点击"替换"按钮,用改写文替换选中文字 |
|
||||
| 取消方式 | 点击外部区域或 Esc 关闭浮层 |
|
||||
| 上下文 | 携带对话上下文 + 选中文字 + 当前完整输入内容 |
|
||||
| 超时处理 | 3s 未返回显示重试按钮 |
|
||||
|
||||
#### 2.2.3 语气定义
|
||||
|
||||
| 语气 | 定义 | 示例 |
|
||||
|------|------|------|
|
||||
| **专业** | 使用准确的技术术语,结构化表达,去除口语化内容 | "经排查,您的VPN连接异常是由本地网络DNS解析超时引起,建议执行以下操作:" |
|
||||
| **友好** | 适当增加问候和关心语,语气更亲和 | "张工您好~VPN连不上确实挺急的,我帮您看了下,可能是DNS解析有点慢,咱们试试这样操作:" |
|
||||
| **简洁** | 去除冗余,直奔主题,控制字数 | "VPN异常原因:DNS超时。操作:1. 刷新DNS 2. 重连VPN" |
|
||||
|
||||
#### 2.2.4 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/tone-adjust
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"selected_text": "VPN连接问题我已经在看了,你别急啊,我查一下",
|
||||
"full_text": "张工您好,VPN连接问题我已经在看了,你别急啊,我查一下",
|
||||
"tone": "professional",
|
||||
"conversation_context": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"rewritten_text": "经排查,您的VPN连接异常由本地网络DNS解析超时引起,建议执行以下操作:",
|
||||
"tone": "professional",
|
||||
"changes_summary": "去除口语化表达,增加技术术语,结构化排版"
|
||||
}
|
||||
```
|
||||
|
||||
**Dify System Prompt(按语气动态选择)**:
|
||||
```
|
||||
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
|
||||
要求:
|
||||
1. 保持原意不变,只调整语气和表达方式
|
||||
2. 改写后的文字长度与原文相近(±30%)
|
||||
3. 符合IT服务坐席的专业规范
|
||||
4. 只返回改写后的文字,不要加引号或解释
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 文字润色
|
||||
|
||||
#### 2.3.1 交互流程
|
||||
|
||||
```
|
||||
坐席点击工具栏"润色"按钮 → 弹出精修面板
|
||||
├─ 扩写 → AI 扩展内容,增加细节和步骤
|
||||
├─ 压缩 → AI 精简内容,去除冗余
|
||||
└─ 纠错 → AI 检查并修正语法/错别字
|
||||
→ 显示结果 → 确认替换 / 取消
|
||||
```
|
||||
|
||||
#### 2.3.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 点击工具栏"润色"按钮(无需选中文字,对全部内容操作) |
|
||||
| 操作选项 | 扩写 / 压缩 / 纠错(3 种) |
|
||||
| 显示方式 | 弹出精修面板,左右对比(原文 | 结果) |
|
||||
| 确认方式 | 点击"替换全部"用结果替换输入框全部内容 |
|
||||
| 部分采纳 | 支持在结果区域手动编辑后再替换 |
|
||||
| 超时处理 | 3s 未返回显示重试按钮 |
|
||||
|
||||
#### 2.3.3 操作定义
|
||||
|
||||
| 操作 | 定义 |
|
||||
|------|------|
|
||||
| **扩写** | 在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整 |
|
||||
| **压缩** | 精简表达,去除重复和冗余,保留核心信息,控制字数 |
|
||||
| **纠错** | 检查并修正错别字、语法错误、标点符号、格式问题 |
|
||||
|
||||
#### 2.3.4 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/polish
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"text": "你先试试重启一下vpn客户端 然后看看能不能连上来",
|
||||
"action": "expand",
|
||||
"conversation_context": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"polished_text": "建议您按以下步骤操作:\n1. 完全退出VPN客户端(不只是断开,需要关闭进程)\n2. 重新启动VPN客户端\n3. 输入账号密码重新连接\n4. 如仍无法连接,请截图错误信息发给我",
|
||||
"action": "expand",
|
||||
"changes_summary": "增加了操作步骤细化、注意事项和兜底方案"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 智能改写
|
||||
|
||||
#### 2.4.1 交互流程
|
||||
|
||||
```
|
||||
坐席点击工具栏"改写"按钮 → AI 基于对话上下文+知识库生成3个备选版本
|
||||
→ 弹出选择面板,展示3个版本
|
||||
→ 坐席选择一个版本 → 替换输入框内容 / 追加到输入框
|
||||
```
|
||||
|
||||
#### 2.4.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 点击工具栏"改写"按钮 |
|
||||
| 生成数量 | 3 个备选版本 |
|
||||
| 版本差异 | 版本1=简洁直接 / 版本2=详细带步骤 / 版本3=带知识库引用 |
|
||||
| 显示方式 | 弹出面板,3 个版本纵向排列,可滚动预览 |
|
||||
| 选择方式 | 点击版本卡片 → "替换"或"追加" |
|
||||
| 上下文 | 携带最近 10 条对话消息 + 知识库检索结果 |
|
||||
| 超时处理 | 5s 未返回显示重试按钮 |
|
||||
| 与草稿生成关系 | 草稿生成是右栏入口;改写是输入框内入口,更轻量 |
|
||||
|
||||
#### 2.4.3 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/rewrite
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"current_text": "",
|
||||
"generate_count": 3,
|
||||
"include_knowledge": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"text": "VPN连接异常,建议重启客户端后重试。",
|
||||
"style": "简洁直接",
|
||||
"source": "对话上下文"
|
||||
},
|
||||
{
|
||||
"text": "VPN连接问题处理步骤:\n1. 退出VPN客户端\n2. 重启客户端\n3. 重新连接\n如仍有问题请截图反馈",
|
||||
"style": "详细带步骤",
|
||||
"source": "对话上下文"
|
||||
},
|
||||
{
|
||||
"text": "根据知识库文档《VPN故障排查指南》,DNS解析超时是常见原因。建议:\n1. 执行 ipconfig /flushdns\n2. 重启VPN客户端\n3. 重连\n参考文档:[KB-VPN-001]",
|
||||
"style": "带知识库引用",
|
||||
"source": "RAGFlow知识库"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. UI 设计规格
|
||||
|
||||
### 3.1 工具栏布局
|
||||
|
||||
在 ReplyBox.vue 现有工具栏中新增 4 个 AI 辅助按钮:
|
||||
|
||||
```
|
||||
现有工具栏: [表情] [文件] [截图] [拍照] [语音] [闪电快捷回复] [邀请] [发送]
|
||||
新增区域: [补齐] [语气] [润色] [改写] [发送]
|
||||
```
|
||||
|
||||
- AI 辅助按钮使用 Element Plus 的 `el-tooltip` 悬浮提示
|
||||
- 按钮图标使用简洁线性图标,AI 相关按钮使用统一的紫色系(#534AB7)以区分
|
||||
- 按钮间距 8px,与现有按钮风格一致
|
||||
|
||||
### 3.2 幽灵文字样式
|
||||
|
||||
```css
|
||||
.ghost-text {
|
||||
color: #B4B2A9; /* 灰色 */
|
||||
font-style: italic; /* 斜体 */
|
||||
pointer-events: none; /* 不可点击 */
|
||||
user-select: none; /* 不可选中 */
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 语气选择浮层
|
||||
|
||||
```
|
||||
+-------------------------------+
|
||||
| 选择语气 |
|
||||
+-------------------------------+
|
||||
| [专业] 使用准确术语,结构化 |
|
||||
| [友好] 增加问候,语气亲和 |
|
||||
| [简洁] 去除冗余,直奔主题 |
|
||||
+-------------------------------+
|
||||
| 原文: VPN连不上别急我看看... |
|
||||
| 改写: 经排查,VPN连接异常... |
|
||||
+-------------------------------+
|
||||
| [替换] [取消] |
|
||||
+-------------------------------+
|
||||
```
|
||||
|
||||
### 3.4 润色精修面板
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| 文字润色 [扩写] [压缩] [纠错] |
|
||||
+------------------------------------------+
|
||||
| 原文 | 结果(可编辑) |
|
||||
| ---------- | ---------- |
|
||||
| 你先试试重启 | 建议您按以下步骤: |
|
||||
| 一下vpn客户端 | 1. 退出VPN客户端 |
|
||||
| 然后看看能不能 | 2. 重新启动 |
|
||||
| 连上来 | 3. 重新连接 |
|
||||
| | 4. 如仍不行请截图 |
|
||||
+------------------------------------------+
|
||||
| [替换全部] [取消] |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
### 3.5 改写版本选择面板
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| 智能改写 — 3 个备选版本 |
|
||||
+------------------------------------------+
|
||||
| 版本1: 简洁直接 |
|
||||
| VPN连接异常,建议重启客户端后重试。 |
|
||||
| [替换] [追加] |
|
||||
+------------------------------------------+
|
||||
| 版本2: 详细带步骤 |
|
||||
| VPN连接问题处理步骤: |
|
||||
| 1. 退出VPN客户端 |
|
||||
| 2. 重启客户端 |
|
||||
| ... |
|
||||
| [替换] [追加] |
|
||||
+------------------------------------------+
|
||||
| 版本3: 带知识库引用 |
|
||||
| 根据知识库文档《VPN故障排查》... |
|
||||
| [替换] [追加] |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 技术实现方案
|
||||
|
||||
### 4.1 后端
|
||||
|
||||
#### 4.1.1 新增 API 端点
|
||||
|
||||
在 `backend/app/api/wingman.py` 中新增 4 个端点:
|
||||
|
||||
| 端点 | 方法 | 功能 |
|
||||
|------|------|------|
|
||||
| `/api/conversations/{id}/wingman/autocomplete` | POST | 实时自动补齐 |
|
||||
| `/api/conversations/{id}/wingman/tone-adjust` | POST | 语气调整 |
|
||||
| `/api/conversations/{id}/wingman/polish` | POST | 文字润色 |
|
||||
| `/api/conversations/{id}/wingman/rewrite` | POST | 智能改写 |
|
||||
|
||||
#### 4.1.2 WingmanService 扩展
|
||||
|
||||
在 `backend/app/services/wingman_service.py` 中新增 4 个方法:
|
||||
|
||||
- `generate_completion()` — 补齐,使用低 temperature (0.2)
|
||||
- `adjust_tone()` — 语气调整,使用中 temperature (0.3)
|
||||
- `polish_text()` — 润色,使用中 temperature (0.3)
|
||||
- `rewrite_versions()` — 改写,使用高 temperature (0.6) 生成多样化版本
|
||||
|
||||
每个方法使用独立的 system prompt,复用现有 Dify API 调用管道。
|
||||
|
||||
#### 4.1.3 性能优化
|
||||
|
||||
- 补齐 API 设置独立超时:`timeout=3s`(比其他 API 更短)
|
||||
- 补齐 API 请求添加 `X-Request-Id` 头,前端可取消过时请求
|
||||
- 考虑对补齐结果做 Redis 缓存(key = hash(当前文字+对话ID),TTL=30s)
|
||||
|
||||
### 4.2 前端
|
||||
|
||||
#### 4.2.1 组件结构
|
||||
|
||||
```
|
||||
frontend-agent/src/components/chat/
|
||||
├── ReplyBox.vue (修改: 新增 AI 工具栏按钮)
|
||||
├── ai-assist/
|
||||
│ ├── GhostText.vue (新建: 幽灵文字渲染层)
|
||||
│ ├── ToneAdjustPopover.vue (新建: 语气选择浮层)
|
||||
│ ├── PolishPanel.vue (新建: 润色精修面板)
|
||||
│ ├── RewritePanel.vue (新建: 改写版本选择面板)
|
||||
│ └── AiAssistToolbar.vue (新建: AI 辅助工具栏组件)
|
||||
```
|
||||
|
||||
#### 4.2.2 API 封装
|
||||
|
||||
在 `frontend-agent/src/api/wingman.ts` 中新增 4 个方法:
|
||||
|
||||
```typescript
|
||||
export function autocomplete(convId: number, text: string, cursorPos: number): Promise<AutocompleteResponse>
|
||||
export function adjustTone(convId: number, selectedText: string, fullText: string, tone: ToneType): Promise<ToneAdjustResponse>
|
||||
export function polishText(convId: number, text: string, action: PolishAction): Promise<PolishResponse>
|
||||
export function rewriteVersions(convId: number, currentText: string): Promise<RewriteResponse>
|
||||
```
|
||||
|
||||
#### 4.2.3 关键实现细节
|
||||
|
||||
**幽灵文字定位**:
|
||||
- 创建一个隐藏的 `<div>` 镜像 textarea 的样式和内容
|
||||
- 通过镜像 div 计算光标在文本中的像素位置
|
||||
- 在该位置覆盖一层透明 `<textarea>` 或使用 `contenteditable` 渲染幽灵文字
|
||||
|
||||
**请求取消**:
|
||||
- 使用 `AbortController` 管理补齐请求
|
||||
- 新请求发起时 abort 上一个未完成的请求
|
||||
|
||||
**防抖**:
|
||||
- 补齐:debounce 800ms
|
||||
- 语气/润色/改写:无防抖(用户主动触发)
|
||||
|
||||
---
|
||||
|
||||
## 5. 与现有功能的关系
|
||||
|
||||
| 现有功能 | 新功能 | 关系 |
|
||||
|----------|--------|------|
|
||||
| AI 草稿生成(右栏 Wingman) | 智能改写(输入框内) | **互补**:草稿生成用于从零开始;改写用于对已有输入的多版本化 |
|
||||
| AI 草稿气泡 | 实时自动补齐 | **互补**:草稿气泡是系统主动推荐;补齐是输入过程中的延续 |
|
||||
| 快速回复模板 | 语气调整 / 润色 | **互补**:快速回复是模板填充;语气/润色是对自由文本的加工 |
|
||||
| AI 推荐回复(内联 Mock) | 智能改写 | **替代路径**:改写功能可使用真实 API,逐步替代 Mock 推荐回复 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 非功能需求
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 补齐延迟 | P95 < 1.5s |
|
||||
| 语气/润色延迟 | P95 < 3s |
|
||||
| 改写延迟 | P95 < 5s |
|
||||
| 可用性 | AI 服务不可用时不影响正常输入和发送 |
|
||||
| 并发 | 单个坐席同一时刻最多 1 个补齐请求 + 1 个其他 AI 请求 |
|
||||
| 数据安全 | 补齐请求不记录到数据库;语气/润色/改写可记录用于后续优化 |
|
||||
| 浏览器兼容 | Chrome 90+ / Edge 90+ / 企微内置浏览器 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 开发计划
|
||||
|
||||
### 阶段一:后端 API + 前端骨架(预计 2 天)
|
||||
|
||||
- [ ] WingmanService 新增 4 个方法 + Dify system prompt
|
||||
- [ ] wingman.py 新增 4 个 API 端点
|
||||
- [ ] 前端 wingman.ts 新增 API 封装
|
||||
- [ ] ReplyBox.vue 新增 AI 工具栏按钮
|
||||
|
||||
### 阶段二:语气调整 + 文字润色(预计 2 天)
|
||||
|
||||
- [ ] ToneAdjustPopover.vue 组件
|
||||
- [ ] PolishPanel.vue 组件
|
||||
- [ ] 联调测试
|
||||
|
||||
### 阶段三:智能改写(预计 1 天)
|
||||
|
||||
- [ ] RewritePanel.vue 组件
|
||||
- [ ] 知识库检索集成
|
||||
- [ ] 联调测试
|
||||
|
||||
### 阶段四:实时自动补齐(预计 2 天)
|
||||
|
||||
- [ ] GhostText.vue 幽灵文字渲染
|
||||
- [ ] 光标定位计算
|
||||
- [ ] 请求防抖 + 取消机制
|
||||
- [ ] 联调测试
|
||||
|
||||
### 阶段五:集成测试 + 优化(预计 1 天)
|
||||
|
||||
- [ ] 端到端测试
|
||||
- [ ] 性能优化
|
||||
- [ ] 边界情况处理
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
### 8.1 功能验收
|
||||
|
||||
- [ ] 坐席输入停顿 > 0.8s 后,光标位置出现灰色幽灵文字
|
||||
- [ ] Tab 键接受补齐,文字变为正常颜色
|
||||
- [ ] 选中文字后点击"语气"按钮,弹出 3 种语气选项
|
||||
- [ ] 选择语气后显示原文/改写文对比,确认后替换
|
||||
- [ ] 点击"润色"按钮,弹出精修面板,支持扩写/压缩/纠错
|
||||
- [ ] 点击"改写"按钮,生成 3 个备选版本,可选择替换或追加
|
||||
- [ ] AI 服务不可用时不影响正常输入和发送
|
||||
|
||||
### 8.2 性能验收
|
||||
|
||||
- [ ] 补齐 P95 延迟 < 1.5s
|
||||
- [ ] 语气/润色 P95 延迟 < 3s
|
||||
- [ ] 改写 P95 延迟 < 5s
|
||||
- [ ] 输入过程中无明显卡顿
|
||||
|
||||
### 8.3 兼容性验收
|
||||
|
||||
- [ ] Chrome 90+ 正常工作
|
||||
- [ ] 企微内置浏览器正常工作
|
||||
- [ ] 移动端 H5 不受影响(本功能仅坐席端)
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|----------|
|
||||
| 幽灵文字定位不准确 | 中 | 中 | 首版可采用建议条过渡方案 |
|
||||
| Dify 补齐延迟过高 | 中 | 高 | 设置 1.5s 超时,降级为不显示 |
|
||||
| 坐席过度依赖 AI | 低 | 中 | 补齐建议有置信度阈值,低置信度不显示 |
|
||||
| 语气改写偏离原意 | 中 | 中 | 显示原文/改写文对比,坐席确认后才替换 |
|
||||
| 并发请求冲突 | 低 | 低 | AbortController 管理请求生命周期 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:Dify System Prompt 汇总
|
||||
|
||||
### A.1 自动补齐
|
||||
```
|
||||
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
|
||||
要求:
|
||||
1. 补齐内容自然衔接当前文字,不要重复已有内容
|
||||
2. 长度控制在1-2个短句,不超过80字
|
||||
3. 语气专业、简洁,符合IT服务规范
|
||||
4. 只返回补齐的文字,不要加引号或其他标记
|
||||
```
|
||||
|
||||
### A.2 语气调整
|
||||
```
|
||||
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
|
||||
语气定义:
|
||||
- 专业:使用准确的技术术语,结构化表达,去除口语化内容
|
||||
- 友好:适当增加问候和关心语,语气更亲和
|
||||
- 简洁:去除冗余,直奔主题,控制字数
|
||||
要求:
|
||||
1. 保持原意不变,只调整语气和表达方式
|
||||
2. 改写后的文字长度与原文相近(±30%)
|
||||
3. 符合IT服务坐席的专业规范
|
||||
4. 只返回改写后的文字,不要加引号或解释
|
||||
```
|
||||
|
||||
### A.3 文字润色
|
||||
```
|
||||
你是一个IT服务文字精修助手。对坐席输入的文字进行{action}处理。
|
||||
操作定义:
|
||||
- 扩写:在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整
|
||||
- 压缩:精简表达,去除重复和冗余,保留核心信息,控制字数
|
||||
- 纠错:检查并修正错别字、语法错误、标点符号、格式问题
|
||||
要求:
|
||||
1. 保持原意不变
|
||||
2. 只返回处理后的文字,不要加引号或解释
|
||||
```
|
||||
|
||||
### A.4 智能改写
|
||||
```
|
||||
你是一个IT服务智能回复助手。基于对话上下文和知识库,为坐席生成3个不同风格的备选回复。
|
||||
版本要求:
|
||||
- 版本1:简洁直接,一句话说明问题和解决方案
|
||||
- 版本2:详细带步骤,包含操作步骤和注意事项
|
||||
- 版本3:带知识库引用,引用相关文档并给出权威解决方案
|
||||
要求:
|
||||
1. 3个版本内容不重复,各有侧重
|
||||
2. 每个版本独立成段,用---分隔
|
||||
3. 只返回回复内容,不要加额外解释
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
# PRD - 坐席代答
|
||||
|
||||
> **REQ编号**: REQ-坐席-003
|
||||
> **版本**: v1.0
|
||||
> **优先级**: P2
|
||||
> **阶段**: 中期(3-4个月)
|
||||
> **作者**: 宋献
|
||||
> **日期**: 2026-07-19
|
||||
|
||||
---
|
||||
|
||||
## 一、问题陈述
|
||||
|
||||
**用户问题**:
|
||||
- AI 推荐答案后,坐席需要复制粘贴再发送
|
||||
- 操作繁琐,效率不高
|
||||
- 坐席倾向于直接自己写答案,不使用 AI 推荐
|
||||
|
||||
**业务目标**:
|
||||
- 坐席可一键采用 AI 推荐的答案
|
||||
- 减少操作步骤,提升坐席效率
|
||||
- 提升 AI 推荐的使用率
|
||||
|
||||
---
|
||||
|
||||
## 二、需求范围
|
||||
|
||||
### 2.1 核心功能
|
||||
|
||||
| 功能 | 描述 |
|
||||
|------|------|
|
||||
| 一键采纳 | 坐席点击按钮直接发送 AI 推荐答案 |
|
||||
| 编辑后发送 | 坐席可修改答案后再发送 |
|
||||
| 推荐历史 | 坐席可查看历史推荐记录 |
|
||||
|
||||
### 2.2 非目标
|
||||
|
||||
- 不支持自动发送(必须人工确认)
|
||||
- 不支持批量采纳
|
||||
|
||||
---
|
||||
|
||||
## 三、用户故事
|
||||
|
||||
| 角色 | 用户故事 | 验收标准 |
|
||||
|------|----------|---------|
|
||||
| 坐席 | 我可以一键发送 AI 推荐答案 | 点击"采用"按钮,答案直接发送到会话 |
|
||||
| 坐席 | 我可以修改答案后再发送 | 点击"编辑",修改后点击发送 |
|
||||
| 坐席 | 我可以查看历史推荐 | 点击"历史"可查看推荐记录 |
|
||||
|
||||
---
|
||||
|
||||
## 四、功能详情
|
||||
|
||||
### 4.1 AI 推荐面板
|
||||
|
||||
**位置**:右侧 AI 助手面板
|
||||
|
||||
**内容**:
|
||||
- AI 生成的推荐答案(文字)
|
||||
- 关联的知识库来源(可点击查看)
|
||||
- 操作按钮:「采用」「编辑」「取消」
|
||||
|
||||
### 4.2 一键采纳
|
||||
|
||||
**流程**:
|
||||
1. 坐席查看 AI 推荐答案
|
||||
2. 点击「采用」按钮
|
||||
3. 答案直接发送到会话
|
||||
4. 会话标记为"已解决"
|
||||
|
||||
### 4.3 编辑后发送
|
||||
|
||||
**流程**:
|
||||
1. 坐席点击「编辑」按钮
|
||||
2. 进入编辑模式,可修改答案内容
|
||||
3. 点击「发送」按钮
|
||||
4. 答案发送到会话
|
||||
|
||||
---
|
||||
|
||||
## 五、指标设计
|
||||
|
||||
| 指标 | 目标 | 测量方式 |
|
||||
|------|------|---------|
|
||||
| 采纳率 | ≥ 60% | 采纳推荐/总会话数 |
|
||||
| 平均处理时间 | ≤ 3min | 采纳推荐的会话平均处理时间 |
|
||||
| 坐席效率提升 | ≥ 30% | 使用代答 vs 手动输入的时间对比 |
|
||||
|
||||
---
|
||||
|
||||
## 六、技术依赖
|
||||
|
||||
| 依赖项 | 说明 |
|
||||
|--------|------|
|
||||
| AI 推荐服务 | REQ-AI-001 复杂场景路由 |
|
||||
| 消息发送 | 现有消息发送能力 |
|
||||
|
||||
---
|
||||
|
||||
## 七、关联文档
|
||||
|
||||
- 前置需求: REQ-AI-001 复杂场景与统一路由
|
||||
- 后续需求: 无
|
||||
@@ -0,0 +1,53 @@
|
||||
# PRD - 任务详情视图切换
|
||||
|
||||
> **REQ编号**: REQ-坐席-004
|
||||
> **版本**: v1.0
|
||||
> **优先级**: P0
|
||||
> **日期**: 2026-06-06
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
点击左栏待办事项条目时,中间栏从聊天视图切换为任务详情视图。返回时切换回聊天视图。
|
||||
|
||||
---
|
||||
|
||||
## 二、需求详情
|
||||
|
||||
### 2.1 触发条件
|
||||
|
||||
点击左栏待办事项条目 → 中间栏从聊天视图切换为任务详情视图
|
||||
|
||||
### 2.2 返回交互
|
||||
|
||||
"← 返回会话" 按钮,切换回聊天视图
|
||||
|
||||
### 2.3 任务类型
|
||||
|
||||
| 类型 | 页面内容 |
|
||||
|------|---------|
|
||||
| 运维工单 | 工单描述卡片 + 处理进度卡片 + 操作按钮 |
|
||||
| 审批单 | 审批内容卡片 + 审批意见输入区 + 操作按钮 |
|
||||
| 设备异常 | 设备状态网格 + 处理记录卡片 + 操作按钮 |
|
||||
|
||||
### 2.4 操作按钮
|
||||
|
||||
| 任务类型 | 操作按钮 |
|
||||
|----------|---------|
|
||||
| 工单 | 接单、开始处理、结单、转派 |
|
||||
| 审批 | 审批通过、拒绝审批、转交审批 |
|
||||
| 设备 | 一键开单、派工、标记恢复、加入巡检 |
|
||||
|
||||
---
|
||||
|
||||
## 三、UI 规格
|
||||
|
||||
- 卡片样式:白底 + border + radius-lg(10px) + padding 14px 16px
|
||||
- 状态色值:正常=success绿、告警=warning黄、异常=danger红
|
||||
|
||||
---
|
||||
|
||||
## 四、关联文档
|
||||
|
||||
- 前端组件: `TaskDetailView.vue`
|
||||
@@ -0,0 +1,44 @@
|
||||
# PRD - 快捷键系统
|
||||
|
||||
> **REQ编号**: REQ-坐席-005
|
||||
> **版本**: v1.0
|
||||
> **优先级**: P1
|
||||
> **日期**: 2026-06-06
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
全局快捷键注册与统一管理,提升坐席操作效率。
|
||||
|
||||
---
|
||||
|
||||
## 二、需求详情
|
||||
|
||||
### 2.1 快捷键列表
|
||||
|
||||
| 快捷键 | 功能 | 生效条件 |
|
||||
|--------|------|---------|
|
||||
| Ctrl+1/2/3 | AI 推荐填入 | 坐席未回复时 |
|
||||
| Alt+1~5 | 快速回复分类切换 | 任意 |
|
||||
| ↑↓ | 快速回复条目导航 | 任意 |
|
||||
| Enter | 确认填入 | 任意 |
|
||||
| / | 聚焦快速回复搜索框 | 任意 |
|
||||
|
||||
### 2.2 约束条件
|
||||
|
||||
- 快捷键仅在未聚焦输入框时生效(避免与打字冲突)
|
||||
- 需提供快捷键提示 UI(右栏底部键盘指南)
|
||||
|
||||
---
|
||||
|
||||
## 三、技术实现
|
||||
|
||||
- 新增 `composables/useKeyboardShortcuts.ts`
|
||||
- 右栏底部常驻键盘指南:`Alt+1-5 切换` / `↑↓ 选择` / `Enter 填入` / `/ 搜索`
|
||||
|
||||
---
|
||||
|
||||
## 四、关联文档
|
||||
|
||||
- 前端组件: `useKeyboardShortcuts.ts`
|
||||
@@ -0,0 +1,51 @@
|
||||
# PRD - IT 等级系统
|
||||
|
||||
> **REQ编号**: REQ-坐席-006
|
||||
> **版本**: v1.0
|
||||
> **优先级**: P0
|
||||
> **日期**: 2026-06-06
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
建立用户 IT 技能等级体系,坐席可查看用户 IT 等级,并支持手动调整。
|
||||
|
||||
---
|
||||
|
||||
## 二、需求详情
|
||||
|
||||
### 2.1 IT 等级定义(7级)
|
||||
|
||||
| 等级 | 标识 | 配色 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 青铜 | 🛡️ Lv.1 | 棕色渐变 | 基础操作需指导 |
|
||||
| 白银 | 🛡️ Lv.2 | 灰色渐变 | 能完成常规操作 |
|
||||
| 黄金 | 🥇 Lv.3 | 金色渐变 | 熟练使用,高级操作需指导 |
|
||||
| 铂金 | 💎 Lv.4 | 青蓝渐变 | 独立解决大部分问题 |
|
||||
| 钻石 | 💎 Lv.5 | 靛蓝渐变 | 高级排障能力 |
|
||||
| 星耀 | ⭐ Lv.6 | 粉紫渐变 | 技术专家级 |
|
||||
| 王者 | 👑 Lv.7 | 橙红渐变 + 发光动画 | IT 管理员级 |
|
||||
|
||||
### 2.2 数据模型扩展
|
||||
|
||||
**Employee 模型新增字段**:
|
||||
- `it_level`: IT 等级枚举(bronze/silver/gold/platinum/diamond/star/king)
|
||||
- `it_level_source`: 等级来源(system/manual)
|
||||
- `notes`: 备注 JSON
|
||||
|
||||
**Conversation 模型新增字段**:
|
||||
- `impact_scope`: 影响范围(整数)
|
||||
- `is_blocking`: 阻断性标记
|
||||
- `emotion_state`: 情绪状态
|
||||
|
||||
### 2.3 手动调整交互
|
||||
|
||||
坐席可在用户信息详情卡片中手动调整用户 IT 等级,调整后记录 `it_level_source=manual`。
|
||||
|
||||
---
|
||||
|
||||
## 三、关联文档
|
||||
|
||||
- 前端组件: `ItLevelBadge.vue`, `UserInfoBar.vue`
|
||||
- 后端模型: `models/employee.py`, `models/conversation.py`
|
||||
@@ -0,0 +1,423 @@
|
||||
# PRD - 分诊排查系统(排查流程图管理)
|
||||
|
||||
> **REQ编号**: REQ-坐席-007
|
||||
> **版本**: v1.3(存量文件名暂保留 v1.0,避免破坏既有引用)
|
||||
> **日期**: 2026-07-28
|
||||
> **状态**: [待评审]
|
||||
> **作者**: 宋献
|
||||
> **优先级**: P0
|
||||
> **需求类型**: 存量功能补充与生产化改造
|
||||
> **关联文档**:
|
||||
> - 原型: `原型-REQ-坐席-007-分诊排查系统-v1.0.html`
|
||||
> - 技术方案: `../../../02-技术文档/技术架构/技术方案-REQ-坐席-007-分诊排查系统-v1.0.md`
|
||||
> - 功能测试: `../../../03-测试文档/03-功能测试用例/TC-坐席-007-分诊排查系统.md`(v1.0,54 条用例,评审通过后补齐)
|
||||
|
||||
---
|
||||
|
||||
## 一、需求背景
|
||||
|
||||
### 1.1 业务问题
|
||||
|
||||
常见 IT 故障依赖坐席个人经验排查,容易出现步骤遗漏、处理口径不一致、新坐席上手慢,以及员工转人工后重复描述和重复排查等问题。当前仓库已经存在“排查流程图管理”页面、坐席排查步骤组件和员工 H5 自助排查页面,但数据契约、持久化、发布机制和跨端执行闭环尚未完成。
|
||||
|
||||
### 1.2 产品定位
|
||||
|
||||
“排查流程图管理”是**分诊排查系统的管理入口**,负责将高频故障的标准排查方法沉淀为可发布、可执行、可追踪的模板。产品统一名称建议为“分诊排查管理”,流程图是模板内容的一种表达方式,不是独立绘图工具。
|
||||
|
||||
完整链路:
|
||||
|
||||
`管理员编制模板 → 校验与发布 → 坐席/员工选择或被推荐模板 → 按节点执行 → 记录路径与结果 → 未解决时携带上下文转人工 → 运营数据回流优化模板`
|
||||
|
||||
### 1.3 目标用户
|
||||
|
||||
| 角色 | 主要诉求 |
|
||||
|---|---|
|
||||
| IT 支持组长/管理员 | 统一维护排查标准,控制版本和发布范围,查看使用效果 |
|
||||
| 呼叫坐席 | 快速选择模板、按步骤推进,避免遗漏,完整记录排查过程 |
|
||||
| 员工 | 对常见问题进行低门槛自助排查,未解决时无损转人工 |
|
||||
| 运营/质量管理员 | 分析模板采用率、解决率、断点和过期内容 |
|
||||
|
||||
---
|
||||
|
||||
## 二、目标与成功标准
|
||||
|
||||
### 2.1 用户目标
|
||||
|
||||
1. 管理员能在一个入口完成模板创建、校验、预览、发布、停用和版本回滚。
|
||||
2. 坐席能在会话中 10 秒内定位并启动合适模板,执行状态与会话绑定。
|
||||
3. 员工能按清晰步骤自助排查,转人工时自动携带已完成路径。
|
||||
4. 管理者可追踪模板是否有效,而不是只统计“建了多少模板”。
|
||||
|
||||
### 2.2 业务目标
|
||||
|
||||
| 指标 | 口径 | 上线后 30 天成功阈值 | 远期目标 |
|
||||
|---|---|---:|---:|
|
||||
| 模板采用率 | 启动模板的适用会话数 / 适用会话总数 | ≥40% | ≥65% |
|
||||
| 模板完成率 | 到达结束节点的执行实例 / 启动实例 | ≥70% | ≥85% |
|
||||
| 模板辅助解决率 | 标记“已解决”的完成实例 / 完成实例 | ≥45% | ≥60% |
|
||||
| 员工自助解决率 | 无需转人工的自助实例 / 自助启动实例 | ≥20% | ≥35% |
|
||||
| 重复排查率 | 转人工后重复执行已完成节点的实例 / 转人工实例 | ≤10% | ≤5% |
|
||||
| 模板执行异常率 | 死路、无后继、同步失败实例 / 启动实例 | ≤2% | ≤1% |
|
||||
|
||||
> 当前无可信基线,目标值为首版假设;上线前需通过埋点获取 2 周基线并评审调整。
|
||||
|
||||
---
|
||||
|
||||
## 三、范围
|
||||
|
||||
### 3.1 P0:生产可用最小闭环
|
||||
|
||||
1. **模板列表**:按名称、分类、状态搜索;显示版本、节点数、预估时间、最后发布人和更新时间。
|
||||
2. **模板编辑**:维护基本信息和节点树;支持步骤节点、判断节点、结束节点及是/否分支。
|
||||
3. **结构校验**:保存前检查根节点、节点 ID 唯一性、分支完整性、不可达节点、循环和结束节点。
|
||||
4. **草稿与发布**:编辑只产生草稿;发布后生成不可变版本;已发布版本供执行端使用。
|
||||
5. **版本管理**:查看历史版本、版本差异、回滚为新草稿;禁止直接覆盖历史版本。
|
||||
6. **启用与停用**:停用后不可新启动,但已有执行实例可继续完成。
|
||||
7. **权限与审计**:管理员可维护;坐席和员工只读已发布模板;记录创建、修改、发布、停用、回滚和删除操作。
|
||||
8. **坐席执行**:模板启动、节点推进、分支选择、暂停/恢复、结束;执行状态绑定会话并持久化。
|
||||
9. **员工自助**:查看可用模板、按节点执行、反馈是否解决;转人工时传递模板版本、已完成节点、选择路径和备注。
|
||||
10. **导入导出**:使用统一 JSON Schema;单条导入、单条导出、全部导出;导入先校验再进入草稿。
|
||||
11. **问诊模板**:通过 `purpose` 字段统一管理,单实例(`code=triage_intake`),由路由层兜底自动启动;实例来源 `triage_fallback`,不计入模板完成率和模板辅助解决率;问诊结束自动回到标准模板选择界面(路由 + 手动双轨),详见 §5.4.4 和 §5.4.8。
|
||||
12. **purpose 字段管理**:所有模板主记录新增 `purpose` 字段,值域 `troubleshooting` / `triage`,新建时强制选择且事后不允许切换;编辑器、列表、API 全部按 `purpose` 区分校验规则集、列表 Tab 和筛选条件,详见 §5.4.8。
|
||||
|
||||
### 3.2 P1:体验与运营增强
|
||||
|
||||
- AI 根据会话意图推荐模板,默认模板直接启动,详见 §5.4。
|
||||
- 节点绑定知识库文章、下载文件、检测接口或审批入口。
|
||||
- 模板灰度发布、适用部门/角色范围和定时生效。
|
||||
- 使用漏斗、节点退出率、版本效果对比和过期提醒。
|
||||
- 可视化拖拽编辑器;首版仍可保留 JSON 高级编辑能力。
|
||||
|
||||
### 3.3 P2:远期能力
|
||||
|
||||
- 节点触发自动化修复动作并支持回滚。
|
||||
- 基于历史成功路径自动推荐模板优化方案。
|
||||
- 模板跨租户/跨组织共享与模板市场。
|
||||
|
||||
### 3.4 非目标(Non-goals)
|
||||
|
||||
1. **不建设通用 BPM 工作流引擎**:首版只服务 IT 故障排查,不承载审批和复杂业务编排。
|
||||
2. **不直接自动执行高风险命令**:涉及终端变更、账号权限和安全策略的动作另行走自动化审批与审计。
|
||||
3. **不替代知识库**:模板负责顺序和决策,详细知识内容通过引用关联。
|
||||
4. **不让 AI 自动发布模板**:AI 可建议,发布责任仍由具备权限的管理员承担。
|
||||
5. **不追求自由画布制图**:首版优先保证结构化、可校验、可执行,而不是视觉排版自由度。
|
||||
|
||||
---
|
||||
|
||||
## 四、用户故事
|
||||
|
||||
### 4.1 管理员
|
||||
|
||||
- 作为 IT 支持组长,我希望把 VPN、邮箱、账号等故障沉淀为标准模板,以便不同坐席按同一口径处理。
|
||||
- 作为模板管理员,我希望发布前获得结构校验结果,以便避免断路、死循环和缺失分支进入生产。
|
||||
- 作为模板管理员,我希望修改已发布模板时产生新版本,以便进行追溯、回滚和效果比较。
|
||||
- 作为质量管理员,我希望查看模板采用率、完成率和节点退出率,以便持续优化无效步骤。
|
||||
|
||||
### 4.2 呼叫坐席
|
||||
|
||||
- 作为呼叫坐席,我希望根据员工问题快速搜索并启动模板,以便减少判断成本。
|
||||
- 作为呼叫坐席,我希望看到当前步骤、已完成步骤和后续分支,以便清楚掌握排查进度。
|
||||
- 作为呼叫坐席,我希望暂停后恢复同一执行实例,以便处理长耗时或跨班次问题。
|
||||
|
||||
### 4.3 员工
|
||||
|
||||
- 作为员工,我希望按照易懂的步骤完成自助排查,以便常见问题无需等待坐席。
|
||||
- 作为员工,我希望在未解决时一键转人工,并自动携带已做过的操作,以便不重复描述和排查。
|
||||
- 作为员工,我希望明确知道某一步是否会修改设备或账号,以便在执行前作出知情选择。
|
||||
|
||||
---
|
||||
|
||||
## 五、核心业务流程
|
||||
|
||||
### 5.1 模板生命周期
|
||||
|
||||
1. 管理员新建或导入模板,系统创建草稿。
|
||||
2. 编辑基本信息、节点、分支、说明和风险提示。
|
||||
3. 系统执行语法校验和结构校验;失败时禁止发布。
|
||||
4. 管理员预览坐席端和员工端呈现效果。
|
||||
5. 管理员填写版本说明并发布,生成版本快照。
|
||||
6. 执行端只读取已发布且启用的版本。
|
||||
7. 后续修改从已发布版本复制为新草稿;重新发布产生新版本。
|
||||
8. 停用模板后禁止新启动;历史实例仍关联原版本快照。
|
||||
|
||||
### 5.2 坐席协同执行
|
||||
|
||||
模板选择采用 **“统一路由推荐 + 坐席手动覆盖”** 的双轨策略,详细策略见 §5.4。
|
||||
|
||||
1. 路由层根据员工描述、会话上下文、历史成功率推荐默认模板和候选模板。
|
||||
2. 路由推荐结果**直接启动**默认模板的执行实例;坐席无需点击二次确认弹窗,会话区立即出现排查步骤栏。
|
||||
3. 启动实例同时记录 `conversation_id`、`employee_id`、`agent_id`、`template_version_id`、`source`(`ai_recommend` / `agent`)和起始时间。
|
||||
4. 坐席可以随时通过排查栏顶部的 “换其他模板” 入口手动覆盖;手动切换会触发确认提示,详见 §5.4。
|
||||
5. 坐席完成步骤或选择判断分支,后端计算下一个合法节点并实时同步给会话双方。
|
||||
6. 任一方断线重连时,从服务端恢复执行状态。
|
||||
7. 结束时记录“已解决/未解决/转其他流程”、耗时、备注和终止原因。
|
||||
|
||||
### 5.3 员工自助转人工
|
||||
|
||||
1. 员工从 H5 选择已发布模板并开始自助排查。
|
||||
2. 系统逐步展示节点,不暴露不相关分支和内部运维信息。
|
||||
3. 员工选择“未解决/需要人工”时,创建或关联会话。
|
||||
4. 坐席接入后可查看执行摘要,并从当前节点继续或重新选择模板。
|
||||
|
||||
---
|
||||
|
||||
### 5.4 模板选择策略 · 路由推荐与手动覆盖
|
||||
|
||||
#### 5.4.1 策略总览
|
||||
|
||||
坐席端采用 **“统一路由推荐 + 手动覆盖”** 的双轨结构:
|
||||
|
||||
- 路由层负责默认选择,坐席负责最终决定。
|
||||
- 路由层不直接修改会话状态,只产生推荐结果;模板启动命令由坐席端发起或由路由层在坐席无操作时自动触发。
|
||||
- 手动覆盖始终是合法路径,不被路由层阻断。
|
||||
- **问诊模板结束时自动回到标准模板选择界面(路由 + 手动双轨)**,由服务端在执行 `triage_fallback` 实例的结束命令时复位路由状态,确保问诊不会把会话卡在兜底分支。
|
||||
|
||||
#### 5.4.2 路由层职责
|
||||
|
||||
| 输入 | 输出 |
|
||||
|---|---|
|
||||
| 员工首句描述、会话上下文、历史类似会话、AI 意图识别结果 | 默认模板 + 2~3 个候选模板 + 每个候选的推荐理由与置信度 |
|
||||
| 员工补充信息或会话状态变化 | 重新计算候选并刷新推荐 |
|
||||
| 路由无任何匹配 | 触发兜底 B:自动推荐“问诊对话模板” |
|
||||
|
||||
#### 5.4.3 启动方式:直接启动
|
||||
|
||||
1. 路由层给出推荐后,**默认模板直接启动**,不弹出“是否确认启动”的二次确认弹窗。
|
||||
2. 排查栏立即出现,展示模板名称、版本、当前节点和“换其他模板”入口。
|
||||
3. 若坐席在 3 秒内点击 “换其他模板”,系统视为“立即覆盖”,仍视为手动选择。
|
||||
4. 自动启动的实例来源记为 `ai_recommend`,用于统计路由采纳率。
|
||||
|
||||
#### 5.4.4 路由失败的兜底 B:问诊对话模板
|
||||
|
||||
1. 当路由无匹配时,由后端创建一个特殊模板 `code=triage_intake`,版本独立维护。
|
||||
2. 模板内容是一组短问题与是/否分支,例如:“故障现象是什么 / 是否仅发生在公司网络 / 是否影响其他同事 / 是否重启过终端 / 是否变更过密码”,目的不是排查根因,而是帮助坐席快速收敛问题方向。
|
||||
3. 问诊对话模板走与正式模板一致的会话级实例、版本快照、同步和结束流程,不在 UI 上做特殊样式,仅在排查栏顶部显示 “问诊中” 标识。
|
||||
4. 问诊模板结束或任一节点触发跳转时,自动回到标准模板选择界面(路由推荐 + 手动覆盖双轨)。
|
||||
5. 问诊模板本身不计入“模板完成率”和“模板辅助解决率”,仅作为路由层的服务能力。
|
||||
6. 问诊模板在数据库层面通过 `purpose=triage` 标识,与排查共享同一套 Schema、版本、执行实例、状态机和权限机制;不允许独立建表或独立 API。
|
||||
7. 同一会话中 `source=triage_fallback` 的活跃实例与普通实例遵循同样的“同会话单活跃实例”约束,问诊实例被取消后下一次路由仍可能再次触发问诊,直到员工描述落入某个排查的覆盖范围。
|
||||
|
||||
#### 5.4.5 手动切换模板:必须提示并结束当前实例
|
||||
|
||||
1. 坐席点击 “换其他模板” 时,前端必须弹出确认提示:“切换模板将结束当前排查实例并开始新的实例,是否继续?”。
|
||||
2. 确认后:
|
||||
- 当前实例状态置为 `cancelled`,写入 `reason="agent_switched_template"`。
|
||||
- 新模板创建新执行实例;旧实例的路径摘要保留在会话归档中,但不计入当前活跃会话的执行进度。
|
||||
3. 取消则保留当前实例继续推进。
|
||||
4. **同一会话同一时刻只允许一个活跃实例**,避免多实例污染指标;该约束由后端在创建实例时校验,同一会话存在 `running` 实例时拒绝新的 `ai_recommend` 自动启动和 `agent` 手动启动。
|
||||
|
||||
#### 5.4.6 关键交互规则
|
||||
|
||||
| 场景 | 行为 |
|
||||
|---|---|
|
||||
| 路由高置信度推荐 | 直接启动默认模板 |
|
||||
| 路由低置信度推荐 | 直接启动默认模板 + 在排查栏顶部显示“推荐置信度低,是否换其他模板?”提示 |
|
||||
| 路由无匹配 | 自动启动问诊对话模板 |
|
||||
| 坐席无操作 | 保持自动启动的实例继续推进 |
|
||||
| 坐席主动换模板 | 弹出确认 → 旧实例 cancelled → 启动新实例 |
|
||||
| 跨模板跳转(如 VPN → 网络) | 仅由坐席主动发起,不允许路由自动跨模板跳转 |
|
||||
|
||||
#### 5.4.7 与现有文档的差异说明
|
||||
|
||||
- 原 §3.2 P1 中描述 “AI 根据会话意图推荐模板,坐席确认后启动” 的措辞在本版被替换为 **“直接启动 + 手动覆盖”**;产品决策点已与本节对齐。
|
||||
- 原 §5.2 第 1 步 “坐席在会话中搜索模板或接受 AI 推荐” 调整为 “路由直接启动 + 手动覆盖”,更准确反映实际操作。
|
||||
- 原 §3.1 P0 #8 “坐席执行” 仍保留作为后端能力,但 UI 入口与交互由本节定义。
|
||||
|
||||
---
|
||||
|
||||
#### 5.4.8 与排查流程图的一体两面关系
|
||||
|
||||
排查流程(`purpose=troubleshooting`)和问诊对话(`purpose=triage`)**不是两个独立系统**,而是同一个模板体系的两个面向:
|
||||
|
||||
| 维度 | 排查流程 | 问诊对话 |
|
||||
|---|---|---|
|
||||
| 主导角色 | 坐席 | 员工 |
|
||||
| 目标 | 标准化排查根因 | 收敛故障方向为下一轮排查铺路 |
|
||||
| 节点粒度 | 中长流程,可能含终端检测、权限变更 | 短问题与是/否分支,≤10 节点 |
|
||||
| 实例来源 | `ai_recommend` / `agent` | `triage_fallback` |
|
||||
| 指标归属 | 计入完成率、辅助解决率 | 仅计转化率 |
|
||||
| 结束行为 | 已解决/未解决/转其他流程 | 自动回到标准模板选择(路由 + 手动双轨) |
|
||||
|
||||
**统一规则**:
|
||||
|
||||
1. 数据模型同一份:模板主表、`nodes[]`、版本、执行实例、状态机、WebSocket 同步、权限矩阵全部复用。
|
||||
2. 唯一区分字段是 `purpose`,值域 `troubleshooting`(默认)/ `triage`。
|
||||
3. 新建模板时强制选择 `purpose`;模板创建后 `purpose` 字段不可修改,只能通过“基于此模板复制为另一种用途”的方式新建。
|
||||
4. **UI 必须分而不拆**:管理后台列表按 `purpose` 分 Tab(Troubleshooting / Triage),编辑器顶部加“模板用途”选项,详见 §7.1。
|
||||
5. **校验规则必须按 purpose 区分**:结构校验入口加载对应 `purpose` 的规则集,`triage` 模板限制节点数 ≤10、深度 ≤4、强制包含结束节点且 `result=redirect`、标签必须包含 `triage` 或 `intake`,详见 §6.1 FR-04。
|
||||
6. **指标必须显式隔离**:指标服务层硬编码 `source=triage_fallback` 的实例不计入 `troubleshooting_*` 指标,仅计入 `troubleshooting_template_start_total{source=triage_conversion}` 转化率指标,详见 §11。
|
||||
7. **权限必须显式隔离**:`triage` 模板对员工**始终可见**,对坐席**始终只读**,不参与排查的部门/角色过滤矩阵,详见 §8.2。
|
||||
8. **禁止新增独立页面或独立 API**:`triage` 模板的入口、编辑、列表、导入导出全部复用现有分诊排查管理页面,不引入“问诊模板管理”新页面。
|
||||
|
||||
**一体两面的承诺**:用户在任一端(员工或坐席)看到的模板都来自同一份后端真相,只是 `purpose` 不同;运营和开发只需要维护一套系统。
|
||||
|
||||
---
|
||||
|
||||
## 六、功能需求与验收标准
|
||||
|
||||
### 6.1 模板管理
|
||||
|
||||
| 编号 | 需求 | 优先级 | 验收标准摘要 |
|
||||
|---|---|---|---|
|
||||
| FR-01 | 列表搜索、分类和状态过滤 | P0 | 组合筛选结果准确,空状态有引导 |
|
||||
| FR-02 | 新建、复制、编辑、预览、归档 | P0 | 操作有权限校验和审计记录 |
|
||||
| FR-03 | 节点树编辑和 JSON 高级编辑 | P0 | 两种模式使用同一 Schema,切换不丢数据 |
|
||||
| FR-04 | 结构校验 | P0 | 无根、重复 ID、缺失分支、循环、无结束节点禁止发布;按 `purpose` 加载校验规则集:`troubleshooting` 节点数 ≤100、深度 ≤20;`triage` 节点数 ≤10、深度 ≤4、必须包含结束节点且 `result=redirect`、标签必须含 `triage` 或 `intake` |
|
||||
| FR-05 | 草稿、发布、停用、回滚 | P0 | 历史版本不可变,回滚产生新草稿 |
|
||||
| FR-06 | 导入导出 | P0 | 导入错误定位到字段/节点;不得静默丢字段;导入时若未声明 `purpose` 默认 `troubleshooting` |
|
||||
| FR-07 | 操作审计 | P0 | 可按模板、操作者、动作和日期查询;`purpose` 变更(仅允许在复制新建场景发生)需写入审计 |
|
||||
| FR-13 | purpose 字段 | P0 | 模板主记录必含 `purpose` 字段;新建强制选择,事后不可修改;列表筛选和列表 Tab 按 `purpose` 区分 |
|
||||
| FR-14 | 问诊模板统一管理 | P0 | 问诊模板与排查共用同一管理入口、编辑器、版本、审计和 API;不在前端新增独立页面,在后端不新增独立表 |
|
||||
|
||||
### 6.2 执行端
|
||||
|
||||
| 编号 | 需求 | 优先级 | 验收标准摘要 |
|
||||
|---|---|---|---|
|
||||
| FR-08 | 创建会话级执行实例 | P0 | 同一会话可暂停恢复;状态持久化 |
|
||||
| FR-09 | 服务端推进节点 | P0 | 客户端不能跳至非法节点;分支结果可追溯 |
|
||||
| FR-10 | 坐席/员工实时同步 | P0 | 正常网络下状态同步 P95 ≤2 秒;重连后恢复一致 |
|
||||
| FR-11 | 自助转人工上下文 | P0 | 坐席能看到模板版本、路径、已完成步骤和员工备注 |
|
||||
| FR-12 | 结束结果采集 | P0 | 必填解决结果;异常退出可标记原因 |
|
||||
|
||||
### 6.3 Given / When / Then
|
||||
|
||||
1. **发布校验**
|
||||
Given 草稿存在缺失的“否”分支,When 管理员点击发布,Then 系统拒绝发布并定位问题节点。
|
||||
2. **版本隔离**
|
||||
Given 会话正在执行 v1.0,When 管理员发布 v1.1,Then该会话继续执行 v1.0,新会话使用 v1.1。
|
||||
3. **停用行为**
|
||||
Given 模板已停用,When 新用户搜索模板,Then 不返回该模板;已有执行实例仍可完成。
|
||||
4. **跨端同步**
|
||||
Given 坐席和员工处于同一执行实例,When 坐席完成节点,Then员工端在 2 秒内显示相同进度。
|
||||
5. **断线恢复**
|
||||
Given 客户端在第 4 个节点断线,When 重新连接,Then从服务端恢复至第 4 个节点且历史路径完整。
|
||||
6. **转人工**
|
||||
Given 员工已完成 3 个节点仍未解决,When 点击转人工,Then坐席收到执行摘要且不要求员工重复已完成步骤。
|
||||
7. **越权保护**
|
||||
Given 普通坐席登录,When 调用创建或发布接口,Then返回 403 并写入安全日志。
|
||||
8. **并发保护**
|
||||
Given 两名管理员同时编辑同一草稿,When后保存者提交旧版本号,Then返回 409 并提示刷新或比较差异。
|
||||
9. **purpose 不可修改**
|
||||
Given 已存在模板 `purpose=troubleshooting`,When 尝试修改 `purpose` 字段为 `triage`,Then 返回 400 并提示“请基于此模板复制为另一种用途”。
|
||||
10. **问诊模板独立指标**
|
||||
Given 路由层无匹配自动启动 `triage_intake` 实例,When 实例结束,Then 该实例不计入 `troubleshooting_template_complete_total` 与 `troubleshooting_template_resolved_total`,仅计入 `troubleshooting_template_start_total{source=triage_conversion}`。
|
||||
11. **问诊结束回到标准模板选择**
|
||||
Given 问诊实例正常结束,When 坐席回到会话区,Then 排查栏消失并恢复路由 + 手动双轨状态,可由路由层再次推荐新模板。
|
||||
12. **Triage 模板可见性**
|
||||
Given 员工登录 H5,When 查询可用模板列表,Then `purpose=triage` 的模板始终可见,不受部门或角色可见范围限制。
|
||||
|
||||
---
|
||||
|
||||
## 七、信息架构与页面
|
||||
|
||||
### 7.1 管理后台
|
||||
|
||||
- 导航:运营配置 / 分诊排查管理
|
||||
- 页面:模板列表(**按 `purpose` 分 Tab:Troubleshooting / Triage**)、模板编辑/预览、版本历史、发布确认、操作审计、使用分析(P1)
|
||||
- 模板编辑区顶部增加 **“模板用途” 选项**,新建时强制选择 `purpose`,事后不可修改
|
||||
- 模板编辑区:基本信息(含 `purpose`)、结构化节点树、JSON 高级模式、实时校验(按 `purpose` 加载校验规则集)、双端预览
|
||||
- 排查 Tab 的表头显示当前选中模板的 `purpose` 标识,避免运营误编辑问诊模板为排查
|
||||
|
||||
### 7.2 坐席工作台
|
||||
|
||||
- 会话区底部“排查步骤”栏
|
||||
- 模板选择器、最优路径、完整决策树、当前节点操作、暂停/结束
|
||||
- 展示与会话关联的执行摘要
|
||||
- **当执行实例 `source=triage_fallback` 时,排查栏顶部显示 “问诊中” 标识**
|
||||
- 问诊实例结束时排查栏收起并恢复路由 + 手动双轨状态
|
||||
|
||||
### 7.3 员工 H5
|
||||
|
||||
- 自助排查入口、分类/搜索、模板详情、逐步执行页、解决结果页、转人工确认页
|
||||
- 内部敏感说明不向员工显示
|
||||
- **员工看到的 Triage 模板永远在分类首位(不参与部门/角色可见范围过滤),且作为路由无匹配时的兜底入口**
|
||||
|
||||
---
|
||||
|
||||
## 八、数据、权限与合规要求
|
||||
|
||||
### 8.1 关键数据
|
||||
|
||||
- 模板主记录、不可变版本快照、节点定义、执行实例、节点执行日志、发布审计日志。
|
||||
- 执行日志仅保存排查必要信息,不在模板节点中采集账号密码、验证码、密钥等敏感数据。
|
||||
- 会话归档时应保留模板版本号和路径摘要,避免模板更新后历史记录失真。
|
||||
|
||||
### 8.2 权限矩阵
|
||||
|
||||
| 能力 | 管理员/组长 | 呼叫坐席 | 员工 | 审计员 |
|
||||
|---|:---:|:---:|:---:|:---:|
|
||||
| 查看已发布排查 | ✓ | ✓ | ✓(员工可见范围) | ✓ |
|
||||
| 查看已发布问诊模板(`purpose=triage`) | ✓ | ✓(只读) | ✓(**始终可见,不受部门/角色过滤**) | ✓ |
|
||||
| 创建/编辑草稿 | ✓ | - | - | - |
|
||||
| 发布/停用/回滚 | ✓ | - | - | - |
|
||||
| 修改 `purpose` 字段 | -(创建后不可修改) | - | - | - |
|
||||
| 启动与执行 | 可测试 | ✓ | ✓(triage 永远允许) | - |
|
||||
| 查看执行明细 | ✓ | 当前会话 | 本人实例 | ✓ |
|
||||
| 查看审计日志 | ✓ | - | - | ✓ |
|
||||
|
||||
### 8.3 合规
|
||||
|
||||
- 操作日志和执行日志按照最小必要原则采集,并遵循《个人信息保护法》相关要求。
|
||||
- 节点若涉及终端检测、截屏、日志采集或权限变更,必须明确告知用途并按既有授权流程执行。
|
||||
- 导出的模板文件不得包含员工个人数据、真实凭据和生产密钥。
|
||||
- `purpose=triage` 的模板对员工始终可见,**不得在该模板的可见范围字段中写入敏感过滤条件**(如部门、角色、外部标签),避免出现“兜底模板被过滤掉”的场景。
|
||||
|
||||
---
|
||||
|
||||
## 九、依赖与约束
|
||||
|
||||
1. 依赖管理后台 RBAC、会话服务、WebSocket 单进程消息管理、员工 H5 认证和数据库迁移能力。
|
||||
2. 当前实现中后端 CRUD 使用进程内 Mock 数据,正式开发必须切换到数据库。
|
||||
3. 当前管理端列表期望数组,而后端返回 `{items,total}`,需统一接口契约。
|
||||
4. 当前存在 `flowchart`、`root_node`、`path_steps` 多套结构,必须建立唯一 Schema 和迁移规则。
|
||||
5. 当前 H5 自助页面未正式注册路由,坐席与员工执行状态未形成后端闭环。
|
||||
|
||||
---
|
||||
|
||||
## 十、风险与对策
|
||||
|
||||
| 风险 | 影响 | 对策 |
|
||||
|---|---|---|
|
||||
| 模板内容过时 | 引导错误,扩大故障 | 设置负责人、复审周期、过期提醒和一键停用 |
|
||||
| 流程结构复杂 | 管理成本和执行放弃率上升 | 限制深度,提供结构校验和双端预览 |
|
||||
| 发布修改影响执行中会话 | 历史路径失真 | 执行实例固定绑定版本快照 |
|
||||
| 双端状态不一致 | 重复或跳步 | 服务端作为唯一状态源,事件幂等和重连恢复 |
|
||||
| 模板包含敏感信息 | 合规与安全风险 | 字段审查、导出脱敏、发布权限和审计 |
|
||||
| 指标驱动坐席机械执行 | 忽略个案 | 允许合规跳出并记录原因,不以完成率单一考核 |
|
||||
| 路由推荐结果不可信时坐席仍机械跟随 | 排查方向偏差 | 路由低置信度时显示提示;问诊模板兜底;手动覆盖始终开放 |
|
||||
| 同一会话存在多个并发实例 | 指标污染、转人工摘要混乱 | 服务端约束“同一会话同一时刻仅一个活跃实例”;手动切换前必须确认 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、开放问题
|
||||
|
||||
| 问题 | 责任方 | 是否阻塞 |
|
||||
|---|---|:---:|
|
||||
| 模板发布权限是否仅限管理员,还是组长也可发布? | 产品/安全 | 是 |
|
||||
| 员工可见模板是否按部门、设备或角色过滤? | 产品/运营 | 是 |
|
||||
| 已发布模板删除采用归档还是硬删除?建议仅归档 | 产品/技术 | 是 |
|
||||
| 模板节点最大深度和节点数量限制 | 产品/技术 | 否 |
|
||||
| 执行日志保留期限及会话归档策略 | 安全/合规 | 是 |
|
||||
| 首批生产模板的负责人和复审周期 | 运营 | 否 |
|
||||
|
||||
---
|
||||
|
||||
## 十二、上线阶段建议
|
||||
|
||||
1. **阶段 A:管理闭环**——数据库持久化、统一 Schema、草稿/发布/版本、RBAC、审计。
|
||||
2. **阶段 B:坐席闭环**——会话级实例、步骤推进、暂停恢复、结果采集。
|
||||
3. **阶段 C:员工自助**——H5 路由、双端同步、转人工上下文。
|
||||
4. **阶段 D:智能与运营**——AI 推荐、效果看板、灰度和内容治理。
|
||||
|
||||
上线闸门:P0 验收通过、独立测试用例完备、端到端验证通过、部署与回滚方案齐备后,方可标记为生产可用。
|
||||
|
||||
---
|
||||
|
||||
## 十三、变更记录
|
||||
|
||||
| 日期 | 版本 | 变更内容 | 变更人 | 变更原因 |
|
||||
|---|---|---|---|---|
|
||||
| 2026-06-06 | v1.0 | 创建简版 PRD,定义排查步骤栏、数据模型和基础 API | 宋献 | 初始需求 |
|
||||
| 2026-07-28 | v1.1 | 补充产品定位、角色与场景、完整范围、非目标、指标、权限、验收标准、风险和生产化路线 | Duckula | 文档规范化补充,解决存量文档不完整问题 |
|
||||
| 2026-07-28 | v1.2 | 新增 §5.4 模板选择策略:明确“统一路由推荐 + 手动覆盖”双轨;路由推荐默认模板直接启动;路由无匹配时兜底使用问诊对话模板;手动切换模板必须确认并结束当前实例;同会话单实例约束由后端校验 | 宋献 + Duckula | 完成坐席端选择策略的产品决策 |
|
||||
| 2026-07-28 | v1.3 | 升级到“一体两面”模型:所有模板通过 `purpose` 字段(`troubleshooting`/`triage`)统一管理;§3.1 P0 新增 #11 问诊模板与 #12 purpose 字段;新增 §5.4.8 “与排查流程图的一体两面关系”;§6.1 新增 FR-13/FR-14;§6.3 新增验收用例 #9~#12;§7 页面分 Tab + 模板用途选项 + 问诊中标识;§8.2 权限矩阵拆分排查/问诊;§11 指标增加 `triage_conversion`;§13 变更记录追加 | 宋献 + Duckula | 落实用户决策:排查流程与问诊对话是同一系统的两个面向,通过 `purpose` 区分而非分系统 |
|
||||
@@ -0,0 +1,100 @@
|
||||
# PRD - AI Wingman(坐席智能辅助)
|
||||
|
||||
> **REQ编号**: REQ-坐席-008
|
||||
> **版本**: v1.0
|
||||
> **优先级**: P1
|
||||
> **日期**: 2026-07-19
|
||||
|
||||
---
|
||||
|
||||
## 一、需求概述
|
||||
|
||||
让AI成为坐席的"智能副驾驶"——消灭重复劳动、增强认知能力、守护情绪健康。
|
||||
|
||||
---
|
||||
|
||||
## 二、三层设计架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ AI Wingman 三层设计架构 │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐│
|
||||
│ │ 情感层 · 守护情绪健康 ││
|
||||
│ │ 情绪识别预警 → 安抚话术推荐 → 语气润色 → 正向激励 → 疲劳检测 ││
|
||||
│ └─────────────────────────────────────────────────────────────────┘│
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐│
|
||||
│ │ 认知层 · 消除认知负荷 ││
|
||||
│ │ 知识推荐 → SOP流程导航 → 相似工单推荐 → 下一步建议 → 客户画像 ││
|
||||
│ └─────────────────────────────────────────────────────────────────┘│
|
||||
│ ┌─────────────────────────────────────────────────────────────────┐│
|
||||
│ │ 效率层 · 消灭重复劳动 ││
|
||||
│ │ AI草稿回复 → 会话自动摘要 → 智能填单 → 自动标签 → 快捷回复库 ││
|
||||
│ └─────────────────────────────────────────────────────────────────┘│
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、Phase 1 效率层(本期)
|
||||
|
||||
### 3.1 AI草稿回复
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|------|
|
||||
| 触发 | 每条新员工消息 |
|
||||
| 生成时间 | <3秒 |
|
||||
| 呈现位置 | 内嵌在对应的员工消息下方 |
|
||||
| 操作按钮 | [采纳] / [编辑] / [忽略] |
|
||||
|
||||
### 3.2 会话自动摘要
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|------|
|
||||
| 触发 | 会话结单时 |
|
||||
| 内容 | 问题 / 原因 / 解决方案 |
|
||||
| 操作 | 坐席可编辑后确认 |
|
||||
|
||||
### 3.3 自动标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|------|
|
||||
| 触发 | 会话结单时 |
|
||||
| 推荐 | AI基于对话内容自动建议标签 |
|
||||
| 自定义 | 支持添加自定义标签 |
|
||||
|
||||
### 3.4 AI建议采纳追踪
|
||||
|
||||
记录坐席对AI建议的采纳/编辑/忽略行为,存入数据库用于持续优化。
|
||||
|
||||
---
|
||||
|
||||
## 四、Phase 2+ 规划
|
||||
|
||||
| 阶段 | 功能 | 预计周期 |
|
||||
|------|------|---------|
|
||||
| Phase 2 认知层 | 知识推荐 + SOP导航 + 相似工单 + 客户画像 | 3-4周 |
|
||||
| Phase 3 情感层 | 情绪识别 + 安抚话术 + 语气润色 + 疲劳检测 | 4-6周 |
|
||||
|
||||
---
|
||||
|
||||
## 五、技术实现
|
||||
|
||||
**底层实现**:扩展现有 Dify,新增 `assistant` 类型的 Dify Agent。
|
||||
|
||||
| Agent | 用途 | system prompt 侧重 |
|
||||
|-------|------|-------------------|
|
||||
| Agent 1 — 员工端 AI | 回答员工问题 | 友好、准确、引导自助 |
|
||||
| Agent 2 — 坐席端 Wingman | 为坐席生成草稿/摘要/知识 | 专业、结构化、可操作 |
|
||||
|
||||
---
|
||||
|
||||
## 六、行业基准数据
|
||||
|
||||
| 指标 | 数值 | 来源 |
|
||||
|------|------|------|
|
||||
| AI草稿回复减少打字量 | 70%-80% | 天润融通、合力亿捷 |
|
||||
| 自动填单节省时间 | 从1分钟降至10秒 | 天润融通 |
|
||||
| 知识推荐缩短新人上手 | 50% | 循环智能 |
|
||||
| 情绪识别预警准确率 | 85%+ | Helpshift |
|
||||
| 自动摘要节省文书时间 | 70% | 合力亿捷 |
|
||||
@@ -0,0 +1,199 @@
|
||||
# 坐席端会话状态 Tab 筛选 — 产品需求文档 (PRD)
|
||||
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-08-03
|
||||
> **作者**: 宋献
|
||||
> **状态**: ✅ 已落地(生产版本 `index-BK_U7e10.js`)
|
||||
> **子系统**: 04-坐席工作台
|
||||
> **模块**: 会话列表筛选
|
||||
> **关联任务**: #134(坐席端左栏会话状态 Tab 顺序调整 + 默认显示待处理)
|
||||
> **关联原型**: `原型-REQ-坐席-000-坐席工作台-v1.2.html`
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状分析
|
||||
|
||||
坐席工作台左栏会话窗口已具备 Tab 筛选能力(v1.0 起即存在),但 v1.1 存在两个影响日常效率的问题:
|
||||
|
||||
| 问题 | 表现 | 影响 |
|
||||
|------|------|------|
|
||||
| **Tab 顺序不合理** | 「全部」Tab 排在第一位(按字母 + 全集概念) | 高频 Tab(待处理)被埋在其他标签后面 |
|
||||
| **默认激活错位** | 默认进入页面激活「全部」 | 坐席每天首次进入要先点 1 下切到「待处理」才看到工作队列 |
|
||||
| **筛选粒度不足** | 「全部」Tab 混排 待处理 + 进行中 + 已完成 + 同事会话 + 历史会话 | 视觉噪音多,重点不突出 |
|
||||
|
||||
### 1.2 用户痛点
|
||||
|
||||
**核心痛点**:坐席每日首次进入坐席工作台 → 应立即看到「我现在该处理的待办队列」。
|
||||
|
||||
但当前版本默认显示「全部」,坐席需要:
|
||||
1. 看完整列表 → 识别哪些是新分派的
|
||||
2. 或点击「待处理」Tab → 才看到工作队列
|
||||
|
||||
**多点击 + 视觉噪音**:每天 8 小时 × 多次重入 = 累计损失明显。
|
||||
|
||||
### 1.3 目标
|
||||
|
||||
通过最小改动(**仅 Tab 顺序调整 + 默认激活态变更**),实现:
|
||||
|
||||
1. **默认即工作队列** — 打开坐席工作台,第一眼就是待处理
|
||||
2. **顺序符合使用频次** — 高频 Tab 在前、低频 Tab 在后
|
||||
3. **「全部」沉底** — 仅作"全部查看"用,不作为默认入口
|
||||
|
||||
### 1.4 设计原则
|
||||
|
||||
| 原则 | 体现 |
|
||||
|------|------|
|
||||
| **最小改动** | 仅顺序 + 初值,不动 store / 不动样式 / 不动 key 名 |
|
||||
| **后向兼容** | 保留 `activeFilter` 的 4 个 key(`'pending' \| 'active' \| 'done' \| 'all'`),switch 逻辑、API 调用、URL 参数都不变 |
|
||||
| **数据准确** | status 映射规则保留:`pending→queued`、`active→serving/ai_handling`、`done→resolved` |
|
||||
| **协同不干扰** | 同事会话(`colleagueConversations`)和历史会话(`historyConversations`)**仅在「全部」Tab 下可见**,其他 Tab 按 `applyFilters` 自然归零 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 需求说明
|
||||
|
||||
### 2.1 核心功能(FR)
|
||||
|
||||
| FR ID | 功能描述 | 优先级 |
|
||||
|-------|----------|--------|
|
||||
| **FR-01** | 左栏会话窗口顶部展示 4 个 Tab,**顺序为「待处理 → 进行中 → 已完成 → 全部」** | P0 |
|
||||
| **FR-02** | **默认进入页面激活「待处理」Tab**(无需点击) | P0 |
|
||||
| **FR-03** | 切换 Tab 时列表实时过滤,按 `applyFilters` 规则(见 §2.3) | P0 |
|
||||
| **FR-04** | 搜索框 + Tab 为 AND 关系(关键词 ∧ 状态) | P1 |
|
||||
| **FR-05** | 「全部」Tab 下展示所有会话(含同事 + 历史) | P1 |
|
||||
|
||||
### 2.2 详细说明
|
||||
|
||||
**4 个 Tab 排序逻辑**(高频在前,低频在后):
|
||||
1. **待处理** — 每日最高频(队列分派 / 求助举手 / 新分派会话)
|
||||
2. **进行中** — 高频(已回复待用户反馈的会话)
|
||||
3. **已完成** — 中频(结单归档的会话)
|
||||
4. **全部** — 低频(异常排查 / 数据回顾)
|
||||
|
||||
**激活态与默认行为**:
|
||||
- 默认 `activeFilter = 'pending'`
|
||||
- 用户切换后,组件不持久化 Tab 选择(刷新即回到「待处理」)— 这是设计选择,避免历史 Tab 状态导致坐席看不到最新队列
|
||||
|
||||
### 2.3 状态映射规则(与 Store API 保持一致)
|
||||
|
||||
| Tab 显示文案 | `activeFilter` 值 | 对应 `conv.status` | Store 归属 |
|
||||
|---|---|---|---|
|
||||
| **待处理** | `'pending'` | `queued`(待接单) | `myConversations` |
|
||||
| **进行中** | `'active'` | `serving`(服务中)或 `ai_handling`(AI 处理中) | `myConversations` / `colleagueConversations` |
|
||||
| **已完成** | `'done'` | `resolved`(90 天内) | `historyConversations` |
|
||||
| **全部** | `'all'` | 不限定 | 三段全部 |
|
||||
|
||||
> 注:`activeFilter` key 名 `'active'`(不是 `'in-progress'`)保持后向兼容;store status 命名(如 `serving` / `ai_handling`)不变。
|
||||
|
||||
### 2.4 跨段渲染规则
|
||||
|
||||
| Tab | 我的会话(mine) | 同事会话(colleague) | 历史会话(history) |
|
||||
|---|---|---|---|
|
||||
| **待处理** | ✅ 显示 pending | ❌ 自然为空(store 隔离) | ❌ 自然为空 |
|
||||
| **进行中** | ✅ 显示 active | ❌ 自然为空 | ❌ 自然为空 |
|
||||
| **已完成** | ✅ 显示 done(跨段渲染 from history) | ❌ 自然为空 | ✅ 显示 resolved |
|
||||
| **全部** | ✅ 显示 all | ✅ 显示 all | ✅ 显示 all |
|
||||
|
||||
> 「同事会话」在所有筛选 Tab 下都不出现(store 中 colleagueConversations 仅含 `serving+!is_mine+!is_collaborator` 和 `ai_handling`,不满足 pending/active/done 的任意状态)— 用户确认接受该行为。
|
||||
|
||||
---
|
||||
|
||||
## 3. 用户故事 & 验收标准
|
||||
|
||||
### 3.1 用户故事
|
||||
|
||||
| US ID | 角色 | 故事 | 优先级 |
|
||||
|-------|------|------|--------|
|
||||
| **US-01** | 坐席 | 作为坐席,我希望打开坐席工作台立即看到待处理会话列表,这样能马上开始工作 | P0 |
|
||||
| **US-02** | 坐席 | 作为坐席,我希望 Tab 按使用频次排序(待处理在最左),这样切换更顺手 | P1 |
|
||||
| **US-03** | 坐席 | 作为坐席,我希望「全部」沉到底部(不是默认入口),避免误点 | P1 |
|
||||
| **US-04** | 资深坐席 | 作为坐席,我希望切换到「全部」Tab 还能看到同事会话和历史会话,便于排查 | P1 |
|
||||
|
||||
### 3.2 验收标准(AC)
|
||||
|
||||
| AC ID | 验证项 | 验收方法 | 预期结果 |
|
||||
|-------|--------|---------|---------|
|
||||
| **AC-01** | Tab 顺序 | 视觉确认 | 从左到右:`待处理 → 进行中 → 已完成 → 全部` |
|
||||
| **AC-02** | 默认激活 | 刷新页面 | 「待处理」Tab 蓝色高亮 |
|
||||
| **AC-03** | 默认列表 | 刷新页面 | 左侧仅展示 `status=queued` 的会话(来自 `myConversations` 自然过滤) |
|
||||
| **AC-04** | Tab 切换「进行中」 | 点击 | 列表展示 `status in {serving, ai_handling}` |
|
||||
| **AC-05** | Tab 切换「已完成」 | 点击 | 列表展示 `status=resolved`(来自 `historyConversations`) |
|
||||
| **AC-06** | Tab 切换「全部」 | 点击 | 三段(my + colleague + history)全部展示 |
|
||||
| **AC-07** | 搜索 + Tab 联动 | 输入关键词后切换 Tab | 关键词 ∧ 状态过滤均生效 |
|
||||
| **AC-08** | 同事会话隐藏(非全 Tab) | 切到「待处理」 | 赵敏/周芳/吴明(同事区)不显示 |
|
||||
| **AC-09** | 接手会话流转 | 同事会话点击「接手」 | 接手后会话归入 `myConversations`,「全部」Tab 下可见 |
|
||||
| **AC-10** | 无数据容错 | DB 清空全部会话 | 「待处理」激活,左侧显示 `el-empty description="暂无会话"` 占位 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 交互示意
|
||||
|
||||
### 4.1 默认进入页面
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ [🔍 搜索用户、关键词...] │
|
||||
│ │
|
||||
│ [● 待处理 5] [进行中 1] ... [全部 10] │ ← Tab 行:默认 ● 待处理
|
||||
│ │
|
||||
│ 头像 张伟 10:25 待回复 张 │ ← 左侧会话语义化分区
|
||||
│ VPN 连接失败 │
|
||||
│ 头像 陈芳 09:42 待回复 陈 │
|
||||
│ 系统卡顿 │
|
||||
│ ... │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 切到「全部」Tab
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ [● 全部 10] ... [● 待处理 5] │ ← 切到全部 Tab
|
||||
│ │
|
||||
│ --- 我的会话 --- │
|
||||
│ 头像 张伟 10:25 待回复 张 │
|
||||
│ ... │
|
||||
│ --- 同事会话(刘明/王强/李静)--- │
|
||||
│ 头像 赵敏 OA 审批流程报错 [接手]│
|
||||
│ ... │
|
||||
│ --- 历史会话 --- │
|
||||
│ 头像 周杰 昨日 VPN 证书过期—已解决 │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 不在本 PRD 范围(明确边界)
|
||||
|
||||
为避免范围蔓延,以下问题由后续独立 PRD 处理:
|
||||
|
||||
| 项 | 说明 | 后续 PRD 候选 |
|
||||
|---|---|---|
|
||||
| Tab 数量徽章(待处理 5 等) | v1.2 原型里有该设计,但需要 store 新增 4 个 computed 计算实时数量 | **#135 候选** |
|
||||
| 空状态文案("暂无待处理会话") | 当前复用 `el-empty description="暂无会话"`,已能覆盖空场景 | 视真实使用反馈决定是否独立 PRD |
|
||||
| 持久化 Tab 选择(localStorage) | 当前切换刷新即回默认;若坐席希望保留,可加 1 行 localStorage | 视需求反馈 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 关联文档
|
||||
|
||||
| 文档 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 原型 v1.2(最新版) | `docs/01-产品文档/04-坐席工作台/原型-REQ-坐席-000-坐席工作台-v1.2.html` | UI 设计基准 |
|
||||
| 技术方案(会话列表) | `docs/02-技术文档/技术架构/技术方案-REQ-坐席-002-AI辅助消息框-v1.0.md` | 新增 §X.Y 段(commit 时同步) |
|
||||
| 任务说明书 #134 | `docs/07-项目管理/任务说明书/任务说明书-134-坐席端左栏会话状态Tab顺序调整+默认显示待处理.md` | 落地过程记录 |
|
||||
| 前端源码 | `src/frontend-agent/src/components/conversation/ConversationList.vue` | `filterTags` + `activeFilter` 2 处改动 |
|
||||
| Pinia Store | `src/frontend-agent/src/stores/conversation.ts` | `myConversations` / `colleagueConversations` / `historyConversations` 数据分区(未改) |
|
||||
| Git commit | `566bb46 feat(agent): 坐席端左栏会话状态 Tab 顺序调整 + 默认显示待处理 (#134)` | 单一原子提交 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 变更记录
|
||||
|
||||
| 日期 | 变更内容 | 变更人 |
|
||||
|------|----------|--------|
|
||||
| 2026-08-03 | 创建 PRD #009,承接 #134 落地需求 | 宋献 |
|
||||
| 2026-08-03 | 同步技术方案 #002 加入交叉引用 | 宋献 |
|
||||
| | | |
|
||||
@@ -0,0 +1,181 @@
|
||||
# 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 实现 | 产品经理+前端 |
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,961 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>IT智能服务台 · 坐席工作台 v1.4(B方案分栏 + 排查步骤全屏流程图)</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-primary: #f5f7fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #f0f2f5;
|
||||
--bg-hover: #e8ecf1;
|
||||
--bg-active: #dce3ec;
|
||||
--border: #e2e8f0;
|
||||
--border-light: #cbd5e1;
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
--accent: #3b82f6;
|
||||
--accent-soft: #dbeafe;
|
||||
--success: #22c55e;
|
||||
--success-soft: #dcfce7;
|
||||
--warning: #f59e0b;
|
||||
--warning-soft: #fef3c7;
|
||||
--danger: #ef4444;
|
||||
--danger-soft: #fee2e2;
|
||||
--info: #60a5fa;
|
||||
--purple: #8b5cf6;
|
||||
--purple-soft: #ede9fe;
|
||||
--orange: #f97316;
|
||||
--orange-soft: #fff7ed;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
--radius: 6px;
|
||||
--radius-lg: 10px;
|
||||
--transition: 0.25s cubic-bezier(0.4,0,0.2,1);
|
||||
|
||||
--qr-c0: #3b82f6; /* 电脑 - 蓝 */
|
||||
--qr-c1: #22c55e; /* 软件 - 绿 */
|
||||
--qr-c2: #f97316; /* 外设 - 橙 */
|
||||
--qr-c3: #8b5cf6; /* 网络 - 紫 */
|
||||
--qr-c4: #ef4444; /* 安全 - 红 */
|
||||
--qr-c5: #0ea5e9; /* 资产 - 青 */
|
||||
--qr-c6: #6b7280; /* 其他 - 灰 */
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #0f1923;
|
||||
--bg-secondary: #151f2b;
|
||||
--bg-tertiary: #1a2736;
|
||||
--bg-hover: #1e3044;
|
||||
--bg-active: #243b52;
|
||||
--border: #1e3044;
|
||||
--border-light: #2a3f56;
|
||||
--text-primary: #e8edf2;
|
||||
--text-secondary: #8ba1b7;
|
||||
--text-muted: #5c7185;
|
||||
--accent: #4da6ff;
|
||||
--accent-soft: #2b5f8a;
|
||||
--success: #34d399;
|
||||
--success-soft: #1a3a2a;
|
||||
--warning: #fbbf24;
|
||||
--warning-soft: #3a2f10;
|
||||
--danger: #f87171;
|
||||
--danger-soft: #3a1a1a;
|
||||
--info: #60a5fa;
|
||||
--purple: #a78bfa;
|
||||
--purple-soft: #2d2060;
|
||||
--orange: #fb923c;
|
||||
--orange-soft: #3d1f08;
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
|
||||
--shadow: 0 2px 8px rgba(0,0,0,0.3);
|
||||
|
||||
--qr-c0: #60a5fa;
|
||||
--qr-c1: #4ade80;
|
||||
--qr-c2: #fb923c;
|
||||
--qr-c3: #a78bfa;
|
||||
--qr-c4: #f87171;
|
||||
--qr-c5: #22d3ee;
|
||||
--qr-c6: #9ca3af;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
transition: background 0.3s, color 0.3s;
|
||||
}
|
||||
|
||||
/* ========== TOP BAR ========== */
|
||||
.theme-toggle-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0;
|
||||
height: 44px;
|
||||
background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
z-index: 100;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.bar-left { display: flex; align-items: center; gap: 10px; }
|
||||
.logo-sm {
|
||||
width: 26px; height: 26px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--purple));
|
||||
border-radius: 6px; display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 700; font-size: 10px; color: #fff;
|
||||
}
|
||||
.sys-name {
|
||||
font-size: 13px; font-weight: 700;
|
||||
background: linear-gradient(135deg, var(--accent), var(--purple));
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text;
|
||||
}
|
||||
.v-tag {
|
||||
font-size: 10px; font-weight: 600; color: #fff;
|
||||
background: var(--purple); padding: 1px 7px; border-radius: 8px;
|
||||
}
|
||||
.bar-right { display: flex; align-items: center; gap: 14px; }
|
||||
.theme-switch { display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none; font-size: 12px; }
|
||||
.session-state-switch {
|
||||
display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
|
||||
font-size: 12px; padding: 4px 10px; border-radius: 16px;
|
||||
border: 1px solid var(--border); background: var(--bg-tertiary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
.ss-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--success); }
|
||||
.session-state-switch .ss-hint { color: var(--text-muted); font-size: 11px; }
|
||||
body.session-ended .session-state-switch { background: var(--purple-soft); border-color: var(--purple); }
|
||||
body.session-ended .session-state-switch .ss-dot { background: var(--purple); }
|
||||
body.session-ended .session-state-switch .ss-label { color: var(--purple); }
|
||||
|
||||
/* ========== WORKSPACE(B 方案:左固定 / 中 flex1.5 / 右 flex1 ≈ 60:40) ========== */
|
||||
.workspace {
|
||||
position: fixed; top: 44px; left: 0; right: 0; bottom: 0;
|
||||
display: flex;
|
||||
height: calc(100vh - 44px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 列宽标注徽标(原型示意,便于评审直观看到 60:40) */
|
||||
.col-badge {
|
||||
font-size: 10px; color: var(--text-muted);
|
||||
background: var(--bg-tertiary); border: 1px dashed var(--border-light);
|
||||
padding: 1px 7px; border-radius: 8px; white-space: nowrap; margin-left: auto;
|
||||
}
|
||||
|
||||
/* ----- 左栏:会话列表(固定 260px) ----- */
|
||||
.sidebar-left {
|
||||
width: 260px; flex-shrink: 0;
|
||||
background: var(--bg-secondary);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sl-header {
|
||||
padding: 10px 14px; font-size: 13px; font-weight: 600;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
border-bottom: 1px solid var(--border); flex-shrink: 0;
|
||||
}
|
||||
.sl-list { flex: 1; overflow-y: auto; padding: 6px; }
|
||||
.conv-item {
|
||||
padding: 9px 10px; border-radius: var(--radius); cursor: pointer;
|
||||
margin-bottom: 4px; display: flex; gap: 9px; align-items: center;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.conv-item:hover { background: var(--bg-hover); }
|
||||
.conv-item.active { background: var(--accent-soft); }
|
||||
.conv-avatar {
|
||||
width: 34px; height: 34px; border-radius: 50%; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-weight: 600; font-size: 13px;
|
||||
}
|
||||
.conv-main { flex: 1; min-width: 0; }
|
||||
.conv-top { display: flex; justify-content: space-between; align-items: baseline; }
|
||||
.conv-name { font-size: 12px; font-weight: 600; }
|
||||
.conv-time { font-size: 10px; color: var(--text-muted); }
|
||||
.conv-preview { font-size: 11px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.conv-unread { background: var(--danger); color: #fff; font-size: 10px; border-radius: 10px; padding: 0 6px; min-width: 16px; text-align: center; }
|
||||
|
||||
/* ----- 中栏:对话区(flex:1.5 ≈ 60%) ----- */
|
||||
.workspace-main {
|
||||
flex: 1.5; min-width: 0;
|
||||
background: var(--bg-primary);
|
||||
display: flex; flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
.user-info-bar {
|
||||
padding: 10px 16px; background: var(--bg-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex; align-items: center; gap: 10px; flex-shrink: 0;
|
||||
}
|
||||
.uib-avatar { width: 36px; height: 36px; border-radius: 50%; background: var(--accent); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600; }
|
||||
.uib-name { font-size: 13px; font-weight: 600; }
|
||||
.uib-meta { font-size: 11px; color: var(--text-muted); }
|
||||
.uib-status { margin-left: auto; font-size: 11px; color: var(--success); display: flex; align-items: center; gap: 5px; }
|
||||
.uib-status .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); }
|
||||
|
||||
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.msg { display: flex; gap: 8px; max-width: 78%; }
|
||||
.msg.user { align-self: flex-end; flex-direction: row-reverse; }
|
||||
.msg-avatar { width: 30px; height: 30px; border-radius: 50%; flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 600; font-size: 11px; }
|
||||
.msg.user .msg-avatar { background: var(--accent); }
|
||||
.msg.agent .msg-avatar { background: var(--purple); }
|
||||
.bubble { padding: 9px 12px; border-radius: 10px; font-size: 12.5px; line-height: 1.55; }
|
||||
.msg.user .bubble { background: var(--accent); color: #fff; border-top-right-radius: 2px; }
|
||||
.msg.agent .bubble { background: var(--bg-secondary); border: 1px solid var(--border); border-top-left-radius: 2px; }
|
||||
.msg-time { font-size: 10px; color: var(--text-muted); margin-top: 3px; }
|
||||
|
||||
/* 快速回复:中栏消息框工具栏上方 */
|
||||
.quick-reply-section {
|
||||
flex-shrink: 0; height: 220px; border-top: 1px solid var(--border);
|
||||
background: var(--bg-secondary); display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
.qr-head { padding: 6px 12px; font-size: 12px; font-weight: 600; display: flex; align-items: center; gap: 6px; border-bottom: 1px solid var(--border); flex-shrink: 0; }
|
||||
.qr-search { padding: 6px 12px; flex-shrink: 0; }
|
||||
.qr-search-input { width: 100%; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 10px; color: var(--text-primary); font-size: 12px; outline: none; }
|
||||
.qr-grid { flex: 1; overflow-y: auto; padding: 6px 10px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; align-content: start; }
|
||||
.qr-cat {
|
||||
padding: 8px 6px; border-radius: var(--radius); cursor: pointer; text-align: center;
|
||||
border: 1px solid var(--border); background: var(--bg-tertiary); transition: var(--transition); font-size: 11px;
|
||||
}
|
||||
.qr-cat:hover { transform: translateY(-1px); box-shadow: var(--shadow-sm); }
|
||||
.qr-cat .qr-cat-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 4px; }
|
||||
.qr-cat .qr-cat-key { font-size: 9px; color: var(--text-muted); margin-left: 3px; }
|
||||
|
||||
.reply-box { flex-shrink: 0; border-top: 1px solid var(--border); background: var(--bg-secondary); padding: 8px 12px 10px; }
|
||||
.reply-toolbar { display: flex; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; }
|
||||
.tb-btn { font-size: 11px; padding: 4px 9px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-tertiary); cursor: pointer; transition: var(--transition); }
|
||||
.tb-btn:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.reply-input { width: 100%; min-height: 38px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 10px; color: var(--text-primary); font-size: 12.5px; font-family: inherit; outline: none; resize: none; }
|
||||
.reply-input:focus { border-color: var(--accent); }
|
||||
.reply-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
|
||||
.btn-send { background: var(--accent); color: #fff; border: none; padding: 6px 18px; border-radius: var(--radius); font-size: 12px; cursor: pointer; font-weight: 600; }
|
||||
.btn-send:hover { filter: brightness(1.05); }
|
||||
|
||||
/* ----- 右栏:辅助面板(flex:1 ≈ 40%,min 320 / max 560) ----- */
|
||||
.sidebar-right {
|
||||
flex: 1; min-width: 320px; max-width: 560px;
|
||||
background: var(--bg-secondary);
|
||||
display: flex; flex-direction: column;
|
||||
overflow-y: auto;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.sidebar-right::-webkit-scrollbar { width: 5px; }
|
||||
.sidebar-right::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
|
||||
|
||||
/* 排查步骤面板(右栏顶部,默认展开,可折叠;新增「全屏查看」) */
|
||||
.ts-panel { flex-shrink: 0; border-bottom: 1px solid var(--border); background: var(--bg-secondary); }
|
||||
.ts-panel-head {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 9px 12px; cursor: pointer; transition: var(--transition);
|
||||
position: sticky; top: 0; background: var(--bg-secondary); z-index: 2;
|
||||
}
|
||||
.ts-panel-head:hover { background: var(--bg-hover); }
|
||||
.ts-panel-title { font-size: 13px; font-weight: 600; display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
||||
.ts-panel-collapse { font-size: 12px; color: var(--text-muted); transition: transform 0.2s; margin-left: 4px; }
|
||||
.ts-panel.collapsed .ts-panel-collapse { transform: rotate(-90deg); }
|
||||
.ts-fullscreen-btn {
|
||||
margin-left: auto; font-size: 11px; color: var(--accent); cursor: pointer;
|
||||
padding: 3px 8px; border: 1px solid var(--border); border-radius: 4px;
|
||||
background: var(--bg-tertiary); transition: var(--transition); flex-shrink: 0;
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.ts-fullscreen-btn:hover { background: var(--accent-soft); border-color: var(--accent); }
|
||||
.ts-panel-body { overflow: hidden; transition: max-height 0.3s ease; max-height: 1600px; }
|
||||
.ts-panel.collapsed .ts-panel-body { max-height: 0; }
|
||||
.ts-path-inline { display: flex; align-items: center; gap: 0; padding: 8px 12px; flex-wrap: wrap; }
|
||||
.path-step { padding: 2px 7px; background: var(--bg-tertiary); border: 1px solid var(--border-light); border-radius: 3px; font-size: 11px; color: var(--text-secondary); white-space: nowrap; }
|
||||
.path-step.done { background: var(--success-soft); border-color: var(--success); color: var(--success); }
|
||||
.path-step.current { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); font-weight: 600; }
|
||||
.path-arrow-inline { color: var(--text-muted); font-size: 10px; padding: 0 3px; }
|
||||
.ts-flowchart-toggle { display: block; padding: 6px 12px; font-size: 12px; color: var(--accent); cursor: pointer; user-select: none; }
|
||||
.ts-flowchart-toggle:hover { background: var(--accent-soft); }
|
||||
|
||||
/* 决策树(侧栏内:默认展开,可收起) */
|
||||
.ts-flowchart { padding: 4px 14px 12px; overflow-y: auto; max-height: 520px; transition: max-height 0.35s ease, padding 0.35s ease; }
|
||||
.ts-flowchart.collapsed-flow { max-height: 0; padding-top: 0; padding-bottom: 0; overflow: hidden; }
|
||||
.flow-step { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 6px; position: relative; }
|
||||
.flow-step:last-child { margin-bottom: 0; }
|
||||
.flow-step-connector { position: absolute; left: 10px; top: 26px; bottom: -14px; width: 2px; background: var(--border); }
|
||||
.flow-step:last-child .flow-step-connector { display: none; }
|
||||
.flow-node { width: 22px; height: 22px; border-radius: 50%; background: var(--accent-soft); color: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; flex-shrink: 0; z-index: 1; border: 1px solid var(--accent); }
|
||||
.flow-node.done { background: var(--success-soft); color: var(--success); border-color: var(--success); }
|
||||
.flow-node.current { background: var(--accent); color: #fff; }
|
||||
.flow-node.judge { width: auto; height: auto; border-radius: var(--radius); padding: 3px 8px; font-size: 10px; font-weight: 500; background: var(--warning-soft); color: var(--warning); border-color: var(--warning); }
|
||||
.flow-content { flex: 1; }
|
||||
.flow-title { font-size: 12px; font-weight: 500; color: var(--text-primary); margin-bottom: 2px; }
|
||||
.flow-desc { font-size: 11px; color: var(--text-secondary); line-height: 1.4; }
|
||||
.flow-branch { margin-left: 20px; margin-top: 4px; padding-left: 10px; border-left: 2px dashed var(--border-light); }
|
||||
.flow-branch-item { font-size: 11px; color: var(--text-muted); padding: 2px 0; cursor: pointer; transition: var(--transition); }
|
||||
.flow-branch-item:hover { color: var(--accent); }
|
||||
.flow-branch-item.active { color: var(--success); font-weight: 500; }
|
||||
|
||||
/* AI 推荐 */
|
||||
.ai-recommend-section { flex-shrink: 0; padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||
.ars-title { font-size: 12px; font-weight: 600; color: var(--accent); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.ars-title::before { content: ''; width: 3px; height: 12px; background: var(--accent); border-radius: 2px; display: inline-block; }
|
||||
.ars-card { background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px 10px; margin-bottom: 5px; cursor: pointer; transition: var(--transition); }
|
||||
.ars-card:hover { border-color: var(--accent); background: var(--bg-hover); }
|
||||
.ars-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 3px; }
|
||||
.ars-confidence { font-size: 10px; padding: 1px 6px; border-radius: 8px; background: var(--accent-soft); color: var(--accent); }
|
||||
.ars-card-text { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
|
||||
.ars-shortcut-hint { font-size: 10px; color: var(--text-muted); margin-top: 3px; display: flex; align-items: center; gap: 4px; }
|
||||
.key-badge { display: inline-flex; align-items: center; justify-content: center; font-size: 10px; padding: 1px 5px; background: var(--bg-hover); border: 1px solid var(--border); border-radius: 3px; color: var(--text-muted); min-width: 18px; font-family: monospace; }
|
||||
|
||||
/* 待办事项 Kanban */
|
||||
.kanban { flex-shrink: 0; padding: 10px 12px; border-bottom: 1px solid var(--border); }
|
||||
.kanban-title { font-size: 12px; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
|
||||
.kanban-title .count { background: var(--accent-soft); color: var(--accent); font-size: 10px; padding: 0 7px; border-radius: 8px; }
|
||||
.kanban-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: var(--radius); cursor: pointer; transition: var(--transition); border: 1px solid transparent; margin-bottom: 3px; }
|
||||
.kanban-item:hover { background: var(--bg-hover); border-color: var(--border); }
|
||||
.ki-priority { width: 6px; height: 26px; border-radius: 3px; flex-shrink: 0; }
|
||||
.ki-priority.urgent { background: var(--danger); }
|
||||
.ki-priority.high { background: var(--warning); }
|
||||
.ki-priority.normal { background: var(--accent); }
|
||||
.ki-priority.done { background: var(--success); }
|
||||
.ki-text { flex: 1; font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.ki-type { font-size: 9px; padding: 1px 5px; border-radius: 4px; flex-shrink: 0; }
|
||||
.ki-type.ticket { background: var(--accent-soft); color: var(--accent); }
|
||||
.ki-type.approval { background: var(--purple-soft); color: var(--purple); }
|
||||
.ki-type.device { background: var(--orange-soft); color: var(--orange); }
|
||||
.ki-time { font-size: 10px; color: var(--text-muted); flex-shrink: 0; }
|
||||
.ki-avatar { width: 22px; height: 22px; border-radius: 50%; color: #fff; font-size: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.ki-avatar.ka-red { background: var(--danger); }
|
||||
.ki-avatar.ka-orange { background: var(--orange); }
|
||||
.ki-avatar.ka-blue { background: var(--accent); }
|
||||
.ki-avatar.ka-green { background: var(--success); }
|
||||
.ki-avatar.ka-purple { background: var(--purple); }
|
||||
.kanban-item.resolved { opacity: 0.6; }
|
||||
.kanban-stats { display: flex; gap: 12px; font-size: 10px; color: var(--text-muted); margin-top: 6px; }
|
||||
.kanban-stats .dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; margin-right: 3px; }
|
||||
.dot-online { background: var(--success); }
|
||||
.dot-busy { background: var(--warning); }
|
||||
.dot-offline { background: var(--text-muted); }
|
||||
|
||||
/* 复盘类模块(仅会话结束后显示) */
|
||||
.post-session-hint { margin: 10px 12px; padding: 8px 10px; background: var(--bg-tertiary); border: 1px dashed var(--border-light); border-radius: var(--radius); font-size: 11px; color: var(--text-secondary); }
|
||||
.post-session-card { display: none; margin: 10px 12px; padding: 10px 12px; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: var(--radius-lg); }
|
||||
body.session-ended .post-session-card { display: block; }
|
||||
body.session-ended .post-session-hint { display: none; }
|
||||
.psc-title { font-size: 13px; font-weight: 600; margin-bottom: 8px; }
|
||||
.psc-field { margin-bottom: 8px; }
|
||||
.psc-field-label { font-size: 10px; color: var(--text-muted); margin-bottom: 3px; }
|
||||
.psc-field-value { font-size: 11.5px; color: var(--text-primary); line-height: 1.5; }
|
||||
.psc-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
|
||||
.psc-tag { font-size: 10px; padding: 2px 8px; border-radius: 10px; background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border); }
|
||||
.psc-tag.auto { background: var(--accent-soft); color: var(--accent); border-color: var(--accent); }
|
||||
.psc-stars { font-size: 14px; color: var(--warning); }
|
||||
.psc-stars .star-off { color: var(--border-light); }
|
||||
.psc-row { display: flex; justify-content: space-between; font-size: 11px; padding: 2px 0; }
|
||||
.psc-row .psc-k { color: var(--text-muted); }
|
||||
.psc-textarea { width: 100%; min-height: 44px; background: var(--bg-secondary); border: 1px solid var(--border); border-radius: var(--radius); padding: 6px 8px; font-size: 11px; font-family: inherit; color: var(--text-primary); outline: none; resize: vertical; }
|
||||
.psc-check { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--text-secondary); margin-top: 6px; }
|
||||
|
||||
/* ========== 全屏流程图浮层 ========== */
|
||||
.flowchart-fullscreen {
|
||||
position: fixed; inset: 0; z-index: 1000;
|
||||
background: var(--bg-primary);
|
||||
display: none; flex-direction: column;
|
||||
}
|
||||
.flowchart-fullscreen.open { display: flex; }
|
||||
.ff-topbar {
|
||||
height: 48px; flex-shrink: 0; display: flex; align-items: center; gap: 12px;
|
||||
padding: 0 18px; background: var(--bg-secondary); border-bottom: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
.ff-title { font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
|
||||
.ff-subtitle { font-size: 11px; color: var(--text-muted); }
|
||||
.ff-close {
|
||||
margin-left: auto; cursor: pointer; font-size: 13px; color: var(--text-secondary);
|
||||
padding: 6px 14px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-tertiary);
|
||||
transition: var(--transition);
|
||||
}
|
||||
.ff-close:hover { border-color: var(--danger); color: var(--danger); }
|
||||
.ff-stage { flex: 1; overflow: auto; padding: 28px 40px; }
|
||||
.ff-tree { max-width: 920px; margin: 0 auto; }
|
||||
.ff-tree .flow-step { margin-bottom: 14px; }
|
||||
.ff-tree .flow-node { width: 28px; height: 28px; font-size: 12px; }
|
||||
.ff-tree .flow-step-connector { left: 13px; top: 32px; bottom: -18px; width: 2px; }
|
||||
.ff-tree .flow-title { font-size: 14px; }
|
||||
.ff-tree .flow-desc { font-size: 12.5px; }
|
||||
.ff-tree .flow-branch-item { font-size: 12.5px; }
|
||||
.ff-hint { text-align: center; font-size: 11px; color: var(--text-muted); padding: 8px; flex-shrink: 0; }
|
||||
|
||||
/* ========== 原型说明对话框 ========== */
|
||||
.modal-mask { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 900; display: none; align-items: center; justify-content: center; }
|
||||
.modal-mask.open { display: flex; }
|
||||
.modal-box { width: 560px; max-width: 92vw; max-height: 80vh; overflow-y: auto; background: var(--bg-secondary); border-radius: var(--radius-lg); box-shadow: var(--shadow); padding: 20px 22px; }
|
||||
.modal-box h3 { font-size: 15px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
||||
.modal-box h4 { font-size: 13px; margin: 14px 0 6px; color: var(--accent); }
|
||||
.modal-box ul { padding-left: 18px; }
|
||||
.modal-box li { font-size: 12px; color: var(--text-secondary); margin-bottom: 5px; line-height: 1.55; }
|
||||
.modal-box .kbd { font-family: monospace; background: var(--bg-tertiary); border: 1px solid var(--border); border-radius: 3px; padding: 1px 6px; font-size: 11px; color: var(--text-primary); }
|
||||
.modal-box .close-x { float: right; cursor: pointer; color: var(--text-muted); font-size: 18px; line-height: 1; }
|
||||
.modal-box .close-x:hover { color: var(--danger); }
|
||||
.proto-btn {
|
||||
position: fixed; right: 16px; bottom: 16px; z-index: 800;
|
||||
background: var(--purple); color: #fff; border: none; border-radius: 20px;
|
||||
padding: 9px 16px; font-size: 12px; font-weight: 600; cursor: pointer; box-shadow: var(--shadow);
|
||||
transition: var(--transition);
|
||||
}
|
||||
.proto-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ============ TOP BAR ============ -->
|
||||
<div class="theme-toggle-bar">
|
||||
<div class="bar-left">
|
||||
<div class="logo-sm">IT</div>
|
||||
<span class="sys-name">IT智能服务台 · 坐席工作台</span>
|
||||
<span class="v-tag">v1.4</span>
|
||||
</div>
|
||||
<div class="bar-right">
|
||||
<div class="theme-switch" onclick="toggleTheme()">
|
||||
<span id="themeIcon">🌙</span><span id="themeLabel">深色</span>
|
||||
</div>
|
||||
<div class="session-state-switch" id="sessionStateSwitch" onclick="toggleSessionState()">
|
||||
<span class="ss-dot"></span>
|
||||
<span class="ss-label" id="sessionStateLabel">会话进行中</span>
|
||||
<span class="ss-hint">(点击模拟「会话已结束」)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ WORKSPACE(B 方案:左 260px / 中 flex1.5 / 右 flex1) ============ -->
|
||||
<div class="workspace">
|
||||
|
||||
<!-- ===== 左栏:会话列表(固定 260px) ===== -->
|
||||
<aside class="sidebar-left">
|
||||
<div class="sl-header">💬 会话列表 <span class="col-badge">260px 固定</span></div>
|
||||
<div class="sl-list">
|
||||
<div class="conv-item active">
|
||||
<div class="conv-avatar" style="background:var(--accent)">王</div>
|
||||
<div class="conv-main">
|
||||
<div class="conv-top"><span class="conv-name">王莉 · CEO办公室</span><span class="conv-time">5m</span></div>
|
||||
<div class="conv-preview">投屏设备无法连接,提示驱动异常…</div>
|
||||
</div>
|
||||
<span class="conv-unread">2</span>
|
||||
</div>
|
||||
<div class="conv-item">
|
||||
<div class="conv-avatar" style="background:var(--orange)">财</div>
|
||||
<div class="conv-main">
|
||||
<div class="conv-top"><span class="conv-name">财务部 · 税控系统</span><span class="conv-time">12m</span></div>
|
||||
<div class="conv-preview">税控系统登录后白屏,报错 500…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conv-item">
|
||||
<div class="conv-avatar" style="background:var(--purple)">研</div>
|
||||
<div class="conv-main">
|
||||
<div class="conv-top"><span class="conv-name">李研发 · VPN 故障</span><span class="conv-time">28m</span></div>
|
||||
<div class="conv-preview">AnyConnect 连不上,提示协商失败…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conv-item">
|
||||
<div class="conv-avatar" style="background:var(--success)">行</div>
|
||||
<div class="conv-main">
|
||||
<div class="conv-top"><span class="conv-name">行政部 · 会议室A</span><span class="conv-time">45m</span></div>
|
||||
<div class="conv-preview">投影仪离线,需现场排查…</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conv-item">
|
||||
<div class="conv-avatar" style="background:var(--accent-soft);color:var(--accent)">客</div>
|
||||
<div class="conv-main">
|
||||
<div class="conv-top"><span class="conv-name">客服部 · 耳机无声</span><span class="conv-time">1h</span></div>
|
||||
<div class="conv-preview">重装驱动后恢复正常,待回访…</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ===== 中栏:对话区(flex:1.5 ≈ 60%) ===== -->
|
||||
<main class="workspace-main">
|
||||
<div class="user-info-bar">
|
||||
<div class="uib-avatar">王</div>
|
||||
<div>
|
||||
<div class="uib-name">王莉 · CEO办公室</div>
|
||||
<div class="uib-meta">工号 E10245 · 杭州滨江 · 桌面云</div>
|
||||
</div>
|
||||
<div class="uib-status"><span class="dot"></span> 在线 · 已认证</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-messages">
|
||||
<div class="msg user">
|
||||
<div class="msg-avatar">王</div>
|
||||
<div>
|
||||
<div class="bubble">投屏设备连不上,提示驱动异常,会议马上要开始了,急!</div>
|
||||
<div class="msg-time">09:12</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg agent">
|
||||
<div class="msg-avatar">坐</div>
|
||||
<div>
|
||||
<div class="bubble">收到,先帮您确认投屏设备型号与连接方式,右栏「排查步骤」已定位到对应决策树,请稍候。</div>
|
||||
<div class="msg-time">09:12</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg user">
|
||||
<div class="msg-avatar">王</div>
|
||||
<div>
|
||||
<div class="bubble">是无线投屏器,插在会议室的 HDMI 上。</div>
|
||||
<div class="msg-time">09:13</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg agent">
|
||||
<div class="msg-avatar">坐</div>
|
||||
<div>
|
||||
<div class="bubble">明白,已派单至会议室现场运维,同时给您一个临时方案:用笔记本直连 HDMI 过渡。详见右栏 AI 推荐。</div>
|
||||
<div class="msg-time">09:14</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速回复:中栏消息框工具栏上方 -->
|
||||
<div class="quick-reply-section">
|
||||
<div class="qr-head">⚡ 快速回复(三层渐进式 · 位于消息框工具栏上方) <span class="col-badge">中栏 ≈ 60%</span></div>
|
||||
<div class="qr-search"><input class="qr-search-input" placeholder="搜索快速回复 / Alt+目录数字"></div>
|
||||
<div class="qr-grid">
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c0)"></span>电脑<span class="qr-cat-key">1</span></div>
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c1)"></span>软件<span class="qr-cat-key">2</span></div>
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c2)"></span>外设<span class="qr-cat-key">3</span></div>
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c3)"></span>网络<span class="qr-cat-key">4</span></div>
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c4)"></span>安全<span class="qr-cat-key">5</span></div>
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c5)"></span>资产<span class="qr-cat-key">6</span></div>
|
||||
<div class="qr-cat"><span class="qr-cat-dot" style="background:var(--qr-c6)"></span>其他<span class="qr-cat-key">7</span></div>
|
||||
<div class="qr-cat" style="border-style:dashed;color:var(--text-muted)">+ 全部</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reply-box">
|
||||
<div class="reply-toolbar">
|
||||
<button class="tb-btn">📎 附件</button>
|
||||
<button class="tb-btn">😊 表情</button>
|
||||
<button class="tb-btn">🖼️ 截图</button>
|
||||
<button class="tb-btn">🤖 AI润色</button>
|
||||
<button class="tb-btn">⚡ 快速回复</button>
|
||||
</div>
|
||||
<textarea class="reply-input" placeholder="输入回复内容…(Enter 发送 / Shift+Enter 换行)"></textarea>
|
||||
<div class="reply-actions">
|
||||
<button class="btn-send">发送</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- ===== 右栏:辅助面板(flex:1 ≈ 40%,min 320 / max 560) ===== -->
|
||||
<aside class="sidebar-right">
|
||||
<!-- 排查步骤(右栏顶部,默认展开;新增「全屏查看」) -->
|
||||
<div class="ts-panel" id="tsPanel">
|
||||
<div class="ts-panel-head" onclick="toggleTsPanel()" title="点击折叠/展开排查步骤">
|
||||
<span class="ts-panel-title">🔧 排查步骤</span>
|
||||
<span class="ts-panel-collapse">▼</span>
|
||||
<span class="col-badge">右栏 ≈ 40%</span>
|
||||
<span class="ts-fullscreen-btn" onclick="event.stopPropagation(); openFlowFullscreen()" title="全屏查看完整决策树">⛶ 全屏查看</span>
|
||||
</div>
|
||||
<div class="ts-panel-body">
|
||||
<div class="ts-path-inline">
|
||||
<span class="path-step done">① 确认型号</span>
|
||||
<span class="path-arrow-inline">→</span>
|
||||
<span class="path-step done">② 检查连接</span>
|
||||
<span class="path-arrow-inline">→</span>
|
||||
<span class="path-step current">③ 驱动排查</span>
|
||||
<span class="path-arrow-inline">→</span>
|
||||
<span class="path-step">④ 现场派单</span>
|
||||
<span class="path-arrow-inline">→</span>
|
||||
<span class="path-step">⑤ 回访确认</span>
|
||||
</div>
|
||||
<span class="ts-flowchart-toggle" id="tsFlowBtn" onclick="toggleTsFlow()">▼ 收起完整排查决策树</span>
|
||||
<div class="ts-flowchart" id="tsFlowchart">
|
||||
<div style="font-size:11px;color:var(--text-muted);margin-bottom:8px;">🔀 无线投屏器故障 — 完整排查决策树</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node done">1</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">确认投屏设备型号</div>
|
||||
<div class="flow-desc">询问用户是无线投屏器 / 会议主机自带投屏 / 第三方软件(如乐播)</div>
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-item active">→ 无线投屏器:进入 HDMI 直连与驱动分支</div>
|
||||
<div class="flow-branch-item">→ 会议主机自带:进入系统投屏服务分支</div>
|
||||
<div class="flow-branch-item">→ 第三方软件:进入网络与权限分支</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node judge">❓ 判断</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">HDMI 直连是否正常?</div>
|
||||
<div class="flow-desc">请用户将笔记本直连会议室 HDMI 验证,排除无线链路问题</div>
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-item active">正常 → 锁定为无线投屏器驱动/固件问题,进入步骤②</div>
|
||||
<div class="flow-branch-item">仍异常 → 进入显示接口/线缆硬件分支</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node done">2</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">重装 / 更新投屏驱动</div>
|
||||
<div class="flow-desc">指导用户通过软件管家重装无线投屏驱动,重启投屏器与笔记本</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node judge">❓ 判断</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">重装驱动后能否投屏?</div>
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-item active">能 → 标记为「已解决」,进入步骤⑤回访</div>
|
||||
<div class="flow-branch-item">不能 → 进入步骤③现场派单</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node current">3</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">现场派单排查</div>
|
||||
<div class="flow-desc">创建现场工单,指派会议室运维检查投屏器固件、WiFi 信道与供电</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node">4</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">固件升级 / 替换设备</div>
|
||||
<div class="flow-desc">若固件过旧则升级;硬件损坏则走备件替换流程</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-step">
|
||||
<div class="flow-node">5</div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">24H 后回访确认</div>
|
||||
<div class="flow-desc">创建提醒任务,24小时后通过企业微信确认投屏使用正常</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI 推荐 -->
|
||||
<div class="ai-recommend-section">
|
||||
<div class="ars-title">🤖 AI 智能推荐</div>
|
||||
<div class="ars-card" onclick="alert('回填至输入框(原型示意)')">
|
||||
<div class="ars-card-header"><span style="font-size:10px;color:var(--text-secondary);">临时过渡方案</span><span class="ars-confidence">94%</span></div>
|
||||
<div class="ars-card-text">可先用笔记本直连 HDMI 过渡,同时已为您派单现场运维,预计 10 分钟内到达。</div>
|
||||
<div class="ars-shortcut-hint"><span class="key-badge">Ctrl</span>+<span class="key-badge">1</span></div>
|
||||
</div>
|
||||
<div class="ars-card" onclick="alert('回填至输入框(原型示意)')">
|
||||
<div class="ars-card-header"><span style="font-size:10px;color:var(--text-secondary);">驱动重装指引</span><span class="ars-confidence">88%</span></div>
|
||||
<div class="ars-card-text">打开软件管家搜索「无线投屏驱动」一键重装,重启投屏器后重试。</div>
|
||||
<div class="ars-shortcut-hint"><span class="key-badge">Ctrl</span>+<span class="key-badge">2</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 待办事项(右栏底部) -->
|
||||
<div class="kanban">
|
||||
<div class="kanban-title">待办事项 <span class="count">6</span></div>
|
||||
<div class="kanban-item">
|
||||
<span class="ki-priority urgent"></span>
|
||||
<span class="ki-text">CEO办公室 - 投屏设备故障</span>
|
||||
<span class="ki-type ticket">工单</span>
|
||||
<span class="ki-time">5m</span>
|
||||
<div class="ki-avatar ka-red">王</div>
|
||||
</div>
|
||||
<div class="kanban-item">
|
||||
<span class="ki-priority urgent"></span>
|
||||
<span class="ki-text">财务部 - 税控系统无法登录</span>
|
||||
<span class="ki-type ticket">工单</span>
|
||||
<span class="ki-time">12m</span>
|
||||
<div class="ki-avatar ka-orange">财</div>
|
||||
</div>
|
||||
<div class="kanban-item">
|
||||
<span class="ki-priority high"></span>
|
||||
<span class="ki-text">IT采购申请 - 研发部笔记本</span>
|
||||
<span class="ki-type approval">审批</span>
|
||||
<span class="ki-time">28m</span>
|
||||
<div class="ki-avatar ka-blue">研</div>
|
||||
</div>
|
||||
<div class="kanban-item">
|
||||
<span class="ki-priority high"></span>
|
||||
<span class="ki-text">会议室 A - 投影仪离线</span>
|
||||
<span class="ki-type device">设备</span>
|
||||
<span class="ki-time">45m</span>
|
||||
<div class="ki-avatar ka-green">行</div>
|
||||
</div>
|
||||
<div class="kanban-item">
|
||||
<span class="ki-priority normal"></span>
|
||||
<span class="ki-text">新员工入职 - 设备申领审批</span>
|
||||
<span class="ki-type approval">审批</span>
|
||||
<span class="ki-time">32m</span>
|
||||
<div class="ki-avatar ka-purple">H</div>
|
||||
</div>
|
||||
<div class="kanban-item resolved">
|
||||
<span class="ki-priority done"></span>
|
||||
<span class="ki-text">客服部 - 耳机无声(驱动重装)</span>
|
||||
<span class="ki-type ticket">工单</span>
|
||||
<span class="ki-time">1h</span>
|
||||
<div class="ki-avatar ka-blue">客</div>
|
||||
</div>
|
||||
<div class="kanban-stats"><span><span class="dot dot-online"></span> 在线 4</span><span><span class="dot dot-busy"></span> 忙碌 2</span><span><span class="dot dot-offline"></span> 离线 1</span></div>
|
||||
</div>
|
||||
|
||||
<!-- 会话进行中占位提示 -->
|
||||
<div class="post-session-hint">
|
||||
📌 <b>复盘模块待激活</b>:以下模块将在 <b>中栏会话结束后</b> 自动显示——<br>
|
||||
🏷️ 智能标注 · ⭐ 质量反馈 · 📚 知识共享
|
||||
</div>
|
||||
|
||||
<!-- 智能标注(仅会话结束后显示) -->
|
||||
<div class="post-session-card">
|
||||
<div class="psc-title">🏷️ 智能标注</div>
|
||||
<div class="psc-field">
|
||||
<div class="psc-field-label">AI 推荐分类(可修改)</div>
|
||||
<div class="psc-field-value">投屏设备故障 / 无线投屏器驱动</div>
|
||||
<div class="psc-tags">
|
||||
<span class="psc-tag auto">无线投屏器</span>
|
||||
<span class="psc-tag auto">驱动重装</span>
|
||||
<span class="psc-tag auto">现场派单</span>
|
||||
<span class="psc-tag">重复问题</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="psc-field">
|
||||
<div class="psc-field-label">优先级(由会话指标推导)</div>
|
||||
<div class="psc-field-value">🔴 紧急 · 影响会议 · 高管</div>
|
||||
</div>
|
||||
<div class="psc-field">
|
||||
<div class="psc-field-label">解决方案摘要(自动提取)</div>
|
||||
<div class="psc-field-value">HDMI 直连验证 → 驱动重装 → 无效 → 现场派单固件升级。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 质量反馈(仅会话结束后显示) -->
|
||||
<div class="post-session-card">
|
||||
<div class="psc-title">⭐ 质量反馈</div>
|
||||
<div class="psc-field">
|
||||
<div class="psc-field-label">本次服务评分</div>
|
||||
<div class="psc-stars">★★★★<span class="star-off">★</span> <span style="font-size:11px;color:var(--text-muted);">4.0 / 5</span></div>
|
||||
</div>
|
||||
<div class="psc-row"><span class="psc-k">解决时长</span><span class="psc-v">12 分钟</span></div>
|
||||
<div class="psc-row"><span class="psc-k">一次解决率</span><span class="psc-v">是(无需二次进线)</span></div>
|
||||
<div class="psc-row"><span class="psc-k">用户情绪走向</span><span class="psc-v">😟 焦虑 → 🙂 满意</span></div>
|
||||
<div class="psc-field" style="margin-top:8px;">
|
||||
<div class="psc-field-label">改进建议(可填写)</div>
|
||||
<textarea class="psc-textarea" placeholder="记录本次可优化点,如:提前推送投屏器自助文档…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 知识共享(仅会话结束后显示) -->
|
||||
<div class="post-session-card">
|
||||
<div class="psc-title">📚 知识共享</div>
|
||||
<div class="psc-field">
|
||||
<div class="psc-field-label">沉淀为知识库条目</div>
|
||||
<div class="psc-field-value">《无线投屏器驱动重装后仍异常 → 现场派单与固件升级指引》</div>
|
||||
</div>
|
||||
<div class="psc-field">
|
||||
<div class="psc-field-label">可见范围</div>
|
||||
<div class="psc-tags">
|
||||
<span class="psc-tag">全员自助</span>
|
||||
<span class="psc-tag">坐席知识库</span>
|
||||
</div>
|
||||
</div>
|
||||
<label class="psc-check"><input type="checkbox"> 已加入知识库(去重校验通过)</label>
|
||||
<div class="psc-field" style="margin-top:8px;">
|
||||
<div class="psc-field-label">补充备注</div>
|
||||
<textarea class="psc-textarea" placeholder="补充适用场景、关联工单号等…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ============ 全屏流程图浮层 ============ -->
|
||||
<div class="flowchart-fullscreen" id="flowFullscreen">
|
||||
<div class="ff-topbar">
|
||||
<div class="ff-title">🔧 排查步骤 · 完整决策树(全屏)</div>
|
||||
<div class="ff-subtitle">无线投屏器故障 — 当前定位:③ 驱动排查</div>
|
||||
<div class="ff-close" onclick="closeFlowFullscreen()">✕ 关闭(Esc)</div>
|
||||
</div>
|
||||
<div class="ff-stage">
|
||||
<div class="ff-tree">
|
||||
<div class="flow-step">
|
||||
<div class="flow-node done">1</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">确认投屏设备型号</div>
|
||||
<div class="flow-desc">询问用户是无线投屏器 / 会议主机自带投屏 / 第三方软件(如乐播)</div>
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-item active">→ 无线投屏器:进入 HDMI 直连与驱动分支</div>
|
||||
<div class="flow-branch-item">→ 会议主机自带:进入系统投屏服务分支</div>
|
||||
<div class="flow-branch-item">→ 第三方软件:进入网络与权限分支</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-node judge">❓ 判断</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">HDMI 直连是否正常?</div>
|
||||
<div class="flow-desc">请用户将笔记本直连会议室 HDMI 验证,排除无线链路问题</div>
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-item active">正常 → 锁定为无线投屏器驱动/固件问题,进入步骤②</div>
|
||||
<div class="flow-branch-item">仍异常 → 进入显示接口/线缆硬件分支</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-node done">2</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">重装 / 更新投屏驱动</div>
|
||||
<div class="flow-desc">指导用户通过软件管家重装无线投屏驱动,重启投屏器与笔记本</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-node judge">❓ 判断</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">重装驱动后能否投屏?</div>
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-item active">能 → 标记为「已解决」,进入步骤⑤回访</div>
|
||||
<div class="flow-branch-item">不能 → 进入步骤③现场派单</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-node current">3</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">现场派单排查</div>
|
||||
<div class="flow-desc">创建现场工单,指派会议室运维检查投屏器固件、WiFi 信道与供电</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-node">4</div>
|
||||
<div class="flow-step-connector"></div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">固件升级 / 替换设备</div>
|
||||
<div class="flow-desc">若固件过旧则升级;硬件损坏则走备件替换流程</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-step">
|
||||
<div class="flow-node">5</div>
|
||||
<div class="flow-content">
|
||||
<div class="flow-title">24H 后回访确认</div>
|
||||
<div class="flow-desc">创建提醒任务,24小时后通过企业微信确认投屏使用正常</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ff-hint">全屏模式便于研读完整决策树与分支;关闭后回到右栏「排查步骤」原状态(展开/收起态保留)</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ 原型说明按钮 ============ -->
|
||||
<button class="proto-btn" onclick="openProtoNote()">📋 原型说明 v1.4</button>
|
||||
|
||||
<!-- ============ 原型说明对话框 ============ -->
|
||||
<div class="modal-mask" id="protoModal">
|
||||
<div class="modal-box">
|
||||
<span class="close-x" onclick="closeProtoNote()">✕</span>
|
||||
<h3>📋 坐席工作台原型 v1.4 变更说明</h3>
|
||||
<p style="font-size:12px;color:var(--text-secondary);margin-bottom:6px;">基于 v1.3(右栏重组)演进,本次两处调整,请重点评审:</p>
|
||||
|
||||
<h4>① 右栏宽度方案 B(加权 60:40 分栏)</h4>
|
||||
<ul>
|
||||
<li><b>左栏</b>:固定 <span class="kbd">260px</span>(会话列表,不变)。</li>
|
||||
<li><b>中栏(对话区)</b>:<span class="kbd">flex: 1.5</span>,约占总宽 60%(扣除左栏后),坐席主工作面保持主导。</li>
|
||||
<li><b>右栏(辅助面板)</b>:<span class="kbd">flex: 1</span>,约占总宽 40%;并设 <span class="kbd">min 320px / max 560px</span> 防止极端屏 collapsing 或过宽。</li>
|
||||
<li>相对 v1.3 的「右栏固定 320px」,方案 B 让右栏随视口自适应拿到更舒适空间,同时不挤占聊天区。</li>
|
||||
<li>评审对比:A 严格 50/50(聊天被砍半,大屏右栏过宽) / C 仅提升默认到 560px(1920 屏仍偏窄)。</li>
|
||||
</ul>
|
||||
|
||||
<h4>② 排查步骤「全屏查看」流程图浮层</h4>
|
||||
<ul>
|
||||
<li>右栏「排查步骤」头部新增 <span class="kbd">⛶ 全屏查看</span> 按钮(与折叠箭头并列)。</li>
|
||||
<li>点击打开<b>全屏浮层</b>,完整决策树以更大字号铺满视口,便于研读分支与定位当前节点。</li>
|
||||
<li>关闭(✕ / <span class="kbd">Esc</span>)后回到右栏「排查步骤」原状态,展开/收起态不丢失。</li>
|
||||
<li>设计取舍:仅浮层承载流程图(非整栏放大、非仅靠加宽),不影响 AI推荐/待办/复盘模块的常态布局。</li>
|
||||
</ul>
|
||||
|
||||
<h4>评审操作提示</h4>
|
||||
<ul>
|
||||
<li>顶栏「会话状态」开关:模拟<b>会话进行中 / 已结束</b>,验证复盘三模块(智能标注/质量反馈/知识共享)显隐。</li>
|
||||
<li>右下角「📋 原型说明」可随时回看本说明。</li>
|
||||
<li>右上角可切换深/浅色主题。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ========== 主题切换 ========== */
|
||||
function toggleTheme() {
|
||||
const body = document.body;
|
||||
const next = body.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
||||
body.setAttribute('data-theme', next);
|
||||
document.getElementById('themeIcon').textContent = next === 'dark' ? '☀️' : '🌙';
|
||||
document.getElementById('themeLabel').textContent = next === 'dark' ? '浅色' : '深色';
|
||||
}
|
||||
|
||||
/* ========== 排查步骤面板折叠/展开 ========== */
|
||||
function toggleTsPanel() {
|
||||
document.getElementById('tsPanel').classList.toggle('collapsed');
|
||||
}
|
||||
/* ========== 决策树 展开/收起 ========== */
|
||||
function toggleTsFlow() {
|
||||
var fc = document.getElementById('tsFlowchart');
|
||||
var btn = document.getElementById('tsFlowBtn');
|
||||
var collapsed = fc.classList.toggle('collapsed-flow');
|
||||
btn.textContent = collapsed ? '▶ 展开完整排查决策树' : '▼ 收起完整排查决策树';
|
||||
}
|
||||
|
||||
/* ========== 全屏流程图浮层 ========== */
|
||||
function openFlowFullscreen() {
|
||||
document.getElementById('flowFullscreen').classList.add('open');
|
||||
}
|
||||
function closeFlowFullscreen() {
|
||||
document.getElementById('flowFullscreen').classList.remove('open');
|
||||
}
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeFlowFullscreen();
|
||||
closeProtoNote();
|
||||
}
|
||||
});
|
||||
|
||||
/* ========== 会话状态开关(验证「仅会话结束后显示」) ========== */
|
||||
function toggleSessionState() {
|
||||
var ended = document.body.classList.toggle('session-ended');
|
||||
var label = document.getElementById('sessionStateLabel');
|
||||
if (label) label.textContent = ended ? '会话已结束' : '会话进行中';
|
||||
if (ended) {
|
||||
var firstCard = document.querySelector('.post-session-card');
|
||||
if (firstCard && firstCard.scrollIntoView) {
|
||||
setTimeout(function(){ firstCard.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 60);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 原型说明对话框 ========== */
|
||||
function openProtoNote() { document.getElementById('protoModal').classList.add('open'); }
|
||||
function closeProtoNote() { document.getElementById('protoModal').classList.remove('open'); }
|
||||
document.getElementById('protoModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) closeProtoNote();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>坐席端 · 版本链可视化</title>
|
||||
<style>
|
||||
:root {
|
||||
--accent: #3b82f6;
|
||||
--primary: #07C160;
|
||||
--bg-primary: #f5f6fa;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #eef0f5;
|
||||
--text-primary: #1a1a2e;
|
||||
--text-secondary: #555770;
|
||||
--text-tertiary: #9ca3af;
|
||||
--border-color: #e5e7eb;
|
||||
--shadow: 0 1px 4px rgba(0,0,0,0.06);
|
||||
--radius: 12px;
|
||||
--color-employee-bg: #3b82f6;
|
||||
--color-agent-bg: #ffffff;
|
||||
--color-agent-border: #e5e7eb;
|
||||
--color-ai-bg: #e8f5e9;
|
||||
--color-ai-text: #1b5e20;
|
||||
--color-system-text: #9ca3af;
|
||||
--color-system-bg: #eef0f5;
|
||||
--color-warning: #ff9800;
|
||||
--color-danger: #ef4444;
|
||||
--color-success: #22c55e;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #0f172a;
|
||||
--bg-secondary: #1e293b;
|
||||
--bg-tertiary: #162032;
|
||||
--text-primary: #e2e8f0;
|
||||
--text-secondary: #94a3b8;
|
||||
--border-color: #334155;
|
||||
--shadow: 0 1px 4px rgba(0,0,0,0.3);
|
||||
--color-ai-bg: #0d3320;
|
||||
--color-ai-text: #86efac;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { height: 100%; font-family: -apple-system, sans-serif; background: var(--bg-primary); color: var(--text-primary); }
|
||||
|
||||
/* 布局 */
|
||||
.container { display: flex; height: 100vh; }
|
||||
|
||||
/* 左侧边栏 */
|
||||
.sidebar { width: 260px; background: var(--bg-secondary); border-right: 1px solid var(--border-color); display: flex; flex-direction: column; }
|
||||
.sidebar-header { padding: 14px 16px; border-bottom: 1px solid var(--border-color); font-weight: 600; font-size: 14px; }
|
||||
.session-list { flex: 1; overflow-y: auto; }
|
||||
.session-item { padding: 12px 16px; border-bottom: 1px solid var(--border-color); cursor: pointer; }
|
||||
.session-item:hover { background: var(--bg-tertiary); }
|
||||
.session-item.active { background: var(--color-ai-bg); border-left: 3px solid var(--primary); }
|
||||
.session-item .title { font-size: 13px; font-weight: 500; margin-bottom: 4px; }
|
||||
.session-item .meta { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-secondary); }
|
||||
.session-item .status { padding: 2px 6px; border-radius: 8px; font-size: 10px; }
|
||||
.status.paused { background: #fff7e6; color: #d46b08; }
|
||||
.status.corrected { background: #e6f7ff; color: #1890ff; }
|
||||
|
||||
/* 主内容区 */
|
||||
.main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.header { padding: 14px 20px; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
|
||||
.header h1 { font-size: 16px; font-weight: 600; }
|
||||
.header .btn { padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-secondary); font-size: 12px; cursor: pointer; }
|
||||
|
||||
/* 标签页 */
|
||||
.tabs { display: flex; background: var(--bg-secondary); border-bottom: 1px solid var(--border-color); }
|
||||
.tab { padding: 12px 20px; font-size: 13px; color: var(--text-secondary); cursor: pointer; position: relative; }
|
||||
.tab.active { color: var(--primary); font-weight: 500; }
|
||||
.tab.active::after { content: ''; position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: var(--primary); }
|
||||
|
||||
/* 内容区 */
|
||||
.content { flex: 1; display: flex; overflow: hidden; }
|
||||
|
||||
/* 聊天面板 */
|
||||
.chat-panel { flex: 1; border-right: 1px solid var(--border-color); overflow-y: auto; padding: 16px; }
|
||||
.message { display: flex; gap: 10px; margin-bottom: 14px; max-width: 80%; }
|
||||
.message.employee { margin-left: auto; flex-direction: row-reverse; }
|
||||
.message .avatar { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; flex-shrink: 0; }
|
||||
.message.employee .avatar { background: var(--color-employee-bg); color: white; }
|
||||
.message.ai .avatar { background: var(--accent); color: white; }
|
||||
.message .bubble { padding: 8px 12px; border-radius: 12px; font-size: 13px; }
|
||||
.message.employee .bubble { background: var(--color-employee-bg); color: white; }
|
||||
.message.ai .bubble { background: var(--bg-secondary); border: 1px solid var(--border-color); }
|
||||
|
||||
/* 右侧面板 */
|
||||
.info-panel { width: 340px; background: var(--bg-secondary); overflow-y: auto; }
|
||||
.panel-section { padding: 14px; border-bottom: 1px solid var(--border-color); }
|
||||
.panel-section h3 { font-size: 12px; font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
|
||||
|
||||
/* 版本时间线 */
|
||||
.version-timeline { position: relative; padding-left: 16px; }
|
||||
.version-timeline::before { content: ''; position: absolute; left: 4px; top: 0; bottom: 0; width: 2px; background: var(--border-color); }
|
||||
.version-item { position: relative; padding: 8px 0 12px; }
|
||||
.version-item::before { content: ''; position: absolute; left: -14px; top: 10px; width: 8px; height: 8px; border-radius: 50%; background: white; border: 2px solid var(--primary); }
|
||||
.version-item.current::before { background: var(--primary); }
|
||||
.version-item .v-header { display: flex; justify-content: space-between; margin-bottom: 4px; }
|
||||
.version-item .v-tag { font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 500; }
|
||||
.version-item .v-tag.v3 { background: var(--color-ai-bg); color: var(--color-ai-text); }
|
||||
.version-item .v-tag.v2 { background: #fff7e6; color: #d46b08; }
|
||||
.version-item .v-tag.v1 { background: var(--bg-tertiary); color: #999; }
|
||||
.version-item .v-time { font-size: 11px; color: var(--text-secondary); }
|
||||
.version-item .v-content { font-size: 13px; }
|
||||
.version-item .v-change { font-size: 11px; color: var(--color-danger); margin-top: 2px; }
|
||||
|
||||
/* Diff代码块 */
|
||||
.diff-block { background: #1e1e1e; border-radius: 8px; padding: 12px; font-family: 'Consolas', monospace; font-size: 11px; overflow-x: auto; }
|
||||
.diff-line { display: flex; line-height: 1.6; }
|
||||
.diff-line .num { width: 24px; color: #666; text-align: right; padding-right: 8px; }
|
||||
.diff-line .content { color: #d4d4d4; }
|
||||
.diff-line.added { background: rgba(34,197,94,0.15); }
|
||||
.diff-line.added .content { color: #4ec9b0; }
|
||||
.diff-line.removed { background: rgba(239,68,68,0.15); }
|
||||
.diff-line.removed .content { color: #f14c4c; }
|
||||
|
||||
/* 排查步骤 */
|
||||
.diagnosis-steps { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.step-chip { display: flex; align-items: center; gap: 6px; padding: 6px 10px; background: var(--bg-tertiary); border-radius: 16px; font-size: 12px; cursor: pointer; }
|
||||
.step-chip.active { background: var(--color-ai-bg); color: var(--color-ai-text); }
|
||||
.step-chip .num { width: 18px; height: 18px; border-radius: 50%; background: #ddd; display: flex; align-items: center; justify-content: center; font-size: 10px; }
|
||||
.step-chip.active .num { background: var(--primary); color: white; }
|
||||
.step-chip.completed .num { background: var(--primary); color: white; }
|
||||
|
||||
/* 按钮组 */
|
||||
.btn-group { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.btn-group button { flex: 1; padding: 8px; border: 1px solid var(--border-color); background: var(--bg-secondary); border-radius: 6px; font-size: 12px; cursor: pointer; }
|
||||
.btn-group button:hover { background: var(--bg-tertiary); }
|
||||
.btn-group .undo { color: var(--color-warning); }
|
||||
|
||||
/* 统计 */
|
||||
.stats-row { display: flex; gap: 10px; }
|
||||
.stat-card { flex: 1; background: var(--bg-tertiary); border-radius: 8px; padding: 10px; text-align: center; }
|
||||
.stat-card .val { font-size: 20px; font-weight: 600; color: var(--primary); }
|
||||
.stat-card .label { font-size: 11px; color: var(--text-secondary); margin-top: 2px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 左侧会话列表 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">📋 活跃会话</div>
|
||||
<div class="session-list">
|
||||
<div class="session-item">
|
||||
<div class="title">打印机故障排查</div>
|
||||
<div class="meta"><span>14:30</span><span class="status paused">已暂停</span></div>
|
||||
</div>
|
||||
<div class="session-item active">
|
||||
<div class="title">VPN连接问题</div>
|
||||
<div class="meta"><span>14:35</span><span class="status corrected">已更正</span></div>
|
||||
</div>
|
||||
<div class="session-item">
|
||||
<div class="title">账号权限申请</div>
|
||||
<div class="meta"><span>14:20</span><span class="status">进行中</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<div class="main">
|
||||
<div class="header">
|
||||
<h1>VPN连接问题 - 会话详情</h1>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="btn">📤 转接</button>
|
||||
<button class="btn">🔒 关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<div class="tab">对话记录</div>
|
||||
<div class="tab active">版本链</div>
|
||||
<div class="tab">排查步骤</div>
|
||||
<div class="tab">用户信息</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<!-- 聊天面板 -->
|
||||
<div class="chat-panel">
|
||||
<div class="message employee"><div class="avatar">👤</div><div class="bubble">VPN连不上了</div></div>
|
||||
<div class="message ai"><div class="avatar">🤖</div><div class="bubble">好的,我来帮您排查。请问您是电脑端还是手机端?</div></div>
|
||||
<div class="message employee"><div class="avatar">👤</div><div class="bubble">电脑端</div></div>
|
||||
<div class="message employee"><div class="avatar">👤</div><div class="bubble">抱歉,部门写错了,我是财务部的</div></div>
|
||||
<div class="message ai"><div class="avatar">🤖</div><div class="bubble">好的,已更新为财务部。排查步骤已重新计算。</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧面板 -->
|
||||
<div class="info-panel">
|
||||
<div class="panel-section">
|
||||
<h3>📜 版本链</h3>
|
||||
<div class="version-timeline">
|
||||
<div class="version-item current">
|
||||
<div class="v-header"><span class="v-tag v3">V3</span><span class="v-time">14:35</span></div>
|
||||
<div class="v-content">部门:财务部</div>
|
||||
<div class="v-change">← 刚刚更正</div>
|
||||
</div>
|
||||
<div class="version-item">
|
||||
<div class="v-header"><span class="v-tag v2">V2</span><span class="v-time">14:32</span></div>
|
||||
<div class="v-content">部门:技术部</div>
|
||||
</div>
|
||||
<div class="version-item">
|
||||
<div class="v-header"><span class="v-tag v1">V1</span><span class="v-time">14:30</span></div>
|
||||
<div class="v-content">初始收集</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="undo">↩️ 撤销更改</button>
|
||||
<button>🔄 恢复此版本</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<h3>📊 V2 → V3 Diff</h3>
|
||||
<div class="diff-block">
|
||||
<div class="diff-line"><span class="num">1</span><span class="content"> "department": "技术部",</span></div>
|
||||
<div class="diff-line removed"><span class="num">2</span><span class="content">- "department": "技术部",</span></div>
|
||||
<div class="diff-line added"><span class="num">2</span><span class="content">+ "department": "财务部",</span></div>
|
||||
<div class="diff-line"><span class="num">3</span><span class="content"> "device": "电脑端",</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<h3>🔧 排查步骤</h3>
|
||||
<div class="diagnosis-steps">
|
||||
<div class="step-chip completed"><span class="num">✓</span>确认端类型</div>
|
||||
<div class="step-chip active"><span class="num">2</span>确认部门</div>
|
||||
<div class="step-chip"><span class="num">3</span>检测网络权限</div>
|
||||
<div class="step-chip"><span class="num">4</span>检查客户端</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-section">
|
||||
<h3>📈 统计</h3>
|
||||
<div class="stats-row">
|
||||
<div class="stat-card"><div class="val">3</div><div class="label">版本数</div></div>
|
||||
<div class="stat-card"><div class="val">1</div><div class="label">更正次数</div></div>
|
||||
<div class="stat-card"><div class="val">2</div><div class="label">触发重算</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,653 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>坐席端 - 群聊参与者展开/缩略双模式原型</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/* === 左侧会话列表(模拟) === */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar-header {
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
.conv-item {
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.conv-item:hover { background: #f5f5f5; }
|
||||
.conv-item.active { background: #e6f7ff; border-left: 3px solid #07C160; }
|
||||
.conv-avatar {
|
||||
width: 40px; height: 40px; border-radius: 4px;
|
||||
background: #ccc; flex-shrink: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: #fff; font-size: 16px;
|
||||
}
|
||||
.conv-info { flex: 1; min-width: 0; }
|
||||
.conv-name { font-size: 14px; color: #333; font-weight: 500; }
|
||||
.conv-preview { font-size: 12px; color: #999; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* === 右侧聊天区域 === */
|
||||
.chat-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f5f5;
|
||||
min-width: 0;
|
||||
}
|
||||
.chat-header {
|
||||
height: 56px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
gap: 12px;
|
||||
}
|
||||
.chat-title { font-size: 16px; font-weight: 600; color: #333; }
|
||||
.chat-subtitle { font-size: 12px; color: #999; }
|
||||
|
||||
/* === 参与者缩略条 === */
|
||||
.participant-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 20px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.participant-bar:hover { background: #f0f0f0; }
|
||||
|
||||
.bar-avatars {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 头像基础样式 */
|
||||
.avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
border: 1.5px solid transparent;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
.avatar + .avatar { margin-left: -4px; }
|
||||
|
||||
/* 角色边框色 */
|
||||
.avatar.role-primary { border-color: #3b82f6; }
|
||||
.avatar.role-collaborator { border-color: #07C160; }
|
||||
.avatar.role-owner { border-color: #FF9800; }
|
||||
.avatar.role-invitee { border-color: transparent; }
|
||||
|
||||
/* 自己高亮环 */
|
||||
.avatar.is-self {
|
||||
box-shadow: 0 0 0 2px #07C160;
|
||||
transform: scale(1.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 超员 +N */
|
||||
.avatar-more {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: -4px;
|
||||
border: 1.5px solid #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 待加入红点 */
|
||||
.avatar-wrap { position: relative; display: inline-flex; }
|
||||
.pending-dot {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: -1px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #ee0a24;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-left: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.bar-expand-icon {
|
||||
font-size: 10px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.bar-expand-icon.expanded { transform: rotate(180deg); }
|
||||
|
||||
/* === 就地展开面板 === */
|
||||
.expanded-panel {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.2s ease, opacity 0.2s ease;
|
||||
opacity: 0;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
.expanded-panel.show {
|
||||
max-height: 150px;
|
||||
overflow-y: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.expanded-panel-inner {
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
/* 参与者列表项 */
|
||||
.participant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 0;
|
||||
}
|
||||
.participant-item:hover { background: #f9f9f9; }
|
||||
|
||||
.participant-item .avatar-md {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 11px;
|
||||
border-width: 1.5px;
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.participant-name {
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 120px;
|
||||
}
|
||||
|
||||
/* 角色标签 */
|
||||
.role-tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 3px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.role-tag.primary { background: #3b82f6; color: #fff; }
|
||||
.role-tag.collaborator { background: #07C160; color: #fff; }
|
||||
.role-tag.owner { background: #FF9800; color: #fff; }
|
||||
.role-tag.invitee { background: #f0f0f0; color: #666; }
|
||||
|
||||
.participant-dept {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 移除图标 */
|
||||
.remove-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #ee0a24;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
border-radius: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.participant-item:hover .remove-icon { opacity: 1; }
|
||||
.remove-icon:hover { background: #fee; }
|
||||
|
||||
/* 消息列表区域 */
|
||||
.message-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 20px;
|
||||
min-height: 200px;
|
||||
}
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
gap: 10px;
|
||||
}
|
||||
.msg-avatar {
|
||||
width: 32px; height: 32px; border-radius: 4px;
|
||||
flex-shrink: 0; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; color: #fff;
|
||||
}
|
||||
.msg-content { max-width: 60%; }
|
||||
.msg-name { font-size: 11px; color: #999; margin-bottom: 2px; }
|
||||
.msg-bubble {
|
||||
background: #fff; padding: 8px 12px; border-radius: 0 8px 8px 8px;
|
||||
font-size: 13px; color: #333; display: inline-block;
|
||||
}
|
||||
|
||||
/* 底部输入区 */
|
||||
.input-area {
|
||||
height: 120px;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.input-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.input-toolbar button {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.input-textbox {
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
.input-textbox:focus { border-color: #07C160; }
|
||||
|
||||
/* 控制面板 */
|
||||
.control-panel {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 200;
|
||||
background: rgba(0,0,0,0.8);
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.control-panel button {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background: #07C160;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 10px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.control-panel button:hover { opacity: 0.85; }
|
||||
.control-panel .info {
|
||||
font-size: 11px;
|
||||
color: #aaa;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
/* P1: expandedStateMap 按会话ID记忆 */
|
||||
.panel-section-title {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
padding: 4px 0 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="control-panel">
|
||||
<button onclick="toggleExpand()">点击缩略条展开/收起</button>
|
||||
<button onclick="toggleScene()">切换场景(3人/7人/10人)</button>
|
||||
<div class="info" id="sceneInfo">场景: 3人</div>
|
||||
</div>
|
||||
|
||||
<!-- 左侧会话列表 -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">会话列表</div>
|
||||
<div class="conv-item active">
|
||||
<div class="conv-avatar" style="background:#FF9800">李</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">李四 · 网络故障</div>
|
||||
<div class="conv-preview">王五: 我看了一下交换机日志...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conv-item">
|
||||
<div class="conv-avatar" style="background:#3b82f6">陈</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">陈六 · 电脑蓝屏</div>
|
||||
<div class="conv-preview">已重启,问题解决</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="conv-item">
|
||||
<div class="conv-avatar" style="background:#07C160">周</div>
|
||||
<div class="conv-info">
|
||||
<div class="conv-name">周九 · 打印机问题</div>
|
||||
<div class="conv-preview">好的,我来看看</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧聊天区域 -->
|
||||
<div class="chat-area">
|
||||
<div class="chat-header">
|
||||
<span class="chat-title">李四 · 网络故障</span>
|
||||
<span class="chat-subtitle" id="convStatus">3人参与</span>
|
||||
</div>
|
||||
|
||||
<!-- 参与者缩略条 -->
|
||||
<div class="participant-bar" onclick="toggleExpand()" id="bar">
|
||||
<div class="bar-avatars" id="barAvatars"></div>
|
||||
<span class="bar-label" id="barLabel">
|
||||
<span id="barText">3位参与者</span>
|
||||
<span class="bar-expand-icon" id="expandIcon">▼</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 就地展开面板 -->
|
||||
<div class="expanded-panel" id="panel">
|
||||
<div class="expanded-panel-inner" id="panelBody"></div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div class="message-list">
|
||||
<div class="message-item">
|
||||
<div class="msg-avatar" style="background:#3b82f6">张</div>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name">张明(主责坐席)</div>
|
||||
<div class="msg-bubble">您好,网络问题我来协助处理。已邀请网络组同事加入。</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-item">
|
||||
<div class="msg-avatar" style="background:#FF9800">李</div>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name">李四(发起人)</div>
|
||||
<div class="msg-bubble">好的谢谢,我们网段的电脑都上不了网</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-item">
|
||||
<div class="msg-avatar" style="background:#07C160">王</div>
|
||||
<div class="msg-content">
|
||||
<div class="msg-name">王五(被邀请人)</div>
|
||||
<div class="msg-bubble">交换机端口有异常告警,正在排查中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部输入区 -->
|
||||
<div class="input-area">
|
||||
<div class="input-toolbar">
|
||||
<button>快速回复</button>
|
||||
<button>转接</button>
|
||||
<button>邀请</button>
|
||||
<button>摇人</button>
|
||||
</div>
|
||||
<textarea class="input-textbox" placeholder="输入回复内容..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 角色配置
|
||||
const ROLE_COLORS = {
|
||||
primary_agent: '#3b82f6',
|
||||
collaborator: '#07C160',
|
||||
owner: '#FF9800',
|
||||
invitee: 'transparent'
|
||||
};
|
||||
const ROLE_LABELS = {
|
||||
primary_agent: '主责',
|
||||
collaborator: '协作',
|
||||
owner: '发起人',
|
||||
invitee: '被邀请'
|
||||
};
|
||||
const ROLE_BG = {
|
||||
primary_agent: '#3b82f6',
|
||||
collaborator: '#07C160',
|
||||
owner: '#FF9800',
|
||||
invitee: '#bbb'
|
||||
};
|
||||
|
||||
const MAX_AVATARS = 6; // 坐席端超员阈值
|
||||
const CURRENT_AGENT_ID = '1'; // 当前坐席 = 张明(主责)
|
||||
|
||||
// 场景数据
|
||||
const scenes = [
|
||||
// 3人场景
|
||||
[
|
||||
{ id: '1', name: '张明', avatar: '', dept: 'IT支持组', role: 'primary_agent', joined: true, isSelf: true, joinedAt: '2026-07-10T10:00:00' },
|
||||
{ id: '2', name: '李四', avatar: '', dept: '财务部', role: 'owner', joined: true, isSelf: false, joinedAt: '2026-07-10T10:01:00' },
|
||||
{ id: '3', name: '王五', avatar: '', dept: '网络运维组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:05:00' }
|
||||
],
|
||||
// 7人场景
|
||||
[
|
||||
{ id: '1', name: '张明', avatar: '', dept: 'IT支持组', role: 'primary_agent', joined: true, isSelf: true, joinedAt: '2026-07-10T10:00:00' },
|
||||
{ id: '2', name: '李四', avatar: '', dept: '财务部', role: 'owner', joined: true, isSelf: false, joinedAt: '2026-07-10T10:01:00' },
|
||||
{ id: '10', name: '陈十', avatar: '', dept: 'IT支持组', role: 'collaborator', joined: true, isSelf: false, joinedAt: '2026-07-10T10:02:00' },
|
||||
{ id: '3', name: '王五', avatar: '', dept: '网络运维组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:05:00' },
|
||||
{ id: '4', name: '赵六', avatar: '', dept: '系统组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:06:00' },
|
||||
{ id: '5', name: '钱七', avatar: '', dept: '安全组', role: 'invitee', joined: false, isSelf: false, joinedAt: null },
|
||||
{ id: '6', name: '孙八', avatar: '', dept: '桌面组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:08:00' }
|
||||
],
|
||||
// 10人场景(超员)
|
||||
[
|
||||
{ id: '1', name: '张明', avatar: '', dept: 'IT支持组', role: 'primary_agent', joined: true, isSelf: true, joinedAt: '2026-07-10T10:00:00' },
|
||||
{ id: '2', name: '李四', avatar: '', dept: '财务部', role: 'owner', joined: true, isSelf: false, joinedAt: '2026-07-10T10:01:00' },
|
||||
{ id: '10', name: '陈十', avatar: '', dept: 'IT支持组', role: 'collaborator', joined: true, isSelf: false, joinedAt: '2026-07-10T10:02:00' },
|
||||
{ id: '11', name: '郑十一', avatar: '', dept: '运维组', role: 'collaborator', joined: true, isSelf: false, joinedAt: '2026-07-10T10:03:00' },
|
||||
{ id: '3', name: '王五', avatar: '', dept: '网络运维组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:05:00' },
|
||||
{ id: '4', name: '赵六', avatar: '', dept: '系统组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:06:00' },
|
||||
{ id: '5', name: '钱七', avatar: '', dept: '安全组', role: 'invitee', joined: false, isSelf: false, joinedAt: null },
|
||||
{ id: '6', name: '孙八', avatar: '', dept: '桌面组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:08:00' },
|
||||
{ id: '7', name: '周九', avatar: '', dept: '数据库组', role: 'invitee', joined: true, isSelf: false, joinedAt: '2026-07-10T10:09:00' },
|
||||
{ id: '8', name: '吴十', avatar: '', dept: '中间件组', role: 'invitee', joined: false, isSelf: false, joinedAt: null }
|
||||
]
|
||||
];
|
||||
|
||||
let currentScene = 0;
|
||||
let participants = [];
|
||||
let isExpanded = false;
|
||||
// P1: expandedStateMap 按会话ID记忆展开状态
|
||||
const expandedStateMap = {};
|
||||
|
||||
function getLastChar(name) {
|
||||
return name ? name.charAt(name.length - 1) : '?';
|
||||
}
|
||||
|
||||
function sortParticipants(list) {
|
||||
// 排列:主责 → 发起人 → 协作坐席 → 被邀请人(按joinedAt, 已加入在前)
|
||||
return [...list].sort((a, b) => {
|
||||
const order = { primary_agent: 0, owner: 1, collaborator: 2, invitee: 3 };
|
||||
if (order[a.role] !== order[b.role]) return order[a.role] - order[b.role];
|
||||
// 已加入在前
|
||||
if (a.joined !== b.joined) return a.joined ? -1 : 1;
|
||||
if (a.joinedAt && b.joinedAt) return new Date(a.joinedAt) - new Date(b.joinedAt);
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function renderBar() {
|
||||
const container = document.getElementById('barAvatars');
|
||||
const text = document.getElementById('barText');
|
||||
const status = document.getElementById('convStatus');
|
||||
container.innerHTML = '';
|
||||
|
||||
const sorted = sortParticipants(participants);
|
||||
const showCount = Math.min(sorted.length, MAX_AVATARS);
|
||||
const hasPending = sorted.some(p => !p.joined);
|
||||
|
||||
for (let i = 0; i < showCount; i++) {
|
||||
const p = sorted[i];
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'avatar-wrap';
|
||||
|
||||
const av = document.createElement('div');
|
||||
av.className = `avatar role-${p.role}`;
|
||||
if (p.isSelf) av.classList.add('is-self');
|
||||
av.style.background = ROLE_BG[p.role];
|
||||
av.textContent = getLastChar(p.name);
|
||||
|
||||
if (!p.joined) {
|
||||
const dot = document.createElement('div');
|
||||
dot.className = 'pending-dot';
|
||||
wrap.appendChild(dot);
|
||||
}
|
||||
|
||||
wrap.appendChild(av);
|
||||
container.appendChild(wrap);
|
||||
}
|
||||
|
||||
if (sorted.length > MAX_AVATARS) {
|
||||
const more = document.createElement('div');
|
||||
more.className = 'avatar-more';
|
||||
more.textContent = `+${sorted.length - MAX_AVATARS}`;
|
||||
container.appendChild(more);
|
||||
}
|
||||
|
||||
text.textContent = `${participants.length}位参与者${hasPending ? ' · 有待加入' : ''}`;
|
||||
status.textContent = `${participants.length}人参与`;
|
||||
}
|
||||
|
||||
function renderPanel() {
|
||||
const body = document.getElementById('panelBody');
|
||||
body.innerHTML = '';
|
||||
|
||||
const sorted = sortParticipants(participants);
|
||||
const isPrimaryAgent = participants.some(p => p.id === CURRENT_AGENT_ID && p.role === 'primary_agent');
|
||||
|
||||
sorted.forEach(p => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'participant-item';
|
||||
|
||||
const av = document.createElement('div');
|
||||
av.className = `avatar avatar-md role-${p.role}`;
|
||||
if (p.isSelf) av.classList.add('is-self');
|
||||
av.style.background = ROLE_BG[p.role];
|
||||
av.textContent = getLastChar(p.name);
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.className = 'participant-info';
|
||||
|
||||
const name = document.createElement('span');
|
||||
name.className = 'participant-name';
|
||||
name.textContent = p.name + (p.isSelf ? '(我)' : '');
|
||||
name.title = p.name; // P1: tooltip仅截断时显示
|
||||
info.appendChild(name);
|
||||
|
||||
const tag = document.createElement('span');
|
||||
tag.className = `role-tag ${p.role}`;
|
||||
tag.textContent = ROLE_LABELS[p.role];
|
||||
info.appendChild(tag);
|
||||
|
||||
const dept = document.createElement('span');
|
||||
dept.className = 'participant-dept';
|
||||
dept.textContent = p.dept + (p.joined ? '' : ' · 待加入');
|
||||
info.appendChild(dept);
|
||||
|
||||
item.appendChild(av);
|
||||
item.appendChild(info);
|
||||
|
||||
// P1: 移除图标仅主责坐席 + invitee角色显示
|
||||
if (isPrimaryAgent && p.role === 'invitee') {
|
||||
const remove = document.createElement('div');
|
||||
remove.className = 'remove-icon';
|
||||
remove.textContent = '×';
|
||||
remove.title = '移除参与者';
|
||||
remove.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
if (confirm(`确认移除 ${p.name}?`)) {
|
||||
alert('已移除(模拟)');
|
||||
}
|
||||
};
|
||||
item.appendChild(remove);
|
||||
}
|
||||
|
||||
body.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpanded = !isExpanded;
|
||||
const panel = document.getElementById('panel');
|
||||
const icon = document.getElementById('expandIcon');
|
||||
|
||||
if (isExpanded) {
|
||||
panel.classList.add('show');
|
||||
icon.classList.add('expanded');
|
||||
renderPanel();
|
||||
// P1: 记忆展开状态
|
||||
expandedStateMap['conv-1'] = true;
|
||||
} else {
|
||||
panel.classList.remove('show');
|
||||
icon.classList.remove('expanded');
|
||||
expandedStateMap['conv-1'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleScene() {
|
||||
currentScene = (currentScene + 1) % scenes.length;
|
||||
participants = JSON.parse(JSON.stringify(scenes[currentScene]));
|
||||
document.getElementById('sceneInfo').textContent = `场景: ${participants.length}人`;
|
||||
renderBar();
|
||||
if (isExpanded) renderPanel();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
participants = JSON.parse(JSON.stringify(scenes[0]));
|
||||
renderBar();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,620 @@
|
||||
# 坐席端 AI 辅助消息框 — 产品需求文档 (PRD)
|
||||
|
||||
> **版本**: v1.0
|
||||
> **日期**: 2026-07-11
|
||||
> **作者**: 宋献
|
||||
> **状态**: 需求已确认,待开发
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
### 1.1 现状分析
|
||||
|
||||
坐席端已具备较完善的 AI 辅助基础设施:
|
||||
|
||||
| 已有功能 | 模式 | 状态 |
|
||||
|----------|------|------|
|
||||
| AI 草稿生成 | 点击右栏按钮 → 生成完整回复 → 采纳 | 已对接 Dify API |
|
||||
| AI 草稿气泡 | 消息流内联气泡 → 采纳/编辑/忽略 | 已对接后端 |
|
||||
| 快速回复模板 | 三层渐进导航 + 变量替换 | 本地静态数据 |
|
||||
| AI 推荐回复(内联) | 3 张卡片 + Ctrl+1/2/3 | Mock 数据,未对接 API |
|
||||
| 会话摘要 + 智能标注 | 结单时自动生成 | 已对接 Dify API |
|
||||
|
||||
**核心差距**:现有功能均为"生成完整草稿 → 坐席采纳"模式,缺失"输入过程中实时辅助 + 发送前精修"模式。
|
||||
|
||||
### 1.2 行业标杆
|
||||
|
||||
- **Freshdesk Freddy AI**:语气增强(更正式/更随意/专业/友好/随性),选中文字后一键改写
|
||||
- **智齿科技**:AI 扩写润色 + 风格设置(友好口吻/专业口吻)
|
||||
- **Yellow.ai**:Tone Changer + Sentiment Detection + AI Copilot
|
||||
|
||||
### 1.3 目标
|
||||
|
||||
在现有 ReplyBox 输入框中新增 4 项 AI 辅助功能,补齐"输入过程中实时辅助 + 发送前精修"能力:
|
||||
|
||||
1. **实时自动补齐** — 输入停顿时显示灰色幽灵文字,Tab 键接受
|
||||
2. **语气调整** — 选中文字后选择专业/友好/简洁风格,一键改写
|
||||
3. **文字润色** — 扩写/压缩/纠错,弹出精修面板确认后替换
|
||||
4. **智能改写** — 基于对话上下文 + 知识库,生成 3 个备选版本
|
||||
|
||||
### 1.4 设计原则
|
||||
|
||||
- **共存不替代**:新增功能与现有草稿生成互补,不替代
|
||||
- **渐进增强**:坐席可选择使用,不强制改变现有工作流
|
||||
- **低延迟感知**:补齐建议 < 1.5s,语气/润色/改写 < 3s
|
||||
- **可降级**:AI 服务不可用时不影响正常输入和发送
|
||||
|
||||
---
|
||||
|
||||
## 2. 功能详细需求
|
||||
|
||||
### 2.1 实时自动补齐
|
||||
|
||||
#### 2.1.1 交互流程
|
||||
|
||||
```
|
||||
坐席输入文字 → 停顿 > 0.8s → 调用 AI 补齐 API → 光标位置显示灰色幽灵文字
|
||||
├─ Tab 键 → 接受补齐,文字变为正常颜色
|
||||
├─ 继续输入 → 忽略补齐,重新触发
|
||||
└─ Esc 键 → 清除补齐建议
|
||||
```
|
||||
|
||||
#### 2.1.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 输入停顿 > 800ms,且输入框内容 > 5 字符 |
|
||||
| 请求防抖 | debounce 800ms,输入中取消上一个请求 |
|
||||
| 显示方式 | 光标后方灰色斜体文字(ghost text) |
|
||||
| 补齐长度 | 1-2 个短句(不超过 80 字符) |
|
||||
| 接受方式 | Tab 键接受全部;Shift+Tab 接受一个词 |
|
||||
| 忽略方式 | 继续输入自动忽略;Esc 清除 |
|
||||
| 上下文 | 携带最近 5 条对话消息 + 当前输入内容 |
|
||||
| 并发控制 | 同一时刻只保留最新的补齐请求 |
|
||||
| 加载状态 | 幽灵文字位置显示 3 个点动画(typing indicator) |
|
||||
| 超时处理 | 1.5s 未返回则取消,不显示 |
|
||||
|
||||
#### 2.1.3 前端实现要点
|
||||
|
||||
- 在 `<textarea>` 上方覆盖一层 `<div>` 渲染幽灵文字,通过计算光标位置定位
|
||||
- 或使用 `contenteditable` div 替换 textarea(更灵活但改动较大)
|
||||
- **推荐方案**:保持 `<textarea>` 不变,在下方独立区域显示补齐建议条(简化实现,降低风险)
|
||||
|
||||
> **决策点**:纯内联幽灵文字需要精确计算光标坐标,实现复杂度高。若需降低首版复杂度,可采用"输入框下方建议条"作为过渡方案。当前需求确认为内联幽灵文字。
|
||||
|
||||
#### 2.1.4 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/autocomplete
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"current_text": "张工您好,您反馈的VPN连接问题我已经",
|
||||
"cursor_position": 22,
|
||||
"max_length": 80
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"completion": "正在查看相关工单记录,请稍候。",
|
||||
"confidence": 0.85
|
||||
}
|
||||
```
|
||||
|
||||
**Dify System Prompt**:
|
||||
```
|
||||
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
|
||||
要求:
|
||||
1. 补齐内容自然衔接当前文字,不要重复已有内容
|
||||
2. 长度控制在1-2个短句,不超过80字
|
||||
3. 语气专业、简洁,符合IT服务规范
|
||||
4. 只返回补齐的文字,不要加引号或其他标记
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 语气调整
|
||||
|
||||
#### 2.2.1 交互流程
|
||||
|
||||
```
|
||||
坐席选中已输入文字 → 点击工具栏"语气"按钮 → 弹出语气选择菜单
|
||||
├─ 专业 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
|
||||
├─ 友好 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
|
||||
└─ 简洁 → 调用 AI 改写 → 原文/改写文对比 → 确认替换
|
||||
```
|
||||
|
||||
#### 2.2.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 选中输入框中的文字(≥ 5 字符),点击工具栏"语气"按钮 |
|
||||
| 语气选项 | 专业 / 友好 / 简洁(3 种) |
|
||||
| 显示方式 | 弹出浮层菜单,3 个选项纵向排列 |
|
||||
| 改写显示 | 浮层中显示"原文 → 改写文"对比 |
|
||||
| 确认方式 | 点击"替换"按钮,用改写文替换选中文字 |
|
||||
| 取消方式 | 点击外部区域或 Esc 关闭浮层 |
|
||||
| 上下文 | 携带对话上下文 + 选中文字 + 当前完整输入内容 |
|
||||
| 超时处理 | 3s 未返回显示重试按钮 |
|
||||
|
||||
#### 2.2.3 语气定义
|
||||
|
||||
| 语气 | 定义 | 示例 |
|
||||
|------|------|------|
|
||||
| **专业** | 使用准确的技术术语,结构化表达,去除口语化内容 | "经排查,您的VPN连接异常是由本地网络DNS解析超时引起,建议执行以下操作:" |
|
||||
| **友好** | 适当增加问候和关心语,语气更亲和 | "张工您好~VPN连不上确实挺急的,我帮您看了下,可能是DNS解析有点慢,咱们试试这样操作:" |
|
||||
| **简洁** | 去除冗余,直奔主题,控制字数 | "VPN异常原因:DNS超时。操作:1. 刷新DNS 2. 重连VPN" |
|
||||
|
||||
#### 2.2.4 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/tone-adjust
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"selected_text": "VPN连接问题我已经在看了,你别急啊,我查一下",
|
||||
"full_text": "张工您好,VPN连接问题我已经在看了,你别急啊,我查一下",
|
||||
"tone": "professional",
|
||||
"conversation_context": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"rewritten_text": "经排查,您的VPN连接异常由本地网络DNS解析超时引起,建议执行以下操作:",
|
||||
"tone": "professional",
|
||||
"changes_summary": "去除口语化表达,增加技术术语,结构化排版"
|
||||
}
|
||||
```
|
||||
|
||||
**Dify System Prompt(按语气动态选择)**:
|
||||
```
|
||||
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
|
||||
要求:
|
||||
1. 保持原意不变,只调整语气和表达方式
|
||||
2. 改写后的文字长度与原文相近(±30%)
|
||||
3. 符合IT服务坐席的专业规范
|
||||
4. 只返回改写后的文字,不要加引号或解释
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 文字润色
|
||||
|
||||
#### 2.3.1 交互流程
|
||||
|
||||
```
|
||||
坐席点击工具栏"润色"按钮 → 弹出精修面板
|
||||
├─ 扩写 → AI 扩展内容,增加细节和步骤
|
||||
├─ 压缩 → AI 精简内容,去除冗余
|
||||
└─ 纠错 → AI 检查并修正语法/错别字
|
||||
→ 显示结果 → 确认替换 / 取消
|
||||
```
|
||||
|
||||
#### 2.3.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 点击工具栏"润色"按钮(无需选中文字,对全部内容操作) |
|
||||
| 操作选项 | 扩写 / 压缩 / 纠错(3 种) |
|
||||
| 显示方式 | 弹出精修面板,左右对比(原文 | 结果) |
|
||||
| 确认方式 | 点击"替换全部"用结果替换输入框全部内容 |
|
||||
| 部分采纳 | 支持在结果区域手动编辑后再替换 |
|
||||
| 超时处理 | 3s 未返回显示重试按钮 |
|
||||
|
||||
#### 2.3.3 操作定义
|
||||
|
||||
| 操作 | 定义 |
|
||||
|------|------|
|
||||
| **扩写** | 在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整 |
|
||||
| **压缩** | 精简表达,去除重复和冗余,保留核心信息,控制字数 |
|
||||
| **纠错** | 检查并修正错别字、语法错误、标点符号、格式问题 |
|
||||
|
||||
#### 2.3.4 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/polish
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"text": "你先试试重启一下vpn客户端 然后看看能不能连上来",
|
||||
"action": "expand",
|
||||
"conversation_context": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"polished_text": "建议您按以下步骤操作:\n1. 完全退出VPN客户端(不只是断开,需要关闭进程)\n2. 重新启动VPN客户端\n3. 输入账号密码重新连接\n4. 如仍无法连接,请截图错误信息发给我",
|
||||
"action": "expand",
|
||||
"changes_summary": "增加了操作步骤细化、注意事项和兜底方案"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.4 智能改写
|
||||
|
||||
#### 2.4.1 交互流程
|
||||
|
||||
```
|
||||
坐席点击工具栏"改写"按钮 → AI 基于对话上下文+知识库生成3个备选版本
|
||||
→ 弹出选择面板,展示3个版本
|
||||
→ 坐席选择一个版本 → 替换输入框内容 / 追加到输入框
|
||||
```
|
||||
|
||||
#### 2.4.2 功能规格
|
||||
|
||||
| 项目 | 规格 |
|
||||
|------|------|
|
||||
| 触发条件 | 点击工具栏"改写"按钮 |
|
||||
| 生成数量 | 3 个备选版本 |
|
||||
| 版本差异 | 版本1=简洁直接 / 版本2=详细带步骤 / 版本3=带知识库引用 |
|
||||
| 显示方式 | 弹出面板,3 个版本纵向排列,可滚动预览 |
|
||||
| 选择方式 | 点击版本卡片 → "替换"或"追加" |
|
||||
| 上下文 | 携带最近 10 条对话消息 + 知识库检索结果 |
|
||||
| 超时处理 | 5s 未返回显示重试按钮 |
|
||||
| 与草稿生成关系 | 草稿生成是右栏入口;改写是输入框内入口,更轻量 |
|
||||
|
||||
#### 2.4.3 后端 API
|
||||
|
||||
```
|
||||
POST /api/conversations/{conversation_id}/wingman/rewrite
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"current_text": "",
|
||||
"generate_count": 3,
|
||||
"include_knowledge": true
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"versions": [
|
||||
{
|
||||
"text": "VPN连接异常,建议重启客户端后重试。",
|
||||
"style": "简洁直接",
|
||||
"source": "对话上下文"
|
||||
},
|
||||
{
|
||||
"text": "VPN连接问题处理步骤:\n1. 退出VPN客户端\n2. 重启客户端\n3. 重新连接\n如仍有问题请截图反馈",
|
||||
"style": "详细带步骤",
|
||||
"source": "对话上下文"
|
||||
},
|
||||
{
|
||||
"text": "根据知识库文档《VPN故障排查指南》,DNS解析超时是常见原因。建议:\n1. 执行 ipconfig /flushdns\n2. 重启VPN客户端\n3. 重连\n参考文档:[KB-VPN-001]",
|
||||
"style": "带知识库引用",
|
||||
"source": "RAGFlow知识库"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. UI 设计规格
|
||||
|
||||
### 3.1 工具栏布局
|
||||
|
||||
在 ReplyBox.vue 现有工具栏中新增 4 个 AI 辅助按钮:
|
||||
|
||||
```
|
||||
现有工具栏: [表情] [文件] [截图] [拍照] [语音] [闪电快捷回复] [邀请] [发送]
|
||||
新增区域: [补齐] [语气] [润色] [改写] [发送]
|
||||
```
|
||||
|
||||
- AI 辅助按钮使用 Element Plus 的 `el-tooltip` 悬浮提示
|
||||
- 按钮图标使用简洁线性图标,AI 相关按钮使用统一的紫色系(#534AB7)以区分
|
||||
- 按钮间距 8px,与现有按钮风格一致
|
||||
|
||||
### 3.2 幽灵文字样式
|
||||
|
||||
```css
|
||||
.ghost-text {
|
||||
color: #B4B2A9; /* 灰色 */
|
||||
font-style: italic; /* 斜体 */
|
||||
pointer-events: none; /* 不可点击 */
|
||||
user-select: none; /* 不可选中 */
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 语气选择浮层
|
||||
|
||||
```
|
||||
+-------------------------------+
|
||||
| 选择语气 |
|
||||
+-------------------------------+
|
||||
| [专业] 使用准确术语,结构化 |
|
||||
| [友好] 增加问候,语气亲和 |
|
||||
| [简洁] 去除冗余,直奔主题 |
|
||||
+-------------------------------+
|
||||
| 原文: VPN连不上别急我看看... |
|
||||
| 改写: 经排查,VPN连接异常... |
|
||||
+-------------------------------+
|
||||
| [替换] [取消] |
|
||||
+-------------------------------+
|
||||
```
|
||||
|
||||
### 3.4 润色精修面板
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| 文字润色 [扩写] [压缩] [纠错] |
|
||||
+------------------------------------------+
|
||||
| 原文 | 结果(可编辑) |
|
||||
| ---------- | ---------- |
|
||||
| 你先试试重启 | 建议您按以下步骤: |
|
||||
| 一下vpn客户端 | 1. 退出VPN客户端 |
|
||||
| 然后看看能不能 | 2. 重新启动 |
|
||||
| 连上来 | 3. 重新连接 |
|
||||
| | 4. 如仍不行请截图 |
|
||||
+------------------------------------------+
|
||||
| [替换全部] [取消] |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
### 3.5 改写版本选择面板
|
||||
|
||||
```
|
||||
+------------------------------------------+
|
||||
| 智能改写 — 3 个备选版本 |
|
||||
+------------------------------------------+
|
||||
| 版本1: 简洁直接 |
|
||||
| VPN连接异常,建议重启客户端后重试。 |
|
||||
| [替换] [追加] |
|
||||
+------------------------------------------+
|
||||
| 版本2: 详细带步骤 |
|
||||
| VPN连接问题处理步骤: |
|
||||
| 1. 退出VPN客户端 |
|
||||
| 2. 重启客户端 |
|
||||
| ... |
|
||||
| [替换] [追加] |
|
||||
+------------------------------------------+
|
||||
| 版本3: 带知识库引用 |
|
||||
| 根据知识库文档《VPN故障排查》... |
|
||||
| [替换] [追加] |
|
||||
+------------------------------------------+
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 技术实现方案
|
||||
|
||||
### 4.1 后端
|
||||
|
||||
#### 4.1.1 新增 API 端点
|
||||
|
||||
在 `backend/app/api/wingman.py` 中新增 4 个端点:
|
||||
|
||||
| 端点 | 方法 | 功能 |
|
||||
|------|------|------|
|
||||
| `/api/conversations/{id}/wingman/autocomplete` | POST | 实时自动补齐 |
|
||||
| `/api/conversations/{id}/wingman/tone-adjust` | POST | 语气调整 |
|
||||
| `/api/conversations/{id}/wingman/polish` | POST | 文字润色 |
|
||||
| `/api/conversations/{id}/wingman/rewrite` | POST | 智能改写 |
|
||||
|
||||
#### 4.1.2 WingmanService 扩展
|
||||
|
||||
在 `backend/app/services/wingman_service.py` 中新增 4 个方法:
|
||||
|
||||
- `generate_completion()` — 补齐,使用低 temperature (0.2)
|
||||
- `adjust_tone()` — 语气调整,使用中 temperature (0.3)
|
||||
- `polish_text()` — 润色,使用中 temperature (0.3)
|
||||
- `rewrite_versions()` — 改写,使用高 temperature (0.6) 生成多样化版本
|
||||
|
||||
每个方法使用独立的 system prompt,复用现有 Dify API 调用管道。
|
||||
|
||||
#### 4.1.3 性能优化
|
||||
|
||||
- 补齐 API 设置独立超时:`timeout=3s`(比其他 API 更短)
|
||||
- 补齐 API 请求添加 `X-Request-Id` 头,前端可取消过时请求
|
||||
- 考虑对补齐结果做 Redis 缓存(key = hash(当前文字+对话ID),TTL=30s)
|
||||
|
||||
### 4.2 前端
|
||||
|
||||
#### 4.2.1 组件结构
|
||||
|
||||
```
|
||||
frontend-agent/src/components/chat/
|
||||
├── ReplyBox.vue (修改: 新增 AI 工具栏按钮)
|
||||
├── ai-assist/
|
||||
│ ├── GhostText.vue (新建: 幽灵文字渲染层)
|
||||
│ ├── ToneAdjustPopover.vue (新建: 语气选择浮层)
|
||||
│ ├── PolishPanel.vue (新建: 润色精修面板)
|
||||
│ ├── RewritePanel.vue (新建: 改写版本选择面板)
|
||||
│ └── AiAssistToolbar.vue (新建: AI 辅助工具栏组件)
|
||||
```
|
||||
|
||||
#### 4.2.2 API 封装
|
||||
|
||||
在 `frontend-agent/src/api/wingman.ts` 中新增 4 个方法:
|
||||
|
||||
```typescript
|
||||
export function autocomplete(convId: number, text: string, cursorPos: number): Promise<AutocompleteResponse>
|
||||
export function adjustTone(convId: number, selectedText: string, fullText: string, tone: ToneType): Promise<ToneAdjustResponse>
|
||||
export function polishText(convId: number, text: string, action: PolishAction): Promise<PolishResponse>
|
||||
export function rewriteVersions(convId: number, currentText: string): Promise<RewriteResponse>
|
||||
```
|
||||
|
||||
#### 4.2.3 关键实现细节
|
||||
|
||||
**幽灵文字定位**:
|
||||
- 创建一个隐藏的 `<div>` 镜像 textarea 的样式和内容
|
||||
- 通过镜像 div 计算光标在文本中的像素位置
|
||||
- 在该位置覆盖一层透明 `<textarea>` 或使用 `contenteditable` 渲染幽灵文字
|
||||
|
||||
**请求取消**:
|
||||
- 使用 `AbortController` 管理补齐请求
|
||||
- 新请求发起时 abort 上一个未完成的请求
|
||||
|
||||
**防抖**:
|
||||
- 补齐:debounce 800ms
|
||||
- 语气/润色/改写:无防抖(用户主动触发)
|
||||
|
||||
---
|
||||
|
||||
## 5. 与现有功能的关系
|
||||
|
||||
| 现有功能 | 新功能 | 关系 |
|
||||
|----------|--------|------|
|
||||
| AI 草稿生成(右栏 Wingman) | 智能改写(输入框内) | **互补**:草稿生成用于从零开始;改写用于对已有输入的多版本化 |
|
||||
| AI 草稿气泡 | 实时自动补齐 | **互补**:草稿气泡是系统主动推荐;补齐是输入过程中的延续 |
|
||||
| 快速回复模板 | 语气调整 / 润色 | **互补**:快速回复是模板填充;语气/润色是对自由文本的加工 |
|
||||
| AI 推荐回复(内联 Mock) | 智能改写 | **替代路径**:改写功能可使用真实 API,逐步替代 Mock 推荐回复 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 非功能需求
|
||||
|
||||
| 项目 | 要求 |
|
||||
|------|------|
|
||||
| 补齐延迟 | P95 < 1.5s |
|
||||
| 语气/润色延迟 | P95 < 3s |
|
||||
| 改写延迟 | P95 < 5s |
|
||||
| 可用性 | AI 服务不可用时不影响正常输入和发送 |
|
||||
| 并发 | 单个坐席同一时刻最多 1 个补齐请求 + 1 个其他 AI 请求 |
|
||||
| 数据安全 | 补齐请求不记录到数据库;语气/润色/改写可记录用于后续优化 |
|
||||
| 浏览器兼容 | Chrome 90+ / Edge 90+ / 企微内置浏览器 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 开发计划
|
||||
|
||||
### 阶段一:后端 API + 前端骨架(预计 2 天)
|
||||
|
||||
- [ ] WingmanService 新增 4 个方法 + Dify system prompt
|
||||
- [ ] wingman.py 新增 4 个 API 端点
|
||||
- [ ] 前端 wingman.ts 新增 API 封装
|
||||
- [ ] ReplyBox.vue 新增 AI 工具栏按钮
|
||||
|
||||
### 阶段二:语气调整 + 文字润色(预计 2 天)
|
||||
|
||||
- [ ] ToneAdjustPopover.vue 组件
|
||||
- [ ] PolishPanel.vue 组件
|
||||
- [ ] 联调测试
|
||||
|
||||
### 阶段三:智能改写(预计 1 天)
|
||||
|
||||
- [ ] RewritePanel.vue 组件
|
||||
- [ ] 知识库检索集成
|
||||
- [ ] 联调测试
|
||||
|
||||
### 阶段四:实时自动补齐(预计 2 天)
|
||||
|
||||
- [ ] GhostText.vue 幽灵文字渲染
|
||||
- [ ] 光标定位计算
|
||||
- [ ] 请求防抖 + 取消机制
|
||||
- [ ] 联调测试
|
||||
|
||||
### 阶段五:集成测试 + 优化(预计 1 天)
|
||||
|
||||
- [ ] 端到端测试
|
||||
- [ ] 性能优化
|
||||
- [ ] 边界情况处理
|
||||
|
||||
---
|
||||
|
||||
## 8. 验收标准
|
||||
|
||||
### 8.1 功能验收
|
||||
|
||||
- [ ] 坐席输入停顿 > 0.8s 后,光标位置出现灰色幽灵文字
|
||||
- [ ] Tab 键接受补齐,文字变为正常颜色
|
||||
- [ ] 选中文字后点击"语气"按钮,弹出 3 种语气选项
|
||||
- [ ] 选择语气后显示原文/改写文对比,确认后替换
|
||||
- [ ] 点击"润色"按钮,弹出精修面板,支持扩写/压缩/纠错
|
||||
- [ ] 点击"改写"按钮,生成 3 个备选版本,可选择替换或追加
|
||||
- [ ] AI 服务不可用时不影响正常输入和发送
|
||||
|
||||
### 8.2 性能验收
|
||||
|
||||
- [ ] 补齐 P95 延迟 < 1.5s
|
||||
- [ ] 语气/润色 P95 延迟 < 3s
|
||||
- [ ] 改写 P95 延迟 < 5s
|
||||
- [ ] 输入过程中无明显卡顿
|
||||
|
||||
### 8.3 兼容性验收
|
||||
|
||||
- [ ] Chrome 90+ 正常工作
|
||||
- [ ] 企微内置浏览器正常工作
|
||||
- [ ] 移动端 H5 不受影响(本功能仅坐席端)
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||||
|------|------|------|----------|
|
||||
| 幽灵文字定位不准确 | 中 | 中 | 首版可采用建议条过渡方案 |
|
||||
| Dify 补齐延迟过高 | 中 | 高 | 设置 1.5s 超时,降级为不显示 |
|
||||
| 坐席过度依赖 AI | 低 | 中 | 补齐建议有置信度阈值,低置信度不显示 |
|
||||
| 语气改写偏离原意 | 中 | 中 | 显示原文/改写文对比,坐席确认后才替换 |
|
||||
| 并发请求冲突 | 低 | 低 | AbortController 管理请求生命周期 |
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:Dify System Prompt 汇总
|
||||
|
||||
### A.1 自动补齐
|
||||
```
|
||||
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
|
||||
要求:
|
||||
1. 补齐内容自然衔接当前文字,不要重复已有内容
|
||||
2. 长度控制在1-2个短句,不超过80字
|
||||
3. 语气专业、简洁,符合IT服务规范
|
||||
4. 只返回补齐的文字,不要加引号或其他标记
|
||||
```
|
||||
|
||||
### A.2 语气调整
|
||||
```
|
||||
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
|
||||
语气定义:
|
||||
- 专业:使用准确的技术术语,结构化表达,去除口语化内容
|
||||
- 友好:适当增加问候和关心语,语气更亲和
|
||||
- 简洁:去除冗余,直奔主题,控制字数
|
||||
要求:
|
||||
1. 保持原意不变,只调整语气和表达方式
|
||||
2. 改写后的文字长度与原文相近(±30%)
|
||||
3. 符合IT服务坐席的专业规范
|
||||
4. 只返回改写后的文字,不要加引号或解释
|
||||
```
|
||||
|
||||
### A.3 文字润色
|
||||
```
|
||||
你是一个IT服务文字精修助手。对坐席输入的文字进行{action}处理。
|
||||
操作定义:
|
||||
- 扩写:在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整
|
||||
- 压缩:精简表达,去除重复和冗余,保留核心信息,控制字数
|
||||
- 纠错:检查并修正错别字、语法错误、标点符号、格式问题
|
||||
要求:
|
||||
1. 保持原意不变
|
||||
2. 只返回处理后的文字,不要加引号或解释
|
||||
```
|
||||
|
||||
### A.4 智能改写
|
||||
```
|
||||
你是一个IT服务智能回复助手。基于对话上下文和知识库,为坐席生成3个不同风格的备选回复。
|
||||
版本要求:
|
||||
- 版本1:简洁直接,一句话说明问题和解决方案
|
||||
- 版本2:详细带步骤,包含操作步骤和注意事项
|
||||
- 版本3:带知识库引用,引用相关文档并给出权威解决方案
|
||||
要求:
|
||||
1. 3个版本内容不重复,各有侧重
|
||||
2. 每个版本独立成段,用---分隔
|
||||
3. 只返回回复内容,不要加额外解释
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
# 坐席端截图 + 拍照功能 PRD
|
||||
|
||||
## 项目信息
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| **语言** | 中文 |
|
||||
| **技术栈** | Vue3 + Element Plus + Tailwind CSS |
|
||||
| **项目名称** | agent_screenshot_camera |
|
||||
| **前端目录** | `D:\资料\03-项目开发\wecom_it_smart_desk\frontend-agent` |
|
||||
| **涉及组件** | `ReplyBox.vue` / `ScreenshotEditor.vue` / `ScreenCapture.vue`(改造)+ 新增 `CameraCapture.vue` |
|
||||
|
||||
### 原始需求复述
|
||||
|
||||
IT智能服务台坐席端(PC Web 应用)需要增加截图和相机拍照功能。客服在处理工单时经常需要截取屏幕内容或拍照来辅助沟通。截图需支持全屏/窗口/标签页选择(含跨屏),截图时屏幕变暗出现矩形选框 + 工具栏(马赛克、文字标注、箭头、撤销),样式参考微信/企微截图工具。拍照需用 `getUserMedia` + `<video>` 实现内嵌摄像头预览,支持重拍。截图和拍照结果均插入到聊天输入区作为图片消息等待发送(非立即发送)。
|
||||
|
||||
---
|
||||
|
||||
## 产品定义
|
||||
|
||||
### 产品目标
|
||||
|
||||
1. **提升沟通效率**:客服无需切换到外部截图工具或手机拍照,直接在工作台内完成截图/拍照→编辑→发送的全流程,减少上下文切换。
|
||||
2. **保证信息准确性**:通过截图标注(马赛克打码、箭头指引、文字说明)和实时拍照,让客服能够精准传递屏幕画面和实物状况,减少口头描述的歧义。
|
||||
3. **统一图片消息发送体验**:截图和拍照结果统一进入输入区待发送队列,客服可在发送前预览、补充文字说明或删除,与现有粘贴图片/文件上传的交互保持一致。
|
||||
|
||||
### 用户故事
|
||||
|
||||
1. **作为客服坐席**,我想直接在工作台点击截图按钮截取屏幕画面,这样我就能把用户报障的屏幕现象快速发给用户确认,而不需要切换到微信截图或系统截图工具。
|
||||
|
||||
2. **作为客服坐席**,我想在截图时用马赛克遮挡敏感信息(如密码、个人信息),这样在发送截图时不会泄露用户隐私。
|
||||
|
||||
3. **作为客服坐席**,我想在截图选区内用箭头和文字标注重点位置,这样用户能快速理解我指的具体是哪里。
|
||||
|
||||
4. **作为客服坐席**,我想通过摄像头拍照来记录实物设备状态(如硬件故障、线缆连接),这样比文字描述更直观。
|
||||
|
||||
5. **作为客服坐席**,我想截图/拍照后的图片先进入输入区等我自己确认后再发送,这样我能在发送前检查标注是否正确、是否打码到位。
|
||||
|
||||
---
|
||||
|
||||
## 技术规范
|
||||
|
||||
### 现有代码分析
|
||||
|
||||
通过对 `frontend-agent/src/components/chat/` 目录的代码审查,发现以下现状:
|
||||
|
||||
| 组件 | 现状 | 差距 |
|
||||
|---|---|---|
|
||||
| `ReplyBox.vue` | 工具栏有表情、文件、邀请、快速回复按钮;已有 `handleScreenshot()`(用 `html2canvas`)和 `handleScreenCapture()`(用 `getDisplayMedia`,但未接入任何按钮) | 缺少截图/拍照入口按钮;截图结果直接上传发送而非插入输入区 |
|
||||
| `ScreenshotEditor.vue` | 裁剪 + 编辑两步模式,工具有画笔/高亮/矩形/箭头/文字,支持撤销/重做 | 缺少马赛克工具 |
|
||||
| `ScreenCapture.vue` | 全屏遮罩框选模式,工具有画笔/矩形/箭头/文字 | 缺少马赛克工具;结果直接发送 |
|
||||
| `upload.ts` | 文件上传 API,支持重试,返回 `{ url, filename, file_size, msg_type }` | 可复用 |
|
||||
| 拍照功能 | **完全不存在** | 需新建 `CameraCapture.vue` 组件 |
|
||||
|
||||
**关键发现**:`ReplyBox.vue` 中已有 `handleScreenCapture()` 方法调用了 `navigator.mediaDevices.getDisplayMedia()`,但该方法未绑定到任何 UI 按钮。当前截图按钮实际走的是 `handleScreenshot()` → `html2canvas`(仅能截取当前页面 DOM,无法截取其他窗口/屏幕)。
|
||||
|
||||
### 需求池
|
||||
|
||||
#### P0 — 必须实现(Must Have)
|
||||
|
||||
| 编号 | 需求 | 验收标准 |
|
||||
|---|---|---|
|
||||
| P0-1 | **截图入口按钮**:在 `ReplyBox.vue` 工具栏新增截图按钮(图标裁剪刀或相机图标),与表情、文件等按钮并列 | 点击后触发 `getDisplayMedia()` 屏幕选择流程 |
|
||||
| P0-2 | **`getDisplayMedia` 屏幕捕获**:调用 `navigator.mediaDevices.getDisplayMedia()` 获取屏幕流,捕获一帧画面传入截图编辑器 | 浏览器弹出屏幕/窗口/标签页选择器;支持选择跨屏显示器;用户取消时静默退出不报错 |
|
||||
| P0-3 | **截图选区编辑**:全屏遮罩 + 矩形选框拖拽,选区内可编辑,选区外半透明遮罩(对标微信截图) | 屏幕变暗;拖拽出现选框;选框可拖动调整大小;8 个方向手柄 |
|
||||
| P0-4 | **马赛克工具**:截图编辑工具栏新增马赛克工具,拖拽涂抹区域进行像素化打码 | 选择马赛克工具后在选区内拖拽,拖过区域被马赛克覆盖;支持撤销 |
|
||||
| P0-5 | **文字标注工具**:点击选区内位置弹出文本输入,输入后渲染为画布上的文字标注 | 点击出现输入框(非 `prompt()`,用内联 input);回车确认渲染到 canvas |
|
||||
| P0-6 | **箭头工具**:拖拽绘制带箭头的线段 | 起点到终点绘制箭头;箭头头部自动计算角度 |
|
||||
| P0-7 | **撤销功能**:工具栏撤销按钮,逐步撤销编辑操作 | 每次操作入栈,撤销弹栈;撤销到空时禁用按钮 |
|
||||
| P0-8 | **截图结果插入输入区**:截图确认后,图片以缩略图形式显示在输入框上方(textarea 之上),作为待发送图片,不自动发送 | 输入区上方出现图片缩略图 + 删除按钮;可继续输入文字;点击发送时图片和文字一起发送 |
|
||||
| P0-9 | **拍照入口按钮**:在 `ReplyBox.vue` 工具栏新增拍照按钮(摄像头图标) | 点击后弹出摄像头预览弹窗 |
|
||||
| P0-10 | **`getUserMedia` 摄像头预览**:用 `navigator.mediaDevices.getUserMedia({ video: true })` 获取摄像头流,渲染到 `<video>` 元素实时预览 | 弹窗内嵌视频预览;自动选择前置摄像头;摄像头不可用时显示错误提示 |
|
||||
| P0-11 | **拍照捕获**:点击拍照按钮,从 `<video>` 当前帧 capture 到 canvas,转为图片 | 拍照后预览切换为静态图片;显示"重拍"和"确认"按钮 |
|
||||
| P0-12 | **重拍功能**:拍照后点击"重拍"返回实时预览状态 | 重拍后清除上一张照片,恢复 `<video>` 实时预览 |
|
||||
| P0-13 | **拍照结果插入输入区**:拍照确认后,图片以缩略图形式显示在输入框上方,作为待发送图片 | 与 P0-8 行为一致 |
|
||||
| P0-14 | **待发送图片发送流程**:发送消息时,如果有待发送图片,先上传图片再发送图片消息(`msg_type: 'image'`),如有文字则额外发送文字消息 | 图片上传复用 `uploadFile()` API;发送成功后清空待发送图片和文字 |
|
||||
| P0-15 | **待发送图片删除**:输入区待发送图片缩略图上有删除按钮,点击移除 | 点击 × 后缩略图消失,不影响文字内容 |
|
||||
|
||||
#### P1 — 应该实现(Should Have)
|
||||
|
||||
| 编号 | 需求 | 验收标准 |
|
||||
|---|---|---|
|
||||
| P1-1 | **多张待发送图片**:输入区可同时插入多张待发送图片(如先截一张再拍一张) | 支持多张缩略图横向排列;发送时按顺序上传发送;可单独删除某一张 |
|
||||
| P1-2 | **颜色选择**:截图编辑工具栏提供颜色选择(红/黄/蓝/绿/黑等常用色) | 点击颜色切换标注颜色;当前选中颜色高亮 |
|
||||
| P1-3 | **矩形框工具**:拖拽绘制矩形边框(用于圈选区域) | 与微信截图矩形工具一致 |
|
||||
| P1-4 | **截图保存到本地**:截图编辑器提供"保存"按钮,将截图下载到本地 | 下载为 PNG 文件,文件名 `screenshot-{timestamp}.png` |
|
||||
| P1-5 | **摄像头切换**:如果有多个摄像头,支持切换前后摄像头 | 弹窗内显示切换按钮;切换后预览更新 |
|
||||
| P1-6 | **ESC 退出**:截图模式和拍照弹窗均支持 ESC 键退出 | 按 ESC 等同于取消 |
|
||||
| P1-7 | **截图选区尺寸显示**:拖拽选区时实时显示选区宽×高像素值 | 选区右下角或工具栏旁显示 `W×H` 像素值 |
|
||||
|
||||
#### P2 — 优先级较低(Nice to Have)
|
||||
|
||||
| 编号 | 需求 | 验收标准 |
|
||||
|---|---|---|
|
||||
| P2-1 | **画笔自由绘制**:截图编辑器提供自由画笔工具 | 拖拽绘制自由曲线;线宽可调 |
|
||||
| P2-2 | **高亮工具**:半透明黄色高亮笔 | 拖拽覆盖区域呈半透明高亮 |
|
||||
| P2-3 | **快捷键截图**:支持 `Ctrl+Shift+A` 快捷键触发截图 | 全局快捷键,焦点不在输入框也能触发 |
|
||||
| P2-4 | **拍照镜像翻转**:前置摄像头预览水平镜像(符合自拍习惯),但保存的图片不镜像 | 预览镜像、输出正向 |
|
||||
| P2-5 | **截图延时捕获**:支持 3 秒延时截图(用于捕获鼠标悬停菜单等动态画面) | 选择延时后倒计时 3-2-1 再捕获 |
|
||||
|
||||
---
|
||||
|
||||
## UI 设计要点
|
||||
|
||||
### 1. 工具栏布局(ReplyBox.vue)
|
||||
|
||||
现有工具栏顺序:`表情 | 文件 | 分隔线 | 邀请 | 快速回复`
|
||||
|
||||
**改造后**:`截图 | 拍照 | 分隔线 | 表情 | 文件 | 分隔线 | 邀请 | 快速回复`
|
||||
|
||||
- 截图按钮放在最左侧(最高频使用),图标用裁剪刀 SVG 或 ✂️
|
||||
- 拍照按钮紧跟截图按钮,图标用摄像头 SVG 或 📷
|
||||
- 与现有按钮样式保持一致(`.tb-btn` 类,32×28px,hover 高亮 + tooltip)
|
||||
|
||||
### 2. 截图模式(全屏覆盖层)
|
||||
|
||||
- **触发**:点击截图按钮 → 浏览器弹出 `getDisplayMedia` 选择器(选屏幕/窗口/标签页)
|
||||
- **选区**:选中后屏幕变暗(`rgba(0,0,0,0.5)` 遮罩),鼠标变为十字光标
|
||||
- **拖拽框选**:按住鼠标拖拽出现矩形选框(绿色边框 `#07C160`,对标微信),选区内画面清晰,选区外半透明
|
||||
- **8 方向手柄**:选框四角 + 四边中点各一个手柄,可拖拽调整大小
|
||||
- **工具栏**:选区下方居中浮动工具栏,从左到右:
|
||||
- 工具组:`马赛克 | 文字 | 箭头 | 矩形`(P1)
|
||||
- 颜色组:`🔴 🟡 🔵 🟢 ⚫`(P1)
|
||||
- 操作组:`撤销 | 保存(P1) | 取消 | 确认✓`
|
||||
- **尺寸提示**:选区右下角实时显示 `宽×高` 像素值(P1)
|
||||
- **ESC 退出**:按 ESC 取消截图
|
||||
|
||||
### 3. 拍照弹窗(CameraCapture.vue)
|
||||
|
||||
- **弹出方式**:Element Plus `el-dialog` 居中弹窗,宽度约 640px
|
||||
- **预览区**:`<video>` 元素填满弹窗主体,自适应摄像头分辨率
|
||||
- **控制区**:弹窗底部居中放置拍照按钮(圆形大按钮,图标 📷)
|
||||
- **拍照后状态**:
|
||||
- `<video>` 隐藏,显示 `<canvas>` 或 `<img>` 静态画面
|
||||
- 底部按钮变为:`重拍` (左)| `确认` (右,绿色主按钮)
|
||||
- **错误状态**:摄像头不可用时显示"无法访问摄像头,请检查权限设置"提示 + 关闭按钮
|
||||
- **关闭**:右上角 ✕ 或 ESC 关闭弹窗,停止摄像头流
|
||||
|
||||
### 4. 待发送图片预览区(输入框上方)
|
||||
|
||||
- **位置**:在 `ReplyBox.vue` 的 `textarea` 上方、工具栏下方,新增一个待发送图片预览条
|
||||
- **布局**:横向排列缩略图,每张缩略图约 80×80px,圆角,右上角有 × 删除按钮
|
||||
- **样式**:浅色背景容器,与现有引用回复预览条(`.reply-preview`)风格一致
|
||||
- **多张图片**:横向排列,超出宽度时可横向滚动
|
||||
- **交互**:
|
||||
- 鼠标悬停缩略图可放大预览(可选 P2)
|
||||
- 点击 × 删除该张待发送图片
|
||||
- 发送后清空所有待发送图片
|
||||
|
||||
### 5. 视觉风格
|
||||
|
||||
- 主色调:与现有坐席端一致,使用 CSS 变量 `var(--accent)`、`var(--bg-secondary)` 等
|
||||
- 截图选框颜色:`#07C160`(微信绿,与现有 `ScreenCapture.vue` 一致)
|
||||
- 工具栏:白色背景 + 阴影,与现有 `ScreenshotEditor.vue` 工具栏风格一致
|
||||
- 弹窗:Element Plus 默认主题,圆角 8px
|
||||
|
||||
---
|
||||
|
||||
## 待确认问题
|
||||
|
||||
| 编号 | 问题 | 影响范围 | 建议 |
|
||||
|---|---|---|---|
|
||||
| Q1 | **企微内嵌浏览器兼容性**:`getDisplayMedia()` 和 `getUserMedia()` 在企微内置浏览器中是否可用?现有代码注释提到"Screen Capture API 在企微桌面端被限制,不稳定"。 | 截图功能核心可行性 | 建议在企微环境下做一次可用性测试;如不可用,保留 `html2canvas` 作为 fallback(仅能截当前页面) |
|
||||
| Q2 | **HTTPS 要求**:`getDisplayMedia()` 和 `getUserMedia()` 要求 HTTPS 环境(或 localhost)。坐席端部署是否为 HTTPS? | 截图 + 拍照功能可行性 | 确认生产环境域名是否为 HTTPS;如非 HTTPS 则两个功能都无法使用 |
|
||||
| Q3 | **待发送图片与文字的组合发送**:用户在输入区有待发送图片 + 文字时,发送方式是:(a) 先发图片消息再发文字消息(两条消息)?还是 (b) 合并为一条富文本消息? | 消息发送逻辑 | 建议方案 (a):先发图片再发文字,与现有粘贴图片行为一致。后端如支持图文混排消息可改为 (b) |
|
||||
| Q4 | **截图工具栏工具优先级**:需求明确提到"马赛克、文字标注、箭头、撤销"四个工具。是否也需要矩形框工具(P1)和高亮/画笔(P2)? | 编辑器功能范围 | 建议 P0 只做需求明确的 4 个工具,矩形框放入 P1 |
|
||||
| Q5 | **摄像头权限提示**:首次使用拍照功能时浏览器会请求摄像头权限。是否需要自定义权限引导提示? | 用户体验 | 建议在调用 `getUserMedia` 前先显示一个简短提示"即将请求摄像头权限,请在弹窗中允许" |
|
||||
| Q6 | **InputBox.vue 是否同步改造**:代码库中存在 `InputBox.vue`(旧版输入组件)和 `ReplyBox.vue`(当前使用)。是否需要同步改造 `InputBox.vue`? | 改造范围 | 建议只改 `ReplyBox.vue`,`InputBox.vue` 如已废弃则不动 |
|
||||
| Q7 | **截图后的编辑器复用**:现有 `ScreenshotEditor.vue`(裁剪+编辑两步)和 `ScreenCapture.vue`(全屏框选+编辑)两个组件功能重叠。新截图功能基于哪个改造? | 架构设计 | 建议基于 `ScreenCapture.vue` 改造(全屏遮罩框选更符合微信截图体验),废弃或保留 `ScreenshotEditor.vue` 供 `html2canvas` fallback 使用 |
|
||||
|
||||
---
|
||||
|
||||
## 附录:现有代码关键路径
|
||||
|
||||
```
|
||||
frontend-agent/src/
|
||||
├── components/chat/
|
||||
│ ├── ReplyBox.vue ← 主输入组件(改造:加按钮 + 待发送图片区)
|
||||
│ ├── InputBox.vue ← 旧版输入组件(待确认是否改造)
|
||||
│ ├── ScreenshotEditor.vue ← 截图编辑器(改造:加马赛克工具)
|
||||
│ ├── ScreenCapture.vue ← 全屏框选截图(改造:接入 getDisplayMedia + 加马赛克)
|
||||
│ └── CameraCapture.vue ← 新建:摄像头拍照组件
|
||||
├── api/
|
||||
│ ├── upload.ts ← 文件上传 API(复用)
|
||||
│ └── message.ts ← 消息发送 API(复用)
|
||||
└── stores/
|
||||
└── conversation.ts ← 会话状态管理(可能需扩展:待发送图片队列)
|
||||
```
|
||||
Reference in New Issue
Block a user