chore: 整理项目结构,清理归档文件,更新部署配置
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# =============================================================================
|
||||
# 管理服务模块 (admin)
|
||||
# =============================================================================
|
||||
# 说明:管理后台相关服务,包括仪表盘、配置、坐席管理、集成配置等
|
||||
#
|
||||
# 本模块包含:
|
||||
# - AdminDashboardService: 仪表盘服务
|
||||
# - AdminConfigService: 配置管理服务
|
||||
# - AdminAgentService: 坐席管理服务
|
||||
# - AdminIntegrationService: 集成管理服务
|
||||
# - AdminModerationService: 审核管理服务
|
||||
# - AdminMonitoringService: 监控管理服务
|
||||
# - AdminAuditService: 审计服务
|
||||
#
|
||||
# 迁移说明:
|
||||
# 旧导入路径:from app.services import get_dashboard_overview
|
||||
# 新导入路径:from app.services.admin import get_dashboard_overview
|
||||
# 两者均支持(向后兼容)
|
||||
# =============================================================================
|
||||
|
||||
# 重新导出管理服务函数(保持兼容)
|
||||
from app.services.admin_service import (
|
||||
get_dashboard_overview,
|
||||
get_config_groups,
|
||||
update_config,
|
||||
get_config_history,
|
||||
list_admin_agents,
|
||||
create_agent,
|
||||
update_agent,
|
||||
delete_agent,
|
||||
get_integrations,
|
||||
update_integration,
|
||||
list_pending_quick_replies,
|
||||
review_quick_reply,
|
||||
get_assignment_mode,
|
||||
update_assignment_mode,
|
||||
get_monitor_sessions,
|
||||
global_search,
|
||||
list_audit_conversations,
|
||||
get_audit_conversation_detail,
|
||||
get_agent_performance,
|
||||
get_system_logs,
|
||||
)
|
||||
|
||||
# 拆分后的服务类(新)
|
||||
from app.services.admin.admin_dashboard_service import AdminDashboardService
|
||||
from app.services.admin.admin_config_service import AdminConfigService
|
||||
from app.services.admin.admin_agent_service import AdminAgentService
|
||||
from app.services.admin.admin_integration_service import AdminIntegrationService
|
||||
from app.services.admin.admin_moderation_service import AdminModerationService
|
||||
from app.services.admin.admin_monitoring_service import AdminMonitoringService
|
||||
from app.services.admin.admin_audit_service import AdminAuditService
|
||||
|
||||
__all__ = [
|
||||
# 兼容旧导入
|
||||
"get_dashboard_overview",
|
||||
"get_config_groups",
|
||||
"update_config",
|
||||
"get_config_history",
|
||||
"list_admin_agents",
|
||||
"create_agent",
|
||||
"update_agent",
|
||||
"delete_agent",
|
||||
"get_integrations",
|
||||
"update_integration",
|
||||
"list_pending_quick_replies",
|
||||
"review_quick_reply",
|
||||
"get_assignment_mode",
|
||||
"update_assignment_mode",
|
||||
"get_monitor_sessions",
|
||||
"global_search",
|
||||
"list_audit_conversations",
|
||||
"get_audit_conversation_detail",
|
||||
"get_agent_performance",
|
||||
"get_system_logs",
|
||||
# 新服务类
|
||||
"AdminDashboardService",
|
||||
"AdminConfigService",
|
||||
"AdminAgentService",
|
||||
"AdminIntegrationService",
|
||||
"AdminModerationService",
|
||||
"AdminMonitoringService",
|
||||
"AdminAuditService",
|
||||
]
|
||||
@@ -0,0 +1,159 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台坐席管理服务
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.agent import Agent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminAgentService:
|
||||
"""管理后台坐席管理服务。
|
||||
|
||||
提供坐席的增删改查功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def list_agents(
|
||||
self,
|
||||
is_active: Optional[bool] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取坐席列表。
|
||||
|
||||
Args:
|
||||
is_active: 是否激活(可选)
|
||||
|
||||
Returns:
|
||||
List[Dict]: 坐席列表
|
||||
"""
|
||||
stmt = select(Agent)
|
||||
if is_active is not None:
|
||||
stmt = stmt.where(Agent.is_active == is_active) # noqa: E712
|
||||
stmt = stmt.order_by(Agent.name)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
agents = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"agent_id": a.agent_id,
|
||||
"name": a.name,
|
||||
"is_active": a.is_active,
|
||||
"max_sessions": a.max_sessions,
|
||||
"created_at": a.created_at.isoformat() if a.created_at else None,
|
||||
}
|
||||
for a in agents
|
||||
]
|
||||
|
||||
async def create_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
name: str,
|
||||
max_sessions: int = 5,
|
||||
) -> Dict[str, Any]:
|
||||
"""创建坐席。
|
||||
|
||||
Args:
|
||||
agent_id: 坐席ID
|
||||
name: 坐席名称
|
||||
max_sessions: 最大服务会话数
|
||||
|
||||
Returns:
|
||||
Dict: 创建的坐席信息
|
||||
"""
|
||||
# 检查是否已存在
|
||||
stmt = select(Agent).where(Agent.agent_id == agent_id)
|
||||
result = await self.db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
raise ValueError(f"坐席已存在: {agent_id}")
|
||||
|
||||
agent = Agent(
|
||||
agent_id=agent_id,
|
||||
name=name,
|
||||
max_sessions=max_sessions,
|
||||
is_active=True,
|
||||
)
|
||||
self.db.add(agent)
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"创建坐席: {agent_id} - {name}")
|
||||
return {
|
||||
"agent_id": agent.agent_id,
|
||||
"name": agent.name,
|
||||
"is_active": agent.is_active,
|
||||
"max_sessions": agent.max_sessions,
|
||||
}
|
||||
|
||||
async def update_agent(
|
||||
self,
|
||||
agent_id: str,
|
||||
name: Optional[str] = None,
|
||||
max_sessions: Optional[int] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""更新坐席信息。
|
||||
|
||||
Args:
|
||||
agent_id: 坐席ID
|
||||
name: 坐席名称(可选)
|
||||
max_sessions: 最大服务会话数(可选)
|
||||
is_active: 是否激活(可选)
|
||||
|
||||
Returns:
|
||||
Dict: 更新后的坐席信息
|
||||
"""
|
||||
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 ValueError(f"坐席不存在: {agent_id}")
|
||||
|
||||
if name is not None:
|
||||
agent.name = name
|
||||
if max_sessions is not None:
|
||||
agent.max_sessions = max_sessions
|
||||
if is_active is not None:
|
||||
agent.is_active = is_active
|
||||
|
||||
self.db.add(agent)
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"更新坐席: {agent_id}")
|
||||
return {
|
||||
"agent_id": agent.agent_id,
|
||||
"name": agent.name,
|
||||
"is_active": agent.is_active,
|
||||
"max_sessions": agent.max_sessions,
|
||||
}
|
||||
|
||||
async def delete_agent(self, agent_id: str) -> None:
|
||||
"""删除坐席(软删除)。
|
||||
|
||||
Args:
|
||||
agent_id: 坐席ID
|
||||
"""
|
||||
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 ValueError(f"坐席不存在: {agent_id}")
|
||||
|
||||
# 软删除:设置为非激活
|
||||
agent.is_active = False
|
||||
self.db.add(agent)
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"删除坐席: {agent_id}")
|
||||
@@ -0,0 +1,209 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台审计服务
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminAuditService:
|
||||
"""管理后台审计服务。
|
||||
|
||||
提供会话审计、坐席绩效和系统日志功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def list_audit_conversations(
|
||||
self,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
agent_id: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取审计会话列表。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期 (ISO格式)
|
||||
end_date: 结束日期 (ISO格式)
|
||||
agent_id: 坐席ID(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
Dict: 审计数据
|
||||
"""
|
||||
conditions = [Conversation.status == "resolved"]
|
||||
|
||||
if start_date:
|
||||
start = datetime.fromisoformat(start_date)
|
||||
conditions.append(Conversation.updated_at >= start)
|
||||
|
||||
if end_date:
|
||||
end = datetime.fromisoformat(end_date)
|
||||
conditions.append(Conversation.updated_at <= end)
|
||||
|
||||
if agent_id:
|
||||
conditions.append(Conversation.assigned_agent_id == agent_id)
|
||||
|
||||
# 查询会话
|
||||
stmt = select(Conversation).where(and_(*conditions))
|
||||
stmt = stmt.order_by(desc(Conversation.updated_at))
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
sessions = result.scalars().all()
|
||||
|
||||
# 查询总数
|
||||
count_stmt = select(func.count(Conversation.id)).where(and_(*conditions))
|
||||
count_result = await self.db.execute(count_stmt)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"conversations": [
|
||||
{
|
||||
"id": str(s.id),
|
||||
"employee_name": s.employee_name,
|
||||
"department": s.department,
|
||||
"assigned_agent_id": s.assigned_agent_id,
|
||||
"status": s.status,
|
||||
"urgency_score": s.urgency_score,
|
||||
"impact_scope": s.impact_scope,
|
||||
"created_at": s.created_at.isoformat() if s.created_at else None,
|
||||
"updated_at": s.updated_at.isoformat() if s.updated_at else None,
|
||||
}
|
||||
for s in sessions
|
||||
],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
async def get_audit_conversation_detail(
|
||||
self,
|
||||
conversation_id: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取审计会话详情。
|
||||
|
||||
Args:
|
||||
conversation_id: 会话ID
|
||||
|
||||
Returns:
|
||||
Dict: 会话详情
|
||||
"""
|
||||
stmt = select(Conversation).where(Conversation.id == conversation_id)
|
||||
result = await self.db.execute(stmt)
|
||||
conversation = result.scalar_one_or_none()
|
||||
|
||||
if not conversation:
|
||||
raise ValueError(f"会话不存在: {conversation_id}")
|
||||
|
||||
return {
|
||||
"id": str(conversation.id),
|
||||
"employee_id": conversation.employee_id,
|
||||
"employee_name": conversation.employee_name,
|
||||
"department": conversation.department,
|
||||
"position": conversation.position,
|
||||
"level": conversation.level,
|
||||
"assigned_agent_id": conversation.assigned_agent_id,
|
||||
"collaborating_agent_ids": conversation.collaborating_agent_ids,
|
||||
"status": conversation.status,
|
||||
"urgency_score": conversation.urgency_score,
|
||||
"tags": conversation.tags,
|
||||
"emotion_state": conversation.emotion_state,
|
||||
"impact_scope": conversation.impact_scope,
|
||||
"is_blocking": conversation.is_blocking,
|
||||
"last_message_summary": conversation.last_message_summary,
|
||||
"created_at": conversation.created_at.isoformat() if conversation.created_at else None,
|
||||
"updated_at": conversation.updated_at.isoformat() if conversation.updated_at else None,
|
||||
}
|
||||
|
||||
async def get_agent_performance(
|
||||
self,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取坐席绩效数据。
|
||||
|
||||
Args:
|
||||
start_date: 开始日期 (ISO格式)
|
||||
end_date: 结束日期 (ISO格式)
|
||||
|
||||
Returns:
|
||||
List[Dict]: 绩效数据列表
|
||||
"""
|
||||
# 构建日期条件
|
||||
conditions = [Conversation.status == "resolved"]
|
||||
|
||||
if start_date:
|
||||
start = datetime.fromisoformat(start_date)
|
||||
conditions.append(Conversation.updated_at >= start)
|
||||
|
||||
if end_date:
|
||||
end = datetime.fromisoformat(end_date)
|
||||
conditions.append(Conversation.updated_at <= end)
|
||||
|
||||
# 按坐席分组统计
|
||||
stmt = select(
|
||||
Conversation.assigned_agent_id,
|
||||
func.count(Conversation.id).label("resolved_count"),
|
||||
func.avg(Conversation.urgency_score).label("avg_urgency"),
|
||||
).where(and_(*conditions)).group_by(Conversation.assigned_agent_id)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
rows = result.fetchall()
|
||||
|
||||
# 补充坐席名称
|
||||
agent_stmt = select(Agent.agent_id, Agent.name)
|
||||
agent_result = await self.db.execute(agent_stmt)
|
||||
agent_map = {a.agent_id: a.name for a in agent_result.fetchall()}
|
||||
|
||||
performance = []
|
||||
for row in rows:
|
||||
if row.assigned_agent_id:
|
||||
performance.append({
|
||||
"agent_id": row.assigned_agent_id,
|
||||
"agent_name": agent_map.get(row.assigned_agent_id, "未知"),
|
||||
"resolved_count": row.resolved_count,
|
||||
"avg_urgency": float(row.avg_urgency or 0),
|
||||
})
|
||||
|
||||
return sorted(performance, key=lambda x: x["resolved_count"], reverse=True)
|
||||
|
||||
async def get_system_logs(
|
||||
self,
|
||||
level: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取系统日志。
|
||||
|
||||
Args:
|
||||
level: 日志级别 (可选)
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
Dict: 日志数据
|
||||
"""
|
||||
# TODO: 实现系统日志查询
|
||||
# 当前日志存储在文件系统中,未持久化到数据库
|
||||
# 后续可考虑接入日志服务(如 ELK)
|
||||
|
||||
return {
|
||||
"logs": [],
|
||||
"total": 0,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台配置服务
|
||||
# =============================================================================
|
||||
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.system_config import SystemConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 配置分组映射
|
||||
CONFIG_GROUP_MAP = {
|
||||
"hand_raise": {
|
||||
"label": "举手相关",
|
||||
"keys": ["hand_raise_keywords"],
|
||||
},
|
||||
"emotion": {
|
||||
"label": "情绪识别",
|
||||
"keys": [
|
||||
"emotion_keywords_angry",
|
||||
"emotion_keywords_urgent",
|
||||
"emotion_keywords_worried",
|
||||
],
|
||||
},
|
||||
"urgency": {
|
||||
"label": "紧急度评分",
|
||||
"keys": [
|
||||
"urgency_base_keyword_score",
|
||||
"urgency_emotion_bonus",
|
||||
"urgency_vip_bonus",
|
||||
"urgency_repeat_bonus",
|
||||
],
|
||||
},
|
||||
"queue": {
|
||||
"label": "队列设置",
|
||||
"keys": ["polling_interval_seconds"],
|
||||
},
|
||||
"token": {
|
||||
"label": "令牌设置",
|
||||
"keys": ["access_token_buffer_seconds"],
|
||||
},
|
||||
"emergency": {
|
||||
"label": "应急模式",
|
||||
"keys": ["emergency_mode"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AdminConfigService:
|
||||
"""管理后台配置服务。
|
||||
|
||||
提供系统配置管理功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_config_groups(self) -> List[Dict[str, Any]]:
|
||||
"""获取配置分组列表。
|
||||
|
||||
Returns:
|
||||
List[Dict]: 配置分组列表
|
||||
"""
|
||||
# 查询所有配置
|
||||
stmt = select(SystemConfig)
|
||||
result = await self.db.execute(stmt)
|
||||
configs = result.scalars().all()
|
||||
|
||||
# 按分组整理
|
||||
config_map = {c.config_key: c for c in configs}
|
||||
groups = []
|
||||
|
||||
for group_id, group_info in CONFIG_GROUP_MAP.items():
|
||||
group_configs = []
|
||||
for key in group_info["keys"]:
|
||||
if key in config_map:
|
||||
config = config_map[key]
|
||||
value = config.config_value
|
||||
# 尝试解析 JSON
|
||||
if value and value.startswith("["):
|
||||
try:
|
||||
value = json.loads(value)
|
||||
except Exception:
|
||||
pass
|
||||
group_configs.append({
|
||||
"key": config.config_key,
|
||||
"value": value,
|
||||
"description": config.description,
|
||||
})
|
||||
|
||||
if group_configs:
|
||||
groups.append({
|
||||
"id": group_id,
|
||||
"label": group_info["label"],
|
||||
"configs": group_configs,
|
||||
})
|
||||
|
||||
logger.debug(f"获取配置分组: {len(groups)} 个分组")
|
||||
return groups
|
||||
|
||||
async def update_config(
|
||||
self,
|
||||
key: str,
|
||||
value: Any,
|
||||
) -> Dict[str, Any]:
|
||||
"""更新配置。
|
||||
|
||||
Args:
|
||||
key: 配置键
|
||||
value: 配置值
|
||||
|
||||
Returns:
|
||||
Dict: 更新后的配置
|
||||
"""
|
||||
# 查询配置
|
||||
stmt = select(SystemConfig).where(SystemConfig.config_key == key)
|
||||
result = await self.db.execute(stmt)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
if not config:
|
||||
raise ValueError(f"配置项不存在: {key}")
|
||||
|
||||
# 序列化值
|
||||
if isinstance(value, (list, dict)):
|
||||
serialized_value = json.dumps(value, ensure_ascii=False)
|
||||
else:
|
||||
serialized_value = str(value)
|
||||
|
||||
config.config_value = serialized_value
|
||||
config.updated_at = datetime.now()
|
||||
self.db.add(config)
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"更新配置: {key} = {serialized_value}")
|
||||
return {
|
||||
"key": key,
|
||||
"value": value,
|
||||
}
|
||||
|
||||
async def get_config_history(
|
||||
self,
|
||||
key: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取配置历史变更记录。
|
||||
|
||||
Args:
|
||||
key: 配置键
|
||||
|
||||
Returns:
|
||||
List[Dict]: 变更历史列表
|
||||
"""
|
||||
# TODO: 实现配置历史记录功能
|
||||
# 当前 SystemConfig 表没有 history 字段
|
||||
return []
|
||||
@@ -0,0 +1,125 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台仪表盘服务
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import and_, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.agent import Agent
|
||||
from app.models.conversation import Conversation
|
||||
from app.models.message import Message
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminDashboardService:
|
||||
"""管理后台仪表盘服务。
|
||||
|
||||
提供系统概览数据统计功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_dashboard_overview(
|
||||
self,
|
||||
) -> dict:
|
||||
"""获取仪表盘概览数据。
|
||||
|
||||
Returns:
|
||||
dict: 包含各项统计指标的字典
|
||||
"""
|
||||
now = datetime.now(timezone.utc)
|
||||
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
week_start = today_start - timedelta(days=7)
|
||||
month_start = today_start.replace(day=1)
|
||||
|
||||
# 1. 今日新建会话数
|
||||
today_new_stmt = select(func.count(Conversation.id)).where(
|
||||
Conversation.created_at >= today_start
|
||||
)
|
||||
today_new_result = await self.db.execute(today_new_stmt)
|
||||
today_new_count = today_new_result.scalar() or 0
|
||||
|
||||
# 2. 今日结单数
|
||||
today_resolved_stmt = select(func.count(Conversation.id)).where(
|
||||
and_(
|
||||
Conversation.status == "resolved",
|
||||
Conversation.updated_at >= today_start,
|
||||
)
|
||||
)
|
||||
today_resolved_result = await self.db.execute(today_resolved_stmt)
|
||||
today_resolved_count = today_resolved_result.scalar() or 0
|
||||
|
||||
# 3. 今日平均响应时间(秒)- 粗略估算
|
||||
# 假设响应时间 = 第一个消息到首次接单的时间
|
||||
# 实际应通过会话时间戳计算
|
||||
|
||||
# 4. 当前排队数
|
||||
queued_stmt = select(func.count(Conversation.id)).where(
|
||||
Conversation.status == "queued"
|
||||
)
|
||||
queued_result = await self.db.execute(queued_stmt)
|
||||
queued_count = queued_result.scalar() or 0
|
||||
|
||||
# 5. 当前服务中数
|
||||
serving_stmt = select(func.count(Conversation.id)).where(
|
||||
Conversation.status == "serving"
|
||||
)
|
||||
serving_result = await self.db.execute(serving_stmt)
|
||||
serving_count = serving_result.scalar() or 0
|
||||
|
||||
# 6. 当前在线坐席数
|
||||
# 实际应通过 Redis 或 Agent 表的 online_status 字段获取
|
||||
online_agents_stmt = select(func.count(Agent.id)).where(
|
||||
Agent.is_active == True # noqa: E712
|
||||
)
|
||||
online_agents_result = await self.db.execute(online_agents_stmt)
|
||||
online_agents_count = online_agents_result.scalar() or 0
|
||||
|
||||
# 7. 本周会话趋势(按天统计)
|
||||
weekly_stmt = select(
|
||||
func.date(Conversation.created_at).label("date"),
|
||||
func.count(Conversation.id).label("count"),
|
||||
).where(
|
||||
Conversation.created_at >= week_start
|
||||
).group_by(
|
||||
func.date(Conversation.created_at)
|
||||
).order_by(
|
||||
func.date(Conversation.created_at)
|
||||
)
|
||||
weekly_result = await self.db.execute(weekly_stmt)
|
||||
weekly_trend = [
|
||||
{"date": str(row.date), "count": row.count}
|
||||
for row in weekly_result.fetchall()
|
||||
]
|
||||
|
||||
# 8. 各状态会话数
|
||||
status_counts_stmt = select(
|
||||
Conversation.status,
|
||||
func.count(Conversation.id).label("count"),
|
||||
).group_by(Conversation.status)
|
||||
status_result = await self.db.execute(status_counts_stmt)
|
||||
status_counts = {
|
||||
row.status: row.count for row in status_result.fetchall()
|
||||
}
|
||||
|
||||
# 9. 平均满意度评分(如果有)
|
||||
# 暂时返回 0,后续接入满意度功能后补充
|
||||
|
||||
logger.debug("获取仪表盘概览数据完成")
|
||||
return {
|
||||
"today_new": today_new_count,
|
||||
"today_resolved": today_resolved_count,
|
||||
"avg_response_time": 0, # TODO: 计算平均响应时间
|
||||
"queued": queued_count,
|
||||
"serving": serving_count,
|
||||
"online_agents": online_agents_count,
|
||||
"weekly_trend": weekly_trend,
|
||||
"status_counts": status_counts,
|
||||
"satisfaction_score": 0, # TODO: 满意度评分
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台集成管理服务
|
||||
# =============================================================================
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.system_config import SystemConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 预定义的集成配置模板
|
||||
INTEGRATION_TEMPLATES = {
|
||||
"dify": {
|
||||
"name": "Dify",
|
||||
"description": "AI 对话服务",
|
||||
"type": "url_key",
|
||||
"fields": ["base_url", "api_key"],
|
||||
},
|
||||
"ragflow": {
|
||||
"name": "RAGFlow",
|
||||
"description": "知识库检索",
|
||||
"type": "url_key",
|
||||
"fields": ["base_url", "api_key"],
|
||||
},
|
||||
"huorong": {
|
||||
"name": "火绒安全",
|
||||
"description": "终端安全",
|
||||
"type": "access_key",
|
||||
"fields": ["access_key_id", "access_key_secret"],
|
||||
},
|
||||
"lianruan": {
|
||||
"name": "联软",
|
||||
"description": "终端管理",
|
||||
"type": "account_password",
|
||||
"fields": ["base_url", "username", "password"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AdminIntegrationService:
|
||||
"""管理后台集成管理服务。
|
||||
|
||||
提供外部系统集成配置功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_integrations(self) -> List[Dict[str, Any]]:
|
||||
"""获取集成列表。
|
||||
|
||||
Returns:
|
||||
List[Dict]: 集成列表
|
||||
"""
|
||||
# 查询所有集成配置
|
||||
stmt = select(SystemConfig).where(
|
||||
SystemConfig.config_key.like("integration_%")
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
configs = result.scalars().all()
|
||||
|
||||
# 整理为集成列表
|
||||
integrations = []
|
||||
for template_id, template in INTEGRATION_TEMPLATES.items():
|
||||
config_key = f"integration_{template_id}_config"
|
||||
enabled_key = f"integration_{template_id}_enabled"
|
||||
|
||||
# 查找配置
|
||||
config_value = None
|
||||
enabled = False
|
||||
for c in configs:
|
||||
if c.config_key == config_key:
|
||||
try:
|
||||
config_value = json.loads(c.config_value)
|
||||
except Exception:
|
||||
config_value = c.config_value
|
||||
elif c.config_key == enabled_key:
|
||||
enabled = c.config_value == "true"
|
||||
|
||||
integrations.append({
|
||||
"id": template_id,
|
||||
"name": template["name"],
|
||||
"description": template["description"],
|
||||
"type": template["type"],
|
||||
"enabled": enabled,
|
||||
"config": config_value,
|
||||
})
|
||||
|
||||
logger.debug(f"获取集成列表: {len(integrations)} 个")
|
||||
return integrations
|
||||
|
||||
async def update_integration(
|
||||
self,
|
||||
integration_id: str,
|
||||
enabled: bool,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""更新集成配置。
|
||||
|
||||
Args:
|
||||
integration_id: 集成ID
|
||||
enabled: 是否启用
|
||||
config: 配置(可选)
|
||||
|
||||
Returns:
|
||||
Dict: 更新后的集成信息
|
||||
"""
|
||||
if integration_id not in INTEGRATION_TEMPLATES:
|
||||
raise ValueError(f"不支持的集成: {integration_id}")
|
||||
|
||||
template = INTEGRATION_TEMPLATES[integration_id]
|
||||
|
||||
# 更新启用状态
|
||||
enabled_key = f"integration_{integration_id}_enabled"
|
||||
await self._upsert_system_config(enabled_key, "true" if enabled else "false")
|
||||
|
||||
# 更新配置
|
||||
if config:
|
||||
config_key = f"integration_{integration_id}_config"
|
||||
config_value = json.dumps(config, ensure_ascii=False)
|
||||
await self._upsert_system_config(config_key, config_value)
|
||||
|
||||
logger.info(f"更新集成: {integration_id}, enabled={enabled}")
|
||||
return {
|
||||
"id": integration_id,
|
||||
"name": template["name"],
|
||||
"enabled": enabled,
|
||||
"config": config,
|
||||
}
|
||||
|
||||
async def _upsert_system_config(self, key: str, value: str) -> None:
|
||||
"""插入或更新系统配置。"""
|
||||
stmt = select(SystemConfig).where(SystemConfig.config_key == key)
|
||||
result = await self.db.execute(stmt)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
if config:
|
||||
config.config_value = value
|
||||
else:
|
||||
config = SystemConfig(config_key=key, config_value=value)
|
||||
self.db.add(config)
|
||||
|
||||
await self.db.flush()
|
||||
@@ -0,0 +1,83 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台审核服务
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.quick_reply_template import QuickReplyTemplate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminModerationService:
|
||||
"""管理后台审核服务。
|
||||
|
||||
提供快速回复审核功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def list_pending_quick_replies(
|
||||
self,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取待审核的快速回复列表。
|
||||
|
||||
Returns:
|
||||
List[Dict]: 待审核列表
|
||||
"""
|
||||
stmt = select(QuickReplyTemplate).where(
|
||||
QuickReplyTemplate.status == "pending"
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
templates = result.scalars().all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": t.id,
|
||||
"category": t.category,
|
||||
"title": t.title,
|
||||
"content": t.content,
|
||||
"variables": t.variables,
|
||||
"sort_order": t.sort_order,
|
||||
"created_at": t.created_at.isoformat() if t.created_at else None,
|
||||
}
|
||||
for t in templates
|
||||
]
|
||||
|
||||
async def review_quick_reply(
|
||||
self,
|
||||
template_id: int,
|
||||
approved: bool,
|
||||
) -> Dict[str, Any]:
|
||||
"""审核快速回复。
|
||||
|
||||
Args:
|
||||
template_id: 模板ID
|
||||
approved: 是否通过
|
||||
|
||||
Returns:
|
||||
Dict: 审核结果
|
||||
"""
|
||||
stmt = select(QuickReplyTemplate).where(
|
||||
QuickReplyTemplate.id == template_id
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
template = result.scalar_one_or_none()
|
||||
|
||||
if not template:
|
||||
raise ValueError(f"快速回复模板不存在: {template_id}")
|
||||
|
||||
template.status = "approved" if approved else "rejected"
|
||||
self.db.add(template)
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"审核快速回复: {template_id}, approved={approved}")
|
||||
return {
|
||||
"id": template.id,
|
||||
"status": template.status,
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 管理后台监控服务
|
||||
# =============================================================================
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.agent import Agent
|
||||
from app.models.conversation import Conversation
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminMonitoringService:
|
||||
"""管理后台监控服务。
|
||||
|
||||
提供会话监控和分配模式管理功能。
|
||||
"""
|
||||
|
||||
def __init__(self, db: AsyncSession):
|
||||
self.db = db
|
||||
|
||||
async def get_monitor_sessions(
|
||||
self,
|
||||
status: Optional[str] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> Dict[str, Any]:
|
||||
"""获取监控会话列表。
|
||||
|
||||
Args:
|
||||
status: 状态过滤(可选)
|
||||
page: 页码
|
||||
page_size: 每页数量
|
||||
|
||||
Returns:
|
||||
Dict: 监控数据
|
||||
"""
|
||||
conditions = []
|
||||
if status:
|
||||
conditions.append(Conversation.status == status)
|
||||
|
||||
# 查询会话
|
||||
stmt = select(Conversation)
|
||||
if conditions:
|
||||
stmt = stmt.where(and_(*conditions))
|
||||
stmt = stmt.order_by(desc(Conversation.updated_at))
|
||||
stmt = stmt.offset((page - 1) * page_size).limit(page_size)
|
||||
|
||||
result = await self.db.execute(stmt)
|
||||
sessions = result.scalars().all()
|
||||
|
||||
# 查询总数
|
||||
from sqlalchemy import func
|
||||
count_stmt = select(func.count(Conversation.id))
|
||||
if conditions:
|
||||
count_stmt = count_stmt.where(and_(*conditions))
|
||||
count_result = await self.db.execute(count_stmt)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
return {
|
||||
"sessions": [
|
||||
{
|
||||
"id": str(s.id),
|
||||
"employee_name": s.employee_name,
|
||||
"status": s.status,
|
||||
"assigned_agent_id": s.assigned_agent_id,
|
||||
"urgency_score": s.urgency_score,
|
||||
"last_message_at": s.last_message_at.isoformat() if s.last_message_at else None,
|
||||
"last_message_summary": s.last_message_summary,
|
||||
}
|
||||
for s in sessions
|
||||
],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
async def get_assignment_mode(self) -> Dict[str, str]:
|
||||
"""获取当前分配模式。
|
||||
|
||||
Returns:
|
||||
Dict: 分配模式
|
||||
"""
|
||||
from app.models.system_config import SystemConfig
|
||||
|
||||
stmt = select(SystemConfig).where(
|
||||
SystemConfig.config_key == "assignment_mode"
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
return {
|
||||
"mode": config.config_value if config else "auto",
|
||||
}
|
||||
|
||||
async def update_assignment_mode(
|
||||
self,
|
||||
mode: str,
|
||||
) -> Dict[str, str]:
|
||||
"""更新分配模式。
|
||||
|
||||
Args:
|
||||
mode: 分配模式 (auto/manual)
|
||||
|
||||
Returns:
|
||||
Dict: 更新后的分配模式
|
||||
"""
|
||||
from app.models.system_config import SystemConfig
|
||||
|
||||
stmt = select(SystemConfig).where(
|
||||
SystemConfig.config_key == "assignment_mode"
|
||||
)
|
||||
result = await self.db.execute(stmt)
|
||||
config = result.scalar_one_or_none()
|
||||
|
||||
if config:
|
||||
config.config_value = mode
|
||||
else:
|
||||
config = SystemConfig(config_key="assignment_mode", config_value=mode)
|
||||
self.db.add(config)
|
||||
|
||||
await self.db.flush()
|
||||
|
||||
logger.info(f"更新分配模式: {mode}")
|
||||
return {"mode": mode}
|
||||
@@ -0,0 +1,25 @@
|
||||
# =============================================================================
|
||||
# 坐席服务模块 (agent)
|
||||
# =============================================================================
|
||||
# 说明:坐席相关服务,包括评分、二维码等
|
||||
#
|
||||
# 本模块包含:
|
||||
# - ScoringService: 坐席评分服务
|
||||
# - 二维码生成服务
|
||||
#
|
||||
# 迁移说明:
|
||||
# 旧导入路径:from app.services import ScoringService
|
||||
# 新导入路径:from app.services.agent import ScoringService
|
||||
# 两者均支持(向后兼容)
|
||||
# =============================================================================
|
||||
|
||||
# 重新导出坐席服务类
|
||||
from app.services.scoring_service import ScoringService
|
||||
|
||||
# 二维码服务(模块级函数)
|
||||
import app.services.qrcode_service as qrcode_service
|
||||
|
||||
__all__ = [
|
||||
"ScoringService",
|
||||
"qrcode_service",
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# =============================================================================
|
||||
# AI 服务模块 (ai)
|
||||
# =============================================================================
|
||||
# 说明:AI 相关服务,包括 AI 处理、Wingman 助手、话术推荐等
|
||||
#
|
||||
# 本模块包含:
|
||||
# - AIHandler: AI 消息处理服务
|
||||
# - AIService: AI 通用服务
|
||||
# - WingmanService: Wingman 坐席助手服务
|
||||
# - FunnyPhraseService: 趣味话术服务
|
||||
#
|
||||
# 迁移说明:
|
||||
# 旧导入路径:from app.services import AIHandler, WingmanService
|
||||
# 新导入路径:from app.services.ai import AIHandler, WingmanService
|
||||
# 两者均支持(向后兼容)
|
||||
# =============================================================================
|
||||
|
||||
# 重新导出 AI 服务类
|
||||
from app.services.ai_handler import AIHandler
|
||||
from app.services.ai_service import AIService
|
||||
from app.services.wingman_service import WingmanService
|
||||
from app.services.funny_phrase_service import FunnyPhraseService
|
||||
|
||||
# 内容审核服务(可选加载)
|
||||
# from app.services.content_moderation_service import ContentModerationService
|
||||
|
||||
__all__ = [
|
||||
"AIHandler",
|
||||
"AIService",
|
||||
"WingmanService",
|
||||
"FunnyPhraseService",
|
||||
# "ContentModerationService",
|
||||
]
|
||||
@@ -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
|
||||
@@ -0,0 +1,34 @@
|
||||
# =============================================================================
|
||||
# 核心服务模块 (core)
|
||||
# =============================================================================
|
||||
# 说明:核心公共服务,包括身份认证、授权、缓存等
|
||||
#
|
||||
# 本模块包含:
|
||||
# - TokenService: Token 管理服务
|
||||
# - RoleMappingService: 角色映射服务
|
||||
# - MFAService: 多因素认证服务
|
||||
# - CacheService: 缓存服务
|
||||
# - rbac_service: RBAC 权限服务
|
||||
#
|
||||
# 迁移说明:
|
||||
# 旧导入路径:from app.services import TokenService
|
||||
# 新导入路径:from app.services.core import TokenService
|
||||
# 两者均支持(向后兼容)
|
||||
# =============================================================================
|
||||
|
||||
# 重新导出核心服务类(保持向后兼容)
|
||||
from app.services.token_service import TokenService
|
||||
from app.services.role_mapping_service import RoleMappingService
|
||||
from app.services.mfa_service import MFAService
|
||||
from app.services.cache_service import CacheService
|
||||
|
||||
# 导入 RBAC 服务模块
|
||||
import app.services.rbac_service as rbac_service
|
||||
|
||||
__all__ = [
|
||||
"TokenService",
|
||||
"RoleMappingService",
|
||||
"MFAService",
|
||||
"CacheService",
|
||||
"rbac_service",
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# =============================================================================
|
||||
# 集成服务模块 (integration)
|
||||
# =============================================================================
|
||||
# 说明:外部系统集成服务,包括企微 API、安全系统对接等
|
||||
#
|
||||
# 本模块包含:
|
||||
# - WecomService: 企业微信 API 服务
|
||||
# - HighRiskGuard: 高风险操作守卫
|
||||
# - security_comparison: 安全对比服务
|
||||
#
|
||||
# 迁移说明:
|
||||
# 旧导入路径:from app.services import WecomService
|
||||
# 新导入路径:from app.services.integration import WecomService
|
||||
# 两者均支持(向后兼容)
|
||||
# =============================================================================
|
||||
|
||||
# 重新导出集成服务类
|
||||
from app.services.wecom_service import WecomService
|
||||
from app.services.high_risk_guard import HighRiskGuard
|
||||
|
||||
# 安全对比服务(模块级函数)
|
||||
import app.services.security_comparison as security_comparison
|
||||
|
||||
__all__ = [
|
||||
"WecomService",
|
||||
"HighRiskGuard",
|
||||
"security_comparison",
|
||||
]
|
||||
@@ -199,11 +199,20 @@ class QrcodeService:
|
||||
def _get_scan_callback_url(self) -> str:
|
||||
"""获取 OAuth 回调地址。
|
||||
|
||||
优先使用 settings 里的配置;没有则用默认值 /api/auth_qrcode/scan。
|
||||
当前没有这个配置,先用兜底;后续可在 Settings 加 qrcode_oauth_callback。
|
||||
优先使用 settings.wecom_sso_callback_base + /api/auth_qrcode/scan 拼接完整 URL。
|
||||
企微要求 redirect_uri 必须是完整的可信域名 URL,不能用相对路径。
|
||||
如果未配置 wecom_sso_callback_base,则抛出异常提醒配置。
|
||||
"""
|
||||
# 兜底:相对路径,企微会带 Host 处理
|
||||
return getattr(settings, "qrcode_oauth_callback", "/api/auth_qrcode/scan")
|
||||
# 优先使用 wecom_sso_callback_base 构建完整 URL
|
||||
callback_base = getattr(settings, "wecom_sso_callback_base", "")
|
||||
if callback_base:
|
||||
# 去除末尾斜杠,确保路径正确拼接
|
||||
base = callback_base.rstrip("/")
|
||||
return f"{base}/api/auth_qrcode/scan"
|
||||
# 没有配置时给出明确提示
|
||||
raise ValueError(
|
||||
"请在环境变量中配置 WECOM_SSO_CALLBACK_BASE (如: https://itsupport.servyou.com.cn)"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# scan: 处理企微 OAuth code 回调
|
||||
|
||||
Reference in New Issue
Block a user