ForcePilot/backend/package/yuxi/channels/services/maintenance.py
Kris 29dac24600 feat(channels): 完成渠道模块多维度功能迭代与优化
本次提交对渠道模块进行了全面升级,包含以下核心改进:
1. 新增二维码登录相关协议方法,完善登录流程
2. 优化配置监听逻辑,增加渠道运行状态前置校验
3. 重构动作注册机制,支持动态注册渠道动作并新增批量操作能力
4. 扩展渠道能力模型,新增广播、文件传输等支持
5. 优化适配器加载路径,新增元宝适配器支持
6. 新增凭证过期检查与告警能力,完善运维监控
7. 重构统计收集器,支持多维度渠道统计数据
8. 优化消息路由策略,新增策略缓存与安全处理逻辑
9. 重构基础适配器,新增凭证管理工具方法
10. 完善状态存储功能,支持凭证数据管理与批量清理
11. 重构渠道管理器,新增配置校验、动态渠道管理、限流能力
12. 优化健康检查与状态上报逻辑,完善审计日志与异常处理
2026-05-14 02:08:36 +08:00

100 lines
3.4 KiB
Python

from __future__ import annotations
import asyncio
from datetime import UTC, datetime, timedelta
from typing import TYPE_CHECKING
from yuxi.utils.logging_config import logger
if TYPE_CHECKING:
from yuxi.channels.manager import ChannelManager
from yuxi.channels.services.runtime_state import RuntimeState
def _utc_now() -> datetime:
import time
return datetime.fromtimestamp(time.time(), tz=UTC)
class MaintenanceRunner:
def __init__(self, channel_manager: ChannelManager, state: RuntimeState):
self._manager = channel_manager
self._state = state
self._credential_expiry_alerts: int = 0
@property
def credential_expiry_alerts(self) -> int:
return self._credential_expiry_alerts
async def run(self, interval: float = 600) -> None:
logger.info("MaintenanceRunner started")
while True:
await asyncio.sleep(interval)
try:
await self._run_maintenance()
except asyncio.CancelledError:
break
except Exception:
logger.exception("MaintenanceRunner error")
async def _run_maintenance(self) -> None:
for channel_id, adapter in self._manager._adapters.items():
try:
await adapter._refresh_token_if_needed()
except Exception:
logger.debug(f"Token refresh skip for {channel_id}")
if self._manager._state_store is not None:
try:
await self._manager._state_store.cleanup_expired()
except Exception:
logger.debug("Plugin state cleanup failed")
try:
await self._check_credential_expiry()
except Exception:
logger.exception("Credential expiry check failed")
logger.debug("Maintenance cycle complete")
async def _check_credential_expiry(self) -> None:
if self._manager._state_store is None:
return
entries = await self._manager._state_store.scan_credential_entries()
now = _utc_now()
alert_count = 0
for entry in entries:
channel_id = entry["channel_id"]
expires_at_str = entry.get("expires_at")
if expires_at_str is None:
continue
expires_at = datetime.fromisoformat(expires_at_str)
remaining = expires_at - now
if remaining <= timedelta(hours=1):
if remaining <= timedelta(0):
logger.error(
f"[CREDENTIAL-ALERT] Channel '{channel_id}' credential "
f"'{entry['entry_key']}' has EXPIRED at {expires_at_str}"
)
else:
logger.warning(
f"[CREDENTIAL-ALERT] Channel '{channel_id}' credential "
f"'{entry['entry_key']}' expires in {remaining.total_seconds() / 60:.0f}min "
f"(at {expires_at_str})"
)
alert_count += 1
elif remaining <= timedelta(hours=24):
logger.warning(
f"[CREDENTIAL-WARN] Channel '{channel_id}' credential "
f"'{entry['entry_key']}' expires in {remaining.total_seconds() / 3600:.1f}h "
f"(at {expires_at_str})"
)
if alert_count > 0:
self._credential_expiry_alerts += alert_count