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

12 KiB
Raw Permalink Blame History

摇人(多坐席协作)— 技术方案

场景:坐席A在处理会话时发现需要坐席B的专业知识,点击「摇人」→ 坐席B收到通知 → 进入同一会话协助。

与现有 Grab 的区别:Grab 是「移交」(所有权转移),摇人是「协作」(所有权不变,B 加入共同处理)。


一、数据模型改动

1.1 Conversation 模型新增字段

# 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

-- 开发环境 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

# backend/app/schemas/conversation.py

class ConversationInvite(BaseModel):
    """摇人邀请请求"""
    agent_id: str = Field(..., description="被邀请的坐席ID")


class ConversationLeave(BaseModel):
    """退出协作请求(可选,也可以从当前坐席推断)"""
    pass

2.2 ConversationResponse 扩展字段

# 在现有基础上新增
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 端点

# 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 新增方法

# 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 会话列表接口改动

# 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 层新增

// 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 改动

// 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 改动

<!-- 新增协作会话排在我的会话其他坐席会话之间 -->
<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 事件处理改动

// 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 广播 + 定向推送双通道 广播让其他人看到协作关系变化,定向推送确保被邀请人收到通知