chore: 整理项目结构,清理归档文件,更新部署配置

This commit is contained in:
Simon
2026-07-04 21:01:39 +08:00
parent 8bd4ab0366
commit 64ff1bf7d5
508 changed files with 43575 additions and 14129 deletions
@@ -0,0 +1,42 @@
# =============================================================================
# 会话服务模块 (conversation)
# =============================================================================
# 说明:会话生命周期管理,包括会话创建、状态流转、坐席分配等
#
# 本模块包含:
# - SessionService: 会话状态管理服务(原始)
# - SessionLifecycleService: 会话生命周期服务(拆分)
# - SessionQueryService: 会话查询服务(拆分)
# - SessionCollaborationService: 会话协作服务(拆分)
# - SessionParticipantService: 会话参与者服务(拆分)
# - MessageRouter: 消息路由服务
# - WsManager: WebSocket 连接管理
#
# 迁移说明:
# 旧导入路径:from app.services import SessionService
# 新导入路径:from app.services.conversation import SessionService
# 两者均支持(向后兼容)
# =============================================================================
# 重新导出会话服务类
from app.services.session_service import SessionService
from app.services.message_router import MessageRouter
from app.services.ws_manager import ConnectionManager
# 拆分后的服务(新)
from app.services.conversation.session_lifecycle_service import SessionLifecycleService
from app.services.conversation.session_query_service import SessionQueryService
from app.services.conversation.session_collaboration_service import SessionCollaborationService
from app.services.conversation.session_participant_service import SessionParticipantService
__all__ = [
# 原始服务(向后兼容)
"SessionService",
"MessageRouter",
"ConnectionManager",
# 拆分后的服务(新)
"SessionLifecycleService",
"SessionQueryService",
"SessionCollaborationService",
"SessionParticipantService",
]
@@ -0,0 +1,275 @@
# =============================================================================
# 企微IT智能服务台 — 会话协作服务
# =============================================================================
# 说明:会话协作相关功能
# 1. 邀请协作坐席
# 2. 移除协作坐席
# 3. 转接会话
# =============================================================================
import json
import logging
from datetime import datetime
from typing import List, Optional
from uuid import UUID
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.agent import Agent
from app.models.conversation import Conversation
from app.services.wecom_service import WecomService
from app.utils.response import AppException
logger = logging.getLogger(__name__)
class SessionCollaborationService:
"""会话协作服务。
提供会话协作(邀请、转接)功能。
"""
def __init__(
self,
db: AsyncSession,
wecom_service: Optional[WecomService] = None,
):
"""初始化会话协作服务。
Args:
db: 异步数据库会话
wecom_service: 企微 API 服务(可选)
"""
self.db = db
self.wecom_service = wecom_service
# --------------------------------------------------------------------------
# 邀请协作坐席
# --------------------------------------------------------------------------
async def invite_collaborator(
self,
conversation_id: UUID,
collaborator_agent_id: str,
inviter_agent_id: str,
) -> Conversation:
"""邀请其他坐席参与会话协作。
Args:
conversation_id: 会话ID
collaborator_agent_id: 被邀请的坐席ID
inviter_agent_id: 邀请人坐席ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 获取被邀请坐席信息
stmt = select(Agent).where(Agent.agent_id == collaborator_agent_id)
result = await self.db.execute(stmt)
collaborator = result.scalar_one_or_none()
if not collaborator:
from app.utils.response import ERR_AGENT_NOT_FOUND
raise ERR_AGENT_NOT_FOUND
# 获取当前协作列表
collaborating_ids = conversation.collaborating_agent_ids or []
# 检查是否已在协作列表中
if collaborator_agent_id in collaborating_ids:
raise AppException(
3020,
f"坐席 {collaborator.name} 已在协作列表中",
)
# 检查是否是自己邀请自己
if collaborator_agent_id == inviter_agent_id:
raise AppException(
3021,
"不能邀请自己参与协作",
)
# 添加到协作列表
collaborating_ids.append(collaborator_agent_id)
conversation.collaborating_agent_ids = collaborating_ids
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 发送系统消息
await self._create_system_message(
conversation_id,
f"坐席 {collaborator.name} 被邀请加入协作",
)
logger.info(
f"邀请协作: conv_id={conversation_id}, "
f"inviter={inviter_agent_id}, collaborator={collaborator_agent_id}"
)
# 发送企微通知
if self.wecom_service:
try:
await self.wecom_service.send_message(
user_id=collaborator_agent_id,
content=f"您被邀请参与会话协作,会话ID: {conversation_id}",
)
except Exception as e:
logger.warning(f"发送协作邀请通知失败: {e}")
return conversation
# --------------------------------------------------------------------------
# 移除协作坐席
# --------------------------------------------------------------------------
async def leave_collaboration(
self,
conversation_id: UUID,
collaborator_agent_id: str,
) -> Conversation:
"""移除协作坐席。
Args:
conversation_id: 会话ID
collaborator_agent_id: 要移除的坐席ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 获取当前协作列表
collaborating_ids = conversation.collaborating_agent_ids or []
# 检查是否在协作列表中
if collaborator_agent_id not in collaborating_ids:
raise AppException(
3022,
"该坐席不在协作列表中",
)
# 从协作列表中移除
collaborating_ids.remove(collaborator_agent_id)
conversation.collaborating_agent_ids = collaborating_ids
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 发送系统消息
await self._create_system_message(
conversation_id,
f"坐席 {collaborator_agent_id} 退出协作",
)
logger.info(
f"移除协作: conv_id={conversation_id}, "
f"collaborator={collaborator_agent_id}"
)
return conversation
# --------------------------------------------------------------------------
# 转接会话
# --------------------------------------------------------------------------
async def transfer_conversation(
self,
conversation_id: UUID,
from_agent_id: str,
to_agent_id: str,
) -> Conversation:
"""将会话转接给其他坐席。
Args:
conversation_id: 会话ID
from_agent_id: 当前坐席ID
to_agent_id: 目标坐席ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 校验权限:只有当前负责的坐席可以转接
if conversation.assigned_agent_id != from_agent_id:
raise AppException(
3023,
"只有当前负责的坐席可以转接会话",
)
# 获取目标坐席信息
stmt = select(Agent).where(Agent.agent_id == to_agent_id)
result = await self.db.execute(stmt)
to_agent = result.scalar_one_or_none()
if not to_agent:
from app.utils.response import ERR_AGENT_NOT_FOUND
raise ERR_AGENT_NOT_FOUND
# 检查目标坐席是否已经有该会话
if conversation.assigned_agent_id == to_agent_id:
raise AppException(
3024,
"该坐席已经是会话负责人",
)
# 更新会话负责人
conversation.assigned_agent_id = to_agent_id
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 发送系统消息
await self._create_system_message(
conversation_id,
f"会话已转接给坐席 {to_agent.name}",
)
logger.info(
f"会话转接: conv_id={conversation_id}, "
f"from={from_agent_id}, to={to_agent_id}"
)
# 发送企微通知
if self.wecom_service:
try:
await self.wecom_service.send_agent_assigned_notification(
employee_id=conversation.employee_id,
agent_name=to_agent.name,
)
except Exception as e:
logger.warning(f"发送转接通知失败: {e}")
return conversation
# --------------------------------------------------------------------------
# 内部方法
# --------------------------------------------------------------------------
async def _get_conversation(self, conversation_id: UUID) -> Conversation:
"""获取会话对象。"""
stmt = select(Conversation).where(Conversation.id == conversation_id)
result = await self.db.execute(stmt)
conversation = result.scalar_one_or_none()
if not conversation:
from app.utils.response import ERR_CONVERSATION_NOT_FOUND
raise ERR_CONVERSATION_NOT_FOUND
return conversation
async def _create_system_message(
self, conversation_id: UUID, content: str
) -> None:
"""创建系统消息(内部方法,由协作操作调用)。"""
from app.models.message import Message
message = Message(
conversation_id=conversation_id,
message_type="system",
sender_type="system",
sender_id="system",
sender_name="系统",
content=content,
)
self.db.add(message)
await self.db.flush()
@@ -0,0 +1,341 @@
# =============================================================================
# 企微IT智能服务台 — 会话生命周期服务
# =============================================================================
# 说明:管理会话的完整生命周期
# 1. 创建会话(新员工发消息时自动创建)
# 2. 更新会话状态(queued → serving → resolved
# 3. 分配坐席
# 4. 结单
# 5. 置顶/取消置顶
# 6. 待办/取消待办
# =============================================================================
import logging
from datetime import datetime
from typing import Optional
from uuid import UUID
from sqlalchemy import and_, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.agent import Agent
from app.models.conversation import Conversation
from app.services.wecom_service import WecomService
from app.utils.response import (
AppException,
ERR_AGENT_BUSY,
ERR_AGENT_NOT_FOUND,
ERR_CONVERSATION_NOT_FOUND,
ERR_CONVERSATION_RESOLVED,
ERR_DUPLICATE_ASSIGN,
)
logger = logging.getLogger(__name__)
class SessionLifecycleService:
"""会话生命周期管理服务。
管理会话的完整生命周期,实现会话状态流转和坐席分配逻辑。
"""
def __init__(
self,
db: AsyncSession,
wecom_service: Optional[WecomService] = None,
):
"""初始化会话生命周期服务。
Args:
db: 异步数据库会话
wecom_service: 企微 API 服务(用于坐席接入时发送通知,可选)
"""
self.db = db
self.wecom_service = wecom_service
# --------------------------------------------------------------------------
# 创建会话
# --------------------------------------------------------------------------
async def create_conversation(
self,
employee_id: str,
employee_name: str = "",
department: str = "",
position: str = "",
level: str = "",
) -> Conversation:
"""创建新会话。
当员工首次发消息或摇人时自动创建。
新会话默认状态为 queued(排队等坐席)。
Args:
employee_id: 企微员工 UserID
employee_name: 员工姓名
department: 部门
position: 岗位
level: 等级
Returns:
Conversation: 新创建的会话对象
"""
conversation = Conversation(
employee_id=employee_id,
employee_name=employee_name,
department=department,
position=position,
level=level,
status="queued",
is_vip=False,
is_pinned=False,
is_todo=False,
urgency_score=1,
tags={},
)
self.db.add(conversation)
await self.db.flush()
logger.info(f"创建会话: conv_id={conversation.id}, employee={employee_id}")
return conversation
# --------------------------------------------------------------------------
# 更新会话状态
# --------------------------------------------------------------------------
async def update_status(
self, conversation_id: UUID, new_status: str
) -> Conversation:
"""更新会话状态。
状态流转规则:
- queued → serving: 坐席接单
- serving → resolved: 结单
- queued → resolved: 直接结单(排队中员工问题已自行解决)
Args:
conversation_id: 会话ID
new_status: 新状态(queued/serving/resolved/ai_handling
Returns:
Conversation: 更新后的会话对象
Raises:
AppException: 会话不存在或状态流转不合法
"""
conversation = await self._get_conversation(conversation_id)
# 校验状态流转合法性
valid_transitions = {
"queued": ["serving", "resolved"],
"serving": ["resolved"],
"ai_handling": ["queued", "serving", "resolved"],
"resolved": [], # 已结单不能再改状态
}
allowed = valid_transitions.get(conversation.status, [])
if new_status not in allowed and new_status != conversation.status:
raise AppException(
3010,
f"会话状态流转不合法: {conversation.status}{new_status}",
)
# 如果是已结单,不能再改状态
if conversation.status == "resolved":
raise ERR_CONVERSATION_RESOLVED
conversation.status = new_status
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
logger.info(
f"会话状态更新: conv_id={conversation_id}, "
f"{conversation.status}{new_status}"
)
return conversation
# --------------------------------------------------------------------------
# 分配坐席(接单)
# --------------------------------------------------------------------------
async def assign_agent(
self, conversation_id: UUID, agent_id: str
) -> Conversation:
"""分配坐席(坐席接单)。
将会话分配给指定坐席,状态从 queued 变为 serving。
如果坐席当前服务会话数已达上限,返回错误。
Args:
conversation_id: 会话ID
agent_id: 坐席ID
Returns:
Conversation: 更新后的会话对象
Raises:
AppException: 会话不存在/已结单/坐席不存在/坐席忙碌
"""
conversation = await self._get_conversation(conversation_id)
# 检查会话状态:只有 queued 和 ai_handling 可以被接单
if conversation.status not in ["queued", "ai_handling"]:
raise AppException(
3009,
f"当前会话状态为 {conversation.status},无法接单",
)
# 检查是否重复接单
if conversation.assigned_agent_id == agent_id:
raise ERR_DUPLICATE_ASSIGN
# 查询坐席信息
stmt = select(Agent).where(Agent.agent_id == agent_id)
result = await self.db.execute(stmt)
agent = result.scalar_one_or_none()
if not agent:
raise ERR_AGENT_NOT_FOUND
# 检查坐席当前服务会话数是否已达上限
count_stmt = select(func.count(Conversation.id)).where(
and_(
Conversation.assigned_agent_id == agent_id,
Conversation.status == "serving",
)
)
count_result = await self.db.execute(count_stmt)
current_count = count_result.scalar() or 0
if current_count >= agent.max_sessions:
raise ERR_AGENT_BUSY
# 更新会话
conversation.assigned_agent_id = agent_id
conversation.status = "serving"
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
logger.info(
f"会话分配: conv_id={conversation_id}, agent={agent_id}"
)
# 发送企微通知给员工(可选)
if self.wecom_service:
try:
await self.wecom_service.send_agent_connected_notification(
employee_id=conversation.employee_id,
agent_name=agent.name,
)
except Exception as e:
logger.warning(f"发送企微通知失败: {e}")
return conversation
# --------------------------------------------------------------------------
# 结单
# --------------------------------------------------------------------------
async def resolve_conversation(
self, conversation_id: UUID, resolution_summary: str = ""
) -> Conversation:
"""结单。
将会话标记为 resolved,记录解决摘要。
只有 serving 状态的会话可以结单。
Args:
conversation_id: 会话ID
resolution_summary: 解决摘要(可选)
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 只有 serving 可以结单
if conversation.status != "serving":
raise AppException(
3008,
f"只有服务中的会话可以结单,当前状态:{conversation.status}",
)
conversation.status = "resolved"
conversation.assigned_agent_id = None # 释放坐席
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
logger.info(
f"会话结单: conv_id={conversation_id}, summary={resolution_summary}"
)
return conversation
# --------------------------------------------------------------------------
# 置顶/取消置顶
# --------------------------------------------------------------------------
async def toggle_pin(self, conversation_id: UUID) -> Conversation:
"""切换会话置顶状态。
Args:
conversation_id: 会话ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
conversation.is_pinned = not conversation.is_pinned
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
logger.info(
f"会话置顶切换: conv_id={conversation_id}, "
f"is_pinned={conversation.is_pinned}"
)
return conversation
# --------------------------------------------------------------------------
# 待办/取消待办
# --------------------------------------------------------------------------
async def toggle_todo(self, conversation_id: UUID) -> Conversation:
"""切换会话待办状态。
Args:
conversation_id: 会话ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
conversation.is_todo = not conversation.is_todo
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
logger.info(
f"会话待办切换: conv_id={conversation_id}, is_todo={conversation.is_todo}"
)
return conversation
# --------------------------------------------------------------------------
# 内部方法
# --------------------------------------------------------------------------
async def _get_conversation(self, conversation_id: UUID) -> Conversation:
"""获取会话对象。
Args:
conversation_id: 会话ID
Returns:
Conversation: 会话对象
Raises:
AppException: 会话不存在
"""
stmt = select(Conversation).where(Conversation.id == conversation_id)
result = await self.db.execute(stmt)
conversation = result.scalar_one_or_none()
if not conversation:
raise ERR_CONVERSATION_NOT_FOUND
return conversation
@@ -0,0 +1,281 @@
# =============================================================================
# 企微IT智能服务台 — 会话参与者服务
# =============================================================================
# 说明:会话参与者(员工)相关功能
# 1. 邀请参与者
# 2. 加入会话
# 3. 移除参与者
# 4. 退出会话
# =============================================================================
import json
import logging
from datetime import datetime
from typing import List, Optional
from uuid import UUID
from sqlalchemy import and_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.conversation import Conversation
from app.services.wecom_service import WecomService
from app.utils.response import AppException
logger = logging.getLogger(__name__)
class SessionParticipantService:
"""会话参与者服务。
提供会话参与者(员工)管理功能。
"""
def __init__(
self,
db: AsyncSession,
wecom_service: Optional[WecomService] = None,
):
"""初始化会话参与者服务。
Args:
db: 异步数据库会话
wecom_service: 企微 API 服务(可选)
"""
self.db = db
self.wecom_service = wecom_service
# --------------------------------------------------------------------------
# 邀请参与者(H5
# --------------------------------------------------------------------------
async def invite_participants(
self,
conversation_id: UUID,
employee_ids: List[str],
inviter_employee_id: str,
) -> Conversation:
"""邀请其他员工参与会话。
Args:
conversation_id: 会话ID
employee_ids: 被邀请的员工ID列表
inviter_employee_id: 邀请人员工ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 获取当前参与者列表
participants = conversation.participants or []
# 添加新参与者
added = []
for employee_id in employee_ids:
if employee_id not in participants and employee_id != inviter_employee_id:
participants.append(employee_id)
added.append(employee_id)
if not added:
raise AppException(
3030,
"没有新的参与者需要添加",
)
conversation.participants = participants
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 广播参与者变化
await self._broadcast_participant_change(
conversation_id, "invited", added
)
logger.info(
f"邀请参与者: conv_id={conversation_id}, "
f"inviter={inviter_employee_id}, added={added}"
)
return conversation
# --------------------------------------------------------------------------
# 加入会话(H5
# --------------------------------------------------------------------------
async def join_conversation(
self,
conversation_id: UUID,
employee_id: str,
) -> Conversation:
"""员工加入会话。
Args:
conversation_id: 会话ID
employee_id: 员工ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 获取当前参与者列表
participants = conversation.participants or []
# 检查是否已在参与者列表中
if employee_id in participants:
raise AppException(
3031,
"您已经在会话中",
)
# 添加到参与者列表
participants.append(employee_id)
conversation.participants = participants
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 广播参与者变化
await self._broadcast_participant_change(
conversation_id, "joined", [employee_id]
)
logger.info(
f"员工加入会话: conv_id={conversation_id}, employee={employee_id}"
)
return conversation
# --------------------------------------------------------------------------
# 移除参与者(坐席)
# --------------------------------------------------------------------------
async def remove_participant(
self,
conversation_id: UUID,
employee_id: str,
operator_agent_id: str,
) -> Conversation:
"""移除会话参与者。
Args:
conversation_id: 会话ID
employee_id: 被移除的员工ID
operator_agent_id: 操作坐席ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 获取当前参与者列表
participants = conversation.participants or []
# 检查是否在参与者列表中
if employee_id not in participants:
raise AppException(
3032,
"该员工不在会话参与者列表中",
)
# 从参与者列表中移除
participants.remove(employee_id)
conversation.participants = participants
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 广播参与者变化
await self._broadcast_participant_change(
conversation_id, "removed", [employee_id]
)
logger.info(
f"移除参与者: conv_id={conversation_id}, "
f"employee={employee_id}, operator={operator_agent_id}"
)
return conversation
# --------------------------------------------------------------------------
# 退出会话(H5
# --------------------------------------------------------------------------
async def leave_as_participant(
self,
conversation_id: UUID,
employee_id: str,
) -> Conversation:
"""员工退出会话。
Args:
conversation_id: 会话ID
employee_id: 员工ID
Returns:
Conversation: 更新后的会话对象
"""
conversation = await self._get_conversation(conversation_id)
# 获取当前参与者列表
participants = conversation.participants or []
# 检查是否在参与者列表中
if employee_id not in participants:
raise AppException(
3033,
"您不在会话参与者列表中",
)
# 从参与者列表中移除
participants.remove(employee_id)
conversation.participants = participants
conversation.updated_at = datetime.now()
self.db.add(conversation)
await self.db.flush()
# 广播参与者变化
await self._broadcast_participant_change(
conversation_id, "left", [employee_id]
)
logger.info(
f"员工退出会话: conv_id={conversation_id}, employee={employee_id}"
)
return conversation
# --------------------------------------------------------------------------
# 内部方法
# --------------------------------------------------------------------------
async def _get_conversation(self, conversation_id: UUID) -> Conversation:
"""获取会话对象。"""
stmt = select(Conversation).where(Conversation.id == conversation_id)
result = await self.db.execute(stmt)
conversation = result.scalar_one_or_none()
if not conversation:
from app.utils.response import ERR_CONVERSATION_NOT_FOUND
raise ERR_CONVERSATION_NOT_FOUND
return conversation
async def _broadcast_participant_change(
self,
conversation_id: UUID,
action: str,
employee_ids: List[str],
) -> None:
"""广播参与者变化事件。
通过 WebSocket 广播给坐席端。
"""
from app.services.ws_manager import ws_manager
event_data = {
"type": "participant_change",
"conversation_id": str(conversation_id),
"action": action, # invited, joined, removed, left
"employee_ids": employee_ids,
}
try:
await ws_manager.broadcast_to_conversation(
str(conversation_id),
event_data,
)
except Exception as e:
logger.warning(f"WebSocket广播参与者变化失败: {e}")
@@ -0,0 +1,214 @@
# =============================================================================
# 企微IT智能服务台 — 会话查询服务
# =============================================================================
# 说明:会话查询相关功能
# 1. 获取会话列表(支持过滤和排序)
# 2. 获取坐席当前服务的会话列表
# 3. 获取单个会话详情
# =============================================================================
import logging
from datetime import datetime
from typing import List, Optional, Tuple
from uuid import UUID
from sqlalchemy import and_, case, desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.conversation import Conversation
logger = logging.getLogger(__name__)
class SessionQueryService:
"""会话查询服务。
提供会话列表查询、排序和详情获取功能。
"""
def __init__(self, db: AsyncSession):
"""初始化会话查询服务。
Args:
db: 异步数据库会话
"""
self.db = db
# --------------------------------------------------------------------------
# 获取会话列表(坐席端)
# --------------------------------------------------------------------------
async def get_conversations(
self,
status: Optional[str] = None,
agent_id: Optional[str] = None,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Conversation], int]:
"""获取会话列表,支持过滤和排序。
排序规则(PRD 定义):
紧急 → 举手 → 需介入 → 活跃 → 已结单
同级别按 last_message_at 倒序
实现方式:先按数据库基础排序(状态+置顶+紧急度),
再在 Python 侧按完整规则精细排序(含 JSON tags 字段)。
Args:
status: 按状态过滤(可选)
agent_id: 按坐席ID过滤(可选,查看某坐席的会话)
page: 页码(从1开始)
page_size: 每页数量
Returns:
tuple[List[Conversation], int]: (会话列表, 总数)
"""
# 构建查询条件
conditions = []
if status:
conditions.append(Conversation.status == status)
if agent_id:
conditions.append(Conversation.assigned_agent_id == agent_id)
# 查询总数
count_stmt = select(func.count(Conversation.id))
if conditions:
count_stmt = count_stmt.where(and_(*conditions))
total_result = await self.db.execute(count_stmt)
total = total_result.scalar() or 0
# 数据库侧基础排序(快速过滤):
# 置顶 > 紧急度5 > 紧急度4 > 紧急度3 > 状态排序 > 最后消息时间
# JSON tags 字段的排序在 Python 侧完成(SQLite 不支持 JSON 操作符)
db_order_weight = case(
(Conversation.is_pinned == True, 1000),
(Conversation.urgency_score >= 5, 900),
(Conversation.urgency_score >= 4, 600),
(Conversation.urgency_score >= 3, 300),
(Conversation.status == "queued", 200),
(Conversation.status == "ai_handling", 150),
(Conversation.status == "serving", 100),
else_=0,
)
stmt = select(Conversation)
if conditions:
stmt = stmt.where(and_(*conditions))
# 数据库侧先按基础权重 + 最后消息时间排序
stmt = stmt.order_by(desc(db_order_weight), desc(Conversation.last_message_at))
# 查询所有符合条件的会话(数据量不大时可行;生产环境建议改用 PostgreSQL + JSONB 操作符)
result = await self.db.execute(stmt)
all_conversations = list(result.scalars().all())
# ===== Python 侧精细排序(支持 JSON tags 字段)=====
def _sort_key(conv: Conversation):
"""计算完整排序权重(数值越大越靠前)"""
weight = 0
tags = conv.tags or {}
# 置顶(最高优先级)
if conv.is_pinned:
weight += 10000
# 紧急度评分(越高越靠前)
urgency = conv.urgency_score or 0
if urgency >= 5:
weight += 9000
elif urgency >= 4:
weight += 6000
elif urgency >= 3:
weight += 3000
# 举手标记
if tags.get("hand_raise"):
weight += 8000
# 需介入标记
if tags.get("need_intervene"):
weight += 7000
# 情绪标记(非 neutral
emotion = tags.get("emotion", "neutral")
if emotion and emotion != "neutral":
weight += 5000
# 状态排序
status_order = {
"queued": 2000,
"ai_handling": 1500,
"serving": 1000,
"resolved": 0,
}
weight += status_order.get(conv.status, 0)
# 最后消息时间(时间戳越大越靠前,除以 1e6 归一化到合理范围)
if conv.last_message_at:
ts = conv.last_message_at.timestamp()
else:
ts = 0
# 用 (weight, ts) 元组排序:先按 weight 降序,再按 ts 降序
return (weight + ts / 1e6, ts)
all_conversations.sort(key=_sort_key, reverse=True)
# 分页
start = (page - 1) * page_size
end = start + page_size
paginated = all_conversations[start:end]
logger.debug(
f"查询会话列表: total={total}, page={page}, page_size={page_size}"
)
return paginated, total
# --------------------------------------------------------------------------
# 获取坐席当前服务的会话列表
# --------------------------------------------------------------------------
async def get_agent_conversations(
self,
agent_id: str,
page: int = 1,
page_size: int = 50,
) -> Tuple[List[Conversation], int]:
"""获取坐席当前服务的会话列表。
Args:
agent_id: 坐席ID
page: 页码
page_size: 每页数量
Returns:
tuple[List[Conversation], int]: (会话列表, 总数)
"""
return await self.get_conversations(
agent_id=agent_id,
page=page,
page_size=page_size,
)
# --------------------------------------------------------------------------
# 获取单个会话详情
# --------------------------------------------------------------------------
async def get_conversation(
self,
conversation_id: UUID,
include_messages: bool = False,
) -> Conversation:
"""获取单个会话详情。
Args:
conversation_id: 会话ID
include_messages: 是否包含消息列表(暂未实现)
Returns:
Conversation: 会话对象
"""
stmt = select(Conversation).where(Conversation.id == conversation_id)
result = await self.db.execute(stmt)
conversation = result.scalar_one_or_none()
if not conversation:
from app.utils.response import ERR_CONVERSATION_NOT_FOUND
raise ERR_CONVERSATION_NOT_FOUND
return conversation