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-*/
48 KiB
48 KiB
增量设计:坐席端 AI 辅助消息框
文档版本:v1.0
创建日期:2026-07-11
架构师:高见远
关联 PRD:01-产品文档/04-坐席工作台/PRD-REQ-坐席-002-AI辅助消息框-v1.0.md
技术栈:后端 FastAPI + Python / 前端 Vue3 + Element Plus + TypeScript
部署方式:Docker Compose,bind mount./app:/app/app
目录
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
# 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 新增方法签名
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 类型定义
// 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 接口
// 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 类图
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/02-技术文档/技术架构/05-架构图/ai-assist-02-技术文档/技术架构/class-diagram-代办事项.mermaid
4. 程序调用流程
4.1 实时自动补齐时序图
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 语气调整时序图
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 文字润色时序图
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 智能改写时序图
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/02-技术文档/技术架构/05-架构图/ai-assist-02-技术文档/技术架构/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(修改)backend/app/api/wingman.py(修改)backend/app/schemas/wingman_assist.py(新建)backend/app/dependencies/__init__.py(修改) |
无 | P0 | 2 天 |
| T02 | 前端 API 封装与状态管理 | frontend-agent/src/api/wingman.ts(修改)frontend-agent/src/composables/useAiAssist.ts(新建)frontend-agent/src/types/ai-assist.d.ts(新建) |
T01 | P0 | 1 天 |
| T03 | AI 辅助 UI 组件(补齐 + 语气 + 工具栏) | frontend-agent/src/components/chat/ai-assist/GhostText.vue(新建)frontend-agent/src/components/chat/ai-assist/ToneAdjustPopover.vue(新建)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(新建)frontend-agent/src/components/chat/ai-assist/RewritePanel.vue(新建)frontend-agent/src/components/chat/ReplyBox.vue(修改) |
T01, T02, T03 | P1 | 2 天 |
任务详细说明
T01: 后端服务层与 API 端点
- 在
schemas/wingman_assist.py中定义 4 个 Pydantic 请求模型(AutocompleteRequest / ToneAdjustRequest / PolishRequest / RewriteRequest) - 在
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可选参数
- 在
wingman.py中新增 4 个 API 端点,复用_validate_conversation+_get_recent_messages - 在
dependencies/__init__.py中修改dep_wingman_service注入 Redis 客户端
T02: 前端 API 封装与状态管理
- 在
wingman.ts中新增 4 个 API 函数(autocomplete / adjustTone / polishText / rewriteVersions),均接受可选signal: AbortSignal参数 - 在
types/ai-assist.d.ts中定义共享类型(ToneType / PolishAction / 各 Result 接口) - 在
useAiAssist.ts中实现 composable:- 自动补齐:debounce 800ms + AbortController 生命周期管理 + ghostText 状态
- 语气/润色/改写:loading 状态 + result 状态 + 面板 visible 状态
cleanup()函数:组件卸载时 abort 所有进行中的请求
T03: AI 辅助 UI 组件(补齐 + 语气 + 工具栏)
GhostText.vue— 幽灵文字渲染层:- 创建隐藏 div 镜像 textarea 样式
- 计算光标像素坐标(截取光标前文本 → 镜像 div → getBoundingClientRect)
- 在光标位置覆盖灰色斜体文字
- 监听 Tab(接受全部)/ Shift+Tab(接受一个词)/ Esc(清除)
ToneAdjustPopover.vue— 语气选择浮层:- 3 种语气按钮(专业/友好/简洁)
- 原文/改写文对比展示
- 替换/取消按钮
AiAssistToolbar.vue— AI 辅助工具栏:- 补齐开关(toggle 按钮)
- 语气/润色/改写按钮
- 按钮状态管理(禁用条件:无选中文字时禁用语气按钮,无文本时禁用润色/改写按钮)
T04: AI 辅助 UI 组件(润色 + 改写)+ ReplyBox 集成
PolishPanel.vue— 润色精修面板:- 扩写/压缩/纠错 3 个操作按钮
- 左右对比布局(原文 | 结果,结果可编辑)
- 替换全部/取消按钮
RewritePanel.vue— 改写版本选择面板:- 3 个版本卡片纵向排列(可滚动)
- 每个版本支持"替换"/"追加"
ReplyBox.vue改造:- 引入
useAiAssistcomposable - 在工具栏中插入
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. 任务依赖图
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. 只返回回复内容,不要加额外解释