Files
wecom_it_smart_desk/docs/03-技术架构/业务路由推荐-架构设计.md
T

715 lines
28 KiB
Markdown
Raw Normal View History

# 业务路由推荐 — 系统架构设计
> **版本**: v1.0
> **日期**: 2026-07-15
> **作者**: Bob(架构师)
> **状态**: 待评审
> **关联PRD**: `docs/02-产品需求/业务路由推荐-PRD.md`
---
## 目录
- [Part A: 系统设计](#part-a-系统设计)
- [1. 实现方案](#1-实现方案)
- [2. 文件列表](#2-文件列表)
- [3. 数据结构与接口](#3-数据结构与接口)
- [4. 程序调用流程](#4-程序调用流程)
- [5. 待明确事项](#5-待明确事项)
- [Part B: 任务分解](#part-b-任务分解)
- [6. 依赖包列表](#6-依赖包列表)
- [7. 任务列表](#7-任务列表)
- [8. 共享知识](#8-共享知识)
- [9. 任务依赖图](#9-任务依赖图)
---
# Part A: 系统设计
## 1. 实现方案
### 1.1 技术栈选型
**沿用现有技术栈,不引入新框架**
| 层 | 技术栈 | 说明 |
|---|--------|------|
| 后端 | FastAPI + SQLAlchemy + PostgreSQL + Redis | 与现有审批/BYOD 系统完全一致 |
| H5前端 | Vue3 + Vant4 + TypeScript | 名片卡片用 Vant 组件实现 |
| 坐席前端 | Vue3 + Element Plus | 名片卡片用 Element Plus 风格适配 |
| AI意图识别 | Dify 原生 API(扩展现有 Prompt | 同一个 Dify 应用,API Key 不变 |
### 1.2 核心技术挑战与对策
| 挑战 | 对策 |
|------|------|
| **向后兼容**:扩展 Dify Prompt 后不能破坏现有审批识别 | 原审批判断规则原文保留不动,仅新增非IT判断段落;原3字段(`is_approval_request`/`confidence`/`approval_type`)语义和取值不变,新增3字段;上线前用审批测试用例集回归 |
| **路由检测时机**:在消息处理流程中何时调用 Dify | 在 `process_h5_ai_reply` 后台任务中新增路由检测步骤(与 BYOD 拦截模式一致),位于 BYOD 检测之后、打招呼/呼叫人工检测之前;通过关键词预过滤减少不必要的 Dify 调用 |
| **误路由防护**IT问题被误识别为非IT | `routing_confidence < 0.7` 不触发名片推荐,走正常 AI 回复流程;路由文本消息中附「仍需IT帮助」兜底引导 |
| **企微跳转可靠性**`openEnterpriseChat` 调用失败 | 前端容错:userid 为空或 SDK 未就绪时 Toast 提示;上线前核对联系人 userid 数据 |
| **双通道下发**:名片消息需同时到达 H5 和坐席端 | 复用现有 `ws_manager.broadcast_to_employees()`(推H5+ `ws_manager.broadcast()`(推坐席)双通道模式 |
### 1.3 架构模式
采用**现有系统的「后台任务 + WS推送」异步架构**,与 BYOD 卡片完全一致的消息处理模式:
```
员工发消息 → HTTP即时返回 → 后台AI任务(process_h5_ai_reply)
┌─ BYOD关键词拦截(现有)
├─ 路由关键词预过滤 → Dify统一意图识别(新增)
│ └─ non_it_routing && confidence≥0.7 → 发送名片卡片
├─ 打招呼/呼叫人工检测(现有)
└─ Dify流式回复(现有)
```
### 1.4 关键设计决策
1. **路由检测放在后台任务而非前端调用**:PRD 要求「后端自动发送」名片消息,且需经 WS 双通道下发。与 BYOD 卡片的处理模式一致,在 `process_h5_ai_reply` 中集成,保证服务端控制消息发送。
2. **统一 Dify Prompt 但独立调用函数**Dify 后台 Prompt 扩展为统一意图识别引擎(一次调用输出审批+路由判断),但后端 `routing_service.py``approval.py` 各自维护调用函数,避免耦合。两者使用相同的 Dify 应用(同一 API Key),只是解析各自关心的字段。
3. **关键词预过滤双层设计**:路由预过滤关键词(打印机/工牌/报销等)与审批预过滤关键词可能重叠(如"办公用品")。Dify Prompt 内部的判断优先级(先审批→再IT咨询→再非IT路由)确保正确分流,预过滤只负责「是否值得调 Dify」。
4. **名片消息三段式发送**:路由文本说明 → 名片卡片 → 系统提示,三条消息分别落库 + WS推送,与 PRD 4.3 交互流程一致。
---
## 2. 文件列表
### 2.1 新建文件
| # | 文件路径 | 说明 |
|---|---------|------|
| 1 | `backend/app/models/business_contact.py` | 业务联系人数据模型 |
| 2 | `backend/app/models/routing_event.py` | 路由命中统计模型(P1 |
| 3 | `backend/app/services/routing_service.py` | 路由服务:Dify调用、联系人查询、名片发送 |
| 4 | `backend/app/api/routing.py` | 路由API:联系人查询、坐席手动发名片(P1) |
| 5 | `backend/alembic/versions/047_business_contacts.py` | 数据库迁移:建表 + 初始数据 |
| 6 | `docs/02-产品需求/dify_unified_intent_prompt_v3.md` | 扩展后的统一意图识别 Dify Prompt |
| 7 | `frontend-h5/src/components/chat/ContactCard.vue` | H5员工名片卡片组件 |
| 8 | `frontend-h5/src/api/routing.ts` | H5路由API(联系人查询,供组件使用) |
| 9 | `frontend-agent/src/components/chat/ContactCardInline.vue` | 坐席端名片卡片组件 |
| 10 | `frontend-agent/src/api/routing.ts` | 坐席端路由API(手动发名片) |
### 2.2 修改文件
| # | 文件路径 | 修改内容 |
|---|---------|---------|
| 11 | `backend/app/config.py` | 新增 `routing_confidence_threshold` 配置项 |
| 12 | `backend/app/models/__init__.py` | 注册 `BusinessContact``RoutingEvent` 模型 |
| 13 | `backend/app/api/router.py` | 注册 `routing` 路由器 |
| 14 | `backend/app/api/approval.py` | `_call_dify_approval_intent()` 解析扩展字段;`ApprovalDetectIntentResponse` 新增 `intent_type`/`business_category`/`routing_confidence` 字段 |
| 15 | `backend/app/tasks/h5_ai_task.py` | 新增路由检测步骤:`_handle_routing()` |
| 16 | `frontend-h5/src/api/conversation.ts` | `MsgContentType` 新增 `'contact_card'` |
| 17 | `frontend-h5/src/components/chat/MessageBubble.vue` | 新增 `msg.msg_type === 'contact_card'` 渲染分支 |
| 18 | `frontend-agent/src/components/chat/MessageBubble.vue` | 新增 `contact_card` 渲染分支 |
| 19 | `frontend-agent/src/components/chat/InputBox.vue` | 新增「发名片」入口按钮(P1) |
---
## 3. 数据结构与接口
### 3.1 类图
> 完整类图见 `docs/03-技术架构/业务路由推荐-类图.mermaid`
```mermaid
classDiagram
class BusinessContact {
+int id
+str name
+str gender
+str department
+str position
+str responsibility
+str extension
+str service_area
+str wecom_userid
+str avatar_url
+str business_category
+bool is_active
+datetime created_at
+datetime updated_at
}
class RoutingEvent {
+int id
+str conversation_id
+str employee_id
+str message_content
+str business_category
+float routing_confidence
+int contact_id
+str contact_name
+bool is_clicked
+datetime created_at
}
class RoutingService {
+ROUTING_PREFILTER_KEYWORDS: list~str~
+ROUTING_KEYWORD_TO_CATEGORY: dict
+async detect_routing_intent(text, employee_id) dict
+async get_contact_by_category(category) BusinessContact
+async send_contact_card(db, conversation, employee_id, contact, reason) None
+async record_routing_event(db, conversation_id, employee_id, content, category, confidence, contact) None
}
class RoutingAPI {
+GET /h5/routing/contact
+GET /routing/contacts
+POST /conversations/{id}/send-contact-card
}
class ApprovalDetectIntentResponse {
+bool is_approval_request
+float confidence
+str approval_type
+str source
+str intent_type
+str business_category
+float routing_confidence
}
BusinessContact "1" --> RoutingEvent : contact_id
RoutingService --> BusinessContact : 查询
RoutingService --> RoutingEvent : 记录
RoutingAPI --> RoutingService : 调用
```
### 3.2 数据库表结构
#### 表 `business_contacts`P0
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | Integer | PK, 自增 | 主键 |
| `name` | String(50) | NOT NULL | 联系人姓名 |
| `gender` | String(10) | NOT NULL, default 'male' | 性别(male/female |
| `department` | String(100) | NOT NULL | 部门名称 |
| `position` | String(100) | NOT NULL | 岗位 |
| `responsibility` | String(500) | NOT NULL | 负责业务描述 |
| `extension` | String(20) | NULL | 分机号 |
| `service_area` | String(200) | NULL | 服务区域/办公地点 |
| `wecom_userid` | String(100) | NOT NULL | 企微用户ID |
| `avatar_url` | String(500) | NULL | 头像URL(为空用姓名首字渲染) |
| `business_category` | String(50) | NOT NULL | 业务类别(行政/人力资源/财务/法务/行政-物业) |
| `is_active` | Boolean | NOT NULL, default True | 是否启用 |
| `created_at` | DateTime | NOT NULL | 创建时间 |
| `updated_at` | DateTime | NOT NULL | 更新时间 |
**索引**: `idx_business_contacts_category` on (`business_category`, `is_active`)
#### 表 `routing_events`P1 — 路由命中统计)
| 字段 | 类型 | 约束 | 说明 |
|------|------|------|------|
| `id` | Integer | PK, 自增 | 主键 |
| `conversation_id` | String(36) | NOT NULL, FK | 会话ID |
| `employee_id` | String(64) | NOT NULL | 员工ID |
| `message_content` | String(500) | NOT NULL | 触发路由的员工消息(截断) |
| `business_category` | String(50) | NOT NULL | 识别的业务类别 |
| `routing_confidence` | Float | NOT NULL | 路由置信度 |
| `contact_id` | Integer | FK | 推荐的联系人ID |
| `contact_name` | String(50) | NOT NULL | 联系人姓名(冗余) |
| `is_clicked` | Boolean | default False | 员工是否点击了「联系TA」 |
| `created_at` | DateTime | NOT NULL | 创建时间 |
**索引**: `idx_routing_events_category` on (`business_category`), `idx_routing_events_conv` on (`conversation_id`)
### 3.3 消息类型定义
#### 新增 `msg_type = 'contact_card'`
与现有 `approval_card` / `byod_card` 采用相同的渲染架构:后端通过 `msg_type` 区分,卡片数据通过 `extra_data` 透传。
**消息记录结构**Message 表):
| 字段 | 值 | 说明 |
|------|-----|------|
| `sender_type` | `'ai'` | 发送者为 AI |
| `sender_id` | `'ai_bot'` | AI 标识 |
| `sender_name` | `'Duckula(达寇拉)'` | 与现有 AI 消息一致 |
| `msg_type` | `'contact_card'` | 新增消息类型 |
| `content` | 路由说明文本 | 如"为您推荐行政服务联系人" |
| `extra_data` | JSON(见下) | 联系人完整信息 |
**`extra_data` 结构规范**
```json
{
"contact": {
"id": 1,
"name": "王芳",
"gender": "female",
"department": "行政部",
"position": "设备管理岗",
"responsibility": "打印机/复印机/扫描仪",
"extension": "8002",
"service_area": "滨江园区 3-5楼",
"wecom_userid": "WangFang",
"avatar_url": "",
"business_category": "行政"
},
"routing_reason": "打印机问题属于行政设备范畴,不在IT服务台服务范围内,为您推荐行政服务联系人",
"business_category": "行政",
"routing_confidence": 0.85
}
```
### 3.4 API 接口定义
#### H5 端 — 查询联系人(P0)
```
GET /api/h5/routing/contact?business_category=行政
```
**响应**
```json
{
"code": 0,
"data": {
"id": 1,
"name": "王芳",
"gender": "female",
"department": "行政部",
"position": "设备管理岗",
"responsibility": "打印机/复印机/扫描仪",
"extension": "8002",
"service_area": "滨江园区 3-5楼",
"wecom_userid": "WangFang",
"avatar_url": "",
"business_category": "行政"
}
}
```
**逻辑**:按 `business_category` + `is_active=true` 查询,取第一条有效联系人。
#### 坐席端 — 获取联系人列表(P1)
```
GET /api/routing/contacts?business_category=行政&keyword=王
```
**响应**`{ "code": 0, "data": { "items": [BusinessContact, ...] } }`
#### 坐席端 — 手动发送名片(P1)
```
POST /api/conversations/{conversation_id}/send-contact-card
```
**请求体**
```json
{ "contact_id": 1, "reason": "员工咨询报销问题,为您推荐财务联系人" }
```
**逻辑**:坐席手动选择联系人 → 后端创建 `contact_card` 消息 → WS 推送双通道。
#### 审批意图检测扩展(P0 — 向后兼容)
```
POST /api/approval/detect-intent
```
**响应(扩展后)**
```json
{
"code": 0,
"data": {
"is_approval_request": false,
"confidence": 0.15,
"approval_type": null,
"source": "dify",
"intent_type": "non_it_routing",
"business_category": "行政",
"routing_confidence": 0.85
}
}
```
> 原 4 个字段(`is_approval_request`/`confidence`/`approval_type`/`source`)语义不变,新增 3 个字段。前端现有审批逻辑只读取原字段,不受影响。
### 3.5 Dify 统一意图识别 Prompt 扩展
扩展后输出格式(原3字段不变,新增3字段):
```json
{
"is_approval_request": true,
"confidence": 0.95,
"approval_type": "设备申请",
"intent_type": "approval",
"business_category": null,
"routing_confidence": 0.0
}
```
**判断优先级链**Prompt 内部逻辑):
```
1. IT审批意图 → intent_type: "approval", is_approval_request: true
2. IT范围内咨询/报修 → intent_type: "it_consult", is_approval_request: false
3. 非IT业务路由 → intent_type: "non_it_routing", business_category: "行政"/"人力资源"/...
4. 闲聊/无关 → intent_type: "chitchat"
```
> 完整 Prompt 见 `docs/02-产品需求/dify_unified_intent_prompt_v3.md`
---
## 4. 程序调用流程
### 4.1 核心时序图
> 完整时序图见 `docs/03-技术架构/业务路由推荐-时序图.mermaid`
```mermaid
sequenceDiagram
participant E as 员工(H5)
participant H5 as H5前端
participant BE as 后端API
participant TASK as 后台AI任务
participant DIFY as Dify API
participant DB as PostgreSQL
participant WS as WebSocket
participant AG as 坐席端
participant WECOM as 企微
E->>H5: 发送非IT消息 "打印机坏了"
H5->>BE: POST /h5/conversations/current/messages
BE->>DB: 存储员工消息
BE->>WS: broadcast(新消息→坐席)
BE-->>H5: 即时返回(user_message)
BE->>TASK: asyncio.create_task(process_h5_ai_reply)
Note over TASK: BYOD关键词检查(未命中)
TASK->>TASK: 路由关键词预过滤("打印机"命中)
TASK->>DIFY: 调用统一意图识别
DIFY-->>TASK: intent_type=non_it_routing, business_category=行政, routing_confidence=0.85
TASK->>DB: 查询business_contacts(行政)
DB-->>TASK: 返回联系人(王芳)
TASK->>DB: 存路由说明文本消息(ai, text)
TASK->>WS: broadcast_to_employees(ai_reply→H5)
TASK->>WS: broadcast(new_message→坐席)
TASK->>DB: 存contact_card消息(ai, contact_card)
TASK->>WS: broadcast_to_employees(ai_reply→H5, 含extra_data)
TASK->>WS: broadcast(new_message→坐席, 含extra_data)
TASK->>DB: 存系统提示消息(system)
TASK->>WS: broadcast_to_employees(ai_reply→H5)
TASK->>WS: broadcast(new_message→坐席)
TASK->>DB: 记录routing_event(P1)
WS-->>H5: ai_reply(路由文本)
H5->>E: 渲染AI文本气泡
WS-->>H5: ai_reply(contact_card)
H5->>E: 渲染ContactCard名片卡片
WS-->>H5: ai_reply(系统提示)
H5->>E: 渲染系统消息
WS-->>AG: new_message x3(路由文本+名片+系统提示)
AG->>AG: 渲染名片卡片(坐席端可见)
E->>H5: 点击「联系TA」
H5->>WECOM: wx.invoke('openEnterpriseChat', {userids: 'WangFang'})
WECOM-->>E: 打开企微单聊
Note over E,BE: 当前IT服务台会话不自动结束
```
### 4.2 路由检测在后台任务中的位置
```
process_h5_ai_reply(conversation_id, employee_id, content, dify_conversation_id)
├─ 1. BYOD 关键词拦截(现有,不变)
│ └─ 命中 → _handle_byod_query() → return
├─ 2. 【新增】路由关键词预过滤
│ └─ 命中非IT关键词 → _handle_routing()
│ ├─ 调用 Dify 统一意图识别
│ ├─ intent_type == "non_it_routing" && routing_confidence >= 0.7
│ │ └─ YES → 发送名片三段式消息 → return
│ │ └─ NO → 继续往下(走正常AI流程)
│ └─ Dify调用失败 → 关键词降级兜底(按关键词映射类别)
├─ 3. 打招呼/呼叫人工检测(现有,不变)
│ └─ 命中 → 同步回复 → return
└─ 4. Dify 流式回复(现有,不变)
└─ 流式推送 → _persist_and_push()
```
### 4.3 坐席手动发名片流程(P1)
```mermaid
sequenceDiagram
participant AG as 坐席端
participant BE as 后端API
participant DB as PostgreSQL
participant WS as WebSocket
participant H5 as H5前端
AG->>BE: POST /conversations/{id}/send-contact-card {contact_id, reason}
BE->>DB: 查询 BusinessContact by id
DB-->>BE: 返回联系人信息
BE->>DB: 创建 contact_card 消息(ai, extra_data=contact)
BE->>WS: broadcast_to_employees(ai_reply→H5)
BE->>WS: broadcast(new_message→坐席)
BE-->>AG: 返回成功
WS-->>H5: ai_reply(contact_card)
H5->>H5: 渲染名片卡片
```
---
## 5. 待明确事项
| # | 问题 | 影响 | 假设/建议 |
|---|------|------|----------|
| 1 | 企微 `openEnterpriseChat``userids` 参数格式(企微账号ID vs 工号) | 数据模型 `wecom_userid` 字段取值 | **假设**:使用企微通讯录同步的 userid(与现有员工 employee_id 格式一致),需上线前核对 |
| 2 | 非IT业务类别是否还有其他部门需覆盖 | Prompt 设计 + 数据初始化 | 当前覆盖 5 类(行政/人力资源/财务/法务/行政-物业),建议上线后根据路由统计补充 |
| 3 | 一个业务类别多个联系人时的选择策略 | 后端查询逻辑 | P0 取第一个 `is_active=true` 的联系人;P2 支持按服务区域匹配 |
| 4 | Dify Prompt 修改后是否需要 A/B 测试 | 上线策略 | 建议灰度上线,监控审批识别准确率无下降后全量;先用 PRD 5.5 回归测试用例集验证 |
| 5 | 坐席手动发名片的触发入口位置 | 坐席端 UI | **假设**:在 InputBox 工具栏新增「发名片」按钮,弹出联系人选择面板 |
| 6 | routing_confidence 在 0.5~0.7 之间是否推荐 | 路由触发策略 | 已确认:**不推荐**,走正常 AI 回复流程 |
---
# Part B: 任务分解
## 6. 依赖包列表
**无需新增第三方依赖包**。所有功能基于现有技术栈实现:
| 已有依赖 | 用途 |
|---------|------|
| `fastapi` | API 路由 |
| `sqlalchemy` | ORM 模型 |
| `alembic` | 数据库迁移 |
| `httpx` | Dify API 调用(已在 approval.py 中使用) |
| `pydantic` | Schema 定义 |
| `vant@^4` | H5 名片卡片 UI 组件 |
| `element-plus` | 坐席端名片卡片 UI 组件 |
---
## 7. 任务列表
### T01: 后端数据层 + 配置 + Prompt 基础设施
| 项 | 内容 |
|----|------|
| **Task ID** | T01 |
| **Task Name** | 后端数据层 + 配置基础设施 |
| **Priority** | P0 |
| **Dependencies** | 无 |
**源文件**
- `backend/app/models/business_contact.py`(新建)— BusinessContact 模型
- `backend/app/models/routing_event.py`(新建)— RoutingEvent 模型(P1
- `backend/app/models/__init__.py`(修改)— 注册新模型
- `backend/alembic/versions/047_business_contacts.py`(新建)— 建表 + 初始数据迁移
- `backend/app/config.py`(修改)— 新增 `routing_confidence_threshold` 配置
- `backend/app/api/router.py`(修改)— 注册 routing 路由器(占位,路由实现在 T02)
- `docs/02-产品需求/dify_unified_intent_prompt_v3.md`(新建)— 扩展后的 Dify Prompt
**验收标准**
- BusinessContact / RoutingEvent 模型可被 Alembic 检测到
- `alembic upgrade head` 成功建表并写入初始联系人数据
- `routing_confidence_threshold` 配置项默认值 0.7
- Dify Prompt 文档包含完整的扩展后 System Prompt(原审批规则 + 新增非IT路由判断段落)
---
### T02: 后端路由服务 + API + 后台任务集成
| 项 | 内容 |
|----|------|
| **Task ID** | T02 |
| **Task Name** | 后端路由服务与消息发送 |
| **Priority** | P0 |
| **Dependencies** | T01 |
**源文件**
- `backend/app/services/routing_service.py`(新建)— 核心路由逻辑:关键词预过滤、Dify调用、联系人查询、名片三段式发送、路由事件记录
- `backend/app/api/routing.py`(新建)— API 端点:`GET /h5/routing/contact``GET /routing/contacts``POST /conversations/{id}/send-contact-card`
- `backend/app/api/approval.py`(修改)— `_call_dify_approval_intent()` 解析扩展字段;`ApprovalDetectIntentResponse` 新增 `intent_type`/`business_category`/`routing_confidence`
- `backend/app/tasks/h5_ai_task.py`(修改)— 在 BYOD 检测后新增 `_handle_routing()` 调用步骤
**验收标准**
- 员工发送"打印机坏了" → 后台任务检测到 non_it_routing → 发送路由文本 + contact_card + 系统提示三条消息
- 员工发送"我要申请笔记本电脑" → 审批意图正常识别(回归无影响)
- `routing_confidence < 0.7` 时不触发名片推荐,走正常 AI 流程
- Dify 调用失败时降级为关键词匹配(按 `ROUTING_KEYWORD_TO_CATEGORY` 映射)
- 坐席端通过 `POST /conversations/{id}/send-contact-card` 可手动发送名片
- WS 双通道推送正常(H5 收到 `ai_reply`,坐席收到 `new_message`
---
### T03: H5 前端名片卡片组件 + 消息映射
| 项 | 内容 |
|----|------|
| **Task ID** | T03 |
| **Task Name** | H5 员工名片卡片与渲染 |
| **Priority** | P0 |
| **Dependencies** | T02 |
**源文件**
- `frontend-h5/src/components/chat/ContactCard.vue`(新建)— 名片卡片组件:头像/姓名/部门岗位/负责业务(绿色高亮)/分机号/服务区域/「联系TA」按钮
- `frontend-h5/src/components/chat/MessageBubble.vue`(修改)— 新增 `v-else-if="msg.msg_type === 'contact_card'"` 渲染分支
- `frontend-h5/src/api/conversation.ts`(修改)— `MsgContentType` 新增 `'contact_card'`
- `frontend-h5/src/api/routing.ts`(新建)— `getContactByCategory()` API 封装
**验收标准**
- 名片卡片按 PRD 4.1 布局渲染:头像44px、姓名加粗、负责业务绿色高亮 #07C160、「联系TA」绿色按钮
- 点击「联系TA」→ 调用 `wx.invoke('openEnterpriseChat', {userids: contact.wecom_userid})` 打开企微单聊
- userid 为空或企微 SDK 未就绪 → Toast 提示「暂时无法发起聊天,请联系管理员」
- `mapMessage()` 无需改动(`msg_type``extra_data` 已透传),仅类型定义扩展
- 历史消息中 contact_card 消息可正常渲染(从 `extra_data.contact` 读取数据)
---
### T04: 坐席端名片展示 + 手动发名片功能
| 项 | 内容 |
|----|------|
| **Task ID** | T04 |
| **Task Name** | 坐席端名片可见性与手动发送 |
| **Priority** | P1 |
| **Dependencies** | T02 |
**源文件**
- `frontend-agent/src/components/chat/ContactCardInline.vue`(新建)— 坐席端名片卡片(Element Plus 风格,展示联系人信息 + 推荐原因,无「联系TA」按钮)
- `frontend-agent/src/components/chat/MessageBubble.vue`(修改)— 新增 `contact_card` 渲染分支
- `frontend-agent/src/api/routing.ts`(新建)— `getContacts()``sendContactCard()` API 封装
- `frontend-agent/src/components/chat/InputBox.vue`(修改)— 工具栏新增「发名片」按钮,弹出联系人选择面板
**验收标准**
- AI 发送的路由名片在坐席端消息流中可见,展示联系人信息 + 推荐原因
- 坐席点击「发名片」→ 弹出联系人选择面板(按业务类别分组)→ 选择联系人 → 发送到当前会话
- 坐席端名片卡片不显示「联系TA」按钮(坐席无需跳转企微单聊)
- 名片消息在坐席端和 H5 端同时可见(双通道验证)
---
## 8. 共享知识
### 8.1 消息类型命名规范
```
msg_type 取值统一使用 snake_case
text / image / file / voice / video / location
system / approval_card / byod_card / contact_card(新增)
```
### 8.2 extra_data 结构规范
所有卡片类消息的 `extra_data` 采用 **命名空间嵌套** 结构:
| msg_type | extra_data 结构 |
|----------|----------------|
| `approval_card` | `{ "approval_type": "设备申请" }` |
| `byod_card` | `{ "byod_result": { eligible, has_subsidy, ... } }` |
| `contact_card`(新增) | `{ "contact": { name, department, ... }, "routing_reason": "...", "business_category": "...", "routing_confidence": 0.85 }` |
### 8.3 WS 推送事件规范
路由名片消息复用现有 WS 事件类型:
| 推送目标 | 事件类型 | data 关键字段 |
|---------|---------|--------------|
| H5 员工 | `ai_reply` | `message_id`, `msg_type: "contact_card"`, `extra_data`, `sender_type: "ai"` |
| 坐席端 | `new_message` | `message_id`, `msg_type: "contact_card"`, `extra_data`, `sender_type: "ai"` |
> H5 前端收到 `ai_reply` 事件时,根据 `msg_type` 决定渲染方式(文本气泡 / 名片卡片),与现有 BYOD 处理逻辑一致。
### 8.4 Dify 统一意图识别响应字段
| 字段 | 类型 | 取值范围 | 说明 |
|------|------|---------|------|
| `is_approval_request` | bool | true/false | **原字段**,是否为审批请求(语义不变) |
| `confidence` | float | 0.0~1.0 | **原字段**,审批置信度(语义不变) |
| `approval_type` | string\|null | 12种类型\|null | **原字段**,审批类型(语义不变) |
| `intent_type` | string | approval/it_consult/non_it_routing/chitchat | **新增**,意图大类 |
| `business_category` | string\|null | 行政/人力资源/财务/法务/行政-物业\|null | **新增**,非IT业务类别 |
| `routing_confidence` | float | 0.0~1.0 | **新增**,路由置信度,≥0.7 触发名片推荐 |
### 8.5 路由关键词预过滤列表
```python
ROUTING_PREFILTER_KEYWORDS = [
# 行政
"打印机", "复印机", "扫描仪", "保洁", "名片印刷",
# 人力资源
"工牌", "考勤", "入职", "离职", "社保", "公积金",
# 财务
"报销", "发票", "借款", "工资条",
# 法务
"合同", "法务", "知识产权",
# 行政-物业
"空调", "电梯", "门禁", "停车",
]
ROUTING_KEYWORD_TO_CATEGORY = {
"打印机": "行政", "复印机": "行政", "扫描仪": "行政", "保洁": "行政", "名片印刷": "行政",
"工牌": "人力资源", "考勤": "人力资源", "入职": "人力资源", "离职": "人力资源",
"社保": "人力资源", "公积金": "人力资源",
"报销": "财务", "发票": "财务", "借款": "财务", "工资条": "财务",
"合同": "法务", "法务": "法务", "知识产权": "法务",
"空调": "行政-物业", "电梯": "行政-物业", "门禁": "行政-物业", "停车": "行政-物业",
}
```
### 8.6 跳转后会话不自动结束
员工点击「联系TA」跳转企微单聊后,当前 IT 服务台会话状态保持不变(`ai_handling`/`serving`),员工可返回继续咨询。不在后端触发任何会话状态变更。
### 8.7 向后兼容性保证
1. **Dify Prompt 扩展**:原审批判断规则原文保留,仅新增非IT判断段落
2. **API 响应扩展**`/approval/detect-intent` 响应新增字段,原字段语义不变;前端现有审批逻辑只读取原字段
3. **消息类型扩展**`contact_card` 是新增 `msg_type`,不影响现有 `text`/`image`/`approval_card`/`byod_card` 的渲染
4. **数据库扩展**:新建表,不修改现有表结构
---
## 9. 任务依赖图
```mermaid
graph LR
T01[T01: 后端数据层+配置+Prompt]
T02[T02: 后端路由服务+API+任务集成]
T03[T03: H5名片卡片+消息映射]
T04[T04: 坐席端名片+手动发名片]
T01 --> T02
T02 --> T03
T02 --> T04
style T01 fill:#07C160,color:#fff
style T02 fill:#07C160,color:#fff
style T03 fill:#1989fa,color:#fff
style T04 fill:#ff976a,color:#fff
```
**依赖说明**
- T01 → T02:路由服务依赖数据模型和配置
- T02 → T03H5 前端依赖后端 API 契约和 WS 推送格式
- T02 → T04:坐席端依赖后端 API 契约
- T03 和 T04 互不依赖,可并行开发
---
> **附**:时序图 `docs/03-技术架构/业务路由推荐-时序图.mermaid`
> **附**:类图 `docs/03-技术架构/业务路由推荐-类图.mermaid`