- 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>
101 lines
3.9 KiB
Python
101 lines
3.9 KiB
Python
"""定时任务的统一默认值与日志(`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))
|