Files
HeXi/hexi/core/scheduler.py
T
sansenhoshiandClaude Code 0ac1958932 feat(core): 定时任务薄封装 scheduler / 跨插件任务槽 task_slots / 出站媒体显式发送方式
- scheduler.py:apscheduler 薄封装(统一 Asia/Shanghai、misfire_grace_time、起止/异常日志)
- task_slots.py:长任务任务槽(进度上报 / 同槽去重 / SSE 推送 / 取消),
  video_analysis temp 清理等长任务接入
- outbound_media:新增 HEXI_MEDIA_MODE 显式发送方式(http/base64/file,/hub 可改),
  修复 alconna 对 base64 载荷补 https:// 前缀导致的 fetch failed(repair_mangled_url)
- learning_chat 定时任务迁入 hexi.core.scheduler
- 测试:新增 test_scheduler / test_task_slots,扩充 test_outbound_media

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-13 17:53:21 +08:00

101 lines
3.9 KiB
Python
Raw 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.
"""定时任务的统一默认值与日志(`nonebot_plugin_apscheduler` 的薄封装)。
直接用 `scheduler.scheduled_job` / `scheduler.add_job` 时,各插件参数各写各的,
而 APScheduler 的原生默认值有两个坑:
1. **不设时区** → 用服务器本地时区。换机器、进容器、系统时区不是
Asia/Shanghai 时,`cron(hour=4)` 这类任务会整体错位 —— 而且错得很安静:
任务照常在跑,只是时间不对(`interval` 类任务不受影响)。
2. **`misfire_grace_time` 默认只有 1 秒** → 事件循环被卡一下(同步 IO、GC、
笔记本休眠唤醒)任务就被判"错过"直接丢弃,一天少跑几次很难发现。
本模块只做两件事:**补默认值** + **包一层日志**(起止 DEBUG、异常 ERROR 后吞掉,
不让坏任务刷爆 apscheduler 自己的日志)。调用方显式给的参数一律优先,
触发条件一个字都不改。
用法(跟原来一模一样,只是换个导入):
from hexi.core.scheduler import add_job, scheduled_job
@scheduled_job("cron", hour=4, id="temp_cleanup")
async def cleanup(): ...
add_job(fn, "interval", minutes=10, id="xxx", replace_existing=True)
动态删任务仍用 `nonebot_plugin_apscheduler.scheduler.remove_job(...)`。
"""
from __future__ import annotations
import functools
from collections.abc import Callable
from typing import Any
from nonebot import require
from nonebot.log import logger
require("nonebot_plugin_apscheduler")
from nonebot_plugin_apscheduler import scheduler # noqa: E402
#: 默认时区:显式钉死,不跟随服务器本地时区(见模块文档第 1 条)
DEFAULT_TIMEZONE = "Asia/Shanghai"
#: 默认宽限:宁可晚做也别不做(见模块文档第 2 条)
DEFAULT_MISFIRE_GRACE_TIME = 60
#: 补进 kwargs 的默认值;`coalesce` 在 APScheduler 里本来就是 True,
#: 写出来是为了表明"积压的任务只补跑一次"是有意为之
_DEFAULTS: dict[str, Any] = {
"timezone": DEFAULT_TIMEZONE,
"misfire_grace_time": DEFAULT_MISFIRE_GRACE_TIME,
"coalesce": True,
}
def _with_defaults(kwargs: dict[str, Any]) -> dict[str, Any]:
for key, value in _DEFAULTS.items():
kwargs.setdefault(key, value)
return kwargs
async def _run_logged(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
"""跑一个任务:起止 DEBUG、异常 ERROR 后吞掉(任务失败不该外抛给调度器)。"""
logger.debug(f"定时任务开始: {func.__name__}")
try:
result = await func(*args, **kwargs)
except Exception:
logger.exception(f"定时任务异常: {func.__name__}")
return None
logger.debug(f"定时任务完成: {func.__name__}")
return result
def scheduled_job(*args: Any, **kwargs: Any) -> Callable[..., Any]:
"""`scheduler.scheduled_job` + 统一默认值与日志(装饰器,用法不变)。"""
_with_defaults(kwargs)
def deco(func: Callable[..., Any]) -> Callable[..., Any]:
# functools.wraps 保住 __name__:APScheduler 不指定 id 时用函数名做任务 id
@functools.wraps(func)
async def wrapper(*fargs: Any, **fkwargs: Any) -> Any:
return await _run_logged(func, *fargs, **fkwargs)
return scheduler.scheduled_job(*args, **kwargs)(wrapper)
return deco
def add_job(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
"""`scheduler.add_job` + 统一默认值(动态增删任务用)。
注意包了一层:注册的是包装函数,`func.__name__` 由 `functools.wraps` 保住,
所以不显式传 `id` 时的默认任务 id 仍是原函数名。删除任务请用原函数的
id(或显式传 `id`)配合 `scheduler.remove_job(...)`。
"""
@functools.wraps(func)
async def wrapper(*fargs: Any, **fkwargs: Any) -> Any:
return await _run_logged(func, *fargs, **fkwargs)
return scheduler.add_job(wrapper, *args, **_with_defaults(kwargs))