chore: initial baseline with P0-safety .gitignore
This commit is contained in:
@@ -0,0 +1,688 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 会话管理 API
|
||||
# =============================================================================
|
||||
# 说明:坐席端的会话管理接口,包括:
|
||||
# 1. GET /api/conversations — 坐席获取会话列表(支持状态过滤、排序)
|
||||
# 2. GET /api/conversations/{id} — 获取会话详情
|
||||
# 3. POST /api/conversations/{id}/assign — 接单(坐席接入会话)
|
||||
# 4. POST /api/conversations/{id}/resolve — 结单
|
||||
# 5. POST /api/conversations/{id}/pin — 置顶/取消置顶
|
||||
# 6. POST /api/conversations/{id}/todo — 代办/取消代办
|
||||
# 7. POST /api/conversations/{id}/transfer — 转接
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.agent import Agent
|
||||
from app.schemas.conversation import (
|
||||
ConversationAssign,
|
||||
ConversationInvite,
|
||||
ConversationListResponse,
|
||||
ConversationResponse,
|
||||
ConversationStatusUpdate,
|
||||
InviteParticipantRequest,
|
||||
JoinConversationRequest,
|
||||
)
|
||||
from app.services.session_service import SessionService
|
||||
from app.services.wecom_service import WecomService
|
||||
from app.utils.response import AppException, success_response
|
||||
|
||||
# 坐席认证依赖(从 agents.py 导入)
|
||||
from app.api.agents import get_current_agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 创建路由器
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/conversations — 获取坐席会话列表(全局可见)
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/conversations")
|
||||
async def list_conversations(
|
||||
status: Optional[str] = Query(None, description="按状态过滤: ai_handling/queued/serving/resolved"),
|
||||
agent_id: Optional[str] = Query(None, description="按坐席ID过滤"),
|
||||
page: int = Query(1, ge=1, description="页码(从1开始)"),
|
||||
page_size: int = Query(50, ge=1, le=100, description="每页数量"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""坐席获取会话列表(全局可见)。
|
||||
|
||||
返回所有活跃会话,每个会话增加字段:
|
||||
- is_mine: 是否为当前坐席的会话
|
||||
- assigned_agent_name: 分配的坐席姓名(其他坐席会话显示用)
|
||||
- can_grab: 是否可以接手(其他坐席已接单的会话为 True)
|
||||
|
||||
排序规则:紧急→举手→需介入→活跃→已结单。
|
||||
|
||||
Args:
|
||||
status: 按状态过滤(可选)
|
||||
agent_id: 按坐席ID过滤(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含会话列表和总数
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversations, total = await session_service.get_conversations(
|
||||
status=status,
|
||||
agent_id=agent_id,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
)
|
||||
|
||||
# 批量查询所有涉及坐席的信息,避免 N+1 查询
|
||||
# 收集所有需要查询姓名的坐席ID(主责坐席 + 协作坐席)
|
||||
agent_ids_to_query = set()
|
||||
for conv in conversations:
|
||||
if conv.assigned_agent_id:
|
||||
agent_ids_to_query.add(conv.assigned_agent_id)
|
||||
for aid in (conv.collaborating_agent_ids or []):
|
||||
agent_ids_to_query.add(aid)
|
||||
|
||||
# 一次性查询所有相关坐席姓名
|
||||
agent_name_map: dict[str, str] = {}
|
||||
if agent_ids_to_query:
|
||||
stmt = select(Agent).where(Agent.user_id.in_(agent_ids_to_query))
|
||||
result = await db.execute(stmt)
|
||||
for agent in result.scalars().all():
|
||||
agent_name_map[agent.user_id] = agent.name
|
||||
|
||||
# 转换为响应 Schema,附加 is_mine / assigned_agent_name / can_grab 字段
|
||||
items = []
|
||||
for conv in conversations:
|
||||
conv_data = ConversationResponse.model_validate(conv).model_dump()
|
||||
# 是否为当前坐席的会话
|
||||
conv_data["is_mine"] = conv.assigned_agent_id == current_agent.user_id
|
||||
# 坐席姓名(从批量查询结果中获取)
|
||||
conv_data["assigned_agent_name"] = agent_name_map.get(conv.assigned_agent_id) if conv.assigned_agent_id else None
|
||||
# 是否可以接手:其他坐席已接单(assigned 且不是自己的)
|
||||
conv_data["can_grab"] = (
|
||||
conv.assigned_agent_id is not None
|
||||
and conv.assigned_agent_id != current_agent.user_id
|
||||
and conv.status == "serving"
|
||||
)
|
||||
# ----- 多坐席协作扩展字段 -----
|
||||
# 协作坐席ID列表
|
||||
collab_ids = conv.collaborating_agent_ids or []
|
||||
conv_data["collaborating_agent_ids"] = collab_ids
|
||||
# 协作坐席姓名映射
|
||||
conv_data["collaborating_agent_names"] = {
|
||||
aid: agent_name_map.get(aid, "未知") for aid in collab_ids
|
||||
}
|
||||
# 是否为协作坐席(在协作列表中但不是主责坐席)
|
||||
conv_data["is_collaborator"] = (
|
||||
current_agent.user_id in collab_ids
|
||||
and conv.assigned_agent_id != current_agent.user_id
|
||||
)
|
||||
items.append(conv_data)
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
"items": items,
|
||||
"total": total,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# GET /api/conversations/{id} — 获取会话详情
|
||||
# --------------------------------------------------------------------------
|
||||
@router.get("/conversations/{conversation_id}")
|
||||
async def get_conversation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取会话详情。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含会话详情
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.get_conversation(conversation_id)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/assign — 坐席接单
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/assign")
|
||||
async def assign_conversation(
|
||||
conversation_id: str,
|
||||
body: ConversationAssign,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""坐席接单(接入会话)。
|
||||
|
||||
坐席点击"接单"按钮时调用,将会话状态从 queued 改为 serving。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 接单请求体(包含 agent_id)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
# 创建企微服务实例用于发送接入通知
|
||||
redis_client = None
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
from app.config import settings
|
||||
redis_client = settings.create_redis_client()
|
||||
wecom_service = WecomService(redis_client)
|
||||
session_service = SessionService(db, wecom_service=wecom_service)
|
||||
except Exception:
|
||||
logger.warning("创建企微服务失败,接入通知将不发送")
|
||||
session_service = SessionService(db)
|
||||
|
||||
conversation = await session_service.assign_agent(
|
||||
conversation_id=conversation_id,
|
||||
agent_id=body.agent_id,
|
||||
)
|
||||
|
||||
# 关闭企微服务连接
|
||||
if redis_client:
|
||||
try:
|
||||
await session_service.wecom_service.close()
|
||||
await redis_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/resolve — 结单
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/resolve")
|
||||
async def resolve_conversation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""结单。
|
||||
|
||||
坐席点击"结单"按钮时调用,将会话状态改为 resolved。
|
||||
|
||||
权限控制:只有主责坐席(assigned_agent_id)才能结单。
|
||||
协作坐席和其他坐席不能结单。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
|
||||
# 先查询会话,验证主责坐席身份
|
||||
from sqlalchemy import select as _select
|
||||
from app.models.conversation import Conversation as _Conversation
|
||||
stmt = _select(_Conversation).where(_Conversation.id == conversation_id)
|
||||
result = await db.execute(stmt)
|
||||
conv = result.scalars().first()
|
||||
if not conv:
|
||||
raise AppException(3003, "会话不存在")
|
||||
if conv.assigned_agent_id != current_agent.user_id:
|
||||
raise AppException(3027, "只有主责坐席才能结单")
|
||||
|
||||
conversation = await session_service.resolve_conversation(conversation_id)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/pin — 置顶/取消置顶
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/pin")
|
||||
async def toggle_pin(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""切换会话置顶状态。
|
||||
|
||||
每次调用切换当前状态:置顶→取消置顶,取消置顶→置顶。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.toggle_pin(conversation_id)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/todo — 代办/取消代办
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/todo")
|
||||
async def toggle_todo(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""切换会话代办状态。
|
||||
|
||||
每次调用切换当前状态:代办→取消代办,取消代办→代办。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.toggle_todo(conversation_id)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/transfer — 转接
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/transfer")
|
||||
async def transfer_conversation(
|
||||
conversation_id: str,
|
||||
body: ConversationAssign,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""转接会话到另一个坐席。
|
||||
|
||||
第一步简化版:只更换坐席,不做转接通知。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 转接请求体(包含 target agent_id)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.transfer_conversation(
|
||||
conversation_id=conversation_id,
|
||||
target_agent_id=body.agent_id,
|
||||
)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/grab — 接手会话(抢单)
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/grab")
|
||||
async def grab_conversation(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""接手其他坐席的会话(抢单)。
|
||||
|
||||
接手后原坐席自动释放,会话 assigned_agent_id 切换为当前坐席。
|
||||
验证规则:
|
||||
1. 会话必须已分配给其他坐席(不能接手自己的,不能接手未分配的)
|
||||
2. 当前坐席未满负荷
|
||||
3. 会话状态为 serving
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含接手后的会话信息
|
||||
"""
|
||||
# 1. 查找目标会话
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.get_conversation(conversation_id)
|
||||
|
||||
# 2. 校验:会话必须已分配给其他坐席
|
||||
if not conversation.assigned_agent_id:
|
||||
raise AppException(3011, "该会话尚未分配坐席,请使用接单功能")
|
||||
if conversation.assigned_agent_id == current_agent.user_id:
|
||||
raise AppException(3012, "不能接手自己的会话")
|
||||
if conversation.status == "resolved":
|
||||
raise AppException(3002, "会话已结单")
|
||||
if conversation.status != "serving":
|
||||
raise AppException(3013, f"只能接手服务中的会话,当前状态: {conversation.status}")
|
||||
|
||||
# 3. 校验当前坐席未满负荷
|
||||
# 刷新坐席数据(current_agent 可能是缓存的旧数据)
|
||||
stmt = select(Agent).where(Agent.user_id == current_agent.user_id)
|
||||
result = await db.execute(stmt)
|
||||
fresh_agent = result.scalars().first()
|
||||
if fresh_agent and fresh_agent.current_load >= fresh_agent.max_load:
|
||||
raise AppException(3005, "您已满负荷,无法接手更多会话")
|
||||
|
||||
# 4. 原坐席 current_load 减 1
|
||||
old_agent_id = conversation.assigned_agent_id
|
||||
stmt = select(Agent).where(Agent.user_id == old_agent_id)
|
||||
result = await db.execute(stmt)
|
||||
old_agent = result.scalars().first()
|
||||
if old_agent and old_agent.current_load > 0:
|
||||
old_agent.current_load -= 1
|
||||
db.add(old_agent)
|
||||
|
||||
# 5. 更新会话 assigned_agent_id 为当前坐席
|
||||
conversation.assigned_agent_id = current_agent.user_id
|
||||
conversation.updated_at = datetime.now()
|
||||
db.add(conversation)
|
||||
|
||||
# 6. 当前坐席 current_load 加 1
|
||||
if fresh_agent:
|
||||
fresh_agent.current_load += 1
|
||||
db.add(fresh_agent)
|
||||
|
||||
await db.flush()
|
||||
|
||||
logger.info(
|
||||
f"会话接手: conv_id={conversation_id}, "
|
||||
f"from={old_agent_id} to={current_agent.user_id}"
|
||||
)
|
||||
|
||||
# 7. WS 广播 conversation_updated 事件(原坐席和当前坐席都能收到)
|
||||
from app.services.ws_manager import manager as ws_manager
|
||||
try:
|
||||
await ws_manager.broadcast({
|
||||
"type": "conversation_updated",
|
||||
"data": {
|
||||
"conversation_id": str(conversation.id),
|
||||
"status": conversation.status,
|
||||
"assigned_agent_id": conversation.assigned_agent_id,
|
||||
"old_agent_id": old_agent_id,
|
||||
"new_agent_id": current_agent.user_id,
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"WebSocket广播失败: {e}")
|
||||
|
||||
# 8. 返回接手成功的会话信息
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
response_data["is_mine"] = True
|
||||
response_data["assigned_agent_name"] = current_agent.name
|
||||
response_data["can_grab"] = False
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/invite — 摇人(邀请坐席协作)
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/invite")
|
||||
async def invite_collaborator(
|
||||
conversation_id: str,
|
||||
body: ConversationInvite,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""坐席A邀请坐席B加入会话协作。
|
||||
|
||||
校验规则:
|
||||
1. 当前坐席必须是主责坐席或已加入的协作坐席
|
||||
2. 被邀请坐席存在且在线
|
||||
3. 被邀请坐席不是主责坐席,也不在协作列表中(防止重复邀请)
|
||||
4. 会话必须为 serving(已结单的不能摇人)
|
||||
|
||||
副作用:
|
||||
- WebSocket 推送给被邀请坐席(collaborator_invited 定向通知)
|
||||
- WebSocket 广播给所有坐席(collaborator_joined 刷新列表)
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 邀请请求(含 agent_id)
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.invite_collaborator(
|
||||
conversation_id=conversation_id,
|
||||
inviter_agent_id=current_agent.user_id,
|
||||
invitee_agent_id=body.agent_id,
|
||||
)
|
||||
|
||||
# 构建响应
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
response_data["is_mine"] = conversation.assigned_agent_id == current_agent.user_id
|
||||
response_data["is_collaborator"] = False # 邀请人自己不是被邀请的协作坐席
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/leave — 退出协作
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/leave")
|
||||
async def leave_collaboration(
|
||||
conversation_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""坐席退出协作。
|
||||
|
||||
校验规则:
|
||||
1. 当前坐席必须在协作列表中
|
||||
2. 当前坐席不能是主责坐席(主责坐席不能"退出",只能转接或结单)
|
||||
|
||||
副作用:
|
||||
- WebSocket 广播给所有坐席(collaborator_left 刷新列表)
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.leave_collaboration(
|
||||
conversation_id=conversation_id,
|
||||
agent_id=current_agent.user_id,
|
||||
)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# 邀请功能 API(P0-09~P0-11)
|
||||
# =============================================================================
|
||||
# 和「摇人」的区别:
|
||||
# 摇人 (invite) = 坐席 → 坐席协作(collaborating_agent_ids)
|
||||
# 邀请 (invite-participant) = 坐席 → 任意员工/部门(participants)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/invite-participant — 邀请员工/部门加入会话
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/invite-participant")
|
||||
async def invite_participant(
|
||||
conversation_id: str,
|
||||
body: InviteParticipantRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""坐席邀请员工/部门加入会话(P0-09 邀请发起)。
|
||||
|
||||
权限:只有主责坐席可以发起邀请。
|
||||
副作用:
|
||||
- 向被邀请人发送企微卡片通知(含「加入会话」按钮)
|
||||
- 在会话中创建系统消息
|
||||
- WebSocket 广播参与者变更
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 邀请请求(含被邀请人列表 + 历史共享模式)
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
# 创建企微服务实例(发送卡片通知用)
|
||||
redis_client = None
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
from app.config import settings
|
||||
redis_client = settings.create_redis_client()
|
||||
wecom_service = WecomService(redis_client)
|
||||
session_service = SessionService(db, wecom_service=wecom_service)
|
||||
except Exception:
|
||||
logger.warning("创建企微服务失败,邀请通知将不发送")
|
||||
session_service = SessionService(db)
|
||||
|
||||
conversation = await session_service.invite_participants(
|
||||
conversation_id=conversation_id,
|
||||
inviter_agent_id=current_agent.user_id,
|
||||
participants=[p.model_dump() for p in body.participants],
|
||||
history_mode=body.history_mode,
|
||||
)
|
||||
|
||||
# 关闭连接
|
||||
if redis_client:
|
||||
try:
|
||||
await session_service.wecom_service.close()
|
||||
await redis_client.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/join — 被邀请人加入会话
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/join")
|
||||
async def join_conversation(
|
||||
conversation_id: str,
|
||||
body: JoinConversationRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""被邀请人通过链接加入会话(P0-10 加入会话)。
|
||||
|
||||
校验:该员工必须在 participants 列表中(被邀请过才能加入)。
|
||||
副作用:
|
||||
- 更新参与者的 joined 状态
|
||||
- 在会话中创建系统消息
|
||||
- WebSocket 广播参与者变更
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 加入请求(含 employee_id)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.join_conversation(
|
||||
conversation_id=conversation_id,
|
||||
employee_id=body.employee_id,
|
||||
)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# DELETE /api/conversations/{id}/participants/{user_id} — 移除参与者
|
||||
# --------------------------------------------------------------------------
|
||||
@router.delete("/conversations/{conversation_id}/participants/{user_id}")
|
||||
async def remove_participant(
|
||||
conversation_id: str,
|
||||
user_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_agent: Agent = Depends(get_current_agent),
|
||||
):
|
||||
"""移除参与者(P0-11 参与者管理)。
|
||||
|
||||
权限:只有主责坐席可以移除参与者。
|
||||
副作用:
|
||||
- 在会话中创建系统消息
|
||||
- WebSocket 广播参与者变更
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
user_id: 被移除的员工UserID
|
||||
db: 数据库会话
|
||||
current_agent: 当前坐席(认证依赖注入)
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.remove_participant(
|
||||
conversation_id=conversation_id,
|
||||
remover_agent_id=current_agent.user_id,
|
||||
target_user_id=user_id,
|
||||
)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# POST /api/conversations/{id}/leave-participant — 参与者主动退出
|
||||
# --------------------------------------------------------------------------
|
||||
@router.post("/conversations/{conversation_id}/leave-participant")
|
||||
async def leave_as_participant(
|
||||
conversation_id: str,
|
||||
body: JoinConversationRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""参与者主动退出会话。
|
||||
|
||||
副作用:
|
||||
- 在会话中创建系统消息
|
||||
- WebSocket 广播参与者变更
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
body: 退出请求(含 employee_id)
|
||||
db: 数据库会话
|
||||
|
||||
Returns:
|
||||
Dict: 统一响应格式,包含更新后的会话信息
|
||||
"""
|
||||
session_service = SessionService(db)
|
||||
conversation = await session_service.leave_as_participant(
|
||||
conversation_id=conversation_id,
|
||||
employee_id=body.employee_id,
|
||||
)
|
||||
|
||||
response_data = ConversationResponse.model_validate(conversation).model_dump()
|
||||
return success_response(data=response_data)
|
||||
Reference in New Issue
Block a user