Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/增量设计-AI辅助消息框-20260711.md
T
Simon facc04aa65 chore: docs 结构整改 + compose 双目录对齐(合并重建提交)
本提交为 .git 对象库损坏后的重建提交,内容等价于原先三个本地提交
(5e2fd4c2 / 57a53c98 / 5d7e1873)的累积结果,未做任何额外改动。

一、docs 结构整改(整改 #14)
根因:重构时新结构为 untracked 文件,执行 git stash(未带 -u)未纳入,
随后 git reset 拉回 HEAD 旧 tracked 树,导致旧树复活、新旧两棵目录
树并存于 docs/,共 791 文件、双分类体系冲突。

修复动作:
- b2 同名异主题文件改名迁移保全 9 个
- C 类 39 个孤立文件按主题正确归类
- A/B1 类 222 个重复文件删除(新结构已有内容副本)
- 9 个旧独有空目录删除
- 270 处内部引用按 verified 映射改写
- 整改记录 #14 登记于 04-运维文档/部署运维

结果:docs 791 → 569 文件,顶层仅规范 8 类 + 治理文件,单树恢复。
残留:约 20 处指向从未存在文件的陈旧死链,归入独立文档卫生任务。

二、compose 双目录对齐(消除踩坑 A)
- docker-compose.yml:nginx 前端挂载全部由根目录 frontend-*/dist
  改为 src/frontend-*/dist(h5 / agent / admin / terminal)
- docker-compose.dev.yml:dev 服务 build context 与卷同步改 src/
- 效果:本地 docker compose up 不再把根目录 stale dist 挂回,
  与线上一致,分叉隐患消除(已 docker compose config 校验通过)

防复发铁律:
- 重构须提交;仓库修复须 git stash -u 或先 commit
- 新结构须 git add 并提交,避免再次 untracked 复活
- H5 改动只动 src/frontend-h5/,禁改根目录遗留 frontend-*/
2026-08-07 22:31:32 +08:00

48 KiB
Raw Blame History

增量设计:坐席端 AI 辅助消息框

文档版本v1.0
创建日期2026-07-11
架构师:高见远
关联 PRD01-产品文档/04-坐席工作台/PRD-REQ-坐席-002-AI辅助消息框-v1.0.md
技术栈:后端 FastAPI + Python / 前端 Vue3 + Element Plus + TypeScript
部署方式Docker Composebind 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() 中先查 Rediskey=wingman:autocomplete:{hash(text+conv_id)},TTL=30s),命中则直接返回;未命中调用 Dify 后写入缓存
改写功能知识库集成 版本 3 需引用知识库文档,需检索 RAGFlow 并注入 prompt rewrite_versions() 调用 build_ragflow_client() 检索相关知识片段,拼入 Dify system promptRAGFlow 不可用时降级为仅生成 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.2Redis 缓存 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_idsrewrite_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 端点

  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 调用使用 apiClientsrc/api/index.ts 导出的 axios 实例)
  - 响应拦截器自动提取 res.dataScheme 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. 只返回回复内容,不要加额外解释