Files
wecom_it_smart_desk/docs/02-技术文档/技术架构/增量设计-知识库迭代-开发任务分解-20260712.md
T

1239 lines
58 KiB
Markdown
Raw Normal View History

# 增量设计:知识库迭代 — 开发任务分解
> **文档版本**v1.0
> **创建日期**2026-07-12
> **文档类型**:增量系统设计 + 开发任务分解
> **前置文档**
> - 技术方案:`docs/02-技术文档/技术架构/增量设计-知识库迭代与痛点缓解-20260711.md`
> - 原型图:`docs/01-产品文档/07-知识库/知识库迭代-未实现功能原型设计-20260711.md`
> **技术栈**FastAPI + SQLAlchemy + PostgreSQL + Redis + Neo4j / Vue3 + Element Plus + Vant4 + ECharts / Dify
---
## 目录
1. [设计概述](#1-设计概述)
2. [分诊交互 — 增量设计](#2-分诊交互--增量设计)
3. [拓扑预览 — 增量设计](#3-拓扑预览--增量设计)
4. [代答排除 — 增量设计](#4-代答排除--增量设计)
5. [数据库迁移方案](#5-数据库迁移方案)
6. [Dify 分诊应用配置规格](#6-dify-分诊应用配置规格)
7. [文件清单](#7-文件清单)
8. [开发任务列表](#8-开发任务列表)
9. [模块依赖关系图](#9-模块依赖关系图)
10. [共享知识(Shared Knowledge](#10-共享知识shared-knowledge)
11. [待明确事项](#11-待明确事项)
---
## 1. 设计概述
### 1.1 背景与决策
基于产品经理产出的原型图文档与用户确认的 4 项关键决策 + 9 项次要决策,本文档对已有技术方案文档中三个未实现功能模块(分诊交互、拓扑预览、代答排除)进行增量设计补充,并分解为可执行的开发任务。
#### 4 项关键决策
| 决策项 | 确认结果 | 设计影响 |
|--------|---------|---------|
| Dify 应用 | **新建独立分诊应用** | 需产出 Dify 应用配置规格,不复用 Wingman |
| 坐席看板 | **本期完整实现** | 原型图有 2 页坐席端看板,需新增 7 个 API 端点 |
| 拓扑预览 | **只读图谱 + 搜索筛选** | 不实现节点编辑/添加/删除写操作,复用已有 GET /graph API |
| 代答排除 | **4 种匹配方式全做** | 关键词 + 正则 + 意图(Dify) + 分类(依赖分诊) |
#### 9 项次要决策
| # | 决策项 | 确认结果 |
|---|--------|---------|
| 1 | 分诊数据存储 | 新建 `triage_sessions` 表 |
| 2 | VIP 判断 | 本期不做,后续接入 eHR |
| 3 | 紧急度判断 | 关键词规则("紧急/马上/宕机/无法工作"→高) |
| 4 | 分诊超时 | 5 秒,超时自动转人工 |
| 5 | 专家模式 | 展示所有步骤预览 |
| 6 | 拓扑预览 | Relation 节点类型和 DERIVED_FROM 关系类型不新增,筛选中置灰 |
| 7 | 排除检查时机 | AI 回复前(消息接收到后、调用 Dify 前) |
| 8 | 导入规则 | 本期不实现 |
| 9 | 命中后动作 | 4 种全实现(转人工/转人工+上下文/仅提示/静默转人工) |
### 1.2 三模块范围对比
| 维度 | 分诊交互 | 拓扑预览 | 代答排除 |
|------|---------|---------|---------|
| 涉及端 | H5 + 坐席端 + Dify | 管理后台 | 管理后台 + H5(消息流) |
| 后端新增 | API 11 个 + 服务 2 个 | 无新增端点 | API 8 个 + 服务 1 个 + 匹配器 4 个 |
| 前端新增 | H5 组件修改 + 坐席页面 1 个 + 组件 3 个 | 管理后台页面 1 个 + 组件 3 个 | 管理后台页面 1 个 + 组件 2 个 |
| DB 新增表 | triage_sessions | 无 | exclusion_rules + exclusion_logs |
| Dify 依赖 | 新建独立分诊应用 | 无 | 复用审批意图识别 Dify 链路 |
| 可并行性 | 基础,其他依赖它 | **完全独立可并行** | 依赖分诊(分类匹配) |
---
## 2. 分诊交互 — 增量设计
### 2.1 范围差异说明
已有技术方案文档(§3.1)仅设计了 H5 端 4 个 APIstart/step/skip/transfer),但用户决策"坐席看板本期完整实现",原型图有 2 页坐席端分诊看板。需补充:
1. **坐席端分诊看板 API**7 个新增端点)
2. **Dify 分诊应用**配置规格(新建独立应用,不复用 Wingman)
3. **triage_sessions 表**结构设计
### 2.2 H5 端 API 设计(已有方案,补充 complete 端点)
已有技术方案定义了 4 个 H5 端 API,现补充第 5 个:
| 方法 | 路径 | 请求 | 响应 | 说明 |
|------|------|------|------|------|
| POST | `/api/h5/triage/start` | `{conversation_id, question}` | `{triage_id, steps: TriageStep[], total: int, confidence, urgency, suggested_route}` | 发起分诊 |
| POST | `/api/h5/triage/step` | `{triage_id, step_index, selected_label}` | `{next_step: TriageStep, collected_context: string[]}` | 提交步骤选择 |
| POST | `/api/h5/triage/skip` | `{triage_id, step_index}` | `{next_step: TriageStep}` | 跳过步骤 |
| POST | `/api/h5/triage/transfer` | `{triage_id, context: string[]}` | `{conversation_id, status: "waiting_agent"}` | 转人工 |
| POST | `/api/h5/triage/complete` | `{triage_id, context: string[]}` | `{reply: string, confidence: float}` | 分诊完成生成最终回复 |
> **设计变更**`/start` 响应新增 `triage_id`(分诊会话唯一 ID),后续所有操作通过 `triage_id` 关联,而非 `conversation_id`。
### 2.3 坐席端分诊看板 API 设计(新增 7 个端点)
路由前缀:`/api/agent/triage`
| 方法 | 路径 | 请求 | 响应 | 说明 |
|------|------|------|------|------|
| GET | `/api/agent/triage/pending` | `?urgency=&problem_type=&page=&page_size=` | `{code:0, data:{total, items: TriageSession[]}}` | 待分诊列表(按紧急度排序) |
| GET | `/api/agent/triage/stats` | — | `{code:0, data:{pending_total, today_triaged, ai_self_count, human_count, auto_approval_count, avg_duration_sec}}` | 分诊看板统计概要 |
| GET | `/api/agent/triage/{triage_id}` | — | `{code:0, data: TriageSessionDetail}` | 分诊详情(含用户画像、问题描述、AI分析、已收集上下文) |
| POST | `/api/agent/triage/{triage_id}/route` | `{route_action: "ai_self"\|"human"\|"auto_approval"\|"skip", route_note?: string}` | `{code:0, data: TriageSession}` | 坐席路由操作(覆盖 AI 建议) |
| GET | `/api/agent/triage/history` | `?date_from=&date_to=&route_action=&page=&page_size=` | `{code:0, data:{total, items: TriageSession[]}}` | 已分诊历史列表 |
| GET | `/api/agent/triage/export` | `?date_from=&date_to=` | `文件流 (xlsx)` | 导出分诊记录 |
| POST | `/api/agent/triage/{triage_id}/exclude-options` | `{excluded_labels: string[], recommended_label?: string}` | `{code:0, data:{excluded: true}}` | 坐席排除/推荐分诊选项(WS 推送到 H5) |
### 2.4 triage_sessions 表结构
```sql
CREATE TABLE triage_sessions (
id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text,
conversation_id VARCHAR(36) NOT NULL,
user_id VARCHAR(100) NOT NULL,
user_name VARCHAR(100),
user_dept VARCHAR(100),
user_level VARCHAR(20),
device_info VARCHAR(200),
request_title VARCHAR(200) NOT NULL,
request_content TEXT NOT NULL,
source VARCHAR(50) DEFAULT 'wecom_h5',
-- AI 分诊分析结果
problem_type VARCHAR(50),
problem_category VARCHAR(100),
confidence FLOAT,
urgency VARCHAR(20) DEFAULT 'medium', -- high/medium/low
suggested_route VARCHAR(50), -- ai_self/human/auto_approval
matched_knowledge VARCHAR(500),
match_score FLOAT,
context_tags JSON DEFAULT '[]',
-- 分诊步骤数据
triage_steps JSON DEFAULT '[]', -- [{question, options:[{label,probability}]}]
collected_context JSON DEFAULT '[]', -- ["Outlook 2019", "Windows 10"]
-- 状态与路由
status VARCHAR(30) DEFAULT 'pending', -- pending/triaging/routed/skipped/timeout
route_action VARCHAR(50), -- ai_self/human/auto_approval/skip
route_note TEXT,
operator_id VARCHAR(100),
operated_at TIMESTAMP,
-- 时间戳
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 索引
CREATE INDEX idx_triage_status ON triage_sessions(status);
CREATE INDEX idx_triage_urgency ON triage_sessions(urgency);
CREATE INDEX idx_triage_conversation ON triage_sessions(conversation_id);
CREATE INDEX idx_triage_created ON triage_sessions(created_at);
CREATE INDEX idx_triage_user ON triage_sessions(user_id);
```
### 2.5 分诊主流程时序图
```mermaid
sequenceDiagram
participant H5 as H5 员工端
participant API as FastAPI 后端
participant TriSvc as TriageService
participant DifyTri as Dify 分诊应用
participant DB as PostgreSQL
participant AG as 坐席端看板
participant WS as WebSocket
rect rgb(255, 243, 224)
Note over H5,DB: 阶段1:发起分诊
H5->>API: POST /api/h5/triage/start {conversation_id, question}
API->>TriSvc: start_triage(conversation_id, question)
TriSvc->>DB: INSERT triage_sessions (status=triaging)
TriSvc->>DifyTri: 调用分诊 prompt(拆分问题为分步选择题)
alt Dify 5秒内响应
DifyTri-->>TriSvc: {steps[], confidence, urgency, suggested_route, problem_type}
TriSvc->>DB: UPDATE triage_sessions SET triage_steps, confidence, urgency, suggested_route
TriSvc-->>API: {triage_id, steps, total, confidence, urgency, suggested_route}
API-->>H5: {triage_id, steps, total, ...}
else Dify 超时(>5s)
TriSvc->>DB: UPDATE triage_sessions SET status=timeout
TriSvc-->>API: 超时,自动转人工
API-->>H5: 分诊超时,已转人工
end
end
rect rgb(227, 242, 253)
Note over H5,DB: 阶段2:分步选择 + 坐席协同
loop 每一步
H5->>API: POST /api/h5/triage/step {triage_id, step_index, selected_label}
API->>TriSvc: submit_step(triage_id, step_index, selected_label)
TriSvc->>DB: 记录 collected_context
TriSvc->>DifyTri: 根据选择动态调整后续步骤
DifyTri-->>TriSvc: next_step
TriSvc-->>API: {next_step, collected_context}
API-->>H5: {next_step, collected_context}
end
Note over AG: 坐席看板实时查看分诊进度
AG->>API: GET /api/agent/triage/pending
API-->>AG: 待分诊列表
AG->>API: GET /api/agent/triage/{triage_id}
API-->>AG: 分诊详情
opt 坐席排除选项
AG->>API: POST /api/agent/triage/{triage_id}/exclude-options {excluded_labels}
API->>WS: WS 推送 excluded_labels 到 H5
WS-->>H5: {type: "triage_exclude", excluded_labels}
H5->>H5: TriageCard.setExcludedOptions(labels)
end
end
rect rgb(232, 245, 233)
Note over H5,DB: 阶段3:分诊完成 / 转人工
alt 所有步骤完成
H5->>API: POST /api/h5/triage/complete {triage_id, context}
API->>TriSvc: complete_triage(triage_id, context)
TriSvc->>DifyTri: 根据收集的上下文生成最终回复
DifyTri-->>TriSvc: {reply, confidence}
TriSvc->>DB: UPDATE triage_sessions SET status=routed, route_action=ai_self
TriSvc-->>API: {reply, confidence}
API-->>H5: AI 回复
else 转人工
H5->>API: POST /api/h5/triage/transfer {triage_id, context}
API->>TriSvc: transfer_to_human(triage_id, context)
TriSvc->>DB: UPDATE triage_sessions SET status=routed, route_action=human
TriSvc-->>API: 转人工成功
API-->>H5: 已转人工
end
end
```
### 2.6 坐席端看板组件设计
| 组件 | 文件路径 | 说明 |
|------|---------|------|
| 分诊看板主页面 | `frontend-agent/src/views/TriageDashboard.vue` | 左列表+右详情双栏布局 |
| 统计概要栏 | `frontend-agent/src/components/triage/TriageStatsBar.vue` | 6 项统计卡片 |
| 待分诊列表 | `frontend-agent/src/components/triage/TriagePendingList.vue` | 紧急度排序+筛选 |
| 分诊详情面板 | `frontend-agent/src/components/triage/TriageDetailPanel.vue` | 用户画像+问题描述+AI分析+上下文+路由操作 |
| 路由操作栏 | 内嵌于 TriageDetailPanel | 4 个路由按钮 + 备注 + 排除选项 |
### 2.7 紧急度判断规则(决策 #3)
```python
# 关键词规则,在 TriageService 中实现
URGENCY_HIGH_KEYWORDS = ["紧急", "马上", "宕机", "无法工作", "崩溃", "死机", "蓝屏"]
URGENCY_MEDIUM_KEYWORDS = ["报错", "失败", "连不上", "打不开", "不能用"]
def determine_urgency(question: str, confidence: float) -> str:
"""根据关键词 + 置信度判断紧急度。"""
if any(kw in question for kw in URGENCY_HIGH_KEYWORDS):
return "high"
if confidence < 0.5:
return "high" # 低置信也视为紧急
if any(kw in question for kw in URGENCY_MEDIUM_KEYWORDS):
return "medium"
return "low"
```
### 2.8 分诊超时处理(决策 #4)
```python
# TriageService.start_triage 中
import asyncio
try:
result = await asyncio.wait_for(
dify_triage_service.analyze(question, context),
timeout=5.0 # 5秒超时
)
except asyncio.TimeoutError:
# 超时自动转人工
await self._transfer_to_human_on_timeout(triage_id)
return {"status": "timeout", "message": "分诊超时,已自动转人工"}
```
---
## 3. 拓扑预览 — 增量设计
### 3.1 范围确认
| 维度 | 确认结果 |
|------|---------|
| 交互模式 | **只读** — 不实现节点编辑/添加/删除写操作 |
| 后端端点 | **不新增** — 复用已有 `GET /api/admin/knowledge-iteration/graph?issue_name=xxx` |
| 节点类型 | Issue + ActionRelation 节点类型和 DERIVED_FROM 关系类型不新增,筛选中置灰) |
| 布局 | 力导向图(ECharts graph),层次树布局预留但不实现 |
| 搜索 | 节点名称模糊搜索 + 高亮定位 |
| 筛选 | 节点类型(Issue/Action+ 关系类型(LEADS_TO/RELATES_TO/CAN_JUMP_TO |
### 3.2 前端组件清单
| 组件 | 文件路径 | 说明 |
|------|---------|------|
| 拓扑预览主页面 | `frontend-admin/src/views/TopologyPreview.vue` | 页面容器,统计栏+工具栏+画布+详情面板 |
| 图谱画布 | `frontend-admin/src/components/topology/GraphCanvas.vue` | ECharts 力导向图渲染 + 搜索高亮 + 缩放控制 |
| 节点详情面板 | `frontend-admin/src/components/topology/NodeDetailPanel.vue` | 节点属性 + 关联关系 + 高亮路径(只读) |
| 工具栏 | `frontend-admin/src/components/topology/GraphToolbar.vue` | 搜索框 + 节点类型筛选 + 关系类型筛选 + 缩放控制 |
| API 封装 | `frontend-admin/src/api/topology.ts` | 封装 GET /graph 调用 |
### 3.3 数据流
```mermaid
sequenceDiagram
participant Page as TopologyPreview.vue
participant API as topology.ts
participant Backend as FastAPI
participant Neo4j as Neo4jClient
Page->>API: fetchGraph({issue_name?, limit?})
API->>Backend: GET /api/admin/knowledge-iteration/graph?issue_name=xxx&limit=200
Backend->>Neo4j: query_full_graph() 或 query_issue_subgraph()
Neo4j-->>Backend: {nodes:[], links:[]}
Backend-->>API: {code:0, data:{nodes, links}}
API-->>Page: graphData
Page->>Page: ECharts setOption(graphData)
Note over Page: 力导向渲染,节点按类型着色
opt 用户搜索节点
Page->>Page: 前端过滤 nodes.filter(n => n.name.includes(keyword))
Page->>Page: 匹配节点高亮脉冲,非匹配变暗
Page->>Page: 右侧浮层展示搜索结果
end
opt 用户点击节点
Page->>Page: 选中节点高亮 + 路径高亮
Page->>Page: NodeDetailPanel 显示详情(从已有数据取,不发请求)
end
opt 用户筛选节点类型
Page->>Page: ECharts legend 过滤 Issue/Action
Note over Page: Relation 类型 checkbox 置灰(disabled
end
```
### 3.4 ECharts 配置要点
```typescript
// GraphCanvas.vue 核心配置
const chartOption = {
tooltip: {
formatter: (params) => `${params.data.name} (${params.data.type})`
},
legend: {
data: [
{ name: 'Issue', itemStyle: { color: '#07C160' } },
{ name: 'Action', itemStyle: { color: '#f59e0b' } },
{ name: 'Relation', itemStyle: { color: '#9ca3af' }, itemStyle: { opacity: 0.3 } } // 置灰
]
},
series: [{
type: 'graph',
layout: 'force',
roam: true, // 支持缩放平移
focusNodeAdjacency: true, // 悬停高亮相邻
force: {
repulsion: 300,
gravity: 0.1,
edgeLength: [100, 250]
},
categories: [
{ name: 'Issue' }, // 绿色 #07C160, symbolSize=28
{ name: 'Action' }, // 橙色 #f59e0b, symbolSize=22
],
data: nodes.map(n => ({
id: n.id,
name: n.name,
category: n.type === 'issue' ? 0 : 1,
symbolSize: n.type === 'issue' ? 28 : 22,
itemStyle: { color: n.type === 'issue' ? '#07C160' : '#f59e0b' }
})),
links: links.map(l => ({
source: l.source,
target: l.target,
lineStyle: {
color: '#c0c4cc',
width: Math.max(0.5, (l.weight || 1) * 1.5),
curveness: 0.3
}
}))
}]
}
```
### 3.5 搜索高亮实现
搜索在前端完成(不发后端请求),对已加载的节点数据做名称模糊匹配:
```typescript
function highlightSearch(keyword: string) {
if (!keyword) {
// 恢复所有节点正常样式
restoreAllNodes()
return
}
const matched = nodes.filter(n => n.name.includes(keyword))
// 匹配节点:蓝色描边 + 脉冲动画
// 非匹配节点:降低透明度
chart.setOption({
series: [{
data: nodes.map(n => ({
...n,
itemStyle: matched.includes(n)
? { borderColor: '#3b82f6', borderWidth: 3, shadowBlur: 10, shadowColor: '#3b82f6' }
: { opacity: 0.15 }
}))
}]
})
// 右侧浮层展示搜索结果
searchResults.value = matched
}
```
---
## 4. 代答排除 — 增量设计
### 4.1 架构设计:策略模式 + 责任链
代答排除采用 **策略模式** 定义 4 种匹配器,**责任链模式** 按优先级依次检查规则。
```mermaid
graph TB
subgraph "代答排除匹配引擎"
Engine[ExclusionService<br/>匹配引擎入口]
Chain[ExclusionChain<br/>责任链调度]
subgraph "策略模式 - 4种匹配器"
M1[KeywordMatcher<br/>关键词匹配]
M2[RegexMatcher<br/>正则匹配]
M3[IntentMatcher<br/>意图匹配]
M4[CategoryMatcher<br/>分类排除]
end
Engine --> Chain
Chain --> M1
Chain --> M2
Chain --> M3
Chain --> M4
end
subgraph "外部依赖"
Dify[Dify 意图识别<br/>复用审批意图链路]
TriageDB[triage_sessions<br/>分诊结果]
RuleDB[(exclusion_rules)]
LogDB[(exclusion_logs)]
end
M3 -->|调用| Dify
M4 -->|查询| TriageDB
Engine -->|读取规则| RuleDB
Engine -->|记录命中| LogDB
style M1 fill:#dbeafe,stroke:#3b82f6
style M2 fill:#fce7f3,stroke:#ec4899
style M3 fill:#ede9fe,stroke:#8b5cf6
style M4 fill:#dcfce7,stroke:#07C160
```
### 4.2 匹配器接口设计
```python
# backend/app/services/matchers/base.py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
@dataclass
class MatchResult:
"""匹配结果。"""
matched: bool
matched_detail: str = "" # 命中的关键词/正则/意图/分类
match_position: str = "" # 匹配位置(用于测试展示)
class BaseMatcher(ABC):
"""匹配器基类 — 策略模式接口。"""
@abstractmethod
async def match(self, message: str, condition: str, context: dict = None) -> MatchResult:
"""检查消息是否匹配规则条件。
Args:
message: 用户消息文本
condition: 匹配条件(关键词列表/正则表达式/意图ID列表/分类名称列表)
context: 上下文(含 conversation_id, triage_result 等)
Returns:
MatchResult
"""
...
```
### 4.3 四种匹配器实现要点
| 匹配器 | 文件 | 匹配逻辑 | 外部依赖 |
|--------|------|---------|---------|
| KeywordMatcher | `matchers/keyword_matcher.py` | 逗号分隔关键词列表,消息包含任一即命中 | 无 |
| RegexMatcher | `matchers/regex_matcher.py` | Python `re.search(pattern, message)`,正则编译缓存 | 无 |
| IntentMatcher | `matchers/intent_matcher.py` | 调用 Dify 意图识别 API,检查返回意图是否在排除列表中 | Dify(复用审批意图链路 `approval_dify_base_url` |
| CategoryMatcher | `matchers/category_matcher.py` | 查询 `triage_sessions` 表获取分诊结果的 `problem_category`,检查是否在排除分类列表中 | triage_sessions 表(依赖分诊模块) |
### 4.4 意图匹配与 Dify 对接方案
**复用现有审批意图识别 Dify 链路**,不新建 Dify 应用:
```python
# backend/app/services/matchers/intent_matcher.py
from app.config import settings
import httpx
class IntentMatcher(BaseMatcher):
async def match(self, message: str, condition: str, context: dict = None) -> MatchResult:
# 1. 调用 Dify 意图识别(复用 approval_dify_base_url
intent = await self._recognize_intent(message)
# 2. 检查意图是否在排除列表中
excluded_intents = [s.strip() for s in condition.split(",")]
if intent in excluded_intents:
return MatchResult(matched=True, matched_detail=f"意图: {intent}")
return MatchResult(matched=False)
async def _recognize_intent(self, message: str) -> str:
"""调用 Dify 意图识别 API。"""
async with httpx.AsyncClient(timeout=settings.approval_dify_timeout) as client:
resp = await client.post(
f"{settings.approval_dify_base_url}/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.approval_dify_api_key}"},
json={
"model": "intent-recognition",
"messages": [
{"role": "system", "content": "识别用户消息的意图类别,输出意图ID。"},
{"role": "user", "content": message}
],
"temperature": 0.1
}
)
return resp.json()["choices"][0]["message"]["content"].strip()
```
### 4.5 分类排除与分诊的依赖关系
CategoryMatcher 依赖分诊结果:
```python
# backend/app/services/matchers/category_matcher.py
class CategoryMatcher(BaseMatcher):
async def match(self, message: str, condition: str, context: dict = None) -> MatchResult:
conversation_id = context.get("conversation_id") if context else None
if not conversation_id:
return MatchResult(matched=False) # 无会话上下文,跳过分类匹配
# 查询 triage_sessions 获取分诊结果
from app.models.triage_session import TriageSession
result = await db.execute(
select(TriageSession).where(
TriageSession.conversation_id == conversation_id
).order_by(TriageSession.created_at.desc()).limit(1)
)
triage = result.scalar_one_or_none()
if not triage or not triage.problem_category:
return MatchResult(matched=False) # 无分诊结果,跳过
excluded_categories = [s.strip() for s in condition.split(",")]
if triage.problem_category in excluded_categories:
return MatchResult(
matched=True,
matched_detail=f"分类: {triage.problem_category}"
)
return MatchResult(matched=False)
```
> **设计说明**:分类排除是软依赖——如果分诊模块尚未运行或无分诊结果,CategoryMatcher 返回未命中,不影响其他匹配器执行。
### 4.6 排除检查时机与消息流集成(决策 #7)
排除检查发生在 **AI 回复前**(消息接收到后、调用 Dify 前),嵌入 `AIHandler` 处理链:
```mermaid
flowchart TD
A[用户发送消息] --> B[AIHandler.handle_message]
B --> C{打招呼/呼叫人工?}
C -->|是| D[引导话术]
C -->|否| E[代答排除检查]
E --> F[ExclusionService.check_exclusions]
F --> G{命中排除规则?}
G -->|是| H[执行命中后动作]
G -->|否| I[正常 AI 回复流程]
H --> J{action_type}
J -->|transfer_human| K[转人工坐席]
J -->|transfer_human_with_context| L[携带上下文转人工]
J -->|prompt_transfer| M[展示提示语 等待确认]
J -->|silent_transfer| N[静默转人工]
K --> O[记录 exclusion_logs]
L --> O
M --> O
N --> O
O --> P[更新 exclusion_rules.hit_count]
I --> Q[调用 Dify AI 生成回复]
Q --> R[返回 AI 回复]
```
### 4.7 命中后动作实现(决策 #9 — 4 种全实现)
| 动作类型 | 标识 | 实现方式 | 员工体验 |
|---------|------|---------|---------|
| 转人工坐席 | `transfer_human` | 会话状态→waiting_agent,推送给坐席队列 | 看到"已转接人工"提示 |
| 转人工+上下文 | `transfer_human_with_context` | 同上 + 附带 collected_context 快照 | 看到"已转接人工"提示 |
| 仅提示转人工 | `prompt_transfer` | 返回 transfer_message,等待用户确认 | 看到提示语,可选择确认转人工 |
| 静默转人工 | `silent_transfer` | 会话状态→waiting_agent,不提示用户 | 无感知,直接进入人工队列 |
### 4.8 DB 表设计
**exclusion_rules 表**
```sql
CREATE TABLE exclusion_rules (
id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text,
rule_name VARCHAR(200) NOT NULL UNIQUE,
rule_description TEXT,
priority VARCHAR(5) NOT NULL DEFAULT 'P2', -- P0/P1/P2/P3
match_type VARCHAR(20) NOT NULL, -- keyword/regex/intent/category
match_condition TEXT NOT NULL, -- 关键词列表/正则/意图ID列表/分类名称列表
match_scope JSON NOT NULL DEFAULT '["ai_auto_reply"]',
action_type VARCHAR(50) NOT NULL DEFAULT 'transfer_human',
transfer_message TEXT,
status VARCHAR(10) NOT NULL DEFAULT 'enabled', -- enabled/disabled
hit_count INTEGER DEFAULT 0,
created_by VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_exclusion_rules_status ON exclusion_rules(status);
CREATE INDEX idx_exclusion_rules_priority ON exclusion_rules(priority);
CREATE INDEX idx_exclusion_rules_match_type ON exclusion_rules(match_type);
```
**exclusion_logs 表**
```sql
CREATE TABLE exclusion_logs (
id VARCHAR(36) PRIMARY KEY DEFAULT gen_random_uuid()::text,
rule_id VARCHAR(36) NOT NULL,
rule_name VARCHAR(200),
conversation_id VARCHAR(36),
user_id VARCHAR(100),
message_content TEXT,
match_type VARCHAR(20),
matched_detail TEXT,
action_type VARCHAR(50),
action_result VARCHAR(50) DEFAULT 'success', -- success/failed
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_exclusion_logs_rule ON exclusion_logs(rule_id);
CREATE INDEX idx_exclusion_logs_created ON exclusion_logs(created_at);
CREATE INDEX idx_exclusion_logs_conversation ON exclusion_logs(conversation_id);
```
### 4.9 管理后台 API 设计
路由前缀:`/api/admin/exclusion-rules`
| 方法 | 路径 | 请求 | 响应 | 说明 |
|------|------|------|------|------|
| GET | `/api/admin/exclusion-rules` | `?status=&match_type=&priority=&keyword=&page=&page_size=` | `{code:0, data:{total, items}}` | 规则列表(分页+筛选) |
| POST | `/api/admin/exclusion-rules` | `ExclusionRuleCreate` | `{code:0, data: ExclusionRule}` | 新建规则 |
| GET | `/api/admin/exclusion-rules/{id}` | — | `{code:0, data: ExclusionRule}` | 规则详情 |
| PUT | `/api/admin/exclusion-rules/{id}` | `ExclusionRuleUpdate` | `{code:0, data: ExclusionRule}` | 编辑规则 |
| DELETE | `/api/admin/exclusion-rules/{id}` | — | `{code:0, message:"删除成功"}` | 删除规则 |
| POST | `/api/admin/exclusion-rules/{id}/toggle` | `{status: "enabled"\|"disabled"}` | `{code:0, data: ExclusionRule}` | 启用/停用 |
| POST | `/api/admin/exclusion-rules/test` | `{message: string, rule_id?: string}` | `{code:0, data:{matched, matched_detail, rule_name, action_type}}` | 测试匹配 |
| GET | `/api/admin/exclusion-rules/stats` | — | `{code:0, data:{enabled_count, disabled_count, monthly_hits, monthly_transfers}}` | 统计概要 |
---
## 5. 数据库迁移方案
### 5.1 迁移文件
新增迁移版本:`045_triage_exclusion_tables.py`
```python
"""triage_sessions + exclusion_rules + exclusion_logs
Revision ID: 045_triage_exclusion
Revises: 044_automation
Create Date: 2026-07-12
"""
from alembic import op
import sqlalchemy as sa
revision = "045_triage_exclusion"
down_revision = "044_automation"
def upgrade():
# 1. triage_sessions 表
op.create_table(
"triage_sessions",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("conversation_id", sa.String(36), nullable=False),
sa.Column("user_id", sa.String(100), nullable=False),
sa.Column("user_name", sa.String(100)),
sa.Column("user_dept", sa.String(100)),
sa.Column("user_level", sa.String(20)),
sa.Column("device_info", sa.String(200)),
sa.Column("request_title", sa.String(200), nullable=False),
sa.Column("request_content", sa.Text, nullable=False),
sa.Column("source", sa.String(50), server_default="wecom_h5"),
sa.Column("problem_type", sa.String(50)),
sa.Column("problem_category", sa.String(100)),
sa.Column("confidence", sa.Float),
sa.Column("urgency", sa.String(20), server_default="medium"),
sa.Column("suggested_route", sa.String(50)),
sa.Column("matched_knowledge", sa.String(500)),
sa.Column("match_score", sa.Float),
sa.Column("context_tags", sa.JSON, server_default="[]"),
sa.Column("triage_steps", sa.JSON, server_default="[]"),
sa.Column("collected_context", sa.JSON, server_default="[]"),
sa.Column("status", sa.String(30), server_default="pending"),
sa.Column("route_action", sa.String(50)),
sa.Column("route_note", sa.Text),
sa.Column("operator_id", sa.String(100)),
sa.Column("operated_at", sa.DateTime),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("idx_triage_status", "triage_sessions", ["status"])
op.create_index("idx_triage_urgency", "triage_sessions", ["urgency"])
op.create_index("idx_triage_conversation", "triage_sessions", ["conversation_id"])
op.create_index("idx_triage_created", "triage_sessions", ["created_at"])
op.create_index("idx_triage_user", "triage_sessions", ["user_id"])
# 2. exclusion_rules 表
op.create_table(
"exclusion_rules",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("rule_name", sa.String(200), nullable=False, unique=True),
sa.Column("rule_description", sa.Text),
sa.Column("priority", sa.String(5), nullable=False, server_default="P2"),
sa.Column("match_type", sa.String(20), nullable=False),
sa.Column("match_condition", sa.Text, nullable=False),
sa.Column("match_scope", sa.JSON, nullable=False, server_default='["ai_auto_reply"]'),
sa.Column("action_type", sa.String(50), nullable=False, server_default="transfer_human"),
sa.Column("transfer_message", sa.Text),
sa.Column("status", sa.String(10), nullable=False, server_default="enabled"),
sa.Column("hit_count", sa.Integer, server_default="0"),
sa.Column("created_by", sa.String(100), nullable=False),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("idx_exclusion_rules_status", "exclusion_rules", ["status"])
op.create_index("idx_exclusion_rules_priority", "exclusion_rules", ["priority"])
op.create_index("idx_exclusion_rules_match_type", "exclusion_rules", ["match_type"])
# 3. exclusion_logs 表
op.create_table(
"exclusion_logs",
sa.Column("id", sa.String(36), primary_key=True),
sa.Column("rule_id", sa.String(36), nullable=False),
sa.Column("rule_name", sa.String(200)),
sa.Column("conversation_id", sa.String(36)),
sa.Column("user_id", sa.String(100)),
sa.Column("message_content", sa.Text),
sa.Column("match_type", sa.String(20)),
sa.Column("matched_detail", sa.Text),
sa.Column("action_type", sa.String(50)),
sa.Column("action_result", sa.String(50), server_default="success"),
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
)
op.create_index("idx_exclusion_logs_rule", "exclusion_logs", ["rule_id"])
op.create_index("idx_exclusion_logs_created", "exclusion_logs", ["created_at"])
op.create_index("idx_exclusion_logs_conversation", "exclusion_logs", ["conversation_id"])
def downgrade():
op.drop_table("exclusion_logs")
op.drop_table("exclusion_rules")
op.drop_table("triage_sessions")
```
### 5.2 迁移顺序
```
044_automation (已有)
045_triage_exclusion (本次新增 — 3 张表一次性创建)
```
> 三张表在一个迁移中创建,因为 exclusion_logs 外键引用 exclusion_rulestriage_sessions 被 exclusion 的 CategoryMatcher 引用,三者紧密关联。
---
## 6. Dify 分诊应用配置规格
### 6.1 应用基本信息
| 配置项 | 值 |
|--------|-----|
| 应用名称 | IT-智能分诊引擎 |
| 应用类型 | 聊天助手 (Chat Assistant) |
| 模型 | 与 Wingman 相同的 LLM(如 Qwen2.5-72B |
| 温度 | 0.3(偏确定性输出) |
| 最大 token | 2000 |
| 超时 | 5 秒(后端 asyncio.wait_for 控制) |
### 6.2 输入参数 Schema
```json
{
"conversation_id": "string — 会话ID",
"question": "string — 员工问题文本(必填)",
"collected_context": "array — 已收集的上下文标签(分步选择中累积)",
"excluded_options": "array — 坐席已排除的选项标签(避免AI后续推荐)",
"step_index": "int — 当前步骤序号(0=首次分诊)"
}
```
### 6.3 输出格式(JSON
```json
{
"triage_type": "confirm|transfer|approval",
"confidence": 0.85,
"urgency": "high|medium|low",
"problem_type": "硬件|软件|网络|安全|账号|其他",
"problem_category": "Outlook",
"suggested_route": "ai_self|human|auto_approval",
"matched_knowledge": "Outlook登录失败 → 密码过期检查",
"match_score": 0.89,
"context_tags": ["Outlook 2019", "Windows 10", "AD域用户"],
"triage_steps": [
{
"question": "Outlook是否弹出了具体的错误提示?",
"options": [
{"label": "弹出了,提示无法连接到服务器", "probability": 0.68},
{"label": "弹出了,提示密码错误", "probability": 0.22},
{"label": "没有弹出任何提示,直接闪退", "probability": 0.08},
{"label": "其他情况", "probability": 0.02}
]
}
],
"total_steps": 3,
"reply": "AI回复文本(当triage_type=confirm时,引导员工选择)"
}
```
### 6.4 Prompt 设计要点
| 要点 | 说明 |
|------|------|
| **角色定义** | 你是IT服务台智能分诊引擎,负责分析员工IT问题并拆分为分步选择题 |
| **任务目标** | 1. 识别问题类型和分类 2. 评估置信度和紧急度 3. 拆分复杂问题为分步选择题 4. 推荐路由渠道 |
| **复杂度自适应** | 简单问题1-2步,复杂问题3-5步,每步最多4个选项 |
| **概率分配** | 每个选项分配概率(0-1),所有选项概率之和为1,最高概率项可标记推荐 |
| **紧急度规则** | 消息含"紧急/马上/宕机/无法工作/崩溃/死机/蓝屏"→high;含"报错/失败/连不上"→medium;其余→low |
| **排除选项** | excluded_options 中的选项不出现在后续步骤中 |
| **输出约束** | 必须输出合法JSON,不要输出解释性文字 |
| **置信度评估** | 基于知识库匹配度、问题清晰度、上下文完整度综合评估 |
### 6.5 后端对接方式
| 配置项 | 环境变量 | 默认值 | 说明 |
|--------|---------|--------|------|
| API URL | `DIFY_TRIAGE_API_URL` | "" | Dify OpenAI 兼容接口地址(如 http://yw-dify/dc.servyou-it.com/dify2openai |
| API Key | `DIFY_TRIAGE_API_KEY` | "" | 格式:base_url\|app_id\|app_name |
| 超时 | `DIFY_TRIAGE_TIMEOUT` | 5 | 秒,超时自动转人工 |
```python
# backend/app/config.py 新增配置项
# ----------------------------------------------------------------------
# AI 分诊服务配置(Dify Agent 3 — 独立分诊应用)
# ----------------------------------------------------------------------
dify_triage_api_url: str = ""
dify_triage_api_key: str = ""
dify_triage_timeout: int = 5
```
```python
# backend/app/services/dify_triage_service.py 对接方式
class DifyTriageService:
async def analyze(self, question: str, context: list = None,
excluded_options: list = None, step_index: int = 0) -> dict:
"""调用 Dify 分诊应用。"""
async with httpx.AsyncClient(timeout=settings.dify_triage_timeout) as client:
resp = await client.post(
f"{settings.dify_triage_api_url}/v1/chat/completions",
headers={"Authorization": f"Bearer {settings.dify_triage_api_key}"},
json={
"model": "triage-engine",
"messages": [
{"role": "system", "content": TRIAGE_SYSTEM_PROMPT},
{"role": "user", "content": json.dumps({
"question": question,
"collected_context": context or [],
"excluded_options": excluded_options or [],
"step_index": step_index
})}
],
"temperature": 0.3
}
)
content = resp.json()["choices"][0]["message"]["content"]
return json.loads(content) # 解析 JSON 输出
```
---
## 7. 文件清单
### 7.1 后端文件
| # | 文件路径 | 操作 | 模块 | 说明 |
|---|---------|------|------|------|
| B01 | `backend/migrations/versions/045_triage_exclusion_tables.py` | [新建] | DB | 迁移:3 张新表 |
| B02 | `backend/app/config.py` | [修改] | 配置 | 新增 dify_triage_* 配置项 |
| B03 | `backend/app/models/triage_session.py` | [新建] | 分诊 | TriageSession ORM 模型 |
| B04 | `backend/app/models/exclusion_rule.py` | [新建] | 排除 | ExclusionRule ORM 模型 |
| B05 | `backend/app/models/exclusion_log.py` | [新建] | 排除 | ExclusionLog ORM 模型 |
| B06 | `backend/app/schemas/triage.py` | [新建] | 分诊 | 请求/响应 SchemaH5端+坐席端) |
| B07 | `backend/app/schemas/exclusion.py` | [新建] | 排除 | 请求/响应 Schema |
| B08 | `backend/app/api/triage.py` | [新建] | 分诊 | 分诊 API(H5端 5 个 + 坐席端 7 个) |
| B09 | `backend/app/api/exclusion_rules.py` | [新建] | 排除 | 代答排除管理 API(8 个端点) |
| B10 | `backend/app/services/triage_service.py` | [新建] | 分诊 | 分诊业务逻辑服务 |
| B11 | `backend/app/services/dify_triage_service.py` | [新建] | 分诊 | Dify 分诊应用对接 |
| B12 | `backend/app/services/exclusion_service.py` | [新建] | 排除 | 代答排除匹配引擎 |
| B13 | `backend/app/services/matchers/__init__.py` | [新建] | 排除 | 匹配器包初始化 |
| B14 | `backend/app/services/matchers/base.py` | [新建] | 排除 | 匹配器基类 + MatchResult |
| B15 | `backend/app/services/matchers/keyword_matcher.py` | [新建] | 排除 | 关键词匹配器 |
| B16 | `backend/app/services/matchers/regex_matcher.py` | [新建] | 排除 | 正则匹配器 |
| B17 | `backend/app/services/matchers/intent_matcher.py` | [新建] | 排除 | 意图匹配器(Dify) |
| B18 | `backend/app/services/matchers/category_matcher.py` | [新建] | 排除 | 分类匹配器(依赖分诊) |
| B19 | `backend/app/api/router.py` | [修改] | 路由 | 注册 triage + exclusion_rules 路由 |
| B20 | `backend/app/services/ai_handler.py` | [修改] | 排除 | 消息流集成排除检查(AI回复前) |
| B21 | `backend/app/models/__init__.py` | [修改] | DB | 导出新模型 |
### 7.2 前端文件
| # | 文件路径 | 操作 | 模块 | 说明 |
|---|---------|------|------|------|
| F01 | `frontend-h5/src/api/triage.ts` | [新建] | 分诊-H5 | H5 端分诊 API 封装 |
| F02 | `frontend-h5/src/components/TriageCard.vue` | [修改] | 分诊-H5 | 对接后端 API(已有组件 530 行) |
| F03 | `frontend-h5/src/composables/useTriage.ts` | [新建] | 分诊-H5 | 分诊状态管理 composable |
| F04 | `frontend-agent/src/views/TriageDashboard.vue` | [新建] | 分诊-坐席 | 分诊看板主页面 |
| F05 | `frontend-agent/src/api/triage.ts` | [新建] | 分诊-坐席 | 坐席端分诊 API 封装 |
| F06 | `frontend-agent/src/components/triage/TriageStatsBar.vue` | [新建] | 分诊-坐席 | 统计概要栏 |
| F07 | `frontend-agent/src/components/triage/TriagePendingList.vue` | [新建] | 分诊-坐席 | 待分诊列表 |
| F08 | `frontend-agent/src/components/triage/TriageDetailPanel.vue` | [新建] | 分诊-坐席 | 分诊详情+路由操作 |
| F09 | `frontend-agent/src/router/index.ts` | [修改] | 分诊-坐席 | 新增分诊看板路由 |
| F10 | `frontend-admin/src/views/TopologyPreview.vue` | [新建] | 拓扑 | 拓扑预览主页面 |
| F11 | `frontend-admin/src/components/topology/GraphCanvas.vue` | [新建] | 拓扑 | ECharts 力导向图画布 |
| F12 | `frontend-admin/src/components/topology/NodeDetailPanel.vue` | [新建] | 拓扑 | 节点详情面板(只读) |
| F13 | `frontend-admin/src/components/topology/GraphToolbar.vue` | [新建] | 拓扑 | 搜索+筛选+缩放工具栏 |
| F14 | `frontend-admin/src/api/topology.ts` | [新建] | 拓扑 | 封装 GET /graph 调用 |
| F15 | `frontend-admin/src/router/index.ts` | [修改] | 拓扑 | 新增拓扑预览路由 |
| F16 | `frontend-admin/src/components/Sidebar.vue` | [修改] | 拓扑 | 侧边栏新增"拓扑预览"菜单项 |
| F17 | `frontend-admin/src/views/ExclusionRules.vue` | [新建] | 排除 | 代答排除规则列表页 |
| F18 | `frontend-admin/src/components/exclusion/ExclusionRuleForm.vue` | [新建] | 排除 | 新建/编辑规则弹窗 |
| F19 | `frontend-admin/src/components/exclusion/ExclusionTestDialog.vue` | [新建] | 排除 | 测试匹配弹窗 |
| F20 | `frontend-admin/src/api/exclusion.ts` | [新建] | 排除 | 代答排除 API 封装 |
| F21 | `frontend-admin/src/router/index.ts` | [修改] | 排除 | 新增代答排除路由 |
### 7.3 Dify / 配置文件
| # | 文件路径 | 操作 | 模块 | 说明 |
|---|---------|------|------|------|
| D01 | `dify/triage-app-prompt.md` | [新建] | Dify | 分诊应用 Prompt 完整文本 |
| D02 | `.env.example` | [修改] | 配置 | 新增 DIFY_TRIAGE_* 环境变量示例 |
### 7.4 文件统计
| 类别 | 新建 | 修改 | 合计 |
|------|------|------|------|
| 后端 | 16 | 5 | 21 |
| 前端 | 16 | 5 | 21 |
| Dify/配置 | 1 | 1 | 2 |
| **合计** | **33** | **11** | **44** |
---
## 8. 开发任务列表
### T01:项目基础设施(数据库迁移 + 配置 + 数据模型 + 路由注册)
| 维度 | 内容 |
|------|------|
| **任务 ID** | T01 |
| **任务名称** | 项目基础设施:数据库迁移 + 配置 + 数据模型 + 路由注册 |
| **涉及文件** | `backend/migrations/versions/045_triage_exclusion_tables.py` [新建], `backend/app/config.py` [修改], `backend/app/models/triage_session.py` [新建], `backend/app/models/exclusion_rule.py` [新建], `backend/app/models/exclusion_log.py` [新建], `backend/app/models/__init__.py` [修改], `backend/app/schemas/triage.py` [新建], `backend/app/schemas/exclusion.py` [新建], `backend/app/api/router.py` [修改], `.env.example` [修改] |
| **依赖** | 无 |
| **优先级** | P0 |
| **预估工作量** | M |
| **关键实现要点** | 1. 创建 3 张新表的 Alembic 迁移(triage_sessions + exclusion_rules + exclusion_logs<br>2. config.py 新增 `dify_triage_api_url``dify_triage_api_key``dify_triage_timeout` 三个配置项<br>3. 定义 3 个 ORM 模型,字段与 DDL 对齐<br>4. 定义 Pydantic Schema(请求/响应模型),含 TriageStep、TriageOption、ExclusionRuleCreate/Update 等<br>5. router.py 注册 `triage_router`prefix 无,内部定义)和 `exclusion_rules_router`prefix `/admin/exclusion-rules`<br>6. `.env.example` 补充环境变量示例 |
### T02:分诊交互模块(后端 API + 服务 + Dify 分诊 + H5 前端 + 坐席端前端)
| 维度 | 内容 |
|------|------|
| **任务 ID** | T02 |
| **任务名称** | 分诊交互模块:H5 端分诊 API + 坐席端看板 API + Dify 分诊服务 + H5/坐席前端 |
| **涉及文件** | `backend/app/api/triage.py` [新建], `backend/app/services/triage_service.py` [新建], `backend/app/services/dify_triage_service.py` [新建], `frontend-h5/src/api/triage.ts` [新建], `frontend-h5/src/components/TriageCard.vue` [修改], `frontend-h5/src/composables/useTriage.ts` [新建], `frontend-agent/src/views/TriageDashboard.vue` [新建], `frontend-agent/src/api/triage.ts` [新建], `frontend-agent/src/components/triage/TriageStatsBar.vue` [新建], `frontend-agent/src/components/triage/TriagePendingList.vue` [新建], `frontend-agent/src/components/triage/TriageDetailPanel.vue` [新建], `frontend-agent/src/router/index.ts` [修改], `dify/triage-app-prompt.md` [新建] |
| **依赖** | T01 |
| **优先级** | P0 |
| **预估工作量** | L |
| **关键实现要点** | 1. **TriageService**:实现 start_triage(含 5 秒超时自动转人工)、submit_step、skip_step、complete_triage、transfer_to_human、determine_urgency(关键词规则)、坐席端 list_pending/get_detail/route/override<br>2. **DifyTriageService**:对接 Dify OpenAI 兼容接口,解析 JSON 输出,降级处理(Dify 不可用时返回友好错误)<br>3. **triage.py API**H5 端 5 个端点(start/step/skip/transfer/complete+ 坐席端 7 个端点(pending/stats/detail/route/history/export/exclude-options<br>4. **H5 TriageCard.vue**:已有 530 行组件,修改为对接后端 API,通过 useTriage composable 管理状态<br>5. **坐席端 TriageDashboard.vue**:左列表+右详情双栏布局,复用原型图设计<br>6. **WS 推送**:坐席排除选项后通过 WS 推送到 H5 端,H5 调用 `TriageCard.setExcludedOptions()`<br>7. **Dify Prompt**:按 §6.4 要点编写完整 prompt,保存到 `dify/triage-app-prompt.md` |
### T03:拓扑预览模块(纯前端页面 + 组件,复用已有 API)
| 维度 | 内容 |
|------|------|
| **任务 ID** | T03 |
| **任务名称** | 拓扑预览模块:管理后台只读图谱页面 + 搜索筛选 + 详情面板 |
| **涉及文件** | `frontend-admin/src/views/TopologyPreview.vue` [新建], `frontend-admin/src/components/topology/GraphCanvas.vue` [新建], `frontend-admin/src/components/topology/NodeDetailPanel.vue` [新建], `frontend-admin/src/components/topology/GraphToolbar.vue` [新建], `frontend-admin/src/api/topology.ts` [新建], `frontend-admin/src/router/index.ts` [修改], `frontend-admin/src/components/Sidebar.vue` [修改] |
| **依赖** | T01(仅需确认路由注册格式,无 DB 依赖) |
| **优先级** | P1 |
| **预估工作量** | M |
| **关键实现要点** | 1. **复用已有 API**`GET /api/admin/knowledge-iteration/graph?issue_name=xxx&limit=200`,不新增后端端点<br>2. **GraphCanvas.vue**ECharts `graph` 类型力导向布局,Issue 绿色 #07C160 / Action 橙色 #f59e0broam=true 支持缩放平移,focusNodeAdjacency 悬停高亮<br>3. **搜索高亮**:前端过滤节点名称,匹配节点蓝色描边+脉冲,非匹配降低透明度,右侧浮层展示结果<br>4. **筛选器**:节点类型 checkboxIssue/Action),Relation 类型置灰 disabled;关系类型下拉(LEADS_TO/RELATES_TO/CAN_JUMP_TO),DERIVED_FROM 置灰<br>5. **NodeDetailPanel.vue**:只读展示节点属性+关联关系+高亮路径,不显示编辑/添加/删除按钮<br>6. **GraphToolbar.vue**:搜索框+筛选+缩放控制(放大/缩小/重置)<br>7. **降级处理**:Neo4j 不可用时 API 返回空数组,前端展示空状态引导<br>8. **Sidebar.vue**:新增"拓扑预览"菜单项,图标 🔮 |
### T04:代答排除模块(后端匹配引擎 + API + 管理后台前端 + 消息流集成)
| 维度 | 内容 |
|------|------|
| **任务 ID** | T04 |
| **任务名称** | 代答排除模块:4 种匹配引擎 + 管理 API + 管理后台前端 + 消息流集成 |
| **涉及文件** | `backend/app/api/exclusion_rules.py` [新建], `backend/app/services/exclusion_service.py` [新建], `backend/app/services/matchers/__init__.py` [新建], `backend/app/services/matchers/base.py` [新建], `backend/app/services/matchers/keyword_matcher.py` [新建], `backend/app/services/matchers/regex_matcher.py` [新建], `backend/app/services/matchers/intent_matcher.py` [新建], `backend/app/services/matchers/category_matcher.py` [新建], `backend/app/services/ai_handler.py` [修改], `frontend-admin/src/views/ExclusionRules.vue` [新建], `frontend-admin/src/components/exclusion/ExclusionRuleForm.vue` [新建], `frontend-admin/src/components/exclusion/ExclusionTestDialog.vue` [新建], `frontend-admin/src/api/exclusion.ts` [新建], `frontend-admin/src/router/index.ts` [修改] |
| **依赖** | T01DB 表 + 模型 + Schema),T02CategoryMatcher 软依赖 triage_sessions 数据) |
| **优先级** | P0 |
| **预估工作量** | L |
| **关键实现要点** | 1. **策略模式**BaseMatcher 抽象基类 + 4 种实现(KeywordMatcher/RegexMatcher/IntentMatcher/CategoryMatcher<br>2. **责任链调度**ExclusionService 按优先级排序规则,依次调用对应 Matcher,命中即停止并记录日志<br>3. **IntentMatcher**:复用 `approval_dify_base_url` + `approval_dify_api_key` 调用 Dify 意图识别<br>4. **CategoryMatcher**:查询 triage_sessions 获取分诊结果的 problem_category,软依赖(无分诊结果时跳过)<br>5. **消息流集成**:修改 `ai_handler.py`,在 AI 回复前插入排除检查,命中后执行 4 种动作(transfer_human/transfer_human_with_context/prompt_transfer/silent_transfer<br>6. **管理 API**8 个端点(CRUD + toggle + test + stats<br>7. **前端**:规则列表页(表格+筛选+分页+批量操作)+ 新建/编辑弹窗(匹配方式切换表单)+ 测试匹配弹窗<br>8. **命中日志**:每次命中记录 exclusion_logs,更新 hit_count |
### 任务汇总表
| 任务 ID | 任务名称 | 文件数 | 依赖 | 优先级 | 工作量 |
|---------|---------|--------|------|--------|--------|
| T01 | 项目基础设施 | 10 | — | P0 | M |
| T02 | 分诊交互模块 | 13 | T01 | P0 | L |
| T03 | 拓扑预览模块 | 7 | T01 | P1 | M |
| T04 | 代答排除模块 | 14 | T01, T02(软) | P0 | L |
---
## 9. 模块依赖关系图
```mermaid
graph TB
subgraph "T01: 项目基础设施"
DB[数据库迁移<br/>3张新表]
CFG[config.py<br/>新增配置项]
MODEL[ORM 模型<br/>3个]
SCHEMA[Pydantic Schema<br/>2个文件]
ROUTER[router.py<br/>路由注册]
end
subgraph "T02: 分诊交互模块"
TRI_API[分诊 API<br/>12个端点]
TRI_SVC[TriageService<br/>+ DifyTriageService]
H5_FE[H5 前端<br/>TriageCard + useTriage]
AG_FE[坐席端前端<br/>TriageDashboard + 3组件]
DIFY_APP[Dify 分诊应用<br/>Prompt + 配置]
end
subgraph "T03: 拓扑预览模块"
TOPO_FE[管理后台前端<br/>TopologyPreview + 3组件]
TOPO_API[复用已有<br/>GET /graph]
end
subgraph "T04: 代答排除模块"
EXC_API[排除管理 API<br/>8个端点]
EXC_SVC[ExclusionService<br/>+ 4个Matcher]
EXC_FE[管理后台前端<br/>ExclusionRules + 2组件]
MSG_INT[消息流集成<br/>ai_handler.py]
end
%% 依赖关系
DB --> TRI_API
DB --> EXC_API
CFG --> TRI_SVC
MODEL --> TRI_API
MODEL --> EXC_API
SCHEMA --> TRI_API
SCHEMA --> EXC_API
ROUTER --> TRI_API
ROUTER --> EXC_API
TRI_API --> TRI_SVC
TRI_SVC --> H5_FE
TRI_SVC --> AG_FE
TRI_SVC --> DIFY_APP
EXC_API --> EXC_SVC
EXC_SVC --> EXC_FE
EXC_SVC --> MSG_INT
%% 软依赖
TRI_SVC -.->|"CategoryMatcher<br/>软依赖分诊结果"| EXC_SVC
%% 拓扑预览独立
TOPO_FE --> TOPO_API
%% 并行标注
style T03 fill:#e8f5e9,stroke:#4caf50,stroke-dasharray: 5 5
style TOPO_FE fill:#e8f5e9,stroke:#4caf50,stroke-dasharray: 5 5
style TOPO_API fill:#e8f5e9,stroke:#4caf50,stroke-dasharray: 5 5
style DB fill:#e3f2fd,stroke:#2196f3
style CFG fill:#e3f2fd,stroke:#2196f3
style MODEL fill:#e3f2fd,stroke:#2196f3
style SCHEMA fill:#e3f2fd,stroke:#2196f3
style ROUTER fill:#e3f2fd,stroke:#2196f3
```
**并行开发说明**
| 并行组 | 可并行任务 | 前置条件 |
|--------|-----------|---------|
| 组 1 | T02(分诊)+ T03(拓扑) | T01 完成 |
| 组 2 | T04(排除)可与 T02 后半段并行 | T01 完成;T04 的 CategoryMatcher 部分可先实现框架,分诊结果接入后联调 |
> T03(拓扑预览)**完全独立**,与 T02、T04 无交叉依赖,可全程并行开发。
---
## 10. 共享知识(Shared Knowledge
### 10.1 API 响应格式
所有 API 响应统一使用 `{code, message, data}` 格式:
```json
{
"code": 0,
"message": "success",
"data": { ... }
}
```
- `code: 0` 表示成功,非 0 表示错误
- 错误时 `data` 为 null`message` 包含错误描述
### 10.2 路由前缀规范
| 端 | 前缀 | 说明 |
|----|------|------|
| H5 员工端 | `/api/h5/triage/*` | 分诊交互(员工发起分诊) |
| 坐席端 | `/api/agent/triage/*` | 分诊看板(坐席查看/路由) |
| 管理后台-知识迭代 | `/api/admin/knowledge-iteration/*` | 已有,拓扑预览复用 |
| 管理后台-代答排除 | `/api/admin/exclusion-rules/*` | 新增 |
### 10.3 认证方式
| 端 | 认证方式 | 装饰器 |
|----|---------|--------|
| H5 端 | H5 用户 token(企微 OAuth | `Depends(get_current_user)` |
| 坐席端 | 坐席 token | `Depends(get_current_agent)` |
| 管理后台 | 管理员 token + IP 白名单 | `@require_admin` |
### 10.4 Dify 对接统一模式
所有 Dify 调用统一使用 OpenAI Chat Completions 兼容接口:
```
POST {dify_base_url}/v1/chat/completions
Authorization: Bearer {api_key}
Content-Type: application/json
{
"model": "{app_identifier}",
"messages": [
{"role": "system", "content": "{system_prompt}"},
{"role": "user", "content": "{user_input}"}
],
"temperature": 0.3
}
```
| Dify 应用 | 配置项前缀 | 用途 |
|-----------|-----------|------|
| Agent 1(员工端 AI | `dify_api_*` | AI 对话回复 |
| Agent 2Wingman | `dify_wingman_*` | 坐席草稿/摘要/标签 |
| Agent 3(分诊引擎) | `dify_triage_*` | **本次新增** — 分诊分析 |
| 审批意图识别 | `approval_dify_*` | 审批路由 + 排除意图匹配 |
### 10.5 数据库模型规范
- 主键:`id VARCHAR(36)`,默认 `gen_random_uuid()::text`(与现有 KnowledgeSuggestion 一致)
- 时间戳:`created_at` / `updated_at`,默认 `NOW()`
- JSON 字段:使用 SQLAlchemy `JSON` 类型,PostgreSQL 原生 JSON
- 软删除:本期不实现软删除,直接物理删除(排除规则)
### 10.6 前端 API 封装规范
所有前端 API 封装统一使用 axios 实例(已有),响应拦截器统一处理 `{code, data}` 解包:
```typescript
// 示例:frontend-admin/src/api/exclusion.ts
import request from './index'
export function getExclusionRules(params: ExclusionRuleQuery) {
return request.get('/admin/exclusion-rules', { params })
}
```
### 10.7 WebSocket 消息格式
坐席排除选项推送到 H5 端的 WS 消息:
```json
{
"type": "triage_exclude",
"data": {
"triage_id": "xxx",
"excluded_labels": ["弹出了,提示密码错误"],
"recommended_label": "弹出了,提示无法连接到服务器"
}
}
```
---
## 11. 待明确事项
| # | 问题 | 影响范围 | 建议处理方式 |
|---|------|---------|-------------|
| 1 | Dify 分诊应用的 `dify2openai` 代理地址是否与 Wingman 共用?还是需要独立部署? | T02 DifyTriageService | 建议共用同一个 dify2openai 代理,通过不同 API Key 区分应用。需运维确认代理是否支持多应用路由。 |
| 2 | 意图匹配复用审批意图识别 Dify 应用,但审批意图的意图分类体系(12种/18流程)是否覆盖代答排除需要的意图(如 data_deletion, data_modification)? | T04 IntentMatcher | 建议在审批意图识别应用中扩展意图分类,或确认现有意图体系已覆盖。需与 Dify 管理员确认。 |
| 3 | 坐席端分诊看板的 WS 实时推送是否复用现有 `ws_manager.py` 的 WebSocket 连接?还是需要新建独立 WS 通道? | T02 坐席端排除选项推送 | 建议复用现有 WS 连接,通过 `type` 字段区分消息类型。需确认现有 WS 消息协议是否支持扩展。 |
| 4 | 分诊看板"导出"功能导出的 xlsx 包含哪些字段?是否需要包含分诊步骤详情? | T02 导出 API | 建议导出基础字段(标题/用户/部门/问题类型/置信度/紧急度/路由/时间),分诊步骤详情可选。 |
| 5 | 代答排除的"仅提示转人工"动作,用户确认转人工后是否需要再次调用后端 API?还是前端直接调用已有的转人工接口? | T04 消息流集成 | 建议前端收到 `prompt_transfer` 类型的排除命中后,展示提示语 + 确认按钮,用户确认后调用已有 `POST /api/conversations/{id}/transfer`。 |
| 6 | 拓扑预览页面是否需要权限控制?所有管理员都能查看,还是需要新增 RBAC 权限项? | T03 路由配置 | 建议复用 `@require_admin` 装饰器,所有管理员可查看。如需细粒度控制,后续新增 `topology:read` 权限。 |
---
> **文档结束**
> 本文档基于已有技术方案文档和原型图文档,结合用户确认的 4 项关键决策 + 9 项次要决策,产出增量系统设计和开发任务分解。
> 工程师拿到后可直接按 T01→T02/T03(并行)→T04 的顺序开始开发。