449c6d4875
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS
## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS
## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)
## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code
## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)
## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过
## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务
## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记✅已实施
- 新增架构图/时序图/类图(mermaid)
## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
1048 lines
48 KiB
Markdown
1048 lines
48 KiB
Markdown
# 增量设计:坐席端 AI 辅助消息框
|
||
|
||
> **文档版本**:v1.0
|
||
> **创建日期**:2026-07-11
|
||
> **架构师**:高见远
|
||
> **关联 PRD**:`docs/02-产品需求/坐席端AI辅助消息框-PRD.md`
|
||
> **技术栈**:后端 FastAPI + Python / 前端 Vue3 + Element Plus + TypeScript
|
||
> **部署方式**:Docker Compose,bind mount `./app:/app/app`
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [Part A: 系统设计](#part-a-系统设计)
|
||
- [1. 实现方案概述](#1-实现方案概述)
|
||
- [2. 文件列表](#2-文件列表)
|
||
- [3. 数据结构和接口](#3-数据结构和接口)
|
||
- [4. 程序调用流程](#4-程序调用流程)
|
||
- [5. 待明确事项](#5-待明确事项)
|
||
- [Part B: 任务分解](#part-b-任务分解)
|
||
- [6. 依赖包列表](#6-依赖包列表)
|
||
- [7. 任务列表](#7-任务列表)
|
||
- [8. 共享知识](#8-共享知识)
|
||
- [9. 任务依赖图](#9-任务依赖图)
|
||
|
||
---
|
||
|
||
## Part A: 系统设计
|
||
|
||
### 1. 实现方案概述
|
||
|
||
#### 1.1 核心技术挑战
|
||
|
||
| 挑战 | 难点 | 解决方案 |
|
||
|------|------|----------|
|
||
| **幽灵文字内联定位** | `<textarea>` 无法直接在光标处叠加文字,需精确计算光标像素坐标 | 创建隐藏 `<div>` 镜像 textarea 的样式(字体、行高、padding、换行),通过镜像 div 截取光标前文本计算像素偏移,在 textarea 同级覆盖一层透明 `<span>` 渲染幽灵文字 |
|
||
| **补齐请求生命周期管理** | 用户连续输入时需取消旧请求、仅保留最新结果 | 前端使用 `AbortController` 管理请求;每次新输入触发时 abort 上一个 controller,创建新的 |
|
||
| **多温度 Dify 调用** | 现有 `_call_wingman_api()` 的 temperature 硬编码为 0.3,4 个新功能需要不同温度 | 改造 `_call_wingman_api()` 增加 `temperature` 参数(默认 0.3 保持向后兼容),各方法按需传入 |
|
||
| **补齐结果 Redis 缓存** | 同一对话中重复输入相同前缀时避免重复调用 Dify | 在 `generate_completion()` 中先查 Redis(key=`wingman:autocomplete:{hash(text+conv_id)}`,TTL=30s),命中则直接返回;未命中调用 Dify 后写入缓存 |
|
||
| **改写功能知识库集成** | 版本 3 需引用知识库文档,需检索 RAGFlow 并注入 prompt | `rewrite_versions()` 调用 `build_ragflow_client()` 检索相关知识片段,拼入 Dify system prompt;RAGFlow 不可用时降级为仅生成 2 个版本 |
|
||
| **AI 不可用降级** | 所有 AI 功能不可用时不能影响坐席正常输入和发送 | 后端方法 catch 所有异常返回空结果/默认值(与现有 `generate_draft` 范式一致);前端 catch 请求错误后静默忽略,不弹错误提示 |
|
||
|
||
#### 1.2 框架与库选型
|
||
|
||
| 层 | 选型 | 理由 |
|
||
|----|------|------|
|
||
| 后端 API | FastAPI + Pydantic | 现有项目已使用,Pydantic 提供请求体验证 |
|
||
| 后端 AI 调用 | httpx AsyncClient | 复用 WingmanService 现有 httpx 连接池 |
|
||
| 后端缓存 | redis.asyncio (aioredis) | 复用 `settings.create_redis_client()`,与 CacheService 同一 Redis 实例 |
|
||
| 后端知识检索 | RAGFlow Client (`app.integrations.ragflow`) | 复用现有 RagflowClient,通过 `build_ragflow_client()` 获取 |
|
||
| 前端框架 | Vue3 Composition API + `<script setup lang="ts">` | 与 ReplyBox.vue 现有范式一致 |
|
||
| 前端 UI | Element Plus (el-popover, el-button, el-tooltip) | 现有项目已引入,浮层/面板组件直接复用 |
|
||
| 前端 HTTP | Axios + AbortController | 复用 `apiClient` 实例,AbortController 管理请求取消 |
|
||
| 前端防抖 | 自实现 debounce(或 lodash-es debounce) | 补齐 800ms debounce;项目已有 lodash-es 依赖 |
|
||
|
||
#### 1.3 整体架构
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────┐
|
||
│ 坐席工作台 (Vue3 + Element Plus) │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ ReplyBox.vue (改造) │ │
|
||
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ AiAssistToolbar.vue (新建) │ │ │
|
||
│ │ │ [补齐ON/OFF] [语气] [润色] [改写] │ │ │
|
||
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ textarea + GhostText.vue (新建, 幽灵文字覆盖层) │ │ │
|
||
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ ToneAdjustPopover.vue / PolishPanel.vue / RewritePanel.vue│ │ │
|
||
│ │ │ (按需弹出, 浮层/面板形式) │ │ │
|
||
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
||
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
|
||
│ │ │ useAiAssist.ts (composable, 状态管理 + 请求调度) │ │ │
|
||
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────────────────┘ │
|
||
│ │ HTTP (Axios + AbortController) │
|
||
└─────────────────────────┼────────────────────────────────────────────┘
|
||
│
|
||
┌─────────────────────────▼────────────────────────────────────────────┐
|
||
│ FastAPI 后端 │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────────────────┐ │
|
||
│ │ wingman.py (路由层, 新增 4 个端点) │ │
|
||
│ │ POST /conversations/{id}/wingman/autocomplete ← 自动补齐 │ │
|
||
│ │ POST /conversations/{id}/wingman/tone-adjust ← 语气调整 │ │
|
||
│ │ POST /conversations/{id}/wingman/polish ← 文字润色 │ │
|
||
│ │ POST /conversations/{id}/wingman/rewrite ← 智能改写 │ │
|
||
│ │ + 现有: draft / summary / tags │ │
|
||
│ └───────────────────────────┬────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌───────────────────────────▼────────────────────────────────────┐ │
|
||
│ │ WingmanService (服务层, 新增 4 个方法 + 改造底层调用) │ │
|
||
│ │ generate_completion() temperature=0.2 + Redis 缓存 │ │
|
||
│ │ adjust_tone() temperature=0.3 │ │
|
||
│ │ polish_text() temperature=0.3 │ │
|
||
│ │ rewrite_versions() temperature=0.6 + RAGFlow 检索 │ │
|
||
│ │ + 现有: generate_draft / generate_summary / suggest_tags │ │
|
||
│ │ │ │
|
||
│ │ _call_wingman_api() ← 改造: 新增 temperature 参数 │ │
|
||
│ │ _build_context_messages() ← 复用: 角色映射 │ │
|
||
│ └──────┬──────────────────────┬───────────────────────────────────┘ │
|
||
│ │ httpx │ redis.asyncio │
|
||
└─────────┼───────────────────────┼─────────────────────────────────────┘
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────────────┐ ┌──────────────────┐
|
||
│ Dify AI (外部) │ │ Redis 缓存 │
|
||
│ Wingman Agent │ │ key: wingman: │
|
||
│ /chat/completions │ │ autocomplete: │
|
||
│ (OpenAI 兼容格式) │ │ {hash} TTL=30s │
|
||
└─────────────────────┘ └──────────────────┘
|
||
│
|
||
▼ (改写功能版本3)
|
||
┌─────────────────────┐
|
||
│ RAGFlow 知识检索 │
|
||
│ /api/v1/retrieval │
|
||
│ (build_ragflow_ │
|
||
│ client() 获取) │
|
||
└─────────────────────┘
|
||
```
|
||
|
||
#### 1.4 与现有系统的关系
|
||
|
||
| 现有组件 | 关系 | 说明 |
|
||
|----------|------|------|
|
||
| `WingmanService` | **扩展** | 新增 4 个方法,改造 `_call_wingman_api()` 支持 temperature 参数 |
|
||
| `wingman.py` 路由 | **扩展** | 新增 4 个端点 + 引入 Pydantic 请求模型 |
|
||
| `schemas/` 目录 | **新增** | 新增 `wingman_assist.py` 定义 4 个请求 Schema |
|
||
| `dependencies/__init__.py` | **修改** | `dep_wingman_service` 注入 Redis 客户端 |
|
||
| `wingman.ts` API 层 | **扩展** | 新增 4 个前端 API 函数 + AbortController 支持 |
|
||
| `ReplyBox.vue` | **改造** | 集成 AiAssistToolbar + GhostText + 三个面板组件 |
|
||
| 现有 `generate_draft` 等 | **不受影响** | `_call_wingman_api` 改造向后兼容(默认 temperature=0.3) |
|
||
|
||
---
|
||
|
||
### 2. 文件列表
|
||
|
||
| # | 文件路径 | 操作 | 说明 |
|
||
|---|---------|------|------|
|
||
| 1 | `backend/app/services/wingman_service.py` | **修改** | 新增 4 个方法 + 4 个 system prompt + 改造 `_call_wingman_api` |
|
||
| 2 | `backend/app/api/wingman.py` | **修改** | 新增 4 个 API 端点 + 引入 Schema 依赖 |
|
||
| 3 | `backend/app/schemas/wingman_assist.py` | **新建** | 4 个请求体 Pydantic 模型 |
|
||
| 4 | `backend/app/dependencies/__init__.py` | **修改** | `dep_wingman_service` 注入 Redis 客户端 |
|
||
| 5 | `frontend-agent/src/api/wingman.ts` | **修改** | 新增 4 个 API 函数 + 4 个响应类型 + AbortController |
|
||
| 6 | `frontend-agent/src/composables/useAiAssist.ts` | **新建** | AI 辅助状态管理 + 请求调度 composable |
|
||
| 7 | `frontend-agent/src/components/chat/ai-assist/GhostText.vue` | **新建** | 幽灵文字渲染层(光标定位 + Tab 接受) |
|
||
| 8 | `frontend-agent/src/components/chat/ai-assist/AiAssistToolbar.vue` | **新建** | AI 辅助工具栏(补齐开关 + 语气/润色/改写按钮) |
|
||
| 9 | `frontend-agent/src/components/chat/ai-assist/ToneAdjustPopover.vue` | **新建** | 语气选择浮层(专业/友好/简洁 + 原文/改写对比) |
|
||
| 10 | `frontend-agent/src/components/chat/ai-assist/PolishPanel.vue` | **新建** | 润色精修面板(扩写/压缩/纠错 + 左右对比) |
|
||
| 11 | `frontend-agent/src/components/chat/ai-assist/RewritePanel.vue` | **新建** | 改写版本选择面板(3 个版本 + 替换/追加) |
|
||
| 12 | `frontend-agent/src/components/chat/ReplyBox.vue` | **修改** | 集成 AI 工具栏 + GhostText + 三个面板组件 |
|
||
|
||
---
|
||
|
||
### 3. 数据结构和接口
|
||
|
||
#### 3.1 后端 Pydantic 请求 Schema
|
||
|
||
```python
|
||
# backend/app/schemas/wingman_assist.py
|
||
|
||
from pydantic import BaseModel, Field
|
||
from typing import List, Optional
|
||
|
||
|
||
class AutocompleteRequest(BaseModel):
|
||
"""自动补齐请求"""
|
||
current_text: str = Field(..., min_length=1, max_length=500, description="当前输入文本")
|
||
cursor_position: int = Field(0, ge=0, description="光标位置")
|
||
max_length: int = Field(80, ge=10, le=200, description="补齐最大长度")
|
||
|
||
|
||
class ToneAdjustRequest(BaseModel):
|
||
"""语气调整请求"""
|
||
selected_text: str = Field(..., min_length=1, max_length=2000, description="选中的文字")
|
||
full_text: str = Field("", max_length=5000, description="输入框完整内容")
|
||
tone: str = Field(..., pattern="^(professional|friendly|concise)$", description="目标语气")
|
||
|
||
|
||
class PolishRequest(BaseModel):
|
||
"""文字润色请求"""
|
||
text: str = Field(..., min_length=1, max_length=5000, description="待润色文字")
|
||
action: str = Field(..., pattern="^(expand|compress|correct)$", description="润色操作")
|
||
conversation_context: bool = Field(True, description="是否携带对话上下文")
|
||
|
||
|
||
class RewriteRequest(BaseModel):
|
||
"""智能改写请求"""
|
||
current_text: str = Field("", max_length=5000, description="当前输入文本(可为空)")
|
||
generate_count: int = Field(3, ge=1, le=5, description="生成版本数")
|
||
include_knowledge: bool = Field(True, description="是否包含知识库引用版本")
|
||
```
|
||
|
||
#### 3.2 后端 WingmanService 新增方法签名
|
||
|
||
```python
|
||
class WingmanService:
|
||
# === 现有方法(保持不变) ===
|
||
# generate_draft() / generate_summary() / suggest_tags() / generate_knowledge_suggestion()
|
||
|
||
# === 新增 System Prompt 常量 ===
|
||
_COMPLETION_SYSTEM_PROMPT: str # 自动补齐
|
||
_TONE_ADJUST_SYSTEM_PROMPT: str # 语气调整
|
||
_POLISH_SYSTEM_PROMPT: str # 文字润色
|
||
_REWRITE_SYSTEM_PROMPT: str # 智能改写
|
||
|
||
# === 新增方法 ===
|
||
async def generate_completion(
|
||
self,
|
||
conversation_id: str,
|
||
current_text: str,
|
||
messages: List[Dict[str, Any]],
|
||
max_length: int = 80,
|
||
) -> Dict[str, Any]:
|
||
"""自动补齐:temperature=0.2,Redis 缓存 TTL=30s"""
|
||
|
||
async def adjust_tone(
|
||
self,
|
||
conversation_id: str,
|
||
selected_text: str,
|
||
full_text: str,
|
||
tone: str,
|
||
messages: List[Dict[str, Any]],
|
||
) -> Dict[str, Any]:
|
||
"""语气调整:temperature=0.3"""
|
||
|
||
async def polish_text(
|
||
self,
|
||
conversation_id: str,
|
||
text: str,
|
||
action: str,
|
||
messages: List[Dict[str, Any]],
|
||
) -> Dict[str, Any]:
|
||
"""文字润色:temperature=0.3"""
|
||
|
||
async def rewrite_versions(
|
||
self,
|
||
conversation_id: str,
|
||
current_text: str,
|
||
messages: List[Dict[str, Any]],
|
||
generate_count: int = 3,
|
||
include_knowledge: bool = True,
|
||
) -> Dict[str, Any]:
|
||
"""智能改写:temperature=0.6,可选 RAGFlow 知识检索"""
|
||
|
||
# === 改造方法 ===
|
||
async def _call_wingman_api(
|
||
self,
|
||
context_messages: List[Dict[str, str]],
|
||
temperature: float = 0.3, # ← 新增参数,默认值保持兼容
|
||
) -> Optional[str]:
|
||
"""调用 Dify API(改造:支持 temperature 参数)"""
|
||
|
||
# === 新增内部方法 ===
|
||
async def _get_cache(self, key: str) -> Optional[Dict[str, Any]]:
|
||
"""读取 Redis 缓存"""
|
||
|
||
async def _set_cache(self, key: str, value: Dict[str, Any], ttl: int = 30) -> None:
|
||
"""写入 Redis 缓存"""
|
||
|
||
def _make_cache_key(self, text: str, conversation_id: str) -> str:
|
||
"""生成缓存 key: wingman:autocomplete:{md5(text+conv_id)}"""
|
||
|
||
async def _search_knowledge(self, query: str) -> Optional[str]:
|
||
"""调用 RAGFlow 检索知识(降级返回 None)"""
|
||
```
|
||
|
||
#### 3.3 前端 TypeScript 类型定义
|
||
|
||
```typescript
|
||
// frontend-agent/src/api/wingman.ts (新增部分)
|
||
|
||
/** 自动补齐响应 */
|
||
export interface AutocompleteResult {
|
||
completion: string
|
||
confidence: number
|
||
}
|
||
|
||
/** 语气类型 */
|
||
export type ToneType = 'professional' | 'friendly' | 'concise'
|
||
|
||
/** 语气调整响应 */
|
||
export interface ToneAdjustResult {
|
||
rewritten_text: string
|
||
tone: ToneType
|
||
changes_summary: string
|
||
}
|
||
|
||
/** 润色操作类型 */
|
||
export type PolishAction = 'expand' | 'compress' | 'correct'
|
||
|
||
/** 文字润色响应 */
|
||
export interface PolishResult {
|
||
polished_text: string
|
||
action: PolishAction
|
||
changes_summary: string
|
||
}
|
||
|
||
/** 改写版本 */
|
||
export interface RewriteVersion {
|
||
text: string
|
||
style: string
|
||
source: string
|
||
}
|
||
|
||
/** 智能改写响应 */
|
||
export interface RewriteResult {
|
||
versions: RewriteVersion[]
|
||
}
|
||
```
|
||
|
||
#### 3.4 前端 useAiAssist Composable 接口
|
||
|
||
```typescript
|
||
// frontend-agent/src/composables/useAiAssist.ts
|
||
|
||
export interface UseAiAssistOptions {
|
||
/** 获取当前会话 ID */
|
||
getConversationId: () => string | undefined
|
||
/** 获取输入框文本 */
|
||
getInputText: () => string
|
||
/** 设置输入框文本 */
|
||
setInputText: (text: string) => void
|
||
/** 获取 textarea DOM 引用(用于光标定位) */
|
||
getTextareaRef: () => HTMLTextAreaElement | null
|
||
}
|
||
|
||
export function useAiAssist(options: UseAiAssistOptions) {
|
||
// === 自动补齐 ===
|
||
const ghostText: Ref<string> // 当前幽灵文字
|
||
const isCompletLoading: Ref<boolean> // 补齐加载中
|
||
const autocompleteEnabled: Ref<boolean> // 补齐开关
|
||
function triggerAutocomplete(): void // 触发补齐(debounce 800ms)
|
||
function acceptGhostText(): void // Tab 接受补齐
|
||
function clearGhostText(): void // 清除补齐(Esc 或继续输入)
|
||
|
||
// === 语气调整 ===
|
||
const tonePopoverVisible: Ref<boolean>
|
||
const toneLoading: Ref<boolean>
|
||
const toneResult: Ref<ToneAdjustResult | null>
|
||
async function adjustTone(selectedText: string, fullText: string, tone: ToneType): Promise<void>
|
||
|
||
// === 文字润色 ===
|
||
const polishPanelVisible: Ref<boolean>
|
||
const polishLoading: Ref<boolean>
|
||
const polishResult: Ref<PolishResult | null>
|
||
async function polishText(action: PolishAction): Promise<void>
|
||
|
||
// === 智能改写 ===
|
||
const rewritePanelVisible: Ref<boolean>
|
||
const rewriteLoading: Ref<boolean>
|
||
const rewriteResult: Ref<RewriteResult | null>
|
||
async function rewriteVersions(): Promise<void>
|
||
|
||
// === 生命周期 ===
|
||
function cleanup(): void // 组件卸载时调用,abort 所有请求
|
||
|
||
return {
|
||
ghostText, isCompletLoading, autocompleteEnabled,
|
||
triggerAutocomplete, acceptGhostText, clearGhostText,
|
||
tonePopoverVisible, toneLoading, toneResult, adjustTone,
|
||
polishPanelVisible, polishLoading, polishResult, polishText,
|
||
rewritePanelVisible, rewriteLoading, rewriteResult, rewriteVersions,
|
||
cleanup,
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.5 类图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
direction TB
|
||
|
||
class WingmanService {
|
||
-str api_url
|
||
-str api_key
|
||
-float timeout
|
||
-httpx.AsyncClient _client
|
||
-aioredis.Redis _redis
|
||
-str _COMPLETION_SYSTEM_PROMPT
|
||
-str _TONE_ADJUST_SYSTEM_PROMPT
|
||
-str _POLISH_SYSTEM_PROMPT
|
||
-str _REWRITE_SYSTEM_PROMPT
|
||
+__init__(redis_client: Optional[Redis])
|
||
+async generate_completion(conversation_id: str, current_text: str, messages: List[Dict], max_length: int) Dict
|
||
+async adjust_tone(conversation_id: str, selected_text: str, full_text: str, tone: str, messages: List[Dict]) Dict
|
||
+async polish_text(conversation_id: str, text: str, action: str, messages: List[Dict]) Dict
|
||
+async rewrite_versions(conversation_id: str, current_text: str, messages: List[Dict], generate_count: int, include_knowledge: bool) Dict
|
||
-async _call_wingman_api(context_messages: List[Dict], temperature: float) Optional[str]
|
||
-_build_context_messages(messages: List[Dict], system_prompt: str) List[Dict]
|
||
-async _get_cache(key: str) Optional[Dict]
|
||
-async _set_cache(key: str, value: Dict, ttl: int) void
|
||
-_make_cache_key(text: str, conversation_id: str) str
|
||
-async _search_knowledge(query: str) Optional[str]
|
||
-_parse_json_response(content: str, default: Dict) Dict
|
||
}
|
||
|
||
class WingmanAPIRouter {
|
||
+POST autocomplete(conversation_id, request: AutocompleteRequest, agent, db, wingman_service)
|
||
+POST tone_adjust(conversation_id, request: ToneAdjustRequest, agent, db, wingman_service)
|
||
+POST polish(conversation_id, request: PolishRequest, agent, db, wingman_service)
|
||
+POST rewrite(conversation_id, request: RewriteRequest, agent, db, wingman_service)
|
||
-async _validate_conversation(conversation_id, agent, db) Conversation
|
||
-async _get_recent_messages(conversation_id, db, limit) List[Dict]
|
||
}
|
||
|
||
class AutocompleteRequest {
|
||
+str current_text
|
||
+int cursor_position
|
||
+int max_length
|
||
}
|
||
|
||
class ToneAdjustRequest {
|
||
+str selected_text
|
||
+str full_text
|
||
+str tone
|
||
}
|
||
|
||
class PolishRequest {
|
||
+str text
|
||
+str action
|
||
+bool conversation_context
|
||
}
|
||
|
||
class RewriteRequest {
|
||
+str current_text
|
||
+int generate_count
|
||
+bool include_knowledge
|
||
}
|
||
|
||
class WingmanTS {
|
||
+autocomplete(convId: str, text: str, cursorPos: int, signal: AbortSignal) Promise~AutocompleteResult~
|
||
+adjustTone(convId: str, selectedText: str, fullText: str, tone: ToneType, signal: AbortSignal) Promise~ToneAdjustResult~
|
||
+polishText(convId: str, text: str, action: PolishAction, signal: AbortSignal) Promise~PolishResult~
|
||
+rewriteVersions(convId: str, currentText: str, signal: AbortSignal) Promise~RewriteResult~
|
||
}
|
||
|
||
class UseAiAssist {
|
||
+Ref~string~ ghostText
|
||
+Ref~boolean~ isCompletLoading
|
||
+Ref~boolean~ autocompleteEnabled
|
||
+Ref~boolean~ tonePopoverVisible
|
||
+Ref~boolean~ polishPanelVisible
|
||
+Ref~boolean~ rewritePanelVisible
|
||
+triggerAutocomplete() void
|
||
+acceptGhostText() void
|
||
+clearGhostText() void
|
||
+adjustTone(selectedText: str, fullText: str, tone: ToneType) Promise~void~
|
||
+polishText(action: PolishAction) Promise~void~
|
||
+rewriteVersions() Promise~void~
|
||
+cleanup() void
|
||
}
|
||
|
||
class GhostText {
|
||
+Props: ghostText: string, textareaRef: HTMLTextAreaElement
|
||
+Emits: accept, dismiss
|
||
-calculateCursorPos(textarea: HTMLTextAreaElement) {x: number, y: number}
|
||
}
|
||
|
||
class AiAssistToolbar {
|
||
+Props: autocompleteEnabled: boolean, hasSelection: boolean, hasText: boolean
|
||
+Emits: toggleAutocomplete, toneAdjust, polish, rewrite
|
||
}
|
||
|
||
class ToneAdjustPopover {
|
||
+Props: visible: boolean, loading: boolean, result: ToneAdjustResult|null, originalText: string
|
||
+Emits: select(tone: ToneType), replace, cancel
|
||
}
|
||
|
||
class PolishPanel {
|
||
+Props: visible: boolean, loading: boolean, result: PolishResult|null, originalText: string
|
||
+Emits: action(action: PolishAction), replace(text: string), cancel
|
||
}
|
||
|
||
class RewritePanel {
|
||
+Props: visible: boolean, loading: boolean, result: RewriteResult|null
|
||
+Emits: replace(text: string), append(text: string), cancel
|
||
}
|
||
|
||
class ReplyBox {
|
||
-UseAiAssist aiAssist
|
||
-Ref~string~ inputText
|
||
-HTMLTextAreaElement inputRef
|
||
+handleKeydown(event: KeyboardEvent) void
|
||
+handleInput() void
|
||
}
|
||
|
||
%% 后端关系
|
||
WingmanAPIRouter --> WingmanService : Depends (DI)
|
||
WingmanAPIRouter --> AutocompleteRequest : 验证请求体
|
||
WingmanAPIRouter --> ToneAdjustRequest : 验证请求体
|
||
WingmanAPIRouter --> PolishRequest : 验证请求体
|
||
WingmanAPIRouter --> RewriteRequest : 验证请求体
|
||
|
||
%% 前端关系
|
||
WingmanTS --> AutocompleteRequest : HTTP 请求
|
||
UseAiAssist --> WingmanTS : 调用 API
|
||
ReplyBox --> UseAiAssist : composable
|
||
ReplyBox --> GhostText : 渲染幽灵文字
|
||
ReplyBox --> AiAssistToolbar : 渲染工具栏
|
||
ReplyBox --> ToneAdjustPopover : 弹出浮层
|
||
ReplyBox --> PolishPanel : 弹出面板
|
||
ReplyBox --> RewritePanel : 弹出面板
|
||
```
|
||
|
||
> **类图文件**:`docs/03-技术架构/05-架构图/ai-assist-class-diagram.mermaid`
|
||
|
||
---
|
||
|
||
### 4. 程序调用流程
|
||
|
||
#### 4.1 实时自动补齐时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 坐席
|
||
participant RB as ReplyBox.vue
|
||
participant GT as GhostText.vue
|
||
participant Cmp as useAiAssist.ts
|
||
participant API as wingman.ts
|
||
participant BE as wingman.py<br/>(autocomplete)
|
||
participant Svc as WingmanService<br/>.generate_completion()
|
||
participant Redis as Redis
|
||
participant Dify as Dify AI
|
||
|
||
User->>RB: 输入文字
|
||
RB->>Cmp: handleInput() → triggerAutocomplete()
|
||
|
||
Note over Cmp: debounce 800ms 等待
|
||
Note over Cmp: 输入停顿 > 800ms 触发
|
||
|
||
Cmp->>Cmp: abort 上一个 AbortController
|
||
Cmp->>Cmp: 创建新 AbortController
|
||
|
||
Note over Cmp: 检查:输入 > 5 字符 && autocompleteEnabled
|
||
|
||
Cmp->>API: autocomplete(convId, text, cursorPos, signal)
|
||
API->>BE: POST /conversations/{id}/wingman/autocomplete
|
||
|
||
BE->>BE: _validate_conversation()
|
||
BE->>BE: _get_recent_messages(limit=5)
|
||
BE->>Svc: generate_completion(conv_id, text, messages)
|
||
|
||
Svc->>Svc: _make_cache_key(text, conv_id)
|
||
Svc->>Redis: GET wingman:autocomplete:{hash}
|
||
|
||
alt Redis 命中缓存
|
||
Redis-->>Svc: {completion, confidence}
|
||
Svc-->>BE: 缓存结果
|
||
else Redis 未命中
|
||
Redis-->>Svc: nil
|
||
Svc->>Svc: _build_context_messages(messages, _COMPLETION_SYSTEM_PROMPT)
|
||
Svc->>Svc: 追加 user 消息: "坐席正在输入:{text}\n请补齐"
|
||
Svc->>Dify: POST /chat/completions<br/>{temperature: 0.2}
|
||
Dify-->>Svc: "正在查看相关工单记录..."
|
||
Svc->>Redis: SETEX wingman:autocomplete:{hash} 30s
|
||
Svc-->>BE: {completion, confidence}
|
||
end
|
||
|
||
BE-->>API: {code: 0, data: {completion, confidence}}
|
||
API-->>Cmp: AutocompleteResult
|
||
|
||
Note over Cmp: 检查:未被 abort && completion 非空
|
||
|
||
Cmp->>Cmp: ghostText.value = completion
|
||
Cmp->>GT: 渲染幽灵文字(灰色斜体)
|
||
GT-->>User: 光标位置显示灰色补齐文字
|
||
|
||
alt Tab 键接受
|
||
User->>RB: 按 Tab
|
||
RB->>Cmp: acceptGhostText()
|
||
Cmp->>RB: inputText += ghostText
|
||
Cmp->>Cmp: ghostText = ''
|
||
GT-->>User: 幽灵文字消失,文字变为正常颜色
|
||
else 继续输入
|
||
User->>RB: 继续输入
|
||
RB->>Cmp: clearGhostText()
|
||
Cmp->>Cmp: ghostText = ''
|
||
Note over Cmp: 重新触发 debounce
|
||
else Esc 键
|
||
User->>RB: 按 Esc
|
||
RB->>Cmp: clearGhostText()
|
||
Cmp->>Cmp: ghostText = ''
|
||
end
|
||
```
|
||
|
||
#### 4.2 语气调整时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 坐席
|
||
participant RB as ReplyBox.vue
|
||
participant TAP as ToneAdjustPopover.vue
|
||
participant Cmp as useAiAssist.ts
|
||
participant API as wingman.ts
|
||
participant BE as wingman.py<br/>(tone-adjust)
|
||
participant Svc as WingmanService<br/>.adjust_tone()
|
||
participant Dify as Dify AI
|
||
|
||
User->>RB: 选中输入框文字(≥ 5 字符)
|
||
User->>RB: 点击工具栏"语气"按钮
|
||
|
||
RB->>TAP: visible = true(弹出浮层)
|
||
TAP-->>User: 显示 3 种语气选项
|
||
|
||
User->>TAP: 点击"专业"
|
||
TAP->>Cmp: adjustTone(selectedText, fullText, 'professional')
|
||
|
||
Cmp->>Cmp: toneLoading = true
|
||
Cmp->>API: adjustTone(convId, selectedText, fullText, 'professional', signal)
|
||
API->>BE: POST /conversations/{id}/wingman/tone-adjust
|
||
BE->>BE: _validate_conversation()
|
||
BE->>BE: _get_recent_messages(limit=5)
|
||
BE->>Svc: adjust_tone(conv_id, selectedText, fullText, 'professional', messages)
|
||
|
||
Svc->>Svc: _build_context_messages(messages, _TONE_ADJUST_SYSTEM_PROMPT)
|
||
Svc->>Svc: 追加 user 消息: "原文:{selectedText}\n完整内容:{fullText}\n改写为{tone}风格"
|
||
Svc->>Dify: POST /chat/completions<br/>{temperature: 0.3}
|
||
Dify-->>Svc: "经排查,您的VPN连接异常..."
|
||
|
||
Svc-->>BE: {rewritten_text, tone, changes_summary}
|
||
BE-->>API: {code: 0, data: {...}}
|
||
API-->>Cmp: ToneAdjustResult
|
||
|
||
Cmp->>Cmp: toneResult = result
|
||
Cmp->>Cmp: toneLoading = false
|
||
Cmp->>TAP: 显示原文/改写文对比
|
||
|
||
TAP-->>User: 原文 → 改写文 + 变更说明
|
||
|
||
alt 点击"替换"
|
||
User->>TAP: 点击"替换"
|
||
TAP->>RB: emit('replace', rewritten_text)
|
||
RB->>RB: 用 rewritten_text 替换选中区域
|
||
RB->>TAP: visible = false
|
||
else 点击"取消"或外部
|
||
User->>TAP: 点击取消
|
||
TAP->>RB: emit('cancel')
|
||
RB->>TAP: visible = false
|
||
end
|
||
```
|
||
|
||
#### 4.3 文字润色时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 坐席
|
||
participant RB as ReplyBox.vue
|
||
participant PP as PolishPanel.vue
|
||
participant Cmp as useAiAssist.ts
|
||
participant API as wingman.ts
|
||
participant BE as wingman.py<br/>(polish)
|
||
participant Svc as WingmanService<br/>.polish_text()
|
||
participant Dify as Dify AI
|
||
|
||
User->>RB: 点击工具栏"润色"按钮
|
||
RB->>PP: visible = true(弹出精修面板)
|
||
PP-->>User: 显示原文 + 3 个操作按钮
|
||
|
||
User->>PP: 点击"扩写"
|
||
PP->>Cmp: polishText('expand')
|
||
|
||
Cmp->>Cmp: polishLoading = true
|
||
Cmp->>API: polishText(convId, inputText, 'expand', signal)
|
||
API->>BE: POST /conversations/{id}/wingman/polish
|
||
BE->>BE: _validate_conversation()
|
||
BE->>BE: _get_recent_messages(limit=5)
|
||
BE->>Svc: polish_text(conv_id, text, 'expand', messages)
|
||
|
||
Svc->>Svc: _build_context_messages(messages, _POLISH_SYSTEM_PROMPT)
|
||
Svc->>Svc: 追加 user 消息: "对以下文字进行扩写:{text}"
|
||
Svc->>Dify: POST /chat/completions<br/>{temperature: 0.3}
|
||
Dify-->>Svc: "建议您按以下步骤操作:\n1. 退出VPN..."
|
||
|
||
Svc-->>BE: {polished_text, action, changes_summary}
|
||
BE-->>API: {code: 0, data: {...}}
|
||
API-->>Cmp: PolishResult
|
||
|
||
Cmp->>Cmp: polishResult = result
|
||
Cmp->>Cmp: polishLoading = false
|
||
Cmp->>PP: 显示左右对比(原文 | 结果)
|
||
|
||
PP-->>User: 左侧原文 | 右侧结果(可编辑)+ 变更说明
|
||
|
||
Note over User,PP: 坐席可在结果区域手动编辑
|
||
|
||
alt 点击"替换全部"
|
||
User->>PP: 编辑后点击"替换全部"
|
||
PP->>RB: emit('replace', editedText)
|
||
RB->>RB: inputText = editedText
|
||
RB->>PP: visible = false
|
||
else 切换操作
|
||
User->>PP: 点击"压缩"或"纠错"
|
||
PP->>Cmp: polishText('compress' | 'correct')
|
||
Note over Cmp,Dify: 重新调用流程
|
||
else 点击"取消"
|
||
User->>PP: 点击取消
|
||
PP->>RB: emit('cancel')
|
||
RB->>PP: visible = false
|
||
end
|
||
```
|
||
|
||
#### 4.4 智能改写时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant User as 坐席
|
||
participant RB as ReplyBox.vue
|
||
participant RP as RewritePanel.vue
|
||
participant Cmp as useAiAssist.ts
|
||
participant API as wingman.ts
|
||
participant BE as wingman.py<br/>(rewrite)
|
||
participant Svc as WingmanService<br/>.rewrite_versions()
|
||
participant RAG as RAGFlow
|
||
participant Dify as Dify AI
|
||
|
||
User->>RB: 点击工具栏"改写"按钮
|
||
RB->>RP: visible = true(弹出选择面板)
|
||
|
||
RP->>Cmp: rewriteVersions()
|
||
Cmp->>Cmp: rewriteLoading = true
|
||
Cmp->>API: rewriteVersions(convId, inputText, signal)
|
||
API->>BE: POST /conversations/{id}/wingman/rewrite
|
||
BE->>BE: _validate_conversation()
|
||
BE->>BE: _get_recent_messages(limit=10)
|
||
BE->>Svc: rewrite_versions(conv_id, text, messages, generate_count=3, include_knowledge=true)
|
||
|
||
Note over Svc: 构建上下文
|
||
|
||
Svc->>Svc: _build_context_messages(messages, _REWRITE_SYSTEM_PROMPT)
|
||
|
||
par 知识库检索(并行)
|
||
Svc->>RAG: retrieval(question=当前问题, dataset_ids=默认知识库)
|
||
RAG-->>Svc: 检索到 3 个相关文档片段
|
||
Svc->>Svc: 将知识片段拼入 system prompt
|
||
and 准备 Dify 调用
|
||
Note over Svc: 等待知识检索完成
|
||
end
|
||
|
||
Svc->>Dify: POST /chat/completions<br/>{temperature: 0.6}
|
||
Dify-->>Svc: "版本1\n---\n版本2\n---\n版本3"
|
||
|
||
Svc->>Svc: 按 "---" 分割为 3 个版本
|
||
Svc->>Svc: 标注 style 和 source
|
||
|
||
Svc-->>BE: {versions: [{text, style, source}, ...]}
|
||
BE-->>API: {code: 0, data: {...}}
|
||
API-->>Cmp: RewriteResult
|
||
|
||
Cmp->>Cmp: rewriteResult = result
|
||
Cmp->>Cmp: rewriteLoading = false
|
||
Cmp->>RP: 显示 3 个版本卡片
|
||
|
||
RP-->>User: 版本1(简洁直接)/ 版本2(详细带步骤)/ 版本3(带知识库引用)
|
||
|
||
alt 选择版本 → 替换
|
||
User->>RP: 点击版本卡片 → "替换"
|
||
RP->>RB: emit('replace', versionText)
|
||
RB->>RB: inputText = versionText
|
||
RB->>RP: visible = false
|
||
else 选择版本 → 追加
|
||
User->>RP: 点击版本卡片 → "追加"
|
||
RP->>RB: emit('append', versionText)
|
||
RB->>RB: inputText += '\n' + versionText
|
||
RB->>RP: visible = false
|
||
else 点击"取消"
|
||
User->>RP: 点击取消
|
||
RP->>RB: emit('cancel')
|
||
RB->>RP: visible = false
|
||
end
|
||
```
|
||
|
||
> **时序图文件**:`docs/03-技术架构/05-架构图/ai-assist-sequence-diagram.mermaid`
|
||
|
||
---
|
||
|
||
### 5. 待明确事项
|
||
|
||
| # | 待明确问题 | 当前假设 | 影响范围 |
|
||
|---|-----------|---------|---------|
|
||
| 1 | **补齐建议条的过渡方案**:PRD §2.1.3 提到可先采用"输入框下方建议条"过渡,但当前需求确认为内联幽灵文字。是否坚持内联方案? | 假设坚持内联幽灵文字方案(GhostText.vue 通过隐藏 div 镜像计算光标位置) | GhostText.vue 实现复杂度 |
|
||
| 2 | **补齐的置信度阈值**:PRD §9 风险表提到"低置信度不显示",但未定义具体阈值 | 假设 confidence < 0.5 时不显示幽灵文字 | useAiAssist.ts `triggerAutocomplete` 逻辑 |
|
||
| 3 | **Shift+Tab 接受一个词**:PRD §2.1.2 提到 Shift+Tab 接受一个词,"词"的界定标准? | 假设按空格/标点分词,接受到下一个空格或标点 | GhostText.vue / useAiAssist.ts 键盘处理 |
|
||
| 4 | **改写功能 RAGFlow dataset_ids**:`rewrite_versions()` 需要指定知识库 ID,当前默认使用哪个? | 假设使用系统默认知识库(通过配置或 RAGFlow 默认 dataset),需确认 | WingmanService._search_knowledge() |
|
||
| 5 | **语气/润色/改写是否记录日志**:PRD §6 提到"语气/润色/改写可记录用于后续优化",记录到哪个表? | 假设首版不记录,后续迭代增加 | 无额外文件(首版不实现) |
|
||
| 6 | **补齐 API 独立超时**:PRD §4.1.3 提到补齐 API 超时 3s,但 §2.1.2 提到 1.5s 超时不显示。最终以后端超时还是前端超时为准? | 假设前端 AbortController 超时 1.5s(用户感知优先),后端 httpx 超时 3s(兜底) | wingman.ts timeout / wingman_service.py timeout |
|
||
| 7 | **AI 工具栏按钮位置**:PRD §3.1 显示 AI 按钮在现有按钮右侧、发送按钮左侧。是否与布局优化任务的工具栏重构冲突? | 假设 AI 按钮独立分组(用分隔线隔开),不与布局优化任务的工具栏重构冲突 | ReplyBox.vue 模板 |
|
||
|
||
---
|
||
|
||
## Part B: 任务分解
|
||
|
||
### 6. 依赖包列表
|
||
|
||
#### 后端(Python / pip)
|
||
|
||
| 包名 | 版本 | 用途 | 是否新增 |
|
||
|------|------|------|---------|
|
||
| `httpx` | 现有 | 异步 HTTP 客户端(调用 Dify API) | ❌ 已有 |
|
||
| `redis[asyncio]` | 现有 | Redis 异步客户端(补齐结果缓存) | ❌ 已有 |
|
||
| `pydantic` | 现有 | 请求体 Schema 验证 | ❌ 已有 |
|
||
|
||
> **后端无新增依赖包**。所有功能复用现有 httpx + redis + pydantic + RAGFlow Client。
|
||
|
||
#### 前端(npm)
|
||
|
||
| 包名 | 版本 | 用途 | 是否新增 |
|
||
|------|------|------|---------|
|
||
| `axios` | 现有 | HTTP 请求(复用 apiClient 实例) | ❌ 已有 |
|
||
| `element-plus` | 现有 | UI 组件(el-popover, el-button, el-tooltip) | ❌ 已有 |
|
||
| `lodash-es` | 现有 | debounce 函数(补齐 800ms 防抖) | ❌ 已有 |
|
||
| `@vueuse/core` | 现有 | useDebounceFn 或 watchDebounced(如已引入) | ❌ 已有 |
|
||
|
||
> **前端无新增依赖包**。所有功能复用现有 Vue3 + Element Plus + lodash-es + axios。
|
||
|
||
---
|
||
|
||
### 7. 任务列表
|
||
|
||
| 任务 ID | 任务名称 | 源文件 | 依赖 | 优先级 | 预估工时 |
|
||
|---------|---------|--------|------|--------|---------|
|
||
| **T01** | **后端服务层与 API 端点** | `backend/app/services/wingman_service.py`(修改)<br>`backend/app/api/wingman.py`(修改)<br>`backend/app/schemas/wingman_assist.py`(新建)<br>`backend/app/dependencies/__init__.py`(修改) | 无 | P0 | 2 天 |
|
||
| **T02** | **前端 API 封装与状态管理** | `frontend-agent/src/api/wingman.ts`(修改)<br>`frontend-agent/src/composables/useAiAssist.ts`(新建)<br>`frontend-agent/src/types/ai-assist.d.ts`(新建) | T01 | P0 | 1 天 |
|
||
| **T03** | **AI 辅助 UI 组件(补齐 + 语气 + 工具栏)** | `frontend-agent/src/components/chat/ai-assist/GhostText.vue`(新建)<br>`frontend-agent/src/components/chat/ai-assist/ToneAdjustPopover.vue`(新建)<br>`frontend-agent/src/components/chat/ai-assist/AiAssistToolbar.vue`(新建) | T01, T02 | P1 | 2 天 |
|
||
| **T04** | **AI 辅助 UI 组件(润色 + 改写)+ ReplyBox 集成** | `frontend-agent/src/components/chat/ai-assist/PolishPanel.vue`(新建)<br>`frontend-agent/src/components/chat/ai-assist/RewritePanel.vue`(新建)<br>`frontend-agent/src/components/chat/ReplyBox.vue`(修改) | T01, T02, T03 | P1 | 2 天 |
|
||
|
||
#### 任务详细说明
|
||
|
||
**T01: 后端服务层与 API 端点**
|
||
|
||
1. 在 `schemas/wingman_assist.py` 中定义 4 个 Pydantic 请求模型(AutocompleteRequest / ToneAdjustRequest / PolishRequest / RewriteRequest)
|
||
2. 在 `wingman_service.py` 中:
|
||
- 新增 4 个 system prompt 常量
|
||
- 改造 `_call_wingman_api()` 增加 `temperature` 参数(默认 0.3)
|
||
- 新增 `generate_completion()` — 含 Redis 缓存读写
|
||
- 新增 `adjust_tone()` — 按 tone 动态构建 prompt
|
||
- 新增 `polish_text()` — 按 action 动态构建 prompt
|
||
- 新增 `rewrite_versions()` — 含 RAGFlow 知识检索 + 版本分割
|
||
- 新增 `_get_cache()` / `_set_cache()` / `_make_cache_key()` / `_search_knowledge()` 内部方法
|
||
- `__init__` 增加 `redis_client` 可选参数
|
||
3. 在 `wingman.py` 中新增 4 个 API 端点,复用 `_validate_conversation` + `_get_recent_messages`
|
||
4. 在 `dependencies/__init__.py` 中修改 `dep_wingman_service` 注入 Redis 客户端
|
||
|
||
**T02: 前端 API 封装与状态管理**
|
||
|
||
1. 在 `wingman.ts` 中新增 4 个 API 函数(autocomplete / adjustTone / polishText / rewriteVersions),均接受可选 `signal: AbortSignal` 参数
|
||
2. 在 `types/ai-assist.d.ts` 中定义共享类型(ToneType / PolishAction / 各 Result 接口)
|
||
3. 在 `useAiAssist.ts` 中实现 composable:
|
||
- 自动补齐:debounce 800ms + AbortController 生命周期管理 + ghostText 状态
|
||
- 语气/润色/改写:loading 状态 + result 状态 + 面板 visible 状态
|
||
- `cleanup()` 函数:组件卸载时 abort 所有进行中的请求
|
||
|
||
**T03: AI 辅助 UI 组件(补齐 + 语气 + 工具栏)**
|
||
|
||
1. `GhostText.vue` — 幽灵文字渲染层:
|
||
- 创建隐藏 div 镜像 textarea 样式
|
||
- 计算光标像素坐标(截取光标前文本 → 镜像 div → getBoundingClientRect)
|
||
- 在光标位置覆盖灰色斜体文字
|
||
- 监听 Tab(接受全部)/ Shift+Tab(接受一个词)/ Esc(清除)
|
||
2. `ToneAdjustPopover.vue` — 语气选择浮层:
|
||
- 3 种语气按钮(专业/友好/简洁)
|
||
- 原文/改写文对比展示
|
||
- 替换/取消按钮
|
||
3. `AiAssistToolbar.vue` — AI 辅助工具栏:
|
||
- 补齐开关(toggle 按钮)
|
||
- 语气/润色/改写按钮
|
||
- 按钮状态管理(禁用条件:无选中文字时禁用语气按钮,无文本时禁用润色/改写按钮)
|
||
|
||
**T04: AI 辅助 UI 组件(润色 + 改写)+ ReplyBox 集成**
|
||
|
||
1. `PolishPanel.vue` — 润色精修面板:
|
||
- 扩写/压缩/纠错 3 个操作按钮
|
||
- 左右对比布局(原文 | 结果,结果可编辑)
|
||
- 替换全部/取消按钮
|
||
2. `RewritePanel.vue` — 改写版本选择面板:
|
||
- 3 个版本卡片纵向排列(可滚动)
|
||
- 每个版本支持"替换"/"追加"
|
||
3. `ReplyBox.vue` 改造:
|
||
- 引入 `useAiAssist` composable
|
||
- 在工具栏中插入 `AiAssistToolbar` 组件(分隔线隔开)
|
||
- 在 textarea 区域叠加 `GhostText` 组件
|
||
- 添加 `ToneAdjustPopover` / `PolishPanel` / `RewritePanel` 组件
|
||
- 改造 `handleKeydown`:Tab 键接受补齐、Esc 键清除补齐
|
||
- 改造 input 事件:触发 debounce 补齐
|
||
- 组件卸载时调用 `aiAssist.cleanup()`
|
||
|
||
---
|
||
|
||
### 8. 共享知识
|
||
|
||
#### 8.1 后端约定
|
||
|
||
```
|
||
- 所有 API 响应使用统一格式:{code: 0, data: {...}, message: "success"}
|
||
- code=0 表示成功,非0表示错误
|
||
- 使用 success_response(data=result) 返回
|
||
- 所有端点需要坐席认证:Depends(get_current_agent)
|
||
- 所有端点复用 _validate_conversation() 验证会话存在性
|
||
- 消息历史获取复用 _get_recent_messages(),按时间正序排列
|
||
- Dify API 调用统一通过 _call_wingman_api(),OpenAI 兼容格式
|
||
- _call_wingman_api() 改造后 temperature 参数默认 0.3,现有方法不传参则行为不变
|
||
- AI 不可用时所有方法返回默认值(空字符串/空列表),不抛异常
|
||
- Redis 缓存 key 格式:wingman:autocomplete:{md5(text + conversation_id)},TTL=30s
|
||
- Redis 不可用时缓存降级(跳过读写,直接调用 Dify)
|
||
- RAGFlow 检索通过 build_ragflow_client() 获取客户端,不可用时返回 None 降级
|
||
- Pydantic Schema 放在 schemas/wingman_assist.py,与现有 schemas/ 目录约定一致
|
||
```
|
||
|
||
#### 8.2 前端约定
|
||
|
||
```
|
||
- 所有 API 调用使用 apiClient(src/api/index.ts 导出的 axios 实例)
|
||
- 响应拦截器自动提取 res.data(Scheme A),API 函数直接返回内层 data
|
||
- 错误时 reject {code, message},调用方 try-catch
|
||
- AbortController 管理补齐请求生命周期:
|
||
- 每次新请求前 abort 上一个 controller
|
||
- 组件卸载时 abort 所有进行中的请求
|
||
- 补齐 debounce 800ms,使用 lodash-es 的 debounce 或 @vueuse/core 的 useDebounceFn
|
||
- 幽灵文字样式:
|
||
- color: #B4B2A9(灰色)
|
||
- font-style: italic(斜体)
|
||
- pointer-events: none(不可点击)
|
||
- user-select: none(不可选中)
|
||
- AI 按钮使用统一紫色系 #534AB7,与常规工具栏按钮区分
|
||
- 组件命名:PascalCase,文件名与组件名一致
|
||
- Composable 命名:use 前缀(useAiAssist),放在 composables/ 目录
|
||
- TypeScript 类型定义:API 响应类型放在 wingman.ts,共享类型放在 types/ai-assist.d.ts
|
||
- AI 请求失败时静默处理(不弹 ElMessage 错误提示),不影响坐席正常输入
|
||
- 面板/浮层使用 Element Plus 的 el-popover 或自定义浮层,z-index 不低于 100
|
||
```
|
||
|
||
#### 8.3 错误处理策略
|
||
|
||
```
|
||
- 后端:WingmanService 所有方法 try-catch 全部异常,返回默认值
|
||
- generate_completion 降级 → {completion: "", confidence: 0.0}
|
||
- adjust_tone 降级 → {rewritten_text: "", tone: "", changes_summary: "AI 服务暂不可用"}
|
||
- polish_text 降级 → {polished_text: "", action: "", changes_summary: "AI 服务暂不可用"}
|
||
- rewrite_versions 降级 → {versions: []}
|
||
- 前端:useAiAssist 中 try-catch 请求错误
|
||
- 补齐失败 → ghostText 保持空,不显示
|
||
- 语气/润色/改写失败 → loading=false,面板保持打开,显示重试按钮
|
||
- AbortError(请求被取消)→ 静默忽略,不处理
|
||
```
|
||
|
||
#### 8.4 性能约定
|
||
|
||
```
|
||
- 补齐 API:前端 AbortController 超时 1.5s,后端 httpx 超时 3s(兜底)
|
||
- 语气/润色 API:前端超时 3s(与 apiClient 默认 20s 不同,需单独设置)
|
||
- 改写 API:前端超时 5s(需单独设置)
|
||
- 补齐 Redis 缓存 TTL=30s,避免短时间重复输入相同前缀时重复调用 Dify
|
||
- 上下文消息条数:补齐/语气/润色取最近 5 条,改写取最近 10 条
|
||
- 改写功能的 RAGFlow 检索与 Dify 调用串行(先检索知识 → 注入 prompt → 调用 Dify)
|
||
```
|
||
|
||
---
|
||
|
||
### 9. 任务依赖图
|
||
|
||
```mermaid
|
||
graph LR
|
||
T01[T01: 后端服务层与 API 端点<br/>wingman_service.py + wingman.py<br/>+ schemas + dependencies<br/>预估 2 天]
|
||
T02[T02: 前端 API 封装与状态管理<br/>wingman.ts + useAiAssist.ts<br/>+ types<br/>预估 1 天]
|
||
T03[T03: UI 组件 - 补齐+语气+工具栏<br/>GhostText + ToneAdjustPopover<br/>+ AiAssistToolbar<br/>预估 2 天]
|
||
T04[T04: UI 组件 - 润色+改写+集成<br/>PolishPanel + RewritePanel<br/>+ ReplyBox 改造<br/>预估 2 天]
|
||
|
||
T01 --> T02
|
||
T01 --> T03
|
||
T02 --> T03
|
||
T01 --> T04
|
||
T02 --> T04
|
||
T03 --> T04
|
||
|
||
style T01 fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
|
||
style T02 fill:#e8f5e9,stroke:#388e3c,stroke-width:2px
|
||
style T03 fill:#fff3e0,stroke:#f57c00,stroke-width:2px
|
||
style T04 fill:#fce4ec,stroke:#c62828,stroke-width:2px
|
||
```
|
||
|
||
**关键路径**:T01 → T02 → T03 → T04(总计约 7 天)
|
||
|
||
**并行机会**:
|
||
- T01 完成后,T02 可独立开始
|
||
- T02 完成后,T03 和 T04 的组件开发可部分并行(但 T04 依赖 T03 的 AiAssistToolbar)
|
||
- T01 的后端 API 可用 Mock 数据先行联调前端组件
|
||
|
||
---
|
||
|
||
## 附录:Dify System Prompt 汇总
|
||
|
||
### A.1 自动补齐(temperature=0.2)
|
||
|
||
```
|
||
你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
|
||
要求:
|
||
1. 补齐内容自然衔接当前文字,不要重复已有内容
|
||
2. 长度控制在1-2个短句,不超过80字
|
||
3. 语气专业、简洁,符合IT服务规范
|
||
4. 只返回补齐的文字,不要加引号或其他标记
|
||
```
|
||
|
||
### A.2 语气调整(temperature=0.3)
|
||
|
||
```
|
||
你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
|
||
语气定义:
|
||
- 专业:使用准确的技术术语,结构化表达,去除口语化内容
|
||
- 友好:适当增加问候和关心语,语气更亲和
|
||
- 简洁:去除冗余,直奔主题,控制字数
|
||
要求:
|
||
1. 保持原意不变,只调整语气和表达方式
|
||
2. 改写后的文字长度与原文相近(±30%)
|
||
3. 符合IT服务坐席的专业规范
|
||
4. 只返回改写后的文字,不要加引号或解释
|
||
```
|
||
|
||
### A.3 文字润色(temperature=0.3)
|
||
|
||
```
|
||
你是一个IT服务文字精修助手。对坐席输入的文字进行{action}处理。
|
||
操作定义:
|
||
- 扩写:在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整
|
||
- 压缩:精简表达,去除重复和冗余,保留核心信息,控制字数
|
||
- 纠错:检查并修正错别字、语法错误、标点符号、格式问题
|
||
要求:
|
||
1. 保持原意不变
|
||
2. 只返回处理后的文字,不要加引号或解释
|
||
```
|
||
|
||
### A.4 智能改写(temperature=0.6)
|
||
|
||
```
|
||
你是一个IT服务智能回复助手。基于对话上下文和知识库,为坐席生成3个不同风格的备选回复。
|
||
版本要求:
|
||
- 版本1:简洁直接,一句话说明问题和解决方案
|
||
- 版本2:详细带步骤,包含操作步骤和注意事项
|
||
- 版本3:带知识库引用,引用相关文档并给出权威解决方案
|
||
要求:
|
||
1. 3个版本内容不重复,各有侧重
|
||
2. 每个版本独立成段,用---分隔
|
||
3. 只返回回复内容,不要加额外解释
|
||
```
|