chore(docs): docs/ 目录全面重新编号 + 重组
**重构前**(旧编号 02-11): - docs/02-产品需求/ → 00 产品规划/PRD - docs/03-技术架构/ → 01-05 子目录散落 - docs/04-原型设计/ → 01-02 产品设计(HTML 原型) - docs/05-原型设计/ → screens/ - docs/06-测试素材/ → 02-E2E / 03-功能 / 04-版本测试 - docs/07-项目管理/ → 任务说明书/日报/计划 - docs/08-安全审计/ → 审计报告 - docs/09-堡垒运维/ → toolbox / deploy - docs/10-项目管理/ → 任务说明书(重复) - docs/11-历史归档/ → deploy-nas-archived **重构后**(新编号 00-07,语义化): - docs/00-产品开发流程与文档管理规范.md - docs/00-版本迭代总览.md - docs/01-产品文档/ (PRD/原型/认证/会话/AI 服务/坐席/集成) - docs/02-技术文档/ (技术方案/架构图/重构记录/前端改造/实现配置) - docs/03-测试文档/ (E2E/功能用例/版本报告/缺陷单) - docs/04-运维文档/ (部署运维/运维指南) - docs/05-运营文档/ (品牌推广/用户手册) - docs/06-安全审计/ (审计报告) - docs/07-项目管理/ (任务说明书/日报/计划/看板) **净收益**: - 目录编号与产品文档管理规范对齐(按文档阶段 01-07 编号) - 消除 02-产品需求 与 10-项目管理 的编号重叠 - 子目录按文档类型分组(如 01-产品文档/00-产品规划、01-产品文档/01-认证与登录) - 把运维/安全/项目管理从 0X 散落改为 04/06/07 合计 494 文件 + 78495 行 / - 14076 行
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
classDiagram
|
||||
class Agent {
|
||||
+str id
|
||||
+str user_id
|
||||
+str name
|
||||
+str status
|
||||
+str role
|
||||
+list skill_tags
|
||||
+int current_load
|
||||
+int max_load
|
||||
+datetime created_at
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
class SystemConfig {
|
||||
+str id
|
||||
+str config_key
|
||||
+str config_value
|
||||
+str description
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
class ConfigChangeLog {
|
||||
+str id
|
||||
+str config_key
|
||||
+str old_value
|
||||
+str new_value
|
||||
+str changed_by
|
||||
+datetime changed_at
|
||||
}
|
||||
|
||||
class QuickReplyTemplate {
|
||||
+str id
|
||||
+str category
|
||||
+str title
|
||||
+str content
|
||||
+list variables
|
||||
+int sort_order
|
||||
+str status
|
||||
+int version
|
||||
+str submitted_by
|
||||
+datetime created_at
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
class Conversation {
|
||||
+str id
|
||||
+str employee_id
|
||||
+str employee_name
|
||||
+str status
|
||||
+int urgency_score
|
||||
+str assigned_agent_id
|
||||
+datetime created_at
|
||||
}
|
||||
|
||||
ConfigChangeLog --> SystemConfig : tracks changes to
|
||||
ConfigChangeLog --> Agent : changed_by
|
||||
QuickReplyTemplate --> Agent : submitted_by
|
||||
Conversation --> Agent : assigned_agent_id
|
||||
@@ -0,0 +1,19 @@
|
||||
sequenceDiagram
|
||||
participant U as 管理员
|
||||
participant FE as frontend-admin
|
||||
participant API as /api/admin/configs/{key}
|
||||
participant SVC as admin_service
|
||||
participant DB as PostgreSQL
|
||||
|
||||
U->>FE: 切换应急模式开关
|
||||
FE->>API: PUT /api/admin/configs/emergency_mode
|
||||
API->>API: require_admin 校验权限
|
||||
API->>SVC: update_config(key, value, agent_id)
|
||||
SVC->>DB: SELECT SystemConfig WHERE key=emergency_mode
|
||||
DB-->>SVC: 当前值 "false"
|
||||
SVC->>DB: INSERT ConfigChangeLog(old="false", new="true", by=agent_id)
|
||||
SVC->>DB: UPDATE SystemConfig SET value="true"
|
||||
DB-->>SVC: 更新成功
|
||||
SVC-->>API: {key, old_value, new_value, changed_at}
|
||||
API-->>FE: 返回变更结果
|
||||
FE->>FE: 显示变更成功提示
|
||||
@@ -0,0 +1,16 @@
|
||||
sequenceDiagram
|
||||
participant U as 管理员(组长)
|
||||
participant FE as frontend-admin
|
||||
participant API as /api/agents/login
|
||||
participant Redis as Redis
|
||||
participant DB as PostgreSQL
|
||||
|
||||
U->>FE: 输入 user_id + name 登录
|
||||
FE->>API: POST /api/agents/login
|
||||
API->>DB: 查询 Agent (user_id)
|
||||
DB-->>API: Agent 记录(含 role 字段)
|
||||
API->>Redis: 存储 token → user_id 映射
|
||||
API-->>FE: {agent_info, token, role: "admin"}
|
||||
FE->>FE: 检查 role === "admin"
|
||||
FE->>FE: 存储 admin_token 到 localStorage
|
||||
FE->>FE: 跳转到 /admin/dashboard
|
||||
@@ -0,0 +1,23 @@
|
||||
sequenceDiagram
|
||||
participant A as 坐席(王丽)
|
||||
participant AG as /api/quick-replies
|
||||
participant U as 管理员(宋献)
|
||||
participant ADM as /api/admin/quick-replies
|
||||
participant DB as PostgreSQL
|
||||
|
||||
A->>AG: POST /api/quick-replies (创建模板, status=draft)
|
||||
A->>AG: PUT /api/quick-replies/{id} (提交审核, status→pending_review)
|
||||
AG->>DB: UPDATE QuickReplyTemplate SET status='pending_review', submitted_by='wang_li'
|
||||
|
||||
U->>ADM: GET /api/admin/quick-replies/pending
|
||||
ADM->>DB: SELECT WHERE status='pending_review'
|
||||
DB-->>ADM: 待审核列表
|
||||
ADM-->>U: 显示待审核模板
|
||||
|
||||
U->>ADM: PUT /api/admin/quick-replies/{id}/review (action=approve)
|
||||
ADM->>DB: UPDATE SET status='approved', version=version+1
|
||||
DB-->>ADM: 更新成功
|
||||
|
||||
A->>AG: GET /api/quick-replies (获取可见模板)
|
||||
AG->>DB: SELECT WHERE status='approved' OR (status='pending_review' AND submitted_by=自己)
|
||||
DB-->>AG: 全员可见(approved) + 仅自己(pending_review)
|
||||
@@ -0,0 +1,137 @@
|
||||
%% 坐席端 AI 辅助消息框 — 类图
|
||||
%% 文档版本: v1.0
|
||||
%% 创建日期: 2026-07-11
|
||||
|
||||
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 : 弹出面板
|
||||
@@ -0,0 +1,261 @@
|
||||
%% 坐席端 AI 辅助消息框 — 时序图
|
||||
%% 文档版本: v1.0
|
||||
%% 创建日期: 2026-07-11
|
||||
%% 包含 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
|
||||
@@ -0,0 +1,77 @@
|
||||
classDiagram
|
||||
direction TB
|
||||
|
||||
class Employee {
|
||||
+str id
|
||||
+str corp_id
|
||||
+str employee_id
|
||||
+str name
|
||||
+str department
|
||||
+str position
|
||||
+str mobile
|
||||
+str email
|
||||
+str avatar
|
||||
+int status
|
||||
+str it_level ★NEW
|
||||
+str it_level_source ★NEW
|
||||
+dict notes ★NEW
|
||||
+datetime last_login_at
|
||||
+datetime created_at
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
class Conversation {
|
||||
+str id
|
||||
+str corp_id
|
||||
+str employee_id
|
||||
+str employee_name
|
||||
+str department
|
||||
+str status
|
||||
+bool is_vip
|
||||
+bool is_pinned
|
||||
+bool is_todo
|
||||
+int urgency_score
|
||||
+dict tags
|
||||
+str assigned_agent_id
|
||||
+list collaborating_agent_ids
|
||||
+int impact_scope ★NEW
|
||||
+bool is_blocking ★NEW
|
||||
+str emotion_state ★NEW
|
||||
+datetime last_message_at
|
||||
+str last_message_summary
|
||||
+datetime created_at
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
class TodoItem {
|
||||
+str id
|
||||
+str type
|
||||
+str title
|
||||
+str priority
|
||||
+dict description
|
||||
+str status
|
||||
+str assigned_agent_id
|
||||
+str corp_id
|
||||
+datetime created_at
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
class TroubleshootingTemplate {
|
||||
+str id
|
||||
+str name
|
||||
+str category
|
||||
+list path_steps
|
||||
+dict flowchart
|
||||
+bool is_active
|
||||
+datetime created_at
|
||||
+datetime updated_at
|
||||
}
|
||||
|
||||
Employee "1" --> "*" Conversation : has
|
||||
Conversation "1" --> "*" TodoItem : may generate
|
||||
TroubleshootingTemplate "1" --> "0..1" Conversation : applied to
|
||||
|
||||
note for Employee "it_level: bronze|silver|gold|platinum|diamond|star|king\nit_level_source: system|manual"
|
||||
note for Conversation "impact_scope: 受影响人数\nis_blocking: 是否阻断性\nemotion_state: normal|anxious|angry|urgent"
|
||||
note for TodoItem "type: ticket|approval|device\npriority: urgent|high|normal\nstatus: pending|processing|resolved"
|
||||
note for TroubleshootingTemplate "category: vpn|email|system|account\npath_steps: [{label, status}]\nflowchart: 递归树结构"
|
||||
@@ -0,0 +1,22 @@
|
||||
sequenceDiagram
|
||||
participant U as 坐席
|
||||
participant TB as TopBar.vue
|
||||
participant TS as useThemeStore
|
||||
participant UT as useTheme.ts
|
||||
participant DOM as document.documentElement
|
||||
participant LS as localStorage
|
||||
|
||||
U->>TB: 点击 ☀️/🌙 切换开关
|
||||
TB->>TS: toggleTheme()
|
||||
TS->>TS: currentTheme = currentTheme === 'light' ? 'dark' : 'light'
|
||||
TS->>UT: applyTheme(currentTheme)
|
||||
UT->>DOM: setAttribute('data-theme', theme)
|
||||
UT->>LS: setItem('theme', theme)
|
||||
DOM-->>DOM: CSS 变量自动切换(:root / [data-theme="dark"])
|
||||
DOM-->>U: 300ms 过渡动画,界面变色
|
||||
|
||||
Note over U,LS: 页面加载时
|
||||
U->>UT: 首次进入页面
|
||||
UT->>LS: getItem('theme')
|
||||
LS-->>UT: 'dark' | 'light' | null
|
||||
UT->>DOM: setAttribute('data-theme', theme || 'light')
|
||||
@@ -0,0 +1,62 @@
|
||||
sequenceDiagram
|
||||
participant Browser as 坐席浏览器
|
||||
participant App as Vue3 App
|
||||
participant Store as Pinia Store
|
||||
participant API as Backend API
|
||||
participant DB as PostgreSQL
|
||||
|
||||
Note over Browser,App: 页面加载
|
||||
|
||||
Browser->>App: 挂载 Workspace.vue
|
||||
App->>Store: 初始化 conversationStore
|
||||
Store->>API: GET /api/conversations
|
||||
API->>DB: SELECT * FROM conversations WHERE status IN ('queued','serving') ORDER BY ...
|
||||
DB-->>API: 会话列表
|
||||
API-->>Store: 会话数据
|
||||
Store-->>App: 渲染会话列表
|
||||
|
||||
loop 每3秒 setInterval
|
||||
App->>Store: pollConversations()
|
||||
Store->>API: GET /api/conversations?page=1&page_size=50
|
||||
API->>DB: SELECT ... (同上)
|
||||
DB-->>API: 最新会话列表
|
||||
API-->>Store: 最新数据
|
||||
|
||||
alt 数据有变化
|
||||
Store->>Store: diff 比较,更新变化的会话
|
||||
Store-->>App: 触发响应式更新
|
||||
App->>App: 更新列表项标签/排序/未读数
|
||||
else 数据无变化
|
||||
Store-->>App: 无需更新
|
||||
end
|
||||
end
|
||||
|
||||
Note over App: 用户点击某个会话
|
||||
|
||||
App->>Store: selectConversation(id)
|
||||
Store->>API: GET /api/conversations/{id}/messages?limit=50
|
||||
API->>DB: SELECT * FROM messages WHERE conversation_id=... ORDER BY created_at
|
||||
DB-->>API: 消息列表
|
||||
API-->>Store: messages
|
||||
Store-->>App: 渲染对话区
|
||||
|
||||
loop 选中会话的消息轮询
|
||||
App->>Store: pollMessages(conv_id)
|
||||
Store->>API: GET /api/conversations/{id}/messages?limit=20&before=latest_id
|
||||
API->>DB: SELECT ... WHERE created_at > latest
|
||||
DB-->>API: 新消息
|
||||
API-->>Store: 新消息列表
|
||||
alt 有新消息
|
||||
Store->>Store: 追加消息到列表
|
||||
Store-->>App: 滚动到底部,显示新消息
|
||||
end
|
||||
end
|
||||
|
||||
Note over App: 坐席发送回复
|
||||
|
||||
App->>Store: sendMessage(conv_id, content)
|
||||
Store->>API: POST /api/conversations/{id}/messages {content}
|
||||
API->>DB: INSERT INTO messages ...
|
||||
API-->>Store: 发送的消息对象
|
||||
Store->>Store: 追加到消息列表
|
||||
Store-->>App: 显示在对话区
|
||||
@@ -0,0 +1,69 @@
|
||||
sequenceDiagram
|
||||
participant WX as 企微消息
|
||||
participant API as FastAPI
|
||||
participant Router as MessageRouter
|
||||
participant Score as ScoringService
|
||||
participant Vip as VipService
|
||||
participant DB as PostgreSQL
|
||||
participant Redis as Redis
|
||||
|
||||
WX->>API: 员工消息回调
|
||||
API->>Router: route_message(msg)
|
||||
|
||||
rect rgb(255, 240, 240)
|
||||
Note over Router,Score: Step 1: VIP检测
|
||||
Router->>Vip: is_vip(employee_id)
|
||||
Vip->>Redis: GET vip_cache:{employee_id}
|
||||
alt 缓存命中
|
||||
Redis-->>Vip: is_vip=True/False
|
||||
else 缓存未命中
|
||||
Vip->>Vip: get_user_info(employee_id)
|
||||
Vip->>Vip: _check_vip_rules(user_info)
|
||||
Note over Vip: 规则: 总监及以上 或 关键部门
|
||||
Vip->>Redis: SET vip_cache:{employee_id} EX 3600
|
||||
end
|
||||
Vip-->>Router: is_vip=True/False
|
||||
end
|
||||
|
||||
rect rgb(255, 255, 220)
|
||||
Note over Router,Score: Step 2: 情绪关键词检测
|
||||
Router->>Score: detect_emotion(msg)
|
||||
Score->>DB: SELECT FROM system_configs WHERE key LIKE 'emotion_keywords_%'
|
||||
DB-->>Score: 关键词列表
|
||||
Score->>Score: 遍历关键词匹配消息内容
|
||||
Score-->>Router: emotion="urgent"
|
||||
end
|
||||
|
||||
rect rgb(220, 255, 220)
|
||||
Note over Router,Score: Step 3: 举手检测
|
||||
Router->>Score: detect_hand_raise(msg)
|
||||
Score->>DB: SELECT FROM system_configs WHERE key='hand_raise_keywords'
|
||||
DB-->>Score: ["转人工","人工",...]
|
||||
Score->>Score: 遍历关键词匹配
|
||||
Score-->>Router: hand_raise=True
|
||||
end
|
||||
|
||||
rect rgb(220, 220, 255)
|
||||
Note over Router,Score: Step 4: 需介入检测
|
||||
Router->>Score: detect_need_intervene(conv)
|
||||
Score->>DB: SELECT COUNT FROM messages WHERE conversation_id=... AND sender_type='employee'
|
||||
DB-->>Score: 员工消息数
|
||||
Score->>DB: SELECT FROM system_configs WHERE key='intervene_round_threshold'
|
||||
DB-->>Score: 3
|
||||
Score->>Score: 员工连续追问 > 3轮?
|
||||
Score-->>Router: need_intervene=True
|
||||
end
|
||||
|
||||
rect rgb(255, 220, 255)
|
||||
Note over Router,Score: Step 5: 紧急度计算
|
||||
Router->>Score: calculate_urgency(conv, msg)
|
||||
Score->>Score: base = keyword_score(1)
|
||||
Score->>Score: + emotion_bonus(1)
|
||||
Score->>Score: + vip_bonus(1)
|
||||
Score->>Score: + repeat_bonus(1)
|
||||
Score->>Score: total = min(5, base+emotion+vip+repeat)
|
||||
Score-->>Router: urgency_score=4
|
||||
end
|
||||
|
||||
Router->>DB: UPDATE conversations SET tags=..., urgency_score=4, is_vip=...
|
||||
Router-->>API: 更新后的会话
|
||||
@@ -0,0 +1,43 @@
|
||||
sequenceDiagram
|
||||
participant Emp as 员工(H5页面)
|
||||
participant API as FastAPI
|
||||
participant ConvSvc as ConversationService
|
||||
participant Score as ScoringService
|
||||
participant WXSvc as WecomService
|
||||
participant Agent as 坐席工作台
|
||||
participant DB as PostgreSQL
|
||||
|
||||
Emp->>API: POST /api/h5/conversation/shake
|
||||
API->>ConvSvc: find_or_create_conversation(employee_id)
|
||||
ConvSvc->>DB: 查询/创建 conversation
|
||||
DB-->>ConvSvc: conversation
|
||||
API->>Score: detect_hand_raise("摇人")
|
||||
Score-->>API: hand_raise=True
|
||||
API->>ConvSvc: update_conversation(tags={hand_raise:true})
|
||||
ConvSvc->>DB: UPDATE conversations SET tags=...
|
||||
API->>ConvSvc: get_funny_phrase(scene='shake')
|
||||
ConvSvc->>DB: SELECT FROM funny_phrases WHERE scene='shake'
|
||||
DB-->>ConvSvc: "大哥,俺这就去摇人,稍等..."
|
||||
ConvSvc->>ConvSvc: send_message(conv_id, 'system', 趣味话术)
|
||||
ConvSvc->>WXSvc: send_text_message(employee_id, 趣味话术)
|
||||
API-->>Emp: {conversation, funny_phrase}
|
||||
|
||||
Note over Emp: H5显示摇人动画 + 趣味话术
|
||||
|
||||
loop 坐席轮询
|
||||
Agent->>API: GET /api/conversations
|
||||
API->>ConvSvc: get_conversations()
|
||||
ConvSvc->>DB: SELECT ... ORDER BY urgency DESC
|
||||
DB-->>ConvSvc: 列表(举手会话靠前)
|
||||
API-->>Agent: 举手标记的会话(黄色标签)
|
||||
end
|
||||
|
||||
Agent->>API: POST /api/conversations/{id}/assign {agent_id}
|
||||
API->>ConvSvc: update_conversation(status='serving', assigned_agent_id)
|
||||
ConvSvc->>DB: UPDATE conversations SET status='serving'
|
||||
ConvSvc->>ConvSvc: send_message(conv_id, 'system', '人摇来了!IT坐席为您服务')
|
||||
ConvSvc->>WXSvc: send_text_message(employee_id, 接入话术)
|
||||
ConvSvc-->>API: updated conversation
|
||||
API-->>Agent: 接单成功
|
||||
|
||||
WXSvc-->>Emp: 企微推送"人摇来了!IT坐席为您服务"
|
||||
@@ -0,0 +1,58 @@
|
||||
%% 方案A — 坐席端 Web Speech API 实时转写时序图
|
||||
sequenceDiagram
|
||||
participant U as 坐席
|
||||
participant RB as ReplyBox.vue
|
||||
participant USR as useSpeechRecognition
|
||||
participant SR as SpeechRecognition
|
||||
|
||||
Note over U,SR: 阶段1:初始化
|
||||
RB->>USR: useSpeechRecognition('zh-CN')
|
||||
USR->>USR: 检查 window.SpeechRecognition || webkitSpeechRecognition
|
||||
alt 浏览器不支持
|
||||
USR-->>RB: state.isSupported = false
|
||||
RB->>RB: 语音按钮显示为禁用/隐藏
|
||||
else 浏览器支持
|
||||
USR->>USR: state.isSupported = true
|
||||
end
|
||||
|
||||
Note over U,SR: 阶段2:开始识别
|
||||
U->>RB: 点击语音按钮 🎤
|
||||
RB->>USR: start()
|
||||
USR->>SR: new SpeechRecognition()
|
||||
USR->>SR: lang='zh-CN', continuous=true, interimResults=true
|
||||
USR->>SR: 注册 onresult / onerror / onend 回调
|
||||
USR->>SR: start()
|
||||
SR-->>USR: onstart 触发
|
||||
USR-->>RB: state.isListening = true
|
||||
RB->>RB: 按钮变红 + textarea 显示光标 + 提示"正在聆听..."
|
||||
|
||||
Note over U,SR: 阶段3:实时转写(边说边出字)
|
||||
U->>SR: 说话:"帮我查一下"
|
||||
SR-->>USR: onresult(event) — interimResults (isFinal=false)
|
||||
USR->>USR: state.interimText = "帮我查一下"
|
||||
USR-->>RB: 实时更新 textarea(灰色临时文字)
|
||||
RB->>RB: inputText.value = finalText + interimText
|
||||
|
||||
U->>SR: 继续说话:"帮我查一下密码"
|
||||
SR-->>USR: onresult(event) — final result (isFinal=true)
|
||||
USR->>USR: state.finalText += "帮我查一下密码"
|
||||
USR-->>RB: 确定文字追加到 textarea
|
||||
RB->>RB: inputText.value = finalText
|
||||
|
||||
Note over U,SR: 阶段4:停止识别
|
||||
U->>RB: 再次点击语音按钮
|
||||
RB->>USR: stop()
|
||||
USR->>SR: stop()
|
||||
SR-->>USR: onend 触发
|
||||
USR-->>RB: state.isListening = false
|
||||
RB->>RB: 按钮恢复 + textarea 保持已识别文字
|
||||
|
||||
Note over U,SR: 异常分支
|
||||
alt 识别出错
|
||||
SR-->>USR: onerror(event) — error='not-allowed'
|
||||
USR-->>RB: showToast("麦克风权限被拒绝")
|
||||
end
|
||||
alt 用户长时间不说话
|
||||
SR-->>USR: onend 自动触发(超时停止)
|
||||
USR-->>RB: state.isListening = false
|
||||
end
|
||||
@@ -0,0 +1,82 @@
|
||||
%% 语音识别转文字 — 类图
|
||||
classDiagram
|
||||
class Wx {
|
||||
<<interface>>
|
||||
+config(options: WxConfigOptions) void
|
||||
+ready(callback: Function) void
|
||||
+error(callback: Function) void
|
||||
+startRecord() void
|
||||
+stopRecord(options: object) void
|
||||
+translateVoice(options: object) void
|
||||
}
|
||||
|
||||
class JsapiConfig {
|
||||
+corp_id: string
|
||||
+agent_id: string
|
||||
+timestamp: number
|
||||
+nonce_str: string
|
||||
+signature: string
|
||||
}
|
||||
|
||||
class UseWecomVoiceReturn {
|
||||
+state: VoiceState
|
||||
+init() Promise~void~
|
||||
+startRecording() Promise~void~
|
||||
+stopAndTranslate() Promise~string~
|
||||
+isSupported() boolean
|
||||
}
|
||||
|
||||
class VoiceState {
|
||||
+isReady: boolean
|
||||
+isRecording: boolean
|
||||
+isTranslating: boolean
|
||||
+error: string|null
|
||||
}
|
||||
|
||||
class SpeechRecognition {
|
||||
<<interface>>
|
||||
+lang: string
|
||||
+continuous: boolean
|
||||
+interimResults: boolean
|
||||
+start() void
|
||||
+stop() void
|
||||
+abort() void
|
||||
+onresult: Function
|
||||
+onerror: Function
|
||||
+onend: Function
|
||||
}
|
||||
|
||||
class UseSpeechRecognitionReturn {
|
||||
+state: SpeechState
|
||||
+start() void
|
||||
+stop() void
|
||||
+reset() void
|
||||
}
|
||||
|
||||
class SpeechState {
|
||||
+isListening: boolean
|
||||
+interimText: string
|
||||
+finalText: string
|
||||
+error: string|null
|
||||
+isSupported: boolean
|
||||
}
|
||||
|
||||
class InputBar {
|
||||
+inputText: string
|
||||
+handleVoiceStart() void
|
||||
+handleVoiceEnd() void
|
||||
}
|
||||
|
||||
class ReplyBox {
|
||||
+inputText: string
|
||||
+toggleVoice() void
|
||||
}
|
||||
|
||||
UseWecomVoiceReturn --> VoiceState
|
||||
UseWecomVoiceReturn ..> Wx : uses
|
||||
UseWecomVoiceReturn ..> JsapiConfig : fetches
|
||||
InputBar --> UseWecomVoiceReturn : composes
|
||||
|
||||
UseSpeechRecognitionReturn --> SpeechState
|
||||
UseSpeechRecognitionReturn ..> SpeechRecognition : wraps
|
||||
ReplyBox --> UseSpeechRecognitionReturn : composes
|
||||
@@ -0,0 +1,52 @@
|
||||
%% 方案C — H5端企微 JS-SDK 录音转文字时序图
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant IB as InputBar.vue
|
||||
participant UWV as useWecomVoice
|
||||
participant API as /api/wecom/jsapi-config
|
||||
participant WX as 企微服务器
|
||||
participant WV as 企微WebView
|
||||
|
||||
Note over U,WV: 阶段1:初始化(页面加载时,幂等)
|
||||
IB->>UWV: init()
|
||||
UWV->>WV: 检查 window.wx 是否存在
|
||||
alt JS-SDK 未加载
|
||||
UWV->>WV: 动态加载 jweixin-1.2.0.js
|
||||
end
|
||||
UWV->>API: GET /api/wecom/jsapi-config?url=当前页面URL
|
||||
API->>API: get_jsapi_ticket() + sha1 签名
|
||||
API-->>UWV: { corp_id, agent_id, timestamp, nonce_str, signature }
|
||||
UWV->>WV: wx.config({ appId, timestamp, ..., jsApiList: ['startRecord','stopRecord','translateVoice'] })
|
||||
WV-->>UWV: wx.ready() 回调
|
||||
UWV-->>IB: state.isReady = true
|
||||
|
||||
Note over U,WV: 阶段2:录音
|
||||
U->>IB: 按下语音按钮 🎤
|
||||
IB->>UWV: startRecording()
|
||||
UWV->>WV: wx.startRecord()
|
||||
UWV-->>IB: state.isRecording = true
|
||||
IB->>IB: UI 切换为录音状态(按钮变红+提示"松开识别文字")
|
||||
|
||||
Note over U,WV: 阶段3:停止录音+转文字
|
||||
U->>IB: 松开语音按钮
|
||||
IB->>UWV: stopAndTranslate()
|
||||
UWV->>WV: wx.stopRecord()
|
||||
WV-->>UWV: { localId: "wxLocalId_xxx" }
|
||||
UWV-->>IB: state.isTranslating = true
|
||||
UWV->>WV: wx.translateVoice({ localId, isShowProgressTips: 1 })
|
||||
WV->>WX: 上传录音+请求语音识别
|
||||
WX-->>WV: 返回识别文字
|
||||
WV-->>UWV: { translateResult: "帮我重置密码" }
|
||||
UWV-->>IB: 返回识别文字
|
||||
IB->>IB: inputText.value += translateResult
|
||||
IB->>IB: state.isTranslating = false, state.isRecording = false
|
||||
|
||||
Note over U,WV: 异常分支
|
||||
alt 录音超过60秒
|
||||
WV-->>UWV: 自动触发 stopRecord 回调
|
||||
UWV->>UWV: 自动执行 translateVoice
|
||||
end
|
||||
alt 转文字失败
|
||||
WV-->>UWV: fail 回调 { errMsg }
|
||||
UWV-->>IB: showToast("语音识别失败")
|
||||
end
|
||||
Reference in New Issue
Block a user