Files
Simon bea288e414 feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) ==
- 代办事项真实数据源集成 (企微审批API 8bug修复链)
- H5/坐席端 Logo样式统一+绿色背景
- 视频引导页修复 (localStorage key v2)
- 坐席端 v9 Vue版本修复 (ElMessage._context)
- 截图按钮 v10 修复 (getDisplayMedia user gesture)
- 扫码样式恢复+H5扫码登录跳转修复
- H5截图快捷键提示

== 代码完成待部署 (3项) ==
- 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查)
- 会议室预定-小鱼易联终端 (40文件, 40/40测试通过)
- IT资产升级审批推送 (asset_service.py)

== 需求文档 (2项) ==
- 坐席端AI辅助消息框-PRD (4项新功能确认)
- 坐席端布局优化建议 v2.0 (7天计划)

== 新增文档 ==
- 日报-2026-07-11.md
- 知识迭代Bug修复报告-20260711.md
- 会议室预定-部署指南.md
- CHANGELOG.md 更新

== 测试 ==
- test_todo_integration.py: 40/40
- test_meetingroom.py: 40/40
- test_bugfix_ki_suggestions.py: 21/21
2026-07-11 23:13:10 +08:00

130 lines
4.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# =============================================================================
# 企微IT智能服务台 — 复杂场景重构 暂停超时清理定时任务
# =============================================================================
# 说明:后台定时任务,扫描 paused 状态且超过 24 小时未恢复的会话,
# 自动标记为 closedclosed_by = "system(timeout)"),
# 并清理 Redis 恢复点与暂停会话集合,推送超时关闭 WS 事件。
#
# 调用方式:
# 1. FastAPI lifespan 中 asyncio.create_task(TimeoutCleaner(...).run_scheduled())
# 2. 或外部调度器定期调用 run_once()
# =============================================================================
from __future__ import annotations
import asyncio
import logging
from datetime import datetime, timedelta, timezone
from typing import Any
from sqlalchemy import select
from app.constants import (
PAUSE_TIMEOUT_HOURS,
REDIS_KEY_PAUSED_SESSIONS,
REDIS_KEY_RESUME_POINT,
)
from app.models.automation import AutoSession
from app.services.automation.progress_publisher import publish_timeout_closed
logger = logging.getLogger(__name__)
class TimeoutCleaner:
"""暂停超时清理器。
扫描 paused 状态的会话,超过 PAUSE_TIMEOUT_HOURS(默认 24 小时)
未恢复的会话自动关闭,并推送 WS 通知。
"""
def __init__(self, db_factory: Any, redis: Any = None):
"""初始化。
Args:
db_factory: 异步 DB 会话工厂(如 app.database._get_session_factory()
redis: Redis 客户端(可选,用于清理恢复点)
"""
self.db_factory = db_factory
self.redis = redis
async def run_once(self) -> int:
"""执行一次扫描,返回关闭的会话数。
Returns:
int: 本次扫描关闭的会话数量
"""
closed_count = 0
async with self.db_factory() as db:
# 查询超时的 paused 会话(paused_at < cutoff 隐含 NOT NULL
cutoff = datetime.now(timezone.utc) - timedelta(hours=PAUSE_TIMEOUT_HOURS)
stmt = select(AutoSession).where(
AutoSession.status == "paused",
AutoSession.paused_at < cutoff,
)
sessions = list((await db.execute(stmt)).scalars().all())
if not sessions:
return 0
for session in sessions:
try:
# 标记关闭
session.status = "closed"
session.closed_by = "system(timeout)"
closed_at = datetime.now(timezone.utc)
# 清理 Redis 恢复点
if self.redis:
await self._cleanup_redis(session.id, session.employee_id)
await db.flush()
closed_count += 1
# 推送 WS 超时关闭事件
await publish_timeout_closed(
session_id=session.id,
closed_at=closed_at.isoformat(),
reason=f"暂停超过 {PAUSE_TIMEOUT_HOURS} 小时未恢复,已自动关闭",
)
logger.info(
f"超时关闭会话: session={session.id} "
f"paused_at={session.paused_at} closed_at={closed_at}"
)
except Exception as e: # noqa: BLE001
logger.error(f"超时关闭会话失败 session={session.id}: {e}")
await db.commit()
if closed_count > 0:
logger.info(f"超时清理完成: 共关闭 {closed_count} 个暂停会话")
return closed_count
async def run_scheduled(self, interval: int = 3600) -> None:
"""定时扫描(每小时一次)。
Args:
interval: 扫描间隔(秒),默认 3600 = 1 小时
"""
logger.info(f"启动暂停超时清理定时任务,间隔 {interval}")
while True:
try:
await self.run_once()
except Exception as e: # noqa: BLE001
logger.error(f"超时清理定时任务异常: {e}")
await asyncio.sleep(interval)
async def _cleanup_redis(self, session_id: str, employee_id: str) -> None:
"""清理 Redis 中的恢复点和暂停会话集合。"""
if not self.redis:
return
try:
# 删除恢复点
resume_key = REDIS_KEY_RESUME_POINT.format(session_id=session_id)
await self.redis.delete(resume_key)
# 从暂停会话集合中移除
paused_key = REDIS_KEY_PAUSED_SESSIONS.format(employee_id=employee_id)
await self.redis.srem(paused_key, session_id)
except Exception as e: # noqa: BLE001
logger.warning(f"Redis 清理失败 session={session_id}: {e}")