facc04aa65
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交 (5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。 一、docs 结构整改(整改 #14) 根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入, 随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录 树并存于 docs/,共 791 文件、双分类体系冲突。 修复动作: - b2 同名异主题文件改名迁移保全 9 个 - C 类 39 个孤立文件按主题正确归类 - A/B1 类 222 个重复文件删除(新结构已有内容副本) - 9 个旧独有空目录删除 - 270 处内部引用按 verified 映射改写 - 整改记录 #14 登记于 04-运维文档/部署运维 结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。 残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。 二、compose 双目录对齐(消除踩坑 A) - docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist 改为 src/frontend-*/dist(h5 / agent / admin / terminal) - docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/ - 效果:本地 docker compose up 不再把根目录 stale dist 挂回, 与线上一致,分叉隐患消除(已 docker compose config 校验通过) 防复发铁律: - 重构须提交;仓库修复须 git stash -u 或先 commit - 新结构须 git add 并提交,避免再次 untracked 复活 - H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
1503 lines
54 KiB
Markdown
1503 lines
54 KiB
Markdown
# 坐席端 AI 辅助消息框与布局优化 — 系统架构设计
|
||
|
||
> **文档版本**: v1.0
|
||
> **日期**: 2026-07-11
|
||
> **架构师**: 宋献 (Simon)
|
||
> **关联 PRD**: `docs/01-产品文档/04-坐席工作台/坐席端AI辅助消息框-PRD.md`
|
||
> **关联布局方案**: `docs/08-历史归档/坐席端布局优化建议-archived-20260719.md`
|
||
> **技术栈**: Vue 3 + TypeScript + Element Plus + FastAPI + Dify AI
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- [Part A: 系统设计](#part-a-系统设计)
|
||
- [1. 架构概述](#1-架构概述)
|
||
- [2. 后端架构设计](#2-后端架构设计)
|
||
- [3. 前端架构设计](#3-前端架构设计)
|
||
- [4. 布局重构架构设计](#4-布局重构架构设计)
|
||
- [5. 数据流设计](#5-数据流设计)
|
||
- [6. 关键设计决策](#6-关键设计决策)
|
||
- [Part B: 接口与数据结构](#part-b-接口与数据结构)
|
||
- [7. API 规范](#7-api-规范)
|
||
- [8. Pydantic 模型](#8-pydantic-模型)
|
||
- [9. Dify Prompt 模板](#9-dify-prompt-模板)
|
||
- [10. TypeScript 类型定义](#10-typescript-类型定义)
|
||
- [Part C: 架构图](#part-c-架构图)
|
||
- [11. 类图](#11-类图)
|
||
- [12. 时序图](#12-时序图)
|
||
- [Part D: 任务分解](#part-d-任务分解)
|
||
- [13. 文件清单](#13-文件清单)
|
||
- [14. 开发计划](#14-开发计划)
|
||
- [15. 风险与缓解](#15-风险与缓解)
|
||
|
||
---
|
||
|
||
## Part A: 系统设计
|
||
|
||
### 1. 架构概述
|
||
|
||
#### 1.1 功能范围
|
||
|
||
本设计覆盖两大模块:
|
||
|
||
| 模块 | 功能 | PRD 参考 |
|
||
|------|------|---------|
|
||
| AI 辅助消息框 | 实时自动补齐、语气调整、文字润色、智能改写 | `坐席端AI辅助消息框-PRD.md` §2 |
|
||
| 布局优化 | 回复建议区、工具栏重构、右栏训练区、放大/缩小开关 | `坐席端布局优化建议.md` §3 |
|
||
|
||
#### 1.2 系统架构图
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ 坐席工作台 (Vue3 + Element Plus) │
|
||
│ │
|
||
│ ┌──────────┐ ┌───────────────────┐ ┌───────────────────────┐ │
|
||
│ │ 左栏 │ │ 中栏 │ │ 右栏 │ │
|
||
│ │ 会话用户 │ │ ┌──────────────┐ │ │ ┌───────────────────┐ │ │
|
||
│ │ 列表 │ │ │ UserInfoBar │ │ │ │ PanelModeToggle │ │ │
|
||
│ │ +待办 │ │ ├──────────────┤ │ │ │ (正常/放大) │ │ │
|
||
│ │ │ │ │ Troubleshoot │ │ │ ├───────────────────┤ │ │
|
||
│ │ │ │ ├──────────────┤ │ │ │ AiTrainingPanel │ │ │
|
||
│ │ │ │ │ 消息列表 │ │ │ │ ├ SmartTagEditor │ │ │
|
||
│ │ │ │ ├──────────────┤ │ │ │ ├ QualityFeedback │ │ │
|
||
│ │ │ │ │ReplySuggestArea│ │ │ │ ├ KnowledgeContrib│ │ │
|
||
│ │ │ │ │ (AI推荐+快回) │ │ │ │ └ UsageStats │ │ │
|
||
│ │ │ │ ├──────────────┤ │ │ └───────────────────┘ │ │
|
||
│ │ │ │ │ ReplyBox │ │ │ │ │
|
||
│ │ │ │ │ ┌──────────┐ │ │ │ │ │
|
||
│ │ │ │ │ │工具栏 │ │ │ │ │ │
|
||
│ │ │ │ │ │常规|AI │ │ │ │ │ │
|
||
│ │ │ │ │ └──────────┘ │ │ │ │ │
|
||
│ │ │ │ │ textarea │ │ │ │ │
|
||
│ │ │ │ └──────────────┘ │ │ │ │
|
||
│ └──────────┘ └───────────────────┘ └───────────────────────┘ │
|
||
└──────────────────────────┬───────────────────────────────────────┘
|
||
│ HTTP / WebSocket
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ FastAPI 后端 │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ wingman.py (路由层) │ │
|
||
│ │ POST /wingman/autocomplete ← 自动补齐 │ │
|
||
│ │ POST /wingman/tone-adjust ← 语气调整 │ │
|
||
│ │ POST /wingman/polish ← 文字润色 │ │
|
||
│ │ POST /wingman/rewrite ← 智能改写 │ │
|
||
│ │ + 现有: draft / summary / tags │ │
|
||
│ └────────────────────────┬────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ┌────────────────────────▼────────────────────────────────┐ │
|
||
│ │ WingmanService (服务层) │ │
|
||
│ │ generate_completion() ← 新增: 自动补齐 │ │
|
||
│ │ adjust_tone() ← 新增: 语气调整 │ │
|
||
│ │ polish_text() ← 新增: 文字润色 │ │
|
||
│ │ rewrite_versions() ← 新增: 智能改写 │ │
|
||
│ │ + 现有: generate_draft / generate_summary / suggest_tags│ │
|
||
│ │ │ │
|
||
│ │ _call_wingman_api() ← 改造: 支持 temperature 参数 │ │
|
||
│ │ _build_context_messages() ← 复用: 角色映射 │ │
|
||
│ └────────────────────────┬────────────────────────────────┘ │
|
||
│ │ httpx (OpenAI 兼容格式) │
|
||
└───────────────────────────┼──────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ Dify AI (外部服务) │
|
||
│ Wingman Agent: /chat/completions (OpenAI 兼容) │
|
||
│ RAGFlow: 知识库检索 (智能改写时调用) │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### 1.3 与现有系统的关系
|
||
|
||
| 现有组件 | 关系 | 说明 |
|
||
|----------|------|------|
|
||
| `WingmanService` | 扩展 | 新增 4 个方法,改造 `_call_wingman_api()` 支持 temperature |
|
||
| `wingman.py` 路由 | 扩展 | 新增 4 个端点 + Pydantic 请求模型 |
|
||
| `wingman.ts` API 层 | 扩展 | 新增 4 个前端 API 调用函数 |
|
||
| `ReplyBox.vue` | 改造 | 工具栏重构 + AI 工具按钮 + 补齐逻辑 |
|
||
| `AiAssistantPanel.vue` | 全面重构 | 移除回复前功能,改为训练区 |
|
||
| `AiRecommendInline.vue` | 删除 | 功能被 `AiRecommendBar.vue` 替代 |
|
||
| `AiSuggestReply.vue` | 删除 | 功能合并到 `AiRecommendBar.vue` |
|
||
| `QuickReplyPanel.vue` | 改造 | 重命名为 `QuickReplyBar.vue`,横向布局 |
|
||
|
||
---
|
||
|
||
### 2. 后端架构设计
|
||
|
||
#### 2.1 WingmanService 扩展设计
|
||
|
||
现有 `WingmanService` 包含 4 个方法(generate_draft / generate_summary / suggest_tags / generate_knowledge_suggestion),全部通过 `_call_wingman_api()` 调用 Dify。本设计新增 4 个方法并改造底层调用。
|
||
|
||
**新增方法签名**:
|
||
|
||
```python
|
||
class WingmanService:
|
||
# === 现有方法(保持不变) ===
|
||
# generate_draft()
|
||
# generate_summary()
|
||
# suggest_tags()
|
||
# generate_knowledge_suggestion()
|
||
|
||
# === 新增方法 ===
|
||
async def generate_completion(
|
||
self,
|
||
conversation_id: str,
|
||
current_text: str,
|
||
messages: List[Dict[str, Any]],
|
||
max_length: int = 80,
|
||
) -> Dict[str, Any]:
|
||
"""自动补齐:根据坐席当前输入内容补齐下一句"""
|
||
|
||
async def adjust_tone(
|
||
self,
|
||
conversation_id: str,
|
||
selected_text: str,
|
||
full_text: str,
|
||
tone: str, # professional / friendly / concise
|
||
messages: List[Dict[str, Any]],
|
||
) -> Dict[str, Any]:
|
||
"""语气调整:将选中文字改写为指定风格"""
|
||
|
||
async def polish_text(
|
||
self,
|
||
conversation_id: str,
|
||
text: str,
|
||
action: str, # expand / compress / correct
|
||
messages: List[Dict[str, Any]],
|
||
) -> Dict[str, Any]:
|
||
"""文字润色:对输入框全部内容进行扩写/压缩/纠错"""
|
||
|
||
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]:
|
||
"""智能改写:基于对话上下文+知识库生成多个备选版本"""
|
||
```
|
||
|
||
#### 2.2 _call_wingman_api 改造
|
||
|
||
**当前问题**:`_call_wingman_api()` 的 temperature 硬编码为 0.3,所有方法共用。新功能需要不同 temperature:
|
||
|
||
| 方法 | temperature | 理由 |
|
||
|------|-------------|------|
|
||
| generate_completion | 0.2 | 补齐需要高确定性,避免创造性发散 |
|
||
| adjust_tone | 0.3 | 改写需保持原意,适度创造性 |
|
||
| polish_text | 0.3 | 润色需保持原意,适度创造性 |
|
||
| rewrite_versions | 0.6 | 改写需要多样化输出,提高创造性 |
|
||
| generate_draft (现有) | 0.3 | 保持不变 |
|
||
| generate_summary (现有) | 0.3 | 保持不变 |
|
||
|
||
**改造方案**:
|
||
|
||
```python
|
||
# 改造前(当前代码)
|
||
async def _call_wingman_api(
|
||
self, context_messages: List[Dict[str, str]]
|
||
) -> Optional[str]:
|
||
payload = {
|
||
"model": "Chat",
|
||
"messages": context_messages,
|
||
"stream": False,
|
||
"temperature": 0.3, # ← 硬编码
|
||
}
|
||
|
||
# 改造后
|
||
async def _call_wingman_api(
|
||
self,
|
||
context_messages: List[Dict[str, str]],
|
||
temperature: float = 0.3, # ← 新增参数,默认值保持兼容
|
||
) -> Optional[str]:
|
||
payload = {
|
||
"model": "Chat",
|
||
"messages": context_messages,
|
||
"stream": False,
|
||
"temperature": temperature,
|
||
}
|
||
```
|
||
|
||
**兼容性**:现有 4 个方法调用时不传 temperature 参数,自动使用默认值 0.3,行为不变。
|
||
|
||
#### 2.3 上下文构建策略
|
||
|
||
现有 `_build_context_messages()` 将完整对话历史构建为 OpenAI Chat 格式。新功能需要差异化上下文:
|
||
|
||
| 方法 | 上下文消息条数 | 附加内容 | 原因 |
|
||
|------|--------------|---------|------|
|
||
| generate_completion | 最近 5 条 | 当前输入文本拼入最后一条 user 消息 | 补齐需要最近上下文 + 当前输入 |
|
||
| adjust_tone | 最近 5 条 | 选中文字 + 完整输入内容作为指令 | 改写需要理解选中文字的上下文 |
|
||
| polish_text | 最近 5 条 | 全部输入内容作为指令 | 润色针对输入框全部内容 |
|
||
| rewrite_versions | 最近 10 条 | 知识库检索结果(可选) | 改写需要更完整上下文 + 知识库引用 |
|
||
|
||
**实现方式**:复用 `_build_context_messages()` 构建基础对话上下文,然后在方法内部追加功能特定的指令消息:
|
||
|
||
```python
|
||
async def generate_completion(self, ...):
|
||
# 1. 构建基础上下文(最近5条对话 → system prompt + 历史消息)
|
||
context = self._build_context_messages(messages, self._COMPLETION_SYSTEM_PROMPT)
|
||
|
||
# 2. 追加当前输入作为最后一条 user 消息
|
||
context.append({
|
||
"role": "user",
|
||
"content": f"坐席正在输入的内容:{current_text}\n请补齐下一句话。"
|
||
})
|
||
|
||
# 3. 调用 Dify(temperature=0.2)
|
||
result = await self._call_wingman_api(context, temperature=0.2)
|
||
```
|
||
|
||
#### 2.4 智能改写的知识库集成
|
||
|
||
`rewrite_versions()` 在生成"带知识库引用"版本时需要调用 RAGFlow 检索:
|
||
|
||
```
|
||
坐席点击"改写"
|
||
→ 后端从对话消息提取关键词
|
||
→ 调用 RAGFlow API 检索相关知识
|
||
→ 将检索结果注入 Dify prompt
|
||
→ Dify 生成 3 个版本(含知识库引用版本)
|
||
```
|
||
|
||
**RAGFlow 调用**:复用现有 `RAGFlowClient`(位于 `app/integrations/`),无需新建。若 RAGFlow 不可用,降级为仅基于对话上下文生成 2 个版本。
|
||
|
||
#### 2.5 降级策略
|
||
|
||
与现有方法保持一致:Dify 不可用时返回默认值而非抛异常。
|
||
|
||
```python
|
||
# 降级返回示例(generate_completion)
|
||
{
|
||
"completion": "",
|
||
"confidence": 0.0,
|
||
"error": "Wingman 服务暂不可用"
|
||
}
|
||
|
||
# 降级返回示例(rewrite_versions)
|
||
{
|
||
"versions": [],
|
||
"error": "AI 服务暂不可用,请稍后重试"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 前端架构设计
|
||
|
||
#### 3.1 组件架构图
|
||
|
||
```
|
||
Workspace.vue (主布局)
|
||
├── ConversationList.vue (左栏 - 会话用户列表)
|
||
│ └── TodoPanel.vue (待办面板)
|
||
│
|
||
├── ChatArea.vue (中栏)
|
||
│ ├── UserInfoBar.vue (用户信息栏 - 详情默认折叠)
|
||
│ ├── TroubleshootBar.vue (排查步骤栏 - 默认折叠为图标条)
|
||
│ ├── MessageList.vue (消息列表)
|
||
│ ├── ReplySuggestArea.vue (回复建议区 - 新增)
|
||
│ │ ├── AiRecommendBar.vue (AI推荐条 - 合并自 AiRecommendInline + AiSuggestReply)
|
||
│ │ └── QuickReplyBar.vue (快速回复条 - 改造自 QuickReplyPanel)
|
||
│ └── ReplyBox.vue (输入框 - 改造)
|
||
│ ├── 工具栏 (单行左右分区)
|
||
│ │ ├── 常规工具组: [截图] [拍照] [表情] [文件] [邀请]
|
||
│ │ └── AI工具组: [补齐toggle] [语气] [润色] [改写]
|
||
│ ├── GhostTextOverlay.vue (幽灵文字覆盖层 - 新增)
|
||
│ ├── ToneAdjustPopover.vue (语气调整浮层 - 新增)
|
||
│ ├── PolishPanel.vue (润色精修面板 - 新增)
|
||
│ └── RewritePanel.vue (改写选择面板 - 新增)
|
||
│
|
||
└── AiAssistantPanel.vue (右栏 - 全面重构)
|
||
├── PanelModeToggle.vue (模式切换: 正常/放大 - 新增)
|
||
└── AiTrainingPanel.vue (训练区主体 - 新增)
|
||
├── SmartTagEditor.vue (智能标注)
|
||
├── QualityFeedback.vue (质量反馈)
|
||
├── KnowledgeContribute.vue (知识贡献)
|
||
└── UsageStats.vue (使用统计)
|
||
```
|
||
|
||
#### 3.2 Composable 设计
|
||
|
||
新增 3 个 composable 封装 AI 辅助逻辑:
|
||
|
||
| Composable | 职责 | 状态范围 |
|
||
|------------|------|---------|
|
||
| `useAutoComplete.ts` | 自动补齐的 debounce、请求、幽灵文字管理 | ReplyBox 局部 |
|
||
| `useAiTextTools.ts` | 语气/润色/改写的统一调用和结果管理 | ReplyBox 局部 |
|
||
| `usePanelMode.ts` | 右栏放大/缩小模式切换 | Workspace 全局 |
|
||
|
||
**useAutoComplete.ts 核心逻辑**:
|
||
|
||
```typescript
|
||
export function useAutoComplete(
|
||
textareaRef: Ref<HTMLTextAreaElement>,
|
||
conversationId: Ref<string>,
|
||
enabled: Ref<boolean>,
|
||
) {
|
||
const ghostText = ref('') // 幽灵文字内容
|
||
const isLoading = ref(false) // 加载状态
|
||
const abortController = ref<AbortController | null>(null)
|
||
|
||
// debounce 800ms,输入停顿后触发
|
||
const debouncedFetch = useDebounceFn(async (text: string) => {
|
||
if (!enabled.value || text.length < 5) {
|
||
ghostText.value = ''
|
||
return
|
||
}
|
||
// 取消上一个请求
|
||
abortController.value?.abort()
|
||
abortController.value = new AbortController()
|
||
|
||
isLoading.value = true
|
||
try {
|
||
const result = await wingmanApi.autocomplete(
|
||
conversationId.value,
|
||
{ current_text: text, cursor_position: text.length },
|
||
{ signal: abortController.value.signal },
|
||
)
|
||
ghostText.value = result.completion || ''
|
||
} catch (e) {
|
||
if (!isAbortError(e)) ghostText.value = ''
|
||
} finally {
|
||
isLoading.value = false
|
||
}
|
||
}, 800)
|
||
|
||
// Tab 接受补齐
|
||
function acceptCompletion() {
|
||
// 将 ghostText 追加到输入框
|
||
// 清空 ghostText
|
||
}
|
||
|
||
// Esc 清除补齐
|
||
function dismissCompletion() {
|
||
ghostText.value = ''
|
||
}
|
||
|
||
return { ghostText, isLoading, debouncedFetch, acceptCompletion, dismissCompletion }
|
||
}
|
||
```
|
||
|
||
**usePanelMode.ts 核心逻辑**:
|
||
|
||
```typescript
|
||
export function usePanelMode() {
|
||
const mode = ref<'normal' | 'expanded'>('normal')
|
||
|
||
const panelWidth = computed(() =>
|
||
mode.value === 'normal' ? '260px' : '560px'
|
||
)
|
||
|
||
const centerColVisible = computed(() =>
|
||
mode.value === 'normal'
|
||
)
|
||
|
||
function toggleMode() {
|
||
mode.value = mode.value === 'normal' ? 'expanded' : 'normal'
|
||
}
|
||
|
||
return { mode, panelWidth, centerColVisible, toggleMode }
|
||
}
|
||
```
|
||
|
||
#### 3.3 幽灵文字实现方案
|
||
|
||
PRD 确认为内联幽灵文字。实现方式为 textarea 上方覆盖透明文字层:
|
||
|
||
```
|
||
┌──────────────────────────────┐
|
||
│ textarea (z-index: 1) │ ← 文字颜色: #303133 (正常)
|
||
│ 透明背景,文字不可见的部分 │ 选中文字后的补齐文字不可见
|
||
├──────────────────────────────┤
|
||
│ ghost overlay (z-index: 2) │ ← 文字颜色: #C0C4CC (灰色)
|
||
│ pointer-events: none │ 仅在 textarea 文字末尾之后显示
|
||
│ 位置与 textarea 文字完全对齐 │ 通过 mirror div 技术计算光标坐标
|
||
└──────────────────────────────┘
|
||
```
|
||
|
||
**技术要点**:
|
||
|
||
1. 创建一个与 textarea 样式完全一致的 `<div>`(mirror div),用于计算光标坐标
|
||
2. ghost overlay 定位在光标坐标处,显示灰色补齐文字
|
||
3. `pointer-events: none` 确保不拦截鼠标事件
|
||
4. Tab 键监听在 textarea 的 `keydown` 事件中,`e.key === 'Tab'` 时阻止默认行为并接受补齐
|
||
5. 继续输入时自动清除 ghost text(`input` 事件触发 debounce 重新请求)
|
||
|
||
**Mirror div 技术**(用于精确计算光标位置):
|
||
|
||
```typescript
|
||
function getCursorCoordinates(textarea: HTMLTextAreaElement, position: number) {
|
||
const div = document.createElement('div')
|
||
// 复制 textarea 的所有影响文字布局的样式
|
||
const styles = window.getComputedStyle(textarea)
|
||
Object.assign(div.style, {
|
||
position: 'absolute',
|
||
visibility: 'hidden',
|
||
whiteSpace: 'pre-wrap',
|
||
wordWrap: 'break-word',
|
||
// ... 复制 font-family, font-size, padding, border, width 等
|
||
})
|
||
// 截取光标位置之前的文字
|
||
div.textContent = textarea.value.substring(0, position)
|
||
document.body.appendChild(div)
|
||
// 创建一个 span 标记光标位置
|
||
const span = document.createElement('span')
|
||
span.textContent = '|'
|
||
div.appendChild(span)
|
||
// 获取 span 的坐标(相对于 textarea)
|
||
const coords = {
|
||
x: span.offsetLeft - textarea.scrollLeft,
|
||
y: span.offsetTop - textarea.scrollTop,
|
||
}
|
||
document.body.removeChild(div)
|
||
return coords
|
||
}
|
||
```
|
||
|
||
#### 3.4 前端 API 层扩展
|
||
|
||
在 `frontend-agent/src/api/wingman.ts` 中新增 4 个函数:
|
||
|
||
```typescript
|
||
// 自动补齐
|
||
export async function autocomplete(
|
||
conversationId: string,
|
||
data: { current_text: string; cursor_position: number; max_length?: number },
|
||
config?: { signal?: AbortSignal },
|
||
): Promise<{ completion: string; confidence: number }>
|
||
|
||
// 语气调整
|
||
export async function toneAdjust(
|
||
conversationId: string,
|
||
data: { selected_text: string; full_text: string; tone: string },
|
||
): Promise<{ rewritten_text: string; tone: string; changes_summary: string }>
|
||
|
||
// 文字润色
|
||
export async function polish(
|
||
conversationId: string,
|
||
data: { text: string; action: string },
|
||
): Promise<{ polished_text: string; action: string; changes_summary: string }>
|
||
|
||
// 智能改写
|
||
export async function rewrite(
|
||
conversationId: string,
|
||
data: { current_text: string; generate_count?: number; include_knowledge?: boolean },
|
||
): Promise<{ versions: Array<{ text: string; style: string; source: string }> }>
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 布局重构架构设计
|
||
|
||
#### 4.1 CSS 变量体系
|
||
|
||
```css
|
||
:root {
|
||
/* === 布局尺寸 === */
|
||
--sidebar-width: 260px; /* 左栏: 280px → 260px */
|
||
--assistant-panel-width: 260px; /* 右栏正常: 320px → 260px */
|
||
--assistant-panel-expanded: 560px; /* 右栏放大: 新增 */
|
||
|
||
/* === 右栏模式控制 === */
|
||
--center-col-flex: 1; /* 正常: 中栏弹性 */
|
||
--center-col-width: auto; /* 正常: 中栏自动 */
|
||
--center-col-overflow: visible; /* 正常: 可见 */
|
||
|
||
/* === 回复建议区颜色 === */
|
||
--suggest-ai-bg: #FAECE7; /* AI推荐背景 - 珊瑚色 */
|
||
--suggest-ai-border: #F0997B;
|
||
--suggest-ai-text: #993C1D;
|
||
--suggest-quick-bg: #EAF3DE; /* 快速回复背景 - 绿色 */
|
||
--suggest-quick-border: #97C459;
|
||
--suggest-quick-text: #27500A;
|
||
|
||
/* === 工具栏分组颜色 === */
|
||
--toolbar-conv-bg: #E1F5EE; /* 常规工具 - 绿色系 */
|
||
--toolbar-ai-bg: #FAEEDA; /* AI工具 - 琥珀色系 */
|
||
|
||
/* === 回复建议区高度 === */
|
||
--suggest-area-height: auto; /* 动态: 选中后 0 */
|
||
--suggest-ai-height: 44px; /* AI推荐区固定高度 */
|
||
--suggest-quick-height: 56px; /* 快速回复区固定2层 */
|
||
}
|
||
```
|
||
|
||
#### 4.2 右栏放大/缩小模式实现
|
||
|
||
```vue
|
||
<!-- Workspace.vue -->
|
||
<template>
|
||
<div class="workspace" :class="{ 'panel-expanded': panelMode === 'expanded' }">
|
||
<aside class="sidebar"><ConversationList /></aside>
|
||
<main class="center-col"><ChatArea /></main>
|
||
<aside class="assistant-panel"><AiAssistantPanel /></aside>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.workspace {
|
||
display: flex;
|
||
height: 100vh;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.sidebar {
|
||
width: var(--sidebar-width);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.center-col {
|
||
flex: var(--center-col-flex);
|
||
width: var(--center-col-width);
|
||
overflow: var(--center-col-overflow);
|
||
min-width: 0;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.assistant-panel {
|
||
width: var(--assistant-panel-width);
|
||
flex-shrink: 0;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
/* 放大模式 */
|
||
.workspace.panel-expanded {
|
||
--assistant-panel-width: var(--assistant-panel-expanded);
|
||
--center-col-flex: 0;
|
||
--center-col-width: 0;
|
||
--center-col-overflow: hidden;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 4.3 回复建议区动态高度
|
||
|
||
```vue
|
||
<!-- ReplySuggestArea.vue -->
|
||
<template>
|
||
<div
|
||
class="reply-suggest-area"
|
||
:class="{ collapsed: !shouldShow }"
|
||
>
|
||
<div class="ai-recommend-section" v-if="shouldShow">
|
||
<AiRecommendBar />
|
||
</div>
|
||
<div class="quick-reply-section" v-if="shouldShow">
|
||
<QuickReplyBar />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
const props = defineProps<{
|
||
inputText: string // 来自 ReplyBox 的输入框内容
|
||
}>()
|
||
|
||
// 输入框为空时展开,有内容时缩回
|
||
const shouldShow = computed(() => props.inputText.length === 0)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.reply-suggest-area {
|
||
max-height: calc(var(--suggest-ai-height) + var(--suggest-quick-height));
|
||
overflow: hidden;
|
||
transition: max-height 0.3s ease;
|
||
}
|
||
|
||
.reply-suggest-area.collapsed {
|
||
max-height: 0;
|
||
}
|
||
</style>
|
||
```
|
||
|
||
#### 4.4 工具栏单行左右分区
|
||
|
||
```vue
|
||
<!-- ReplyBox.vue 工具栏部分 -->
|
||
<template>
|
||
<div class="toolbar">
|
||
<!-- 左侧: 常规工具 -->
|
||
<div class="toolbar-left">
|
||
<button class="tool-btn conv" @click="screenshot">截图</button>
|
||
<button class="tool-btn conv" @click="camera">拍照</button>
|
||
<button class="tool-btn conv" @click="emoji">表情</button>
|
||
<button class="tool-btn conv" @click="file">文件</button>
|
||
<button class="tool-btn conv" @click="invite">邀请</button>
|
||
</div>
|
||
|
||
<!-- 右侧: AI工具 + 发送 -->
|
||
<div class="toolbar-right">
|
||
<button
|
||
class="tool-btn ai"
|
||
:class="{ active: autoCompleteEnabled }"
|
||
@click="autoCompleteEnabled = !autoCompleteEnabled"
|
||
>补齐</button>
|
||
<button class="tool-btn ai" @click="showTonePopover" :disabled="!hasSelection">语气</button>
|
||
<button class="tool-btn ai" @click="showPolishPanel">润色</button>
|
||
<button class="tool-btn ai" @click="showRewritePanel">改写</button>
|
||
<button class="tool-btn send" @click="send">发送</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 数据流设计
|
||
|
||
#### 5.1 自动补齐数据流
|
||
|
||
```
|
||
坐席输入文字
|
||
→ textarea input 事件
|
||
→ useAutoComplete.debouncedFetch(text) [debounce 800ms]
|
||
→ 取消上一个 AbortController
|
||
→ 调用 wingmanApi.autocomplete(conversationId, { current_text })
|
||
→ POST /api/conversations/{id}/wingman/autocomplete
|
||
→ FastAPI 路由层
|
||
→ _get_recent_messages(conversationId, db, limit=5)
|
||
→ WingmanService.generate_completion(conversationId, text, messages)
|
||
→ _build_context_messages(messages, _COMPLETION_SYSTEM_PROMPT)
|
||
→ 追加 current_text 到上下文末尾
|
||
→ _call_wingman_api(context, temperature=0.2)
|
||
→ Dify API POST /chat/completions
|
||
→ 解析 choices[0].message.content
|
||
→ 返回 { completion, confidence }
|
||
→ 前端设置 ghostText.value = result.completion
|
||
→ ghost overlay 渲染灰色文字
|
||
|
||
坐席按 Tab
|
||
→ acceptCompletion()
|
||
→ inputText.value += ghostText.value
|
||
→ ghostText.value = ''
|
||
```
|
||
|
||
#### 5.2 语气调整数据流
|
||
|
||
```
|
||
坐席选中文字 → 点击"语气"按钮 → 弹出 ToneAdjustPopover
|
||
→ 选择"专业/友好/简洁"
|
||
→ 调用 wingmanApi.toneAdjust(conversationId, { selected_text, full_text, tone })
|
||
→ POST /api/conversations/{id}/wingman/tone-adjust
|
||
→ WingmanService.adjust_tone(...)
|
||
→ _build_context_messages(messages, _TONE_SYSTEM_PROMPT)
|
||
→ 注入 selected_text + full_text + tone 指令
|
||
→ _call_wingman_api(context, temperature=0.3)
|
||
→ 返回 { rewritten_text, tone, changes_summary }
|
||
→ 浮层显示"原文 → 改写文"对比
|
||
→ 坐席点击"替换" → 用 rewritten_text 替换选中区域
|
||
→ 坐席点击"取消" → 关闭浮层,不修改
|
||
```
|
||
|
||
#### 5.3 右栏模式切换数据流
|
||
|
||
```
|
||
坐席点击 PanelModeToggle "放大"按钮
|
||
→ usePanelMode.toggleMode()
|
||
→ mode.value = 'expanded'
|
||
→ CSS 变量更新:
|
||
--assistant-panel-width → 560px
|
||
--center-col-flex → 0
|
||
--center-col-width → 0
|
||
--center-col-overflow → hidden
|
||
→ CSS transition 0.3s 平滑动画
|
||
→ 中栏压缩隐藏,右栏展开为 560px
|
||
|
||
坐席点击"正常"按钮
|
||
→ toggleMode() → mode.value = 'normal'
|
||
→ CSS 变量恢复
|
||
→ 中栏恢复显示,右栏缩回 260px
|
||
```
|
||
|
||
---
|
||
|
||
### 6. 关键设计决策
|
||
|
||
#### 决策 1:自动补齐用 Mirror Div 而非 contenteditable
|
||
|
||
**选择**:保持 `<textarea>` + mirror div 计算 + ghost overlay 覆盖
|
||
|
||
**理由**:
|
||
- `contenteditable` div 虽然原生支持幽灵文字,但会破坏现有的 Ctrl+Enter 发送、Enter 换行等快捷键逻辑
|
||
- textarea 的所有现有功能(粘贴图片、拖拽文件等)无需改动
|
||
- mirror div 是业界成熟方案(VS Code、GitHub Copilot 均用此方案)
|
||
- 实现复杂度可控,仅需一个坐标计算函数
|
||
|
||
#### 决策 2:补齐 API 用 AbortController 而非 WebSocket
|
||
|
||
**选择**:HTTP POST + AbortController 取消
|
||
|
||
**理由**:
|
||
- 补齐请求频率高(每次停顿 800ms 触发),但每次请求轻量(< 500 字符)
|
||
- WebSocket 需要新增消息类型和连接管理,增加复杂度
|
||
- AbortController 可在浏览器层取消请求,避免无效响应
|
||
- 现有 WingmanService 全部使用 HTTP 同步调用,保持一致性
|
||
|
||
#### 决策 3:语气/润色/改写用浮层面板而非全屏弹窗
|
||
|
||
**选择**:`el-popover` / `el-drawer` 浮层
|
||
|
||
**理由**:
|
||
- 坐席需要在操作时仍能看到对话上下文
|
||
- 全屏 `el-dialog` 会遮挡消息列表,打断工作流
|
||
- 浮层可定位在工具栏按钮附近,视线不跳转
|
||
- 润色和改写面板使用 `el-drawer` 从右侧滑出,宽度 400px,不遮挡消息列表
|
||
|
||
#### 决策 4:右栏模式切换用 CSS 变量而非 v-if
|
||
|
||
**选择**:CSS 变量 + class 切换,不用 v-if 销毁/重建组件
|
||
|
||
**理由**:
|
||
- `v-if` 会导致中栏组件销毁/重建,丢失消息列表滚动位置和输入框内容
|
||
- CSS 变量方案仅改变视觉布局,组件实例保持存活
|
||
- `overflow: hidden` + `width: 0` 确保中栏不可见但不销毁
|
||
- 切回正常模式时立即可见,无需重新渲染
|
||
|
||
#### 决策 5:回复建议区用 max-height 动画而非 v-show
|
||
|
||
**选择**:`max-height` transition + `collapsed` class
|
||
|
||
**理由**:
|
||
- `v-show`(display: none)会导致回复建议区下方布局跳动
|
||
- `max-height` 从 `auto` 到 `0` 的过渡更平滑
|
||
- 组件实例保持存活,AI推荐数据不丢失
|
||
- 选中条目后缩回的动画体验更流畅
|
||
|
||
#### 决策 6:_call_wingman_api 改造为可选参数而非新增方法
|
||
|
||
**选择**:在现有方法签名上新增 `temperature` 参数,默认值 0.3
|
||
|
||
**理由**:
|
||
- 现有 4 个方法的调用无需修改(默认值兼容)
|
||
- 避免代码重复(不需要新增 `_call_wingman_api_with_temp()`)
|
||
- 未来其他方法也可按需传递 temperature
|
||
- 改动面最小,降低回归风险
|
||
|
||
---
|
||
|
||
## Part B: 接口与数据结构
|
||
|
||
### 7. API 规范
|
||
|
||
#### 7.1 自动补齐
|
||
|
||
```
|
||
POST /api/conversations/{conversation_id}/wingman/autocomplete
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| current_text | string | 是 | 坐席当前输入的文字 |
|
||
| cursor_position | int | 是 | 光标位置(字符偏移量) |
|
||
| max_length | int | 否 | 补齐最大长度,默认 80 |
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"data": {
|
||
"completion": "正在查看相关工单记录,请稍候。",
|
||
"confidence": 0.85
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7.2 语气调整
|
||
|
||
```
|
||
POST /api/conversations/{conversation_id}/wingman/tone-adjust
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| selected_text | string | 是 | 坐席选中的文字 |
|
||
| full_text | string | 是 | 输入框全部内容 |
|
||
| tone | string | 是 | `professional` / `friendly` / `concise` |
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"data": {
|
||
"rewritten_text": "经排查,您的VPN连接异常由本地网络DNS解析超时引起。",
|
||
"tone": "professional",
|
||
"changes_summary": "去除口语化表达,增加技术术语"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7.3 文字润色
|
||
|
||
```
|
||
POST /api/conversations/{conversation_id}/wingman/polish
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| text | string | 是 | 输入框全部内容 |
|
||
| action | string | 是 | `expand` / `compress` / `correct` |
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"data": {
|
||
"polished_text": "建议您按以下步骤操作:\n1. 完全退出VPN客户端\n2. 重新启动\n3. 重新连接",
|
||
"action": "expand",
|
||
"changes_summary": "增加了操作步骤细化和兜底方案"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 7.4 智能改写
|
||
|
||
```
|
||
POST /api/conversations/{conversation_id}/wingman/rewrite
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| current_text | string | 否 | 当前输入框内容(可为空) |
|
||
| generate_count | int | 否 | 生成数量,默认 3 |
|
||
| include_knowledge | bool | 否 | 是否检索知识库,默认 true |
|
||
|
||
**响应**:
|
||
```json
|
||
{
|
||
"code": 200,
|
||
"data": {
|
||
"versions": [
|
||
{ "text": "VPN连接异常,建议重启客户端后重试。", "style": "简洁直接", "source": "对话上下文" },
|
||
{ "text": "VPN连接问题处理步骤:\n1. 退出VPN客户端\n2. 重启\n3. 重新连接", "style": "详细带步骤", "source": "对话上下文" },
|
||
{ "text": "根据知识库文档《VPN故障排查指南》...", "style": "带知识库引用", "source": "RAGFlow知识库" }
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 8. Pydantic 模型
|
||
|
||
现有 Wingman 端点未使用 Pydantic 请求模型。本设计新增请求体验证模型,提升 API 健壮性:
|
||
|
||
```python
|
||
# backend/app/schemas/wingman.py
|
||
|
||
from pydantic import BaseModel, Field, field_validator
|
||
from typing import List, Optional
|
||
from enum import Enum
|
||
|
||
|
||
class ToneType(str, Enum):
|
||
PROFESSIONAL = "professional"
|
||
FRIENDLY = "friendly"
|
||
CONCISE = "concise"
|
||
|
||
|
||
class PolishAction(str, Enum):
|
||
EXPAND = "expand"
|
||
COMPRESS = "compress"
|
||
CORRECT = "correct"
|
||
|
||
|
||
class AutocompleteRequest(BaseModel):
|
||
current_text: str = Field(..., min_length=1, max_length=2000, description="坐席当前输入文字")
|
||
cursor_position: int = Field(..., ge=0, description="光标位置")
|
||
max_length: int = Field(default=80, ge=10, le=200, description="补齐最大长度")
|
||
|
||
|
||
class ToneAdjustRequest(BaseModel):
|
||
selected_text: str = Field(..., min_length=5, max_length=2000, description="选中的文字")
|
||
full_text: str = Field(..., min_length=1, max_length=5000, description="输入框全部内容")
|
||
tone: ToneType = Field(..., description="目标语气")
|
||
|
||
|
||
class PolishRequest(BaseModel):
|
||
text: str = Field(..., min_length=1, max_length=5000, description="待润色文字")
|
||
action: PolishAction = Field(..., description="润色操作")
|
||
|
||
|
||
class RewriteRequest(BaseModel):
|
||
current_text: str = Field(default="", max_length=5000, description="当前输入内容")
|
||
generate_count: int = Field(default=3, ge=1, le=5, description="生成数量")
|
||
include_knowledge: bool = Field(default=True, description="是否检索知识库")
|
||
|
||
|
||
# 响应模型
|
||
class AutocompleteResponse(BaseModel):
|
||
completion: str = Field(default="", description="补齐文字")
|
||
confidence: float = Field(default=0.0, ge=0.0, le=1.0, description="置信度")
|
||
|
||
|
||
class ToneAdjustResponse(BaseModel):
|
||
rewritten_text: str
|
||
tone: str
|
||
changes_summary: str = ""
|
||
|
||
|
||
class PolishResponse(BaseModel):
|
||
polished_text: str
|
||
action: str
|
||
changes_summary: str = ""
|
||
|
||
|
||
class RewriteVersion(BaseModel):
|
||
text: str
|
||
style: str
|
||
source: str
|
||
|
||
|
||
class RewriteResponse(BaseModel):
|
||
versions: List[RewriteVersion] = Field(default_factory=list)
|
||
```
|
||
|
||
---
|
||
|
||
### 9. Dify Prompt 模板
|
||
|
||
在 `WingmanService` 中新增 4 个类属性常量:
|
||
|
||
```python
|
||
class WingmanService:
|
||
# === 现有 Prompts(保持不变) ===
|
||
# _DRAFT_SYSTEM_PROMPT
|
||
# _SUMMARY_SYSTEM_PROMPT
|
||
# _TAGS_SYSTEM_PROMPT
|
||
# _KNOWLEDGE_SUGGESTION_PROMPT
|
||
|
||
# === 新增 Prompts ===
|
||
|
||
_COMPLETION_SYSTEM_PROMPT = """你是一个IT服务坐席输入助手。根据坐席当前正在输入的内容和对话上下文,补齐下一句话。
|
||
要求:
|
||
1. 补齐内容自然衔接当前文字,不要重复已有内容
|
||
2. 长度控制在1-2个短句,不超过80字
|
||
3. 语气专业、简洁,符合IT服务规范
|
||
4. 只返回补齐的文字,不要加引号或其他标记"""
|
||
|
||
_TONE_SYSTEM_PROMPT = """你是一个IT服务话术改写助手。将坐席选中的文字改写为{tone}风格。
|
||
语气定义:
|
||
- professional(专业):使用准确的技术术语,结构化表达,去除口语化内容
|
||
- friendly(友好):适当增加问候和关心语,语气更亲和
|
||
- concise(简洁):去除冗余,直奔主题,控制字数
|
||
要求:
|
||
1. 保持原意不变,只调整语气和表达方式
|
||
2. 改写后的文字长度与原文相近(±30%)
|
||
3. 符合IT服务坐席的专业规范
|
||
4. 只返回改写后的文字,不要加引号或解释"""
|
||
|
||
_POLISH_SYSTEM_PROMPT = """你是一个IT服务文字润色助手。对坐席输入的文字进行{action}。
|
||
操作定义:
|
||
- expand(扩写):在原文基础上增加操作步骤、注意事项、解释说明,使回复更完整
|
||
- compress(压缩):精简表达,去除重复和冗余,保留核心信息,控制字数
|
||
- correct(纠错):检查并修正错别字、语法错误、标点符号、格式问题
|
||
要求:
|
||
1. 保持原文核心意思不变
|
||
2. 符合IT服务坐席的专业规范
|
||
3. 只返回润色后的文字,不要加引号或解释"""
|
||
|
||
_REWRITE_SYSTEM_PROMPT = """你是一个IT服务回复生成助手。基于对话上下文{with_knowledge},生成{count}个不同风格的回复版本。
|
||
版本要求:
|
||
- 版本1:简洁直接,一句话说明解决方案
|
||
- 版本2:详细带步骤,包含操作步骤和注意事项
|
||
- 版本3:带知识库引用,引用相关知识库文档{knowledge_note}
|
||
要求:
|
||
1. 每个版本独立完整,可直接发送
|
||
2. 语气专业、符合IT服务规范
|
||
3. 以JSON格式输出:[{{"text": "回复内容", "style": "风格描述", "source": "来源"}}]"""
|
||
```
|
||
|
||
---
|
||
|
||
### 10. TypeScript 类型定义
|
||
|
||
```typescript
|
||
// frontend-agent/src/types/wingman-assist.ts
|
||
|
||
/** 语气类型 */
|
||
export type Tone = 'professional' | 'friendly' | 'concise'
|
||
|
||
/** 润色操作 */
|
||
export type PolishAction = 'expand' | 'compress' | 'correct'
|
||
|
||
/** 自动补齐请求 */
|
||
export interface AutocompleteRequest {
|
||
current_text: string
|
||
cursor_position: number
|
||
max_length?: number
|
||
}
|
||
|
||
/** 自动补齐响应 */
|
||
export interface AutocompleteResponse {
|
||
completion: string
|
||
confidence: number
|
||
}
|
||
|
||
/** 语气调整请求 */
|
||
export interface ToneAdjustRequest {
|
||
selected_text: string
|
||
full_text: string
|
||
tone: Tone
|
||
}
|
||
|
||
/** 语气调整响应 */
|
||
export interface ToneAdjustResponse {
|
||
rewritten_text: string
|
||
tone: string
|
||
changes_summary: string
|
||
}
|
||
|
||
/** 润色请求 */
|
||
export interface PolishRequest {
|
||
text: string
|
||
action: PolishAction
|
||
}
|
||
|
||
/** 润色响应 */
|
||
export interface PolishResponse {
|
||
polished_text: string
|
||
action: string
|
||
changes_summary: string
|
||
}
|
||
|
||
/** 改写版本 */
|
||
export interface RewriteVersion {
|
||
text: string
|
||
style: string
|
||
source: string
|
||
}
|
||
|
||
/** 改写请求 */
|
||
export interface RewriteRequest {
|
||
current_text?: string
|
||
generate_count?: number
|
||
include_knowledge?: boolean
|
||
}
|
||
|
||
/** 改写响应 */
|
||
export interface RewriteResponse {
|
||
versions: RewriteVersion[]
|
||
}
|
||
|
||
/** 右栏面板模式 */
|
||
export type PanelMode = 'normal' | 'expanded'
|
||
|
||
/** 语气选项配置 */
|
||
export const TONE_OPTIONS: Array<{ value: Tone; label: string; desc: string }> = [
|
||
{ value: 'professional', label: '专业', desc: '准确术语、结构化表达' },
|
||
{ value: 'friendly', label: '友好', desc: '增加关心语、语气亲和' },
|
||
{ value: 'concise', label: '简洁', desc: '去除冗余、直奔主题' },
|
||
]
|
||
|
||
/** 润色操作配置 */
|
||
export const POLISH_ACTIONS: Array<{ value: PolishAction; label: string; desc: string }> = [
|
||
{ value: 'expand', label: '扩写', desc: '增加步骤和细节' },
|
||
{ value: 'compress', label: '压缩', desc: '精简保留核心' },
|
||
{ value: 'correct', label: '纠错', desc: '修正错别字和语法' },
|
||
]
|
||
```
|
||
|
||
---
|
||
|
||
## Part C: 架构图
|
||
|
||
### 11. 类图
|
||
|
||
```mermaid
|
||
classDiagram
|
||
%% ===== 后端 =====
|
||
class WingmanService {
|
||
-api_url: str
|
||
-api_key: str
|
||
-timeout: int
|
||
-_client: httpx.AsyncClient
|
||
+generate_draft() Dict
|
||
+generate_summary() Dict
|
||
+suggest_tags() Dict
|
||
+generate_knowledge_suggestion() Dict
|
||
+generate_completion() Dict %% 新增
|
||
+adjust_tone() Dict %% 新增
|
||
+polish_text() Dict %% 新增
|
||
+rewrite_versions() Dict %% 新增
|
||
-_get_client() AsyncClient
|
||
-_build_context_messages() List
|
||
-_call_wingman_api(text, temp) str %% 改造: 新增temp参数
|
||
-_parse_json_response() Dict
|
||
-_estimate_confidence() float
|
||
+close() void
|
||
}
|
||
|
||
class AutocompleteRequest {
|
||
+current_text: str
|
||
+cursor_position: int
|
||
+max_length: int
|
||
}
|
||
|
||
class ToneAdjustRequest {
|
||
+selected_text: str
|
||
+full_text: str
|
||
+tone: ToneType
|
||
}
|
||
|
||
class PolishRequest {
|
||
+text: str
|
||
+action: PolishAction
|
||
}
|
||
|
||
class RewriteRequest {
|
||
+current_text: str
|
||
+generate_count: int
|
||
+include_knowledge: bool
|
||
}
|
||
|
||
class ToneType {
|
||
<<enumeration>>
|
||
PROFESSIONAL
|
||
FRIENDLY
|
||
CONCISE
|
||
}
|
||
|
||
class PolishAction {
|
||
<<enumeration>>
|
||
EXPAND
|
||
COMPRESS
|
||
CORRECT
|
||
}
|
||
|
||
%% ===== 前端 =====
|
||
class ReplyBox {
|
||
+inputText: string
|
||
+autoCompleteEnabled: boolean
|
||
-useAutoComplete()
|
||
-useAiTextTools()
|
||
+acceptCompletion()
|
||
+showTonePopover()
|
||
+showPolishPanel()
|
||
+showRewritePanel()
|
||
+send()
|
||
}
|
||
|
||
class ReplySuggestArea {
|
||
+inputText: string
|
||
+shouldShow: boolean
|
||
-AiRecommendBar
|
||
-QuickReplyBar
|
||
}
|
||
|
||
class AiAssistantPanel {
|
||
+mode: PanelMode
|
||
-PanelModeToggle
|
||
-AiTrainingPanel
|
||
+toggleMode()
|
||
}
|
||
|
||
class AiTrainingPanel {
|
||
-SmartTagEditor
|
||
-QualityFeedback
|
||
-KnowledgeContribute
|
||
-UsageStats
|
||
}
|
||
|
||
class useAutoComplete {
|
||
+ghostText: Ref~string~
|
||
+isLoading: Ref~boolean~
|
||
-debouncedFetch(text)
|
||
+acceptCompletion()
|
||
+dismissCompletion()
|
||
}
|
||
|
||
class usePanelMode {
|
||
+mode: Ref~PanelMode~
|
||
+panelWidth: ComputedRef
|
||
+centerColVisible: ComputedRef
|
||
+toggleMode()
|
||
}
|
||
|
||
WingmanService --> AutocompleteRequest : 接收
|
||
WingmanService --> ToneAdjustRequest : 接收
|
||
WingmanService --> PolishRequest : 接收
|
||
WingmanService --> RewriteRequest : 接收
|
||
ToneAdjustRequest --> ToneType
|
||
PolishRequest --> PolishAction
|
||
|
||
ReplyBox --> useAutoComplete : 使用
|
||
ReplyBox --> ReplySuggestArea : 包含
|
||
AiAssistantPanel --> usePanelMode : 使用
|
||
AiAssistantPanel --> AiTrainingPanel : 包含
|
||
```
|
||
|
||
---
|
||
|
||
### 12. 时序图
|
||
|
||
#### 12.1 自动补齐时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant A as 坐席
|
||
participant R as ReplyBox
|
||
participant U as useAutoComplete
|
||
participant API as wingman.ts
|
||
participant F as FastAPI
|
||
participant W as WingmanService
|
||
participant D as Dify
|
||
|
||
A->>R: 输入文字
|
||
R->>U: input event (text)
|
||
U->>U: debounce 800ms
|
||
U->>U: abort previous request
|
||
U->>API: autocomplete(convId, {current_text})
|
||
API->>F: POST /wingman/autocomplete
|
||
F->>F: _get_recent_messages(limit=5)
|
||
F->>W: generate_completion(convId, text, msgs)
|
||
W->>W: _build_context_messages(msgs, prompt)
|
||
W->>W: append current_text to context
|
||
W->>D: POST /chat/completions (temp=0.2)
|
||
D-->>W: {choices[0].message.content}
|
||
W-->>F: {completion, confidence}
|
||
F-->>API: {code:200, data:{completion,confidence}}
|
||
API-->>U: result
|
||
U->>U: ghostText.value = result.completion
|
||
U-->>R: ghost overlay 渲染灰色文字
|
||
R-->>A: 显示幽灵文字
|
||
|
||
A->>R: 按 Tab 键
|
||
R->>U: keydown Tab
|
||
U->>U: inputText += ghostText
|
||
U->>U: ghostText = ''
|
||
R-->>A: 补齐文字变为正常颜色
|
||
```
|
||
|
||
#### 12.2 语气调整时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant A as 坐席
|
||
participant R as ReplyBox
|
||
participant T as ToneAdjustPopover
|
||
participant API as wingman.ts
|
||
participant F as FastAPI
|
||
participant W as WingmanService
|
||
participant D as Dify
|
||
|
||
A->>R: 选中文字 (≥5字符)
|
||
A->>R: 点击"语气"按钮
|
||
R->>T: 弹出浮层 (3个语气选项)
|
||
A->>T: 选择"专业"
|
||
T->>API: toneAdjust(convId, {selected_text, full_text, tone})
|
||
API->>F: POST /wingman/tone-adjust
|
||
F->>F: _get_recent_messages(limit=5)
|
||
F->>W: adjust_tone(convId, selected, full, tone, msgs)
|
||
W->>W: _build_context_messages(msgs, _TONE_SYSTEM_PROMPT)
|
||
W->>W: 注入 selected_text + full_text + tone
|
||
W->>D: POST /chat/completions (temp=0.3)
|
||
D-->>W: 改写后的文字
|
||
W-->>F: {rewritten_text, tone, changes_summary}
|
||
F-->>API: response
|
||
API-->>T: result
|
||
T->>T: 显示"原文 → 改写文"对比
|
||
T-->>A: 展示对比结果
|
||
|
||
alt 坐席点击"替换"
|
||
A->>T: 点击"替换"
|
||
T->>R: 替换选中区域文字
|
||
T->>T: 关闭浮层
|
||
else 坐席点击"取消"
|
||
A->>T: 点击外部/Esc
|
||
T->>T: 关闭浮层,不修改
|
||
end
|
||
```
|
||
|
||
#### 12.3 右栏模式切换时序图
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant A as 坐席
|
||
participant P as PanelModeToggle
|
||
participant U as usePanelMode
|
||
participant W as Workspace
|
||
participant C as ChatArea (中栏)
|
||
participant R as AiAssistantPanel (右栏)
|
||
|
||
A->>P: 点击"放大"按钮
|
||
P->>U: toggleMode()
|
||
U->>U: mode.value = 'expanded'
|
||
U->>W: class="panel-expanded"
|
||
W->>W: CSS变量更新 (transition 0.3s)
|
||
Note over W: --assistant-panel-width: 560px<br/>--center-col-flex: 0<br/>--center-col-width: 0<br/>--center-col-overflow: hidden
|
||
W->>C: flex=0, width=0, overflow=hidden
|
||
W->>R: width=560px
|
||
Note over C,R: 0.3s 平滑动画<br/>中栏压缩隐藏<br/>右栏展开为完整训练面板
|
||
|
||
A->>P: 点击"正常"按钮
|
||
P->>U: toggleMode()
|
||
U->>U: mode.value = 'normal'
|
||
U->>W: 移除 class="panel-expanded"
|
||
W->>W: CSS变量恢复
|
||
W->>C: flex=1, width=auto, overflow=visible
|
||
W->>R: width=260px
|
||
Note over C,R: 0.3s 平滑动画<br/>中栏恢复显示<br/>右栏缩回正常宽度
|
||
```
|
||
|
||
---
|
||
|
||
## Part D: 任务分解
|
||
|
||
### 13. 文件清单
|
||
|
||
#### 13.1 后端新建文件
|
||
|
||
| 文件路径 | 说明 |
|
||
|----------|------|
|
||
| `backend/app/schemas/wingman_assist.py` | 新增 Pydantic 请求/响应模型 |
|
||
|
||
#### 13.2 后端修改文件
|
||
|
||
| 文件路径 | 修改范围 | 说明 |
|
||
|----------|----------|------|
|
||
| `backend/app/services/wingman_service.py` | 扩展 | 新增 4 个方法 + 4 个 system prompt 常量;改造 `_call_wingman_api()` 新增 temperature 参数 |
|
||
| `backend/app/api/wingman.py` | 扩展 | 新增 4 个端点;引入 Pydantic 模型做请求验证 |
|
||
|
||
#### 13.3 前端新建文件
|
||
|
||
| 文件路径 | 说明 |
|
||
|----------|------|
|
||
| `frontend-agent/src/types/wingman-assist.ts` | TypeScript 类型定义 + 常量配置 |
|
||
| `frontend-agent/src/composables/useAutoComplete.ts` | 自动补齐 composable |
|
||
| `frontend-agent/src/composables/useAiTextTools.ts` | 语气/润色/改写统一 composable |
|
||
| `frontend-agent/src/composables/usePanelMode.ts` | 右栏模式切换 composable |
|
||
| `frontend-agent/src/components/chat/ReplySuggestArea.vue` | 回复建议区容器 |
|
||
| `frontend-agent/src/components/chat/AiRecommendBar.vue` | AI推荐条(合并自 AiRecommendInline + AiSuggestReply) |
|
||
| `frontend-agent/src/components/chat/QuickReplyBar.vue` | 快速回复条(改造自 QuickReplyPanel) |
|
||
| `frontend-agent/src/components/chat/GhostTextOverlay.vue` | 幽灵文字覆盖层 |
|
||
| `frontend-agent/src/components/chat/ToneAdjustPopover.vue` | 语气调整浮层 |
|
||
| `frontend-agent/src/components/chat/PolishPanel.vue` | 润色精修面板 |
|
||
| `frontend-agent/src/components/chat/RewritePanel.vue` | 改写选择面板 |
|
||
| `frontend-agent/src/components/assistant/PanelModeToggle.vue` | 右栏模式切换按钮组 |
|
||
| `frontend-agent/src/components/assistant/AiTrainingPanel.vue` | AI训练区主体 |
|
||
| `frontend-agent/src/components/assistant/SmartTagEditor.vue` | 智能标注编辑面板 |
|
||
| `frontend-agent/src/components/assistant/QualityFeedback.vue` | 质量反馈面板 |
|
||
| `frontend-agent/src/components/assistant/KnowledgeContribute.vue` | 知识贡献面板 |
|
||
| `frontend-agent/src/components/assistant/UsageStats.vue` | 使用统计面板 |
|
||
|
||
#### 13.4 前端修改文件
|
||
|
||
| 文件路径 | 修改范围 | 说明 |
|
||
|----------|----------|------|
|
||
| `frontend-agent/src/api/wingman.ts` | 扩展 | 新增 4 个 API 调用函数 |
|
||
| `frontend-agent/src/views/Workspace.vue` | 改造 | 三栏宽度调整;右栏模式切换 class 绑定;引入 usePanelMode |
|
||
| `frontend-agent/src/styles/global.css` | 改造 | CSS 变量体系更新 |
|
||
| `frontend-agent/src/components/chat/ChatArea.vue` | 改造 | 新增 ReplySuggestArea 层;TroubleshootBar 默认折叠 |
|
||
| `frontend-agent/src/components/chat/UserInfoBar.vue` | 小幅修改 | 详情默认折叠 |
|
||
| `frontend-agent/src/components/chat/TroubleshootBar.vue` | 小幅修改 | 默认折叠为图标条 |
|
||
| `frontend-agent/src/components/chat/ReplyBox.vue` | 大幅改造 | 工具栏重构为左右分区;新增 AI 工具按钮;集成 useAutoComplete;新增浮层组件引用 |
|
||
| `frontend-agent/src/components/assistant/AiAssistantPanel.vue` | 全面重构 | 移除回复前功能;改为 PanelModeToggle + AiTrainingPanel |
|
||
| `frontend-agent/src/components/conversation/TodoPanel.vue` | 小幅修改 | max-height 260px → 220px |
|
||
|
||
#### 13.5 前端删除文件
|
||
|
||
| 文件路径 | 原因 |
|
||
|----------|------|
|
||
| `frontend-agent/src/components/chat/AiRecommendInline.vue` | 功能被 AiRecommendBar.vue 替代 |
|
||
| `frontend-agent/src/components/assistant/AiSuggestReply.vue` | 功能合并到 AiRecommendBar.vue |
|
||
| `frontend-agent/src/components/assistant/QuickReplyPanel.vue` | 改造为 QuickReplyBar.vue(改名+横向布局) |
|
||
|
||
---
|
||
|
||
### 14. 开发计划
|
||
|
||
#### Phase 1: 后端 WingmanService 扩展(预计 1.5 天)
|
||
|
||
- [ ] 新建 `schemas/wingman_assist.py` — Pydantic 模型
|
||
- [ ] 改造 `wingman_service.py` — `_call_wingman_api()` 新增 temperature 参数
|
||
- [ ] 新增 4 个方法 + 4 个 system prompt 常量
|
||
- [ ] 扩展 `wingman.py` — 新增 4 个端点,引入 Pydantic 模型
|
||
- [ ] 后端单元测试:每个端点正常 + 降级场景
|
||
|
||
#### Phase 2: 前端 API 层 + Composable(预计 1 天)
|
||
|
||
- [ ] 新建 `types/wingman-assist.ts` — 类型定义
|
||
- [ ] 扩展 `api/wingman.ts` — 4 个 API 函数
|
||
- [ ] 新建 `useAutoComplete.ts` — debounce + AbortController + ghost text 管理
|
||
- [ ] 新建 `useAiTextTools.ts` — 语气/润色/改写统一调用
|
||
- [ ] 新建 `usePanelMode.ts` — 模式切换
|
||
|
||
#### Phase 3: ReplyBox 工具栏 + AI 辅助组件(预计 2 天)
|
||
|
||
- [ ] `ReplyBox.vue` 工具栏重构为左右分区
|
||
- [ ] `GhostTextOverlay.vue` — mirror div + 幽灵文字渲染
|
||
- [ ] `ToneAdjustPopover.vue` — 语气选择 + 原文/改写文对比
|
||
- [ ] `PolishPanel.vue` — 精修面板
|
||
- [ ] `RewritePanel.vue` — 改写选择面板
|
||
- [ ] 键盘快捷键:Tab 接受补齐、Esc 清除
|
||
|
||
#### Phase 4: 布局重构 + 回复建议区(预计 2 天)
|
||
|
||
- [ ] `global.css` CSS 变量更新
|
||
- [ ] `Workspace.vue` 三栏宽度调整 + 模式切换 class
|
||
- [ ] `ChatArea.vue` 新增 ReplySuggestArea 层
|
||
- [ ] `ReplySuggestArea.vue` — 容器组件
|
||
- [ ] `AiRecommendBar.vue` — 合并 AiRecommendInline + AiSuggestReply
|
||
- [ ] `QuickReplyBar.vue` — 横向 2 层布局 + 数字快捷键
|
||
- [ ] `UserInfoBar.vue` 详情默认折叠
|
||
- [ ] `TroubleshootBar.vue` 默认折叠为图标条
|
||
|
||
#### Phase 5: 右栏训练区 + 模式切换(预计 1.5 天)
|
||
|
||
- [ ] `AiAssistantPanel.vue` 全面重构
|
||
- [ ] `PanelModeToggle.vue` — 正常/放大切换
|
||
- [ ] `AiTrainingPanel.vue` — 训练区主体
|
||
- [ ] `SmartTagEditor.vue` — 智能标注(合并原"推荐排查流程"+ 标注)
|
||
- [ ] `QualityFeedback.vue` — 质量反馈面板
|
||
- [ ] `KnowledgeContribute.vue` — 知识贡献面板
|
||
- [ ] `UsageStats.vue` — 使用统计面板
|
||
|
||
#### Phase 6: 功能清理 + 联调测试(预计 1 天)
|
||
|
||
- [ ] 删除 `AiRecommendInline.vue`、`AiSuggestReply.vue`
|
||
- [ ] 重命名 `QuickReplyPanel.vue` → `QuickReplyBar.vue`
|
||
- [ ] 端到端测试:补齐/语气/润色/改写全流程
|
||
- [ ] 右栏放大/缩小切换测试
|
||
- [ ] 回复建议区展开/缩回动画测试
|
||
- [ ] 响应式断点验证(1280px / 1024px)
|
||
- [ ] Dify 不可用时的降级测试
|
||
|
||
**总预估**: 9 天
|
||
|
||
---
|
||
|
||
### 15. 风险与缓解
|
||
|
||
| 风险 | 概率 | 影响 | 缓解措施 |
|
||
|------|------|------|---------|
|
||
| 幽灵文字定位不准(mirror div 坐标计算误差) | 中 | 中 | 参照 VS Code textarea-caret-position 方案;增加 padding/font 精确复制逻辑;添加回退:定位不准时降级为底部建议条 |
|
||
| 补齐 API 延迟 > 1.5s 影响体验 | 中 | 中 | 前端 1.5s 超时取消请求;Dify prompt 精简(max_length 限制 80 字);考虑后续迭代引入流式 SSE |
|
||
| Dify Wingman Agent 不支持多 temperature | 低 | 高 | 改造前先验证 Dify API 是否传递 temperature 参数;若不支持,在后端不传 temperature(使用 Dify 默认值),通过 system prompt 控制创造性 |
|
||
| 右栏放大模式下中栏组件状态丢失 | 低 | 中 | 使用 CSS 隐藏而非 v-if 销毁;组件实例保持存活;切回时立即可见 |
|
||
| 快速回复从竖向改为横向,三层导航体验下降 | 中 | 中 | 数字快捷键固定 2 层(L1: Alt+1~7, L2: 1~9),键盘操作不依赖鼠标;L3 条目列表在选中 L2 后弹出 |
|
||
| 坐席习惯右栏快速回复,移位后找不到 | 中 | 低 | 工具栏保留"快回"入口;输入框为空时回复建议区自动展开;首次使用引导提示 |
|
||
| AiAssistantPanel 全面重构影响现有草稿/摘要/标注功能 | 中 | 高 | 分步重构:先新增训练区组件,再移除旧功能;保持 WingmanService 后端不变,仅前端组件重构;保留 git 分支可回滚 |
|
||
| RAGFlow 知识库检索延迟影响改写响应 | 中 | 低 | 知识库检索设 3s 超时;超时降级为仅基于对话上下文生成 2 个版本;前端显示"知识库检索中"提示 |
|
||
|
||
---
|
||
|
||
## 附录:与主架构文档的映射关系
|
||
|
||
| 本文档章节 | 主文档对应位置 |
|
||
|-----------|-------------|
|
||
| §1 架构概述 | 主文档 §8 AI Wingman 设计(更新) |
|
||
| §2 后端架构 | 主文档 §15.8 坐席端 AI 辅助消息框(新增) |
|
||
| §3 前端架构 | 主文档 §7.2 坐席工作台模块(更新) |
|
||
| §4 布局重构 | 主文档 §7.2 坐席工作台模块(更新) |
|
||
| §7 API 规范 | 主文档 §14 API设计规范(引用) |
|
||
| §11 类图 | 主文档 §15.8(引用) |
|
||
| §12 时序图 | 主文档 §15.8(引用) |
|
||
|
||
---
|
||
|
||
> **文档结束** | 版本 v1.0 | 2026-07-11 | 宋献 (Simon)
|