2026-07-05 17:03:36 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 企微IT智能服务台 — 超时提醒定时任务
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 说明:定时检查超时未回复的会话,发送企微提醒消息
|
|
|
|
|
|
# 运行频率:每 30 秒执行一次
|
2026-07-13 02:17:03 +08:00
|
|
|
|
# 超时逻辑(改造后,决策 G4):
|
2026-07-05 17:03:36 +08:00
|
|
|
|
# 1. 坐席回复后 3 分钟员工未回复 -> 发送企微提醒(只发 1 次)
|
|
|
|
|
|
# 2. 坐席回复后 10 分钟员工未回复 -> 标记会话为 pending_close
|
2026-07-13 02:17:03 +08:00
|
|
|
|
# 3. pending_close 后 5 分钟员工未响应 -> 自动 resolved(新增)
|
|
|
|
|
|
# 4. ai_handling 30 分钟无互动 -> 发送提醒 -> 再 10 分钟 -> 自动 resolved(新增)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
|
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
|
|
|
|
|
|
|
from app.models.conversation import Conversation
|
|
|
|
|
|
from app.services.reminder_service import send_reminder_message
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
# 超时配置(分钟)
|
|
|
|
|
|
REMINDER_TIMEOUT_MINUTES = 3 # 未回复超时时间
|
|
|
|
|
|
CLOSE_TIMEOUT_MINUTES = 10 # 自动待关闭时间
|
2026-07-13 02:17:03 +08:00
|
|
|
|
PENDING_CLOSE_RESOLVE_MINUTES = 5 # pending_close → resolved 超时
|
|
|
|
|
|
|
|
|
|
|
|
# AI处理阶段超时配置
|
|
|
|
|
|
AI_HANDLING_TIMEOUT_MINUTES = 30 # AI处理超时(无互动)
|
|
|
|
|
|
AI_HANDLING_REMINDER_TO_CLOSE_MINUTES = 10 # 提醒后仍未响应的关闭时间
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def check_unreplied_sessions():
|
2026-07-13 02:17:03 +08:00
|
|
|
|
"""检查超时未回复的会话,发送提醒并标记待关闭/自动关闭。
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
此函数由 APScheduler 定时调用(每 30 秒)。
|
|
|
|
|
|
执行流程:
|
2026-07-13 02:17:03 +08:00
|
|
|
|
1. serving 状态:发送提醒 + 标记 pending_close(原有逻辑)
|
|
|
|
|
|
2. pending_close 状态:超时自动 resolved(新增)
|
|
|
|
|
|
3. ai_handling 状态:超时提醒 + 自动 resolved(新增)
|
2026-07-05 17:03:36 +08:00
|
|
|
|
"""
|
|
|
|
|
|
from app.database import _get_session_factory
|
2026-07-13 02:17:03 +08:00
|
|
|
|
from app.services.closing_service import ClosingService
|
2026-07-05 17:03:36 +08:00
|
|
|
|
|
|
|
|
|
|
async_session_factory = _get_session_factory()
|
|
|
|
|
|
|
|
|
|
|
|
async with async_session_factory() as db:
|
|
|
|
|
|
try:
|
2026-07-13 02:17:03 +08:00
|
|
|
|
closing_service = ClosingService(db)
|
|
|
|
|
|
|
|
|
|
|
|
# ==================================================================
|
|
|
|
|
|
# 1. serving 状态:发送提醒 + 标记 pending_close(原有逻辑)
|
|
|
|
|
|
# ==================================================================
|
|
|
|
|
|
|
|
|
|
|
|
# 1a. 查找需要发送提醒的会话
|
|
|
|
|
|
# 条件:serving + 有坐席回复 + 超过3分钟未回复 + 未发送过提醒
|
2026-07-05 17:03:36 +08:00
|
|
|
|
reminder_threshold = datetime.now() - timedelta(minutes=REMINDER_TIMEOUT_MINUTES)
|
|
|
|
|
|
|
|
|
|
|
|
reminder_stmt = select(Conversation).where(
|
|
|
|
|
|
Conversation.status == "serving",
|
|
|
|
|
|
Conversation.last_agent_reply_at.isnot(None),
|
|
|
|
|
|
Conversation.last_agent_reply_at < reminder_threshold,
|
|
|
|
|
|
Conversation.reminder_sent == False,
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await db.execute(reminder_stmt)
|
|
|
|
|
|
sessions_to_remind = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"发现 {len(sessions_to_remind)} 个需要发送提醒的会话")
|
|
|
|
|
|
|
2026-07-13 02:17:03 +08:00
|
|
|
|
# 1b. 发送企微提醒消息
|
2026-07-05 17:03:36 +08:00
|
|
|
|
for session in sessions_to_remind:
|
|
|
|
|
|
try:
|
|
|
|
|
|
success = await send_reminder_message(session.employee_id)
|
|
|
|
|
|
if success:
|
|
|
|
|
|
session.reminder_sent = True
|
|
|
|
|
|
session.reminder_sent_at = datetime.now()
|
|
|
|
|
|
logger.info(f"会话 {session.id} 已发送提醒: employee_id={session.employee_id}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"会话 {session.id} 提醒发送失败,跳过")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"会话 {session.id} 发送提醒异常: {e}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-07-13 02:17:03 +08:00
|
|
|
|
# 1c. 查找需要标记待关闭的会话
|
|
|
|
|
|
# 条件:serving + 有坐席回复 + 超过10分钟未回复
|
2026-07-05 17:03:36 +08:00
|
|
|
|
close_threshold = datetime.now() - timedelta(minutes=CLOSE_TIMEOUT_MINUTES)
|
|
|
|
|
|
|
|
|
|
|
|
close_stmt = select(Conversation).where(
|
|
|
|
|
|
Conversation.status == "serving",
|
|
|
|
|
|
Conversation.last_agent_reply_at.isnot(None),
|
|
|
|
|
|
Conversation.last_agent_reply_at < close_threshold,
|
|
|
|
|
|
)
|
|
|
|
|
|
result = await db.execute(close_stmt)
|
|
|
|
|
|
sessions_to_close = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"发现 {len(sessions_to_close)} 个需要标记待关闭的会话")
|
|
|
|
|
|
|
2026-07-13 02:17:03 +08:00
|
|
|
|
# 1d. 更新会话状态为 pending_close
|
2026-07-05 17:03:36 +08:00
|
|
|
|
for session in sessions_to_close:
|
|
|
|
|
|
session.status = "pending_close"
|
2026-07-13 02:17:03 +08:00
|
|
|
|
session.pending_close_at = datetime.now()
|
2026-07-05 17:03:36 +08:00
|
|
|
|
logger.info(f"会话 {session.id} 已标记为待关闭: employee_id={session.employee_id}")
|
|
|
|
|
|
|
2026-07-13 02:17:03 +08:00
|
|
|
|
# ==================================================================
|
|
|
|
|
|
# 2. pending_close 状态:超时自动 resolved(新增,决策 G4)
|
|
|
|
|
|
# ==================================================================
|
|
|
|
|
|
# 坐席发起结单后,员工5分钟内未确认 → 系统自动关闭
|
|
|
|
|
|
pending_close_sessions = await closing_service.get_pending_close_timeout_sessions()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"发现 {len(pending_close_sessions)} 个 pending_close 超时会话")
|
|
|
|
|
|
|
|
|
|
|
|
for session in pending_close_sessions:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await closing_service.auto_timeout_close(
|
|
|
|
|
|
str(session.id), timeout_type="pending_close"
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"会话 {session.id} pending_close 超时已自动关闭")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"会话 {session.id} pending_close 自动关闭失败: {e}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# ==================================================================
|
|
|
|
|
|
# 3. ai_handling 状态:超时提醒 + 自动关闭(新增,决策 G4)
|
|
|
|
|
|
# ==================================================================
|
|
|
|
|
|
# AI处理阶段30分钟无互动 → 发送提醒
|
|
|
|
|
|
# 提醒后10分钟仍无响应 → 自动关闭
|
|
|
|
|
|
|
|
|
|
|
|
# 3a. 发送 ai_handling 超时提醒
|
|
|
|
|
|
ai_reminder_sessions = await closing_service.get_ai_handling_reminder_sessions()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"发现 {len(ai_reminder_sessions)} 个 ai_handling 需要提醒的会话")
|
|
|
|
|
|
|
|
|
|
|
|
for session in ai_reminder_sessions:
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 推送 WS 超时警告
|
|
|
|
|
|
from app.services.ws_manager import manager as ws_manager
|
|
|
|
|
|
await ws_manager.send_to_employee(
|
|
|
|
|
|
session.employee_id,
|
|
|
|
|
|
{
|
|
|
|
|
|
"type": "auto_close_warning",
|
|
|
|
|
|
"data": {
|
|
|
|
|
|
"conversation_id": str(session.id),
|
|
|
|
|
|
"minutes_remaining": AI_HANDLING_REMINDER_TO_CLOSE_MINUTES,
|
|
|
|
|
|
"message": f"您的会话已空闲超过{AI_HANDLING_TIMEOUT_MINUTES}分钟,"
|
|
|
|
|
|
f"将在{AI_HANDLING_REMINDER_TO_CLOSE_MINUTES}分钟后自动关闭。"
|
|
|
|
|
|
f"如需继续请发送消息。",
|
|
|
|
|
|
"timestamp": datetime.now().isoformat(),
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
session.reminder_sent = True
|
|
|
|
|
|
session.reminder_sent_at = datetime.now()
|
|
|
|
|
|
logger.info(f"会话 {session.id} ai_handling 超时提醒已发送")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"会话 {session.id} ai_handling 提醒发送失败: {e}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
|
|
# 3b. ai_handling 超时自动关闭
|
|
|
|
|
|
ai_timeout_sessions = await closing_service.get_ai_handling_timeout_sessions()
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(f"发现 {len(ai_timeout_sessions)} 个 ai_handling 超时关闭会话")
|
|
|
|
|
|
|
|
|
|
|
|
for session in ai_timeout_sessions:
|
|
|
|
|
|
try:
|
|
|
|
|
|
await closing_service.auto_timeout_close(
|
|
|
|
|
|
str(session.id), timeout_type="ai_handling"
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.info(f"会话 {session.id} ai_handling 超时已自动关闭")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"会话 {session.id} ai_handling 自动关闭失败: {e}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
2026-07-05 17:03:36 +08:00
|
|
|
|
# 提交数据库变更
|
|
|
|
|
|
await db.commit()
|
|
|
|
|
|
logger.info("超时检查任务执行完成")
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
await db.rollback()
|
|
|
|
|
|
logger.error(f"超时检查任务执行异常: {e}")
|
|
|
|
|
|
raise
|