Files
wecom_it_smart_desk/docs/摇人-多坐席协作-技术方案.md

390 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 摇人(多坐席协作)— 技术方案
> **场景**:坐席A在处理会话时发现需要坐席B的专业知识,点击「摇人」→ 坐席B收到通知 → 进入同一会话协助。
>
> **与现有 Grab 的区别**:Grab 是「移交」(所有权转移),摇人是「协作」(所有权不变,B 加入共同处理)。
---
## 一、数据模型改动
### 1.1 Conversation 模型新增字段
```python
# backend/app/models/conversation.py
# 协作坐席ID列表(JSON 数组,存储所有被邀请来协作的坐席ID)
# 和 assigned_agent_id 的区别:
# - assigned_agent_id:会话的"主责"坐席(接单人),只有他才能结单/转接
# - collaborating_agent_ids:被邀请来协助的坐席,可以查看和回复,但不能结单
collaborating_agent_ids: Mapped[list] = mapped_column(
JSON,
nullable=False,
default=list,
comment="协作坐席ID列表",
)
```
### 1.2 数据库迁移 SQL
```sql
-- 开发环境 SQLite / 生产环境 PostgreSQL 通用
ALTER TABLE conversations ADD COLUMN collaborating_agent_ids JSON NOT NULL DEFAULT '[]';
```
### 1.3 数据关系示意
```
Conversation
├── assigned_agent_id = "agent_A" ← 主责坐席(接单人)
├── collaborating_agent_ids = ["agent_B", "agent_C"] ← 协作坐席
└── status = "serving"
权限矩阵:
主责坐席(A) 协作坐席(B/C) 其他坐席
查看会话 ✅ ✅ ✅(只读)
发送回复 ✅ ✅ ❌
结单 ✅ ❌ ❌
转接 ✅ ❌ ❌
摇人(邀请其他人) ✅ ✅ ❌
退出协作 ❌(不能) ✅ -
标记(置顶/代办) ✅ ❌ ❌
```
---
## 二、后端实现
### 2.1 新增 Schema
```python
# backend/app/schemas/conversation.py
class ConversationInvite(BaseModel):
"""摇人邀请请求"""
agent_id: str = Field(..., description="被邀请的坐席ID")
class ConversationLeave(BaseModel):
"""退出协作请求(可选,也可以从当前坐席推断)"""
pass
```
### 2.2 ConversationResponse 扩展字段
```python
# 在现有基础上新增
class ConversationResponse(BaseModel):
# ... 现有字段 ...
# ----- 多坐席协作扩展字段 -----
# 协作坐席列表
collaborating_agent_ids: list[str] = Field(default_factory=list)
# 协作坐席姓名映射(agent_id → name
collaborating_agent_names: dict[str, str] = Field(default_factory=dict)
# 当前坐席是否为协作坐席(非主责)
is_collaborator: bool = Field(default=False)
```
### 2.3 新增 API 端点
```python
# backend/app/api/conversations.py
# POST /api/conversations/{id}/invite
# 坐席A邀请坐席B加入协作
@router.post("/conversations/{conversation_id}/invite")
async def invite_collaborator(
conversation_id: UUID,
body: ConversationInvite,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""
邀请另一个坐席加入会话协作。
校验规则:
1. 当前坐席必须是主责坐席或已加入的协作坐席
2. 被邀请坐席存在且在线
3. 被邀请坐席不是主责坐席,也不在协作列表中(防止重复邀请)
4. 会话状态必须为 serving(已结单的不能摇人)
副作用:
- WebSocket 推送给被邀请坐席
- 企微消息通知被邀请坐席
"""
pass
# POST /api/conversations/{id}/leave
# 坐席B退出协作
@router.post("/conversations/{conversation_id}/leave")
async def leave_collaboration(
conversation_id: UUID,
db: AsyncSession = Depends(get_db),
current_agent: Agent = Depends(get_current_agent),
):
"""
坐席退出协作。
校验规则:
1. 当前坐席必须在协作列表中
2. 当前坐席不能是主责坐席(主责坐席不能"退出",只能转接或结单)
副作用:
- WebSocket 广播会话更新
"""
pass
```
### 2.4 SessionService 新增方法
```python
# backend/app/services/session_service.py
async def invite_collaborator(
self,
conversation_id: UUID,
inviter_agent_id: str,
invitee_agent_id: str,
) -> Conversation:
"""邀请坐席加入协作。
流程:
1. 校验:会话存在且为 serving
2. 校验:邀请人在主责或协作列表中
3. 校验:被邀请人不在主责和协作列表中
4. 校验:被邀请人在线
5. 将被邀请人加入 collaborating_agent_ids
6. (可选)企微通知被邀请人
7. WS 广播 + 定向推送
"""
async def leave_collaboration(
self,
conversation_id: UUID,
agent_id: str,
) -> Conversation:
"""退出协作。
流程:
1. 校验:坐席在协作列表中
2. 从 collaborating_agent_ids 中移除
3. WS 广播
"""
```
### 2.5 会话列表接口改动
```python
# GET /api/conversations — 增加 is_collaborator 和 collaborating_agent_names
# 原来:
conv_data["is_mine"] = conv.assigned_agent_id == current_agent.user_id
conv_data["can_grab"] = (...)
# 新增:
conv_data["is_collaborator"] = (
current_agent.user_id in conv.collaborating_agent_ids
and conv.assigned_agent_id != current_agent.user_id
)
# 协作坐席姓名映射(需要批量查询坐席表)
collab_agent_ids = conv.collaborating_agent_ids or []
conv_data["collaborating_agent_ids"] = collab_agent_ids
conv_data["collaborating_agent_names"] = {
aid: agent_name_map.get(aid, "未知") for aid in collab_agent_ids
}
```
### 2.6 WebSocket 事件定义
| 事件类型 | 推送范围 | 数据 |
|---------|---------|------|
| `collaborator_invited` | 被邀请人(定向)+ 所有在线坐席(广播) | `{ conversation_id, inviter_id, invitee_id, inviter_name }` |
| `collaborator_joined` | 所有在线坐席(广播) | `{ conversation_id, agent_id, agent_name }` |
| `collaborator_left` | 所有在线坐席(广播) | `{ conversation_id, agent_id, agent_name }` |
### 2.7 企微通知(可选增强)
被邀请时发送企微卡片消息:
```
┌─────────────────────────────┐
│ 🔔 摇人邀请 │
│ │
│ 坐席A 邀请你协助处理会话 │
│ 员工:张三 │
│ 问题:打印机连接失败 │
│ │
│ [点击查看] │
└─────────────────────────────┘
```
---
## 三、前端实现(坐席工作台)
### 3.1 API 层新增
```typescript
// frontend-agent/src/api/conversation.ts
/** 邀请坐席协作 */
export function inviteCollaborator(
conversationId: string,
agentId: string
): Promise<Conversation>
/** 退出协作 */
export function leaveCollaboration(
conversationId: string
): Promise<Conversation>
```
### 3.2 Store 改动
```typescript
// frontend-agent/src/stores/conversation.ts
// 新增计算属性:协作会话(我是协作者但不是主责的会话)
const collaboratingConversations = computed(() => {
return sortedConversations.value.filter(
c => c.is_collaborator && c.status === 'serving'
)
})
// 新增方法
async function inviteCollaborator(convId: string, agentId: string): Promise<void>
async function leaveCollaboration(convId: string): Promise<void>
// WS 事件处理
function handleCollaboratorInvited(data: {...}): void // 弹出通知
function handleCollaboratorJoined(data: {...}): void // 刷新列表
function handleCollaboratorLeft(data: {...}): void // 刷新列表
```
### 3.3 ConversationList.vue 改动
```vue
<!-- 新增协作会话排在我的会话其他坐席会话之间 -->
<template v-if="filteredCollaborating.length > 0">
<div class="section-title">
<span>🤝 协作会话 ({{ filteredCollaborating.length }})</span>
</div>
<ConversationItem
v-for="conv in filteredCollaborating"
:key="conv.id"
:conversation="conv"
:active="conv.id === conversationStore.currentConversationId"
:show-leave="true" <!-- 新增退出按钮 -->
@click="conversationStore.selectConversation(conv.id)"
@leave="handleLeave(conv)"
/>
</template>
```
### 3.4 新增:摇人弹窗组件
```
┌──────────────────────────────────┐
│ 摇人 — 邀请坐席协作 │
│ │
│ 🔍 [搜索坐席姓名...] │
│ │
│ ┌──────────────────────────────┐│
│ │ ○ 张三 在线 负载 2/5 ││
│ │ ○ 李四 在线 负载 1/5 (推荐)││
│ │ ○ 王五 忙碌 负载 5/5 ││
│ └──────────────────────────────┘│
│ │
│ 已选:李四 │
│ │
│ [取消] [确认邀请] │
└──────────────────────────────────┘
```
### 3.5 会话详情区域改动
在会话详情的头部工具栏(自己的会话或协作的会话)增加「摇人」按钮:
```
┌──────────────────────────────────────────┐
│ 👤 张三 · 技术部 [摇人] [⋮] │ ← 工具栏
│ 状态:服务中 | 主责:坐席A | 协作:坐席B │ ← 协作信息
└──────────────────────────────────────────┘
```
### 3.6 WebSocket 事件处理改动
```typescript
// frontend-agent/src/composables/useWebSocket.ts
case 'collaborator_invited':
// 如果被邀请的是当前坐席,弹出通知
if (msg.data?.invitee_id === agentStore.userId) {
ElNotification({
title: '摇人邀请',
message: `${msg.data.inviter_name} 邀请你协助处理会话`,
type: 'info',
duration: 0, // 不自动关闭
onClick: () => {
conversationStore.selectConversation(msg.data.conversation_id)
}
})
}
conversationStore.fetchConversations()
break
case 'collaborator_joined':
case 'collaborator_left':
conversationStore.fetchConversations()
break
```
---
## 四、改动清单汇总
| 文件 | 改动类型 | 说明 |
|------|---------|------|
| `backend/app/models/conversation.py` | 修改 | +`collaborating_agent_ids` 字段 |
| `backend/app/schemas/conversation.py` | 修改 | +`ConversationInvite`、响应扩展字段 |
| `backend/app/api/conversations.py` | 修改 | +`invite`/`leave` 两个端点,列表接口扩展 |
| `backend/app/services/session_service.py` | 修改 | +`invite_collaborator`/`leave_collaboration` |
| `frontend-agent/src/api/conversation.ts` | 修改 | +2 个 API 函数 |
| `frontend-agent/src/stores/conversation.ts` | 修改 | +计算属性、方法、WS 处理 |
| `frontend-agent/src/components/conversation/ConversationList.vue` | 修改 | +协作会话区 |
| `frontend-agent/src/components/conversation/ConversationItem.vue` | 修改 | +退出按钮 |
| `frontend-agent/src/components/conversation/InviteDialog.vue` | **新建** | 摇人选人弹窗 |
| `frontend-agent/src/composables/useWebSocket.ts` | 修改 | +3 个 WS 事件处理 |
| 数据库迁移 SQL | **新建** | `ALTER TABLE` 加列 |
---
## 五、开发顺序
| 步骤 | 内容 | 依赖 |
|------|------|------|
| 1 | 模型 + 迁移 SQL | 无 |
| 2 | Schema + SessionService | 1 |
| 3 | API 端点(invite/leave/列表扩展) | 2 |
| 4 | 后端测试 | 3 |
| 5 | 前端 API 层 + Store | 无(可并行) |
| 6 | 摇人弹窗组件 | 5 |
| 7 | ConversationList 改动 | 5, 6 |
| 8 | WebSocket 事件处理 | 3 |
| 9 | 端到端集成测试 | 全部 |
---
## 六、设计决策
| 决策 | 理由 |
|------|------|
| 协作坐席不增加 `current_load` | 协作是轻量参与,不影响坐席接单能力 |
| 协作坐席不能结单/转接 | 避免多人操作冲突,只有主责坐席有权关闭会话 |
| 使用 JSON 数组而非关联表 | 协作人数少(1-3人),JSON 查询足够;参考现有 `tags` 字段设计 |
| WS 广播 + 定向推送双通道 | 广播让其他人看到协作关系变化,定向推送确保被邀请人收到通知 |