mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-20 19:50:02 +08:00
♻️ refactor(core): 重构 AI 能力与定时任务调度系统 (#2148)
* ♻️ refactor(core): 重构 AI 能力与定时任务调度系统 - 【AI 能力与工具】重构 Capability 注册与管理机制,引入 CapabilityManager 统一管理 - 移除全局能力注册表,改用声明式装饰器 `@capability` 进行解耦注册 - 重构工具解析器链,使用统一的 BaseToolResolver 代替原有的多个特定解析器 - 增强工具查询过滤,支持通配符匹配、工具箱过滤和排除标签 - 【定时任务调度】重构定时任务管理器,引入 SchedulerRegistry 统一管理任务元数据 - 引入 JobConfig 聚合定时任务配置,支持用户维度的定时任务调度 - 重构执行分发器,支持并发限制、串行间隔和随机延迟打散 - 【运行上下文】引入 ScheduledDeps 以支持后台和定时任务环境下的依赖注入 - 优化 RunContext,支持从定时任务上下文快速构造,并提供 emit 辅助方法 - 【日志与监控】引入 AILoggerProxy,实现 AI 各模块的专属日志输出 - 将各模块的全局 logger 替换为对应的模块专属日志代理 - 【其他优化】修复 Pydantic V1 兼容层中 model_validator 的装饰器兼容性问题 - 在非交互式环境(如定时任务)中自动隐藏 HITL 交互工具以节省 Token * ♻️ refactor(core): 优化内部导入路径并提升 Pydantic 兼容性 - 【重构】将 `services/ai` 模块内的绝对导入重构为相对导入,优化包结构 - 【重构】移除不必要的 `if TYPE_CHECKING` 保护,通过 `from __future__ import annotations` 直接导入类型 - 【清理】清理 `core/messages/types.py` 中未使用的 `AssistantContentUnion` 等联合类型定义 - 【优化】在 `utils/pydantic_compat.py` 中新增 `model_rebuild` 兼容函数,统一 Pydantic V1/V2 的模型重建逻辑 - 【优化】将部分函数内部的延迟导入提升至模块顶部,规范代码结构 * ♻️ refactor(imports): 优化导入路径为相对导入并清理冗余导入 - 【重构】将 AI 服务相关模块中的绝对导入路径修改为相对导入,提升模块内聚性与可移植性 - 【清理】移除多处函数内部或类方法中未使用的冗余导入,避免循环引用和资源浪费 - 【格式化】微调部分工具装饰器和返回语句的格式与尾随逗号 * 🚨 auto fix by pre-commit hooks --------- Co-authored-by: webjoin111 <455457521@qq.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
webjoin111
pre-commit-ci[bot]
parent
0b32d69c9c
commit
922d092650
@@ -7,12 +7,12 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import random
|
||||
import time
|
||||
from typing import cast
|
||||
|
||||
import nonebot
|
||||
from apscheduler.jobstores.base import JobLookupError
|
||||
from nonebot.adapters import Bot
|
||||
from nonebot.dependencies import Dependent
|
||||
from nonebot.exception import FinishedException, PausedException, SkippedException
|
||||
@@ -30,45 +30,15 @@ from zhenxun.utils.decorator.retry import Retry
|
||||
from zhenxun.utils.platform import PlatformUtils
|
||||
from zhenxun.utils.pydantic_compat import parse_as
|
||||
|
||||
from .registry import scheduler_registry
|
||||
from .repository import ScheduleRepository
|
||||
from .types import ExecutionPolicy, ScheduleContext
|
||||
from .types import ExecutionPolicy, ScheduleContext, TargetType
|
||||
|
||||
JOB_PREFIX = "zhenxun_schedule_"
|
||||
SCHEDULE_CONCURRENCY_KEY = "all_groups_concurrency_limit"
|
||||
_LAST_PRESSURE_SKIP = 0.0
|
||||
|
||||
|
||||
def _resolve_scheduler_bot(bot_id: str | None, log_target: str) -> Bot | None:
|
||||
if bot_id:
|
||||
try:
|
||||
return nonebot.get_bot(bot_id)
|
||||
except KeyError:
|
||||
logger.warning(f"{log_target} 需要的 Bot {bot_id} 不在线,本次执行跳过。")
|
||||
return None
|
||||
|
||||
bots = list(nonebot.get_bots().values())
|
||||
if not bots:
|
||||
logger.warning(f"{log_target} 当前没有可用 Bot,本次执行跳过。")
|
||||
return None
|
||||
if len(bots) == 1:
|
||||
return bots[0]
|
||||
|
||||
qq_client_bots = [
|
||||
bot for bot in bots if PlatformUtils.get_platform_scope(bot) == "qq_client"
|
||||
]
|
||||
if len(qq_client_bots) == 1:
|
||||
bot = qq_client_bots[0]
|
||||
logger.warning(
|
||||
f"{log_target} 未指定 Bot,多 Bot 在线,自动选择 OneBot {bot.self_id}。"
|
||||
)
|
||||
return bot
|
||||
|
||||
logger.warning(
|
||||
f"{log_target} 未指定 Bot 且多 Bot 在线,无法安全选择," "本次执行跳过。"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class APSchedulerAdapter:
|
||||
"""封装对 APScheduler 的操作"""
|
||||
|
||||
@@ -97,8 +67,10 @@ class APSchedulerAdapter:
|
||||
|
||||
try:
|
||||
scheduler.remove_job(job_id)
|
||||
except Exception:
|
||||
except JobLookupError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.debug(f"尝试覆盖 APScheduler 任务 {job_id} 时发生异常: {e}")
|
||||
|
||||
if not isinstance(schedule.trigger_config, dict):
|
||||
logger.error(
|
||||
@@ -132,7 +104,7 @@ class APSchedulerAdapter:
|
||||
job_params["coalesce"] = False
|
||||
|
||||
scheduler.add_job(
|
||||
_execute_job,
|
||||
_execute_persistent_job,
|
||||
trigger=schedule.trigger_type,
|
||||
**job_params,
|
||||
**trigger_params,
|
||||
@@ -154,8 +126,10 @@ class APSchedulerAdapter:
|
||||
try:
|
||||
scheduler.remove_job(job_id)
|
||||
logger.debug(f"已从APScheduler中移除任务: {job_id}")
|
||||
except Exception:
|
||||
pass
|
||||
except JobLookupError:
|
||||
logger.debug(f"APScheduler 中未找到任务: {job_id},无需移除。")
|
||||
except Exception as e:
|
||||
logger.debug(f"从 APScheduler 移除任务 {job_id} 时发生异常: {e}")
|
||||
|
||||
@staticmethod
|
||||
def pause_job(schedule_id: int):
|
||||
@@ -168,8 +142,10 @@ class APSchedulerAdapter:
|
||||
job_id = APSchedulerAdapter._get_job_id(schedule_id)
|
||||
try:
|
||||
scheduler.pause_job(job_id)
|
||||
except Exception:
|
||||
pass
|
||||
except JobLookupError:
|
||||
logger.debug(f"APScheduler 中未找到任务: {job_id},无法暂停。")
|
||||
except Exception as e:
|
||||
logger.debug(f"暂停 APScheduler 任务 {job_id} 时发生异常: {e}")
|
||||
|
||||
@staticmethod
|
||||
def resume_job(schedule_id: int):
|
||||
@@ -182,8 +158,8 @@ class APSchedulerAdapter:
|
||||
job_id = APSchedulerAdapter._get_job_id(schedule_id)
|
||||
try:
|
||||
scheduler.resume_job(job_id)
|
||||
except Exception:
|
||||
import asyncio
|
||||
except JobLookupError:
|
||||
logger.debug(f"APScheduler 中未找到任务: {job_id},尝试重新注册...")
|
||||
|
||||
async def _re_add_job():
|
||||
schedule = await ScheduleRepository.get_by_id(schedule_id)
|
||||
@@ -191,6 +167,8 @@ class APSchedulerAdapter:
|
||||
APSchedulerAdapter.add_or_reschedule_job(schedule)
|
||||
|
||||
asyncio.create_task(_re_add_job()) # noqa: RUF006
|
||||
except Exception as e:
|
||||
logger.debug(f"恢复 APScheduler 任务 {job_id} 时发生异常: {e}")
|
||||
|
||||
@staticmethod
|
||||
def get_job_status(schedule_id: int) -> dict:
|
||||
@@ -236,38 +214,68 @@ class APSchedulerAdapter:
|
||||
return
|
||||
|
||||
scheduler.add_job(
|
||||
_execute_job,
|
||||
_execute_ephemeral_job,
|
||||
trigger=trigger_type,
|
||||
id=job_id,
|
||||
misfire_grace_time=60,
|
||||
args=[None],
|
||||
kwargs={"context_override": context},
|
||||
kwargs={"context": context},
|
||||
**trigger_config,
|
||||
)
|
||||
logger.debug(f"已添加新的临时APScheduler任务: {job_id}")
|
||||
|
||||
|
||||
async def _run_dependent_task(
|
||||
func: Callable, injected_params: dict, bot: Bot, state: T_State
|
||||
):
|
||||
"""将普通函数包装为 NoneBot Dependent 并执行的内部辅助方法"""
|
||||
|
||||
async def wrapper(bot: Bot):
|
||||
return await func(bot=bot, **injected_params)
|
||||
|
||||
dependent = Dependent.parse(call=wrapper, allow_types=Matcher.HANDLER_PARAM_TYPES)
|
||||
return await dependent(bot=bot, state=state)
|
||||
|
||||
|
||||
async def _execute_single_job_instance(
|
||||
schedule: ScheduledJob, bot, group_id: str | None = None
|
||||
schedule: ScheduledJob, bot, target_id: str | None = None
|
||||
):
|
||||
"""
|
||||
负责执行一个具体目标的任务实例。
|
||||
"""
|
||||
from .manager import scheduler_manager
|
||||
|
||||
plugin_name = schedule.plugin_name
|
||||
if group_id is None and schedule.target_type == "GROUP":
|
||||
group_id = schedule.target_identifier
|
||||
|
||||
task_meta = scheduler_manager._registered_tasks.get(plugin_name)
|
||||
actual_group_id = None
|
||||
actual_user_id = None
|
||||
|
||||
if schedule.target_type in (
|
||||
TargetType.GROUP,
|
||||
TargetType.TAG,
|
||||
TargetType.ALL_GROUPS,
|
||||
):
|
||||
actual_group_id = target_id or (
|
||||
schedule.target_identifier
|
||||
if schedule.target_type == TargetType.GROUP
|
||||
else None
|
||||
)
|
||||
elif schedule.target_type == TargetType.USER:
|
||||
actual_user_id = target_id or schedule.target_identifier
|
||||
|
||||
target_log = actual_group_id or actual_user_id
|
||||
|
||||
task_meta = scheduler_registry.tasks.get(plugin_name)
|
||||
|
||||
if not task_meta:
|
||||
logger.error(f"无法执行任务:插件 '{plugin_name}' 在执行期间变得不可用。")
|
||||
return
|
||||
|
||||
is_blocked = await CommonUtils.task_is_block(bot, plugin_name, group_id)
|
||||
is_blocked = await CommonUtils.task_is_block(bot, plugin_name, actual_group_id)
|
||||
if is_blocked:
|
||||
target_desc = f"群 {group_id}" if group_id else "全局"
|
||||
target_desc = (
|
||||
f"群 {actual_group_id}"
|
||||
if actual_group_id
|
||||
else (f"用户 {actual_user_id}" if actual_user_id else "全局")
|
||||
)
|
||||
logger.info(
|
||||
f"插件 '{plugin_name}' 的定时任务在目标 [{target_desc}] "
|
||||
f"因功能被禁用而跳过执行。"
|
||||
@@ -279,7 +287,8 @@ async def _execute_single_job_instance(
|
||||
plugin_name=plugin_name,
|
||||
bot_id=bot.self_id,
|
||||
platform_scope=PlatformUtils.get_platform_scope(bot),
|
||||
group_id=group_id,
|
||||
group_id=actual_group_id,
|
||||
user_id=actual_user_id,
|
||||
job_kwargs=schedule.job_kwargs if isinstance(schedule.job_kwargs, dict) else {},
|
||||
)
|
||||
state: T_State = {ScheduleContext: context}
|
||||
@@ -300,18 +309,13 @@ async def _execute_single_job_instance(
|
||||
injected_params["params"] = params_instance # type: ignore
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"任务 {schedule.id} (目标: {group_id}) 参数验证失败: {e}", e=e
|
||||
f"任务 {schedule.id} (目标: {target_log}) 参数验证失败: {e}",
|
||||
e=e,
|
||||
)
|
||||
raise
|
||||
|
||||
async def wrapper(bot: Bot):
|
||||
return await task_meta["func"](bot=bot, **injected_params) # type: ignore
|
||||
|
||||
dependent = Dependent.parse(
|
||||
call=wrapper,
|
||||
allow_types=Matcher.HANDLER_PARAM_TYPES,
|
||||
)
|
||||
return await dependent(bot=bot, state=state)
|
||||
func = cast(Callable, task_meta["func"])
|
||||
return await _run_dependent_task(func, injected_params, bot, state)
|
||||
|
||||
try:
|
||||
if policy.retries > 0:
|
||||
@@ -332,117 +336,37 @@ async def _execute_single_job_instance(
|
||||
exception=retry_exceptions,
|
||||
on_success=on_success_handler,
|
||||
on_failure=on_failure_handler,
|
||||
log_name=f"ScheduledJob-{schedule.id}-{group_id or 'global'}",
|
||||
log_name=f"ScheduledJob-{schedule.id}-{target_log or 'global'}",
|
||||
)
|
||||
|
||||
decorated_executor = retry_decorator(task_execution_coro)
|
||||
await decorated_executor()
|
||||
else:
|
||||
logger.info(
|
||||
f"插件 '{plugin_name}' 开始为目标 [{group_id or '全局'}] "
|
||||
f"插件 '{plugin_name}' 开始为目标 [{target_log or '全局'}] "
|
||||
f"执行定时任务 (ID: {schedule.id})。"
|
||||
)
|
||||
await task_execution_coro()
|
||||
|
||||
except (PausedException, FinishedException, SkippedException) as e:
|
||||
logger.warning(
|
||||
f"定时任务 {schedule.id} (目标: {group_id}) 被中断: {type(e).__name__}"
|
||||
f"定时任务 {schedule.id} (目标: {target_log}) 被中断: {type(e).__name__}"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"执行定时任务 {schedule.id} (目标: {group_id}) "
|
||||
f"执行定时任务 {schedule.id} (目标: {target_log}) "
|
||||
f"时发生未被策略处理的最终错误",
|
||||
e=e,
|
||||
)
|
||||
|
||||
|
||||
async def _execute_job(
|
||||
schedule_id: int | None,
|
||||
force: bool = False,
|
||||
context_override: ScheduleContext | None = None,
|
||||
):
|
||||
"""
|
||||
APScheduler 调度的入口函数,现在作为分发器。
|
||||
"""
|
||||
from .manager import scheduler_manager
|
||||
|
||||
schedule = None
|
||||
|
||||
if context_override:
|
||||
plugin_name = context_override.plugin_name
|
||||
task_meta = scheduler_manager._registered_tasks.get(plugin_name)
|
||||
if not task_meta or not task_meta["func"]:
|
||||
logger.error(f"无法执行临时任务:函数 '{plugin_name}' 未注册。")
|
||||
return
|
||||
|
||||
try:
|
||||
bot = _resolve_scheduler_bot(
|
||||
context_override.bot_id,
|
||||
f"临时任务 {plugin_name}",
|
||||
)
|
||||
if bot is None:
|
||||
return
|
||||
context_override.platform_scope = PlatformUtils.get_platform_scope(bot)
|
||||
logger.info(f"开始执行临时任务: {plugin_name}")
|
||||
injected_params = {"context": context_override}
|
||||
state: T_State = {ScheduleContext: context_override}
|
||||
|
||||
async def wrapper(bot: Bot):
|
||||
return await task_meta["func"](bot=bot, **injected_params) # type: ignore
|
||||
|
||||
dependent = Dependent.parse(
|
||||
call=wrapper,
|
||||
allow_types=Matcher.HANDLER_PARAM_TYPES,
|
||||
)
|
||||
await dependent(bot=bot, state=state)
|
||||
logger.info(f"临时任务 '{plugin_name}' 执行完成。")
|
||||
except Exception as e:
|
||||
logger.error(f"执行临时任务 '{plugin_name}' 时发生错误", e=e)
|
||||
return
|
||||
|
||||
if schedule_id is None:
|
||||
logger.error("执行持久化任务时 schedule_id 不能为空。")
|
||||
return
|
||||
|
||||
global _LAST_PRESSURE_SKIP
|
||||
if should_pause_tasks():
|
||||
now = time.time()
|
||||
if now - _LAST_PRESSURE_SKIP > 30:
|
||||
_LAST_PRESSURE_SKIP = now
|
||||
logger.info("scheduler paused due to message pressure")
|
||||
return
|
||||
|
||||
scheduler_manager._running_tasks.add(schedule_id)
|
||||
try:
|
||||
schedule = await ScheduleRepository.get_by_id(schedule_id)
|
||||
if not schedule or (not schedule.is_enabled and not force):
|
||||
logger.warning(f"定时任务 {schedule_id} 不存在或已禁用,跳过执行。")
|
||||
return
|
||||
|
||||
bot = _resolve_scheduler_bot(schedule.bot_id, f"任务 {schedule_id}")
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
resolver = scheduler_manager._target_resolvers.get(schedule.target_type)
|
||||
if not resolver:
|
||||
logger.error(
|
||||
f"任务 {schedule.id} 的目标类型 '{schedule.target_type}' "
|
||||
f"没有注册解析器,执行跳过。"
|
||||
)
|
||||
raise ValueError(f"未知的目标类型: {schedule.target_type}")
|
||||
|
||||
try:
|
||||
resolved_targets = await resolver(schedule.target_identifier, bot)
|
||||
except Exception as e:
|
||||
logger.error(f"为任务 {schedule.id} 解析目标失败", e=e)
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"任务 {schedule.id} ({schedule.name or schedule.plugin_name}) 开始执行, "
|
||||
f"目标类型: {schedule.target_type}, "
|
||||
f"解析出 {len(resolved_targets)} 个目标"
|
||||
)
|
||||
class ExecutionDispatcher:
|
||||
"""执行管线分发器:处理并发限制、串行间隔、随机散列打散逻辑"""
|
||||
|
||||
@staticmethod
|
||||
async def dispatch(
|
||||
schedule: ScheduledJob, bot: Bot, resolved_targets: list[str | None]
|
||||
):
|
||||
concurrency_limit = Config.get_config(
|
||||
"SchedulerManager", SCHEDULE_CONCURRENCY_KEY, 5
|
||||
)
|
||||
@@ -462,15 +386,10 @@ async def _execute_job(
|
||||
)
|
||||
for i, target_id in enumerate(resolved_targets):
|
||||
if i > 0:
|
||||
logger.debug(
|
||||
f"任务 {schedule.id} 目标 [{target_id or '全局'}]: "
|
||||
f"等待 {interval_seconds} 秒后执行。"
|
||||
)
|
||||
await asyncio.sleep(interval_seconds)
|
||||
await _execute_single_job_instance(schedule, bot, group_id=target_id)
|
||||
await _execute_single_job_instance(schedule, bot, target_id=target_id)
|
||||
else:
|
||||
spread_seconds = spread_config.get("spread", 1.0)
|
||||
|
||||
logger.debug(
|
||||
f"任务 {schedule.id}: 将在 {spread_seconds:.2f} 秒内分散执行 "
|
||||
f"{len(resolved_targets)} 个目标。"
|
||||
@@ -478,46 +397,112 @@ async def _execute_job(
|
||||
|
||||
async def worker(target_id: str | None):
|
||||
delay = random.uniform(0.1, spread_seconds)
|
||||
logger.debug(
|
||||
f"任务 {schedule.id} 目标 [{target_id or '全局'}]: "
|
||||
f"随机延迟 {delay:.2f} 秒后执行。"
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
async with semaphore:
|
||||
await _execute_single_job_instance(
|
||||
schedule, bot, group_id=target_id
|
||||
schedule, bot, target_id=target_id
|
||||
)
|
||||
|
||||
tasks_to_run = [worker(target_id) for target_id in resolved_targets]
|
||||
if tasks_to_run:
|
||||
await asyncio.gather(*tasks_to_run, return_exceptions=True)
|
||||
|
||||
schedule.last_run_at = datetime.now()
|
||||
schedule.last_run_status = "SUCCESS"
|
||||
schedule.consecutive_failures = 0
|
||||
await schedule.save(
|
||||
update_fields=["last_run_at", "last_run_status", "consecutive_failures"]
|
||||
|
||||
async def _execute_ephemeral_job(context: ScheduleContext):
|
||||
"""
|
||||
执行临时的、内存态的定时任务 (Ephemeral Job)
|
||||
"""
|
||||
plugin_name = context.plugin_name
|
||||
task_meta = scheduler_registry.tasks.get(plugin_name)
|
||||
if not task_meta or not task_meta["func"]:
|
||||
logger.error(f"无法执行临时任务:函数 '{plugin_name}' 未注册。")
|
||||
return
|
||||
|
||||
try:
|
||||
bot = PlatformUtils.resolve_bot(
|
||||
bot_id=context.bot_id,
|
||||
log_cmd=f"临时任务 {plugin_name}",
|
||||
)
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
context.platform_scope = PlatformUtils.get_platform_scope(bot)
|
||||
logger.info(f"开始执行临时任务: {plugin_name}")
|
||||
injected_params = {"context": context}
|
||||
state: T_State = {ScheduleContext: context}
|
||||
|
||||
func = cast(Callable, task_meta["func"])
|
||||
await _run_dependent_task(func, injected_params, bot, state)
|
||||
logger.info(f"临时任务 '{plugin_name}' 执行完成。")
|
||||
except Exception as e:
|
||||
logger.error(f"执行临时任务 '{plugin_name}' 时发生错误", e=e)
|
||||
|
||||
|
||||
async def _execute_persistent_job(schedule_id: int, force: bool = False):
|
||||
"""
|
||||
执行数据库中持久化的定时任务 (Persistent Job)
|
||||
"""
|
||||
schedule = None
|
||||
|
||||
global _LAST_PRESSURE_SKIP
|
||||
if should_pause_tasks():
|
||||
now = time.time()
|
||||
if now - _LAST_PRESSURE_SKIP > 30:
|
||||
_LAST_PRESSURE_SKIP = now
|
||||
logger.info("scheduler paused due to message pressure")
|
||||
return
|
||||
|
||||
scheduler_registry.running_tasks.add(schedule_id)
|
||||
try:
|
||||
schedule = await ScheduleRepository.get_by_id(schedule_id)
|
||||
if not schedule or (not schedule.is_enabled and not force):
|
||||
logger.warning(f"定时任务 {schedule_id} 不存在或已禁用,跳过执行。")
|
||||
return
|
||||
|
||||
bot = PlatformUtils.resolve_bot(
|
||||
bot_id=schedule.bot_id, log_cmd=f"任务 {schedule_id}"
|
||||
)
|
||||
if bot is None:
|
||||
return
|
||||
|
||||
resolver = scheduler_registry.target_resolvers.get(schedule.target_type)
|
||||
if not resolver:
|
||||
logger.error(
|
||||
f"任务 {schedule.id} 的目标类型 '{schedule.target_type}' "
|
||||
f"没有注册解析器,执行跳过。"
|
||||
)
|
||||
raise ValueError(f"未知的目标类型: {schedule.target_type}")
|
||||
|
||||
try:
|
||||
resolved_targets = await resolver(schedule.target_identifier, bot)
|
||||
except Exception as e:
|
||||
logger.error(f"为任务 {schedule.id} 解析目标失败", e=e)
|
||||
raise
|
||||
|
||||
logger.info(
|
||||
f"任务 {schedule.id} ({schedule.name or schedule.plugin_name}) 开始执行, "
|
||||
f"目标类型: {schedule.target_type}, "
|
||||
f"解析出 {len(resolved_targets)} 个目标"
|
||||
)
|
||||
|
||||
await ExecutionDispatcher.dispatch(schedule, bot, resolved_targets)
|
||||
|
||||
await ScheduleRepository.update_run_status(schedule, is_success=True)
|
||||
|
||||
if schedule.is_one_off:
|
||||
logger.info(f"一次性任务 {schedule.id} 执行成功,将被删除。")
|
||||
await ScheduledJob.filter(id=schedule.id).delete()
|
||||
APSchedulerAdapter.remove_job(schedule.id)
|
||||
if schedule.plugin_name.startswith("runtime_one_off__"):
|
||||
scheduler_manager._registered_tasks.pop(schedule.plugin_name, None)
|
||||
scheduler_registry.tasks.pop(schedule.plugin_name, None)
|
||||
logger.debug(f"已注销一次性运行时任务: {schedule.plugin_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行任务 {schedule_id} 期间发生严重错误", e=e)
|
||||
|
||||
if schedule:
|
||||
schedule.last_run_at = datetime.now()
|
||||
schedule.last_run_status = "FAILURE"
|
||||
schedule.consecutive_failures = (schedule.consecutive_failures or 0) + 1
|
||||
await schedule.save(
|
||||
update_fields=["last_run_at", "last_run_status", "consecutive_failures"]
|
||||
)
|
||||
await ScheduleRepository.update_run_status(schedule, is_success=False)
|
||||
|
||||
finally:
|
||||
if schedule_id is not None:
|
||||
scheduler_manager._running_tasks.discard(schedule_id)
|
||||
scheduler_registry.running_tasks.discard(schedule_id)
|
||||
|
||||
@@ -10,8 +10,9 @@ from zhenxun.utils.pydantic_compat import model_dump
|
||||
|
||||
from .engine import APSchedulerAdapter
|
||||
from .manager import scheduler_manager
|
||||
from .registry import scheduler_registry
|
||||
from .repository import ScheduleRepository
|
||||
from .types import ScheduleContext
|
||||
from .types import JobConfig, ScheduleContext
|
||||
|
||||
|
||||
@PriorityLifecycle.on_startup(priority=90)
|
||||
@@ -21,7 +22,7 @@ async def _load_schedules_from_db():
|
||||
schedules = await ScheduleRepository.get_all_enabled()
|
||||
count = 0
|
||||
for schedule in schedules:
|
||||
if schedule.plugin_name in scheduler_manager._registered_tasks:
|
||||
if schedule.plugin_name in scheduler_registry.tasks:
|
||||
APSchedulerAdapter.add_or_reschedule_job(schedule)
|
||||
count += 1
|
||||
else:
|
||||
@@ -30,7 +31,7 @@ async def _load_schedules_from_db():
|
||||
|
||||
logger.info("正在检查并注册声明式默认任务...")
|
||||
declared_count = 0
|
||||
for task_info in scheduler_manager._declared_tasks:
|
||||
for task_info in scheduler_registry.persistent_declarations:
|
||||
plugin_name = task_info.plugin_name
|
||||
group_id = task_info.group_id
|
||||
bot_id = task_info.bot_id
|
||||
@@ -45,21 +46,21 @@ async def _load_schedules_from_db():
|
||||
if not exists:
|
||||
logger.info(f"为插件 '{plugin_name}' 注册新的默认定时任务...")
|
||||
|
||||
trigger_config_dict = model_dump(
|
||||
task_info.trigger, exclude={"trigger_type"}
|
||||
)
|
||||
|
||||
target_type = "GROUP" if group_id else "GLOBAL"
|
||||
target_identifier = group_id or ""
|
||||
|
||||
config = JobConfig(
|
||||
trigger=task_info.trigger,
|
||||
job_kwargs=task_info.job_kwargs,
|
||||
bot_id=bot_id,
|
||||
source="PLUGIN_DEFAULT",
|
||||
)
|
||||
|
||||
schedule = await scheduler_manager.add_schedule(
|
||||
plugin_name=plugin_name,
|
||||
target_type=target_type,
|
||||
target_identifier=target_identifier,
|
||||
trigger_type=task_info.trigger.trigger_type,
|
||||
trigger_config=trigger_config_dict,
|
||||
job_kwargs=task_info.job_kwargs,
|
||||
bot_id=bot_id,
|
||||
config=config,
|
||||
)
|
||||
if schedule:
|
||||
declared_count += 1
|
||||
@@ -74,7 +75,7 @@ async def _load_schedules_from_db():
|
||||
|
||||
logger.info("正在调度声明式临时任务...")
|
||||
ephemeral_count = 0
|
||||
for declaration in scheduler_manager._ephemeral_declared_tasks:
|
||||
for declaration in scheduler_registry.ephemeral_declarations:
|
||||
try:
|
||||
job_id = f"runtime::{declaration.plugin_name}::{declaration.func.__name__}"
|
||||
|
||||
|
||||
@@ -10,69 +10,66 @@ from __future__ import annotations
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
import json
|
||||
from typing import Any, ClassVar
|
||||
import uuid
|
||||
|
||||
from arclet.alconna import Alconna, Option
|
||||
from arclet.alconna import Alconna
|
||||
import nonebot
|
||||
from nonebot.adapters import Bot
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun.configs.config import Config
|
||||
from zhenxun.models.scheduled_job import ScheduledJob
|
||||
from zhenxun.services.log import logger
|
||||
from zhenxun.utils.pydantic_compat import model_dump, model_validate
|
||||
from zhenxun.utils.pydantic_compat import dump_json_safely, model_dump, model_validate
|
||||
|
||||
from .engine import APSchedulerAdapter
|
||||
from .engine import APSchedulerAdapter, _execute_persistent_job
|
||||
from .registry import scheduler_registry
|
||||
from .repository import ScheduleRepository
|
||||
from .targeting import (
|
||||
ScheduleTargeter,
|
||||
_resolve_all_groups,
|
||||
_resolve_global_or_user,
|
||||
_resolve_group,
|
||||
_resolve_tag,
|
||||
_resolve_user,
|
||||
)
|
||||
from .types import (
|
||||
BaseTrigger,
|
||||
EphemeralJobDeclaration,
|
||||
ExecutionOptions,
|
||||
ExecutionPolicy,
|
||||
JobConfig,
|
||||
ScheduleContext,
|
||||
ScheduledJobDeclaration,
|
||||
TargetType,
|
||||
Trigger,
|
||||
)
|
||||
|
||||
|
||||
class SchedulerManager:
|
||||
ALL_GROUPS: ClassVar[str] = "__ALL_GROUPS__"
|
||||
_registered_tasks: ClassVar[
|
||||
dict[
|
||||
str,
|
||||
dict[str, Callable | type[BaseModel] | int | list[Option] | Alconna | None],
|
||||
]
|
||||
] = {}
|
||||
_declared_tasks: ClassVar[list[ScheduledJobDeclaration]] = []
|
||||
_ephemeral_declared_tasks: ClassVar[list[EphemeralJobDeclaration]] = []
|
||||
_running_tasks: ClassVar[set] = set()
|
||||
_target_resolvers: ClassVar[
|
||||
dict[str, Callable[[str, Bot], Awaitable[list[str | None]]]]
|
||||
] = {}
|
||||
ALL_GROUPS: ClassVar[str] = scheduler_registry.ALL_GROUPS
|
||||
|
||||
def __init__(self):
|
||||
self._register_builtin_resolvers()
|
||||
|
||||
def _register_builtin_resolvers(self):
|
||||
"""在管理器初始化时注册所有内置的目标解析器。"""
|
||||
from .targeting import (
|
||||
_resolve_all_groups,
|
||||
_resolve_global_or_user,
|
||||
_resolve_group,
|
||||
_resolve_tag,
|
||||
_resolve_user,
|
||||
)
|
||||
|
||||
if "GROUP" in self._target_resolvers:
|
||||
if TargetType.GROUP.value in scheduler_registry.target_resolvers:
|
||||
return
|
||||
self.register_target_resolver("GROUP", _resolve_group)
|
||||
self.register_target_resolver("TAG", _resolve_tag)
|
||||
self.register_target_resolver("ALL_GROUPS", _resolve_all_groups)
|
||||
self.register_target_resolver("GLOBAL", _resolve_global_or_user)
|
||||
self.register_target_resolver("USER", _resolve_user)
|
||||
scheduler_registry.register_target_resolver(
|
||||
TargetType.GROUP.value, _resolve_group
|
||||
)
|
||||
scheduler_registry.register_target_resolver(TargetType.TAG.value, _resolve_tag)
|
||||
scheduler_registry.register_target_resolver(
|
||||
TargetType.ALL_GROUPS.value, _resolve_all_groups
|
||||
)
|
||||
scheduler_registry.register_target_resolver(
|
||||
TargetType.GLOBAL.value, _resolve_global_or_user
|
||||
)
|
||||
scheduler_registry.register_target_resolver(
|
||||
TargetType.USER.value, _resolve_user
|
||||
)
|
||||
logger.debug("已注册所有内置的定时任务目标解析器。")
|
||||
|
||||
def register_target_resolver(
|
||||
@@ -83,20 +80,14 @@ class SchedulerManager:
|
||||
"""
|
||||
注册一个新的目标类型解析器。
|
||||
"""
|
||||
if target_type in self._target_resolvers:
|
||||
if target_type in scheduler_registry.target_resolvers:
|
||||
logger.warning(f"目标解析器 '{target_type}' 已存在,将被覆盖。")
|
||||
self._target_resolvers[target_type.upper()] = resolver_func
|
||||
logger.info(f"已注册新的定时任务目标解析器: '{target_type}'")
|
||||
scheduler_registry.register_target_resolver(target_type, resolver_func)
|
||||
logger.debug(f"已注册新的定时任务目标解析器: '{target_type}'")
|
||||
|
||||
def target(self, **filters: Any) -> ScheduleTargeter:
|
||||
"""
|
||||
创建目标选择器以执行批量操作
|
||||
|
||||
参数:
|
||||
**filters: 过滤条件,支持plugin_name、group_id、bot_id等字段。
|
||||
|
||||
返回:
|
||||
ScheduleTargeter: 目标选择器对象,可用于批量操作。
|
||||
"""
|
||||
return ScheduleTargeter(self, **filters)
|
||||
|
||||
@@ -112,7 +103,20 @@ class SchedulerManager:
|
||||
default_interval: int | None = None,
|
||||
):
|
||||
"""
|
||||
声明式定时任务的统一装饰器。
|
||||
声明式定时任务的统一装饰器
|
||||
|
||||
参数:
|
||||
trigger: 定时触发器配置。
|
||||
group_id: 默认目标群号,如果不指定则为全局任务。
|
||||
bot_id: 执行此任务时应优先匹配的 Bot 标识符。
|
||||
default_params: 任务函数的默认参数模型实例。
|
||||
policy: 执行和重试策略配置。
|
||||
default_jitter: 默认抖动延迟时间(秒)。
|
||||
default_spread: 默认并发散列随机延迟打散的最大延迟范围(秒)。
|
||||
default_interval: 默认串行派发任务时的固定等待间隔(秒)。
|
||||
|
||||
返回:
|
||||
Callable: 装饰器函数,用于包裹并注册声明式任务。
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., Coroutine]) -> Callable[..., Coroutine]:
|
||||
@@ -123,7 +127,6 @@ class SchedulerManager:
|
||||
plugin_name = plugin.name
|
||||
|
||||
params_model = None
|
||||
from .types import ScheduleContext
|
||||
|
||||
for param in inspect.signature(func).parameters.values():
|
||||
if (
|
||||
@@ -134,9 +137,9 @@ class SchedulerManager:
|
||||
params_model = param.annotation
|
||||
break
|
||||
|
||||
if plugin_name in self._registered_tasks:
|
||||
if plugin_name in scheduler_registry.tasks:
|
||||
logger.warning(f"插件 '{plugin_name}' 的定时任务已被重复注册。")
|
||||
self._registered_tasks[plugin_name] = {
|
||||
scheduler_registry.tasks[plugin_name] = {
|
||||
"func": func,
|
||||
"model": params_model,
|
||||
"default_jitter": default_jitter,
|
||||
@@ -155,7 +158,7 @@ class SchedulerManager:
|
||||
trigger=trigger,
|
||||
job_kwargs=job_kwargs,
|
||||
)
|
||||
self._declared_tasks.append(task_declaration)
|
||||
scheduler_registry.persistent_declarations.append(task_declaration)
|
||||
logger.debug(
|
||||
f"发现声明式定时任务 '{plugin_name}',将在启动时进行注册。"
|
||||
)
|
||||
@@ -178,7 +181,9 @@ class SchedulerManager:
|
||||
raise ValueError(f"函数 {func.__name__} 不在任何已加载的插件中。")
|
||||
plugin_name = plugin.name
|
||||
|
||||
self._registered_tasks[f"ephemeral::{plugin_name}::{func.__name__}"] = {
|
||||
scheduler_registry.tasks[
|
||||
f"ephemeral::{plugin_name}::{func.__name__}"
|
||||
] = {
|
||||
"func": func,
|
||||
"model": None,
|
||||
}
|
||||
@@ -188,7 +193,7 @@ class SchedulerManager:
|
||||
func=func,
|
||||
trigger=trigger,
|
||||
)
|
||||
self._ephemeral_declared_tasks.append(declaration)
|
||||
scheduler_registry.ephemeral_declarations.append(declaration)
|
||||
logger.debug(
|
||||
f"发现临时定时任务 '{plugin_name}:{func.__name__}',将在启动时调度"
|
||||
)
|
||||
@@ -211,12 +216,24 @@ class SchedulerManager:
|
||||
) -> Callable:
|
||||
"""
|
||||
注册可调度的任务函数
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称。
|
||||
params_model: 参数的模型定义,如为 None 则任务不接收额外参数。
|
||||
cli_parser: 用于解析命令行参数的 Alconna 解析器。
|
||||
default_permission: 运行此定时任务所需的默认权限等级。
|
||||
default_jitter: 默认抖动延迟时间(秒)。
|
||||
default_spread: 默认并发散列随机延迟打散的最大延迟范围(秒)。
|
||||
default_interval: 默认串行派发任务时的固定等待间隔(秒)。
|
||||
|
||||
返回:
|
||||
Callable: 装饰器函数,用于包裹并注册任务。
|
||||
"""
|
||||
|
||||
def decorator(func: Callable[..., Coroutine]) -> Callable[..., Coroutine]:
|
||||
if plugin_name in self._registered_tasks:
|
||||
if plugin_name in scheduler_registry.tasks:
|
||||
logger.warning(f"插件 '{plugin_name}' 的定时任务已被重复注册。")
|
||||
self._registered_tasks[plugin_name] = {
|
||||
scheduler_registry.tasks[plugin_name] = {
|
||||
"func": func,
|
||||
"model": params_model,
|
||||
"cli_parser": cli_parser,
|
||||
@@ -237,7 +254,7 @@ class SchedulerManager:
|
||||
"""
|
||||
获取已注册插件列表
|
||||
"""
|
||||
return list(self._registered_tasks.keys())
|
||||
return list(scheduler_registry.tasks.keys())
|
||||
|
||||
async def run_at(self, func: Callable[..., Coroutine], trigger: BaseTrigger) -> str:
|
||||
"""
|
||||
@@ -252,14 +269,18 @@ class SchedulerManager:
|
||||
bot_id=None,
|
||||
platform_scope=None,
|
||||
group_id=None,
|
||||
user_id=None,
|
||||
job_kwargs={},
|
||||
)
|
||||
|
||||
trigger_config_dict = json.loads(dump_json_safely(trigger))
|
||||
trigger_config_dict.pop("trigger_type", None)
|
||||
|
||||
APSchedulerAdapter.add_ephemeral_job(
|
||||
job_id=job_id,
|
||||
func=func,
|
||||
trigger_type=trigger.trigger_type,
|
||||
trigger_config=model_dump(trigger, exclude={"trigger_type"}),
|
||||
trigger_config=trigger_config_dict,
|
||||
context=context,
|
||||
)
|
||||
logger.info(f"已动态调度一个临时任务 (ID: {job_id}),将在 {trigger} 触发。")
|
||||
@@ -279,26 +300,40 @@ class SchedulerManager:
|
||||
required_permission: int = 5,
|
||||
) -> "ScheduledJob | None":
|
||||
"""
|
||||
编程式API,用于动态调度一个持久化的、一次性的任务。
|
||||
编程式API,用于动态调度一个持久化的、一次性的任务
|
||||
|
||||
参数:
|
||||
func: 待执行的协程函数。
|
||||
trigger: 定时触发器配置。
|
||||
user_id: 目标用户 ID,与 group_id 互斥。
|
||||
group_id: 目标群号,与 user_id 互斥。
|
||||
bot_id: 指定运行此任务的 Bot 标识符。
|
||||
job_kwargs: 传递给任务函数的实际参数字典。
|
||||
name: 任务的可读别名。
|
||||
created_by: 任务创建者的标识。
|
||||
required_permission: 运行该任务需要的最低权限等级。
|
||||
|
||||
返回:
|
||||
ScheduledJob | None: 持久化定时任务的数据模型实例,如果失败则返回 None。
|
||||
"""
|
||||
if user_id and group_id:
|
||||
raise ValueError("user_id 和 group_id 不能同时提供。")
|
||||
|
||||
temp_plugin_name = f"runtime_one_off__{func.__module__}.{func.__name__}__{uuid.uuid4().hex[:8]}" # noqa: E501
|
||||
|
||||
self._registered_tasks[temp_plugin_name] = {"func": func, "model": None}
|
||||
scheduler_registry.tasks[temp_plugin_name] = {"func": func, "model": None}
|
||||
logger.debug(f"为一次性任务动态注册临时插件: '{temp_plugin_name}'")
|
||||
|
||||
target_type = "USER" if user_id else ("GROUP" if group_id else "GLOBAL")
|
||||
target_type = (
|
||||
TargetType.USER.value
|
||||
if user_id
|
||||
else (TargetType.GROUP.value if group_id else TargetType.GLOBAL.value)
|
||||
)
|
||||
target_identifier = user_id or group_id or ""
|
||||
|
||||
return await self.add_schedule(
|
||||
plugin_name=temp_plugin_name,
|
||||
target_type=target_type,
|
||||
target_identifier=target_identifier,
|
||||
trigger_type=trigger.trigger_type,
|
||||
trigger_config=model_dump(trigger, exclude={"trigger_type"}),
|
||||
job_kwargs=job_kwargs,
|
||||
config = JobConfig(
|
||||
trigger=trigger,
|
||||
job_kwargs=job_kwargs or {},
|
||||
bot_id=bot_id,
|
||||
name=name,
|
||||
created_by=created_by,
|
||||
@@ -306,6 +341,13 @@ class SchedulerManager:
|
||||
is_one_off=True,
|
||||
)
|
||||
|
||||
return await self.add_schedule(
|
||||
plugin_name=temp_plugin_name,
|
||||
target_type=target_type,
|
||||
target_identifier=target_identifier,
|
||||
config=config,
|
||||
)
|
||||
|
||||
async def add_daily_task(
|
||||
self,
|
||||
plugin_name: str,
|
||||
@@ -318,6 +360,18 @@ class SchedulerManager:
|
||||
) -> "ScheduledJob | None":
|
||||
"""
|
||||
添加每日定时任务
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称。
|
||||
group_id: 目标群号,如果不指定则为全局任务。
|
||||
hour: 触发的小时数(0-23)。
|
||||
minute: 触发的分钟数(0-59)。
|
||||
second: 触发的秒数(0-59)。
|
||||
job_kwargs: 传递给任务函数的参数字典。
|
||||
bot_id: 运行此任务的首选 Bot。
|
||||
|
||||
返回:
|
||||
ScheduledJob | None: 定时任务的数据模型对象,如果失败则返回 None。
|
||||
"""
|
||||
trigger_config = {
|
||||
"hour": hour,
|
||||
@@ -325,14 +379,15 @@ class SchedulerManager:
|
||||
"second": second,
|
||||
"timezone": Config.get_config("SchedulerManager", "SCHEDULER_TIMEZONE"),
|
||||
}
|
||||
|
||||
trigger = Trigger.cron(**trigger_config)
|
||||
return await self.add_schedule(
|
||||
plugin_name,
|
||||
target_type="GROUP" if group_id else "GLOBAL",
|
||||
target_type=TargetType.GROUP.value if group_id else TargetType.GLOBAL.value,
|
||||
target_identifier=group_id or "",
|
||||
trigger_type="cron",
|
||||
trigger_config=trigger_config,
|
||||
job_kwargs=job_kwargs,
|
||||
bot_id=bot_id,
|
||||
config=JobConfig(
|
||||
trigger=trigger, job_kwargs=job_kwargs or {}, bot_id=bot_id
|
||||
),
|
||||
)
|
||||
|
||||
async def add_interval_task(
|
||||
@@ -351,6 +406,21 @@ class SchedulerManager:
|
||||
) -> "ScheduledJob | None":
|
||||
"""
|
||||
添加间隔性定时任务
|
||||
|
||||
参数:
|
||||
plugin_name: 插件名称。
|
||||
group_id: 目标群号,如果不指定则为全局任务。
|
||||
weeks: 间隔的周数。
|
||||
days: 间隔的天数。
|
||||
hours: 间隔的小时数。
|
||||
minutes: 间隔的分钟数。
|
||||
seconds: 间隔的秒数。
|
||||
start_date: 间隔计算的起始时间。
|
||||
job_kwargs: 传递给任务函数的参数字典。
|
||||
bot_id: 运行此任务的首选 Bot。
|
||||
|
||||
返回:
|
||||
ScheduledJob | None: 定时任务的数据模型对象,如果失败则返回 None。
|
||||
"""
|
||||
trigger_config = {
|
||||
"weeks": weeks,
|
||||
@@ -361,23 +431,22 @@ class SchedulerManager:
|
||||
"start_date": start_date,
|
||||
}
|
||||
trigger_config = {k: v for k, v in trigger_config.items() if v}
|
||||
|
||||
trigger = Trigger.interval(**trigger_config)
|
||||
return await self.add_schedule(
|
||||
plugin_name,
|
||||
target_type="GROUP" if group_id else "GLOBAL",
|
||||
target_type=TargetType.GROUP.value if group_id else TargetType.GLOBAL.value,
|
||||
target_identifier=group_id or "",
|
||||
trigger_type="interval",
|
||||
trigger_config=trigger_config,
|
||||
job_kwargs=job_kwargs,
|
||||
bot_id=bot_id,
|
||||
config=JobConfig(
|
||||
trigger=trigger, job_kwargs=job_kwargs or {}, bot_id=bot_id
|
||||
),
|
||||
)
|
||||
|
||||
def _validate_and_prepare_kwargs(
|
||||
self, plugin_name: str, job_kwargs: dict | None
|
||||
) -> tuple[bool, str | dict]:
|
||||
"""验证并准备任务参数,应用默认值"""
|
||||
from pydantic import ValidationError
|
||||
|
||||
task_meta = self._registered_tasks.get(plugin_name)
|
||||
task_meta = scheduler_registry.tasks.get(plugin_name)
|
||||
if not task_meta:
|
||||
return False, f"插件 '{plugin_name}' 未注册。"
|
||||
|
||||
@@ -410,17 +479,7 @@ class SchedulerManager:
|
||||
plugin_name: str,
|
||||
target_type: str,
|
||||
target_identifier: str,
|
||||
trigger_type: str,
|
||||
trigger_config: dict,
|
||||
job_kwargs: dict | None = None,
|
||||
bot_id: str | None = None,
|
||||
*,
|
||||
name: str | None = None,
|
||||
created_by: str | None = None,
|
||||
required_permission: int = 5,
|
||||
source: str = "USER",
|
||||
is_one_off: bool = False,
|
||||
execution_options: dict | None = None,
|
||||
config: JobConfig,
|
||||
) -> "ScheduledJob | None":
|
||||
"""
|
||||
添加定时任务(通用方法)
|
||||
@@ -429,51 +488,43 @@ class SchedulerManager:
|
||||
plugin_name: 插件名称。
|
||||
target_type: 目标类型 (GROUP, USER, TAG, ALL_GROUPS, GLOBAL)。
|
||||
target_identifier: 目标标识符。
|
||||
trigger_type: 触发器类型 (cron, interval, date)。
|
||||
trigger_config: 触发器配置字典。
|
||||
job_kwargs: 传递给任务函数的额外参数。
|
||||
bot_id: Bot ID约束。
|
||||
name: 任务别名。
|
||||
created_by: 创建者ID。
|
||||
required_permission: 管理此任务所需的权限。
|
||||
source: 任务来源 (USER, PLUGIN_DEFAULT)。
|
||||
is_one_off: 是否为一次性任务。
|
||||
execution_options: 任务执行的额外选项 (例如: jitter, spread)。
|
||||
|
||||
返回:
|
||||
ScheduledJob | None: 创建的任务信息,失败时返回None。
|
||||
config: JobConfig 参数配置聚合对象。
|
||||
"""
|
||||
if plugin_name not in self._registered_tasks:
|
||||
if plugin_name not in scheduler_registry.tasks:
|
||||
logger.error(f"插件 '{plugin_name}' 没有注册可用的定时任务。")
|
||||
return None
|
||||
|
||||
is_valid, result = self._validate_and_prepare_kwargs(plugin_name, job_kwargs)
|
||||
is_valid, result = self._validate_and_prepare_kwargs(
|
||||
plugin_name, config.job_kwargs
|
||||
)
|
||||
if not is_valid:
|
||||
logger.error(f"任务参数校验失败: {result}")
|
||||
return None
|
||||
|
||||
options_dict = execution_options or {}
|
||||
validated_options = ExecutionOptions(**options_dict)
|
||||
|
||||
search_kwargs = {
|
||||
"plugin_name": plugin_name,
|
||||
"target_type": target_type,
|
||||
"target_identifier": target_identifier,
|
||||
}
|
||||
if bot_id:
|
||||
search_kwargs["bot_id"] = bot_id
|
||||
if config.bot_id:
|
||||
search_kwargs["bot_id"] = config.bot_id
|
||||
|
||||
trigger_config_dict = json.loads(dump_json_safely(config.trigger))
|
||||
trigger_config_dict.pop("trigger_type", None)
|
||||
|
||||
defaults = {
|
||||
"name": name,
|
||||
"trigger_type": trigger_type,
|
||||
"trigger_config": trigger_config,
|
||||
"name": config.name,
|
||||
"trigger_type": config.trigger.trigger_type,
|
||||
"trigger_config": trigger_config_dict,
|
||||
"job_kwargs": result,
|
||||
"is_enabled": True,
|
||||
"created_by": created_by,
|
||||
"required_permission": required_permission,
|
||||
"source": source,
|
||||
"is_one_off": is_one_off,
|
||||
"execution_options": model_dump(validated_options, exclude_none=True),
|
||||
"created_by": config.created_by,
|
||||
"required_permission": config.required_permission,
|
||||
"source": config.source,
|
||||
"is_one_off": config.is_one_off,
|
||||
"execution_options": model_dump(
|
||||
config.execution_options, exclude_none=True
|
||||
),
|
||||
}
|
||||
|
||||
defaults = {k: v for k, v in defaults.items() if v is not None}
|
||||
@@ -485,7 +536,7 @@ class SchedulerManager:
|
||||
|
||||
action_str = "创建" if created else "更新"
|
||||
logger.info(
|
||||
f"已成功{action_str}任务 '{name or plugin_name}' (ID: {schedule.id})"
|
||||
f"已成功{action_str}任务 '{config.name or plugin_name}' (ID: {schedule.id})"
|
||||
)
|
||||
return schedule
|
||||
|
||||
@@ -494,7 +545,15 @@ class SchedulerManager:
|
||||
) -> tuple[list[ScheduledJob], int]:
|
||||
"""
|
||||
根据条件获取定时任务列表
|
||||
"""
|
||||
|
||||
参数:
|
||||
page: 分页页码,从 1 开始。
|
||||
page_size: 每页的任务数量。
|
||||
**filters: 过滤条件,支持 plugin_name, target_type, is_enabled 等字段。
|
||||
|
||||
返回:
|
||||
tuple[list[ScheduledJob], int]: 包含任务对象列表和总符合条件的任务数量的元组。
|
||||
""" # noqa: E501
|
||||
cleaned_filters = {k: v for k, v in filters.items() if v is not None}
|
||||
return await ScheduleRepository.query_schedules(
|
||||
page=page, page_size=page_size, **cleaned_filters
|
||||
@@ -523,7 +582,7 @@ class SchedulerManager:
|
||||
status_dict.update(status_from_scheduler)
|
||||
status_dict["is_enabled"] = (
|
||||
"运行中"
|
||||
if schedule_id in self._running_tasks
|
||||
if schedule_id in scheduler_registry.running_tasks
|
||||
else ("启用" if schedule.is_enabled else "暂停")
|
||||
)
|
||||
statuses.append(status_dict)
|
||||
@@ -539,6 +598,15 @@ class SchedulerManager:
|
||||
) -> tuple[bool, str]:
|
||||
"""
|
||||
更新定时任务配置
|
||||
|
||||
参数:
|
||||
schedule_id: 定时任务的ID。
|
||||
trigger_type: 触发器类型,例如 'cron' 或 'interval'。
|
||||
trigger_config: 触发器配置字典。
|
||||
job_kwargs: 更新后的任务参数字典。
|
||||
|
||||
返回:
|
||||
tuple[bool, str]: 包含执行结果(成功为 True)和对应状态消息的元组。
|
||||
"""
|
||||
schedule = await ScheduleRepository.get_by_id(schedule_id)
|
||||
if not schedule:
|
||||
@@ -589,7 +657,7 @@ class SchedulerManager:
|
||||
|
||||
status_text = (
|
||||
"运行中"
|
||||
if schedule_id in self._running_tasks
|
||||
if schedule_id in scheduler_registry.running_tasks
|
||||
else ("启用" if schedule.is_enabled else "暂停")
|
||||
)
|
||||
|
||||
@@ -636,16 +704,14 @@ class SchedulerManager:
|
||||
"""
|
||||
立即手动触发指定的定时任务
|
||||
"""
|
||||
from .engine import _execute_job
|
||||
|
||||
schedule = await ScheduleRepository.get_by_id(schedule_id)
|
||||
if not schedule:
|
||||
return False, f"未找到 ID 为 {schedule_id} 的定时任务。"
|
||||
if schedule.plugin_name not in self._registered_tasks:
|
||||
if schedule.plugin_name not in scheduler_registry.tasks:
|
||||
return False, f"插件 '{schedule.plugin_name}' 没有注册可用的定时任务。"
|
||||
|
||||
try:
|
||||
await _execute_job(schedule.id, force=True)
|
||||
await _execute_persistent_job(schedule.id, force=True)
|
||||
return True, f"已手动触发任务 (ID: {schedule.id})。"
|
||||
except Exception as e:
|
||||
logger.error(f"手动触发任务失败: {e}")
|
||||
@@ -654,12 +720,6 @@ class SchedulerManager:
|
||||
async def get_schedule_by_id(self, schedule_id: int) -> "ScheduledJob | None":
|
||||
"""
|
||||
通过ID获取任务对象的公共方法。
|
||||
|
||||
参数:
|
||||
schedule_id: 任务ID。
|
||||
|
||||
返回:
|
||||
ScheduledJob | None: 任务对象,不存在时返回None。
|
||||
"""
|
||||
return await ScheduleRepository.get_by_id(schedule_id)
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from arclet.alconna import Alconna, Option
|
||||
from nonebot.adapters import Bot
|
||||
from pydantic import BaseModel
|
||||
|
||||
from zhenxun.services.log import logger
|
||||
|
||||
from .types import EphemeralJobDeclaration, ScheduledJobDeclaration
|
||||
|
||||
|
||||
class SchedulerRegistry:
|
||||
"""定时任务注册中心,统一管理所有任务的元数据和解析器"""
|
||||
|
||||
ALL_GROUPS = "__ALL_GROUPS__"
|
||||
|
||||
def __init__(self):
|
||||
"""初始化调度任务注册中心容器。"""
|
||||
self.tasks: dict[
|
||||
str,
|
||||
dict[str, Callable | type[BaseModel] | int | list[Option] | Alconna | None],
|
||||
] = {}
|
||||
self.persistent_declarations: list[ScheduledJobDeclaration] = []
|
||||
self.ephemeral_declarations: list[EphemeralJobDeclaration] = []
|
||||
self.target_resolvers: dict[
|
||||
str, Callable[[str, Bot], Awaitable[list[str | None]]]
|
||||
] = {}
|
||||
self.running_tasks: set[int] = set()
|
||||
|
||||
def register_target_resolver(
|
||||
self,
|
||||
target_type: str,
|
||||
resolver_func: Callable[[str, Bot], Awaitable[list[str | None]]],
|
||||
):
|
||||
"""注册指定执行目标的解析策略。"""
|
||||
if target_type in self.target_resolvers:
|
||||
logger.warning(f"目标解析器 '{target_type}' 已存在,将被覆盖。")
|
||||
self.target_resolvers[target_type.upper()] = resolver_func
|
||||
logger.info(f"已注册新的定时任务目标解析器: '{target_type}'")
|
||||
|
||||
|
||||
scheduler_registry = SchedulerRegistry()
|
||||
@@ -4,6 +4,7 @@
|
||||
封装所有对 ScheduledJob 模型的数据库操作,将数据访问逻辑与业务逻辑分离。
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from tortoise.queryset import QuerySet
|
||||
@@ -44,6 +45,23 @@ class ScheduleRepository:
|
||||
return await ScheduledJob.filter(plugin_name=plugin_name).all()
|
||||
return await ScheduledJob.all()
|
||||
|
||||
@staticmethod
|
||||
async def update_run_status(schedule: ScheduledJob, is_success: bool):
|
||||
"""
|
||||
统一更新任务的最后运行状态
|
||||
"""
|
||||
schedule.last_run_at = datetime.now()
|
||||
if is_success:
|
||||
schedule.last_run_status = "SUCCESS"
|
||||
schedule.consecutive_failures = 0
|
||||
else:
|
||||
schedule.last_run_status = "FAILURE"
|
||||
schedule.consecutive_failures = (schedule.consecutive_failures or 0) + 1
|
||||
|
||||
await schedule.save(
|
||||
update_fields=["last_run_at", "last_run_status", "consecutive_failures"]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def save(schedule: ScheduledJob, update_fields: list[str] | None = None):
|
||||
"""
|
||||
|
||||
@@ -11,6 +11,9 @@ from nonebot.adapters import Bot
|
||||
|
||||
from zhenxun.services.tags import tag_manager
|
||||
|
||||
from .engine import APSchedulerAdapter
|
||||
from .repository import ScheduleRepository
|
||||
|
||||
__all__ = [
|
||||
"ScheduleTargeter",
|
||||
"_resolve_all_groups",
|
||||
@@ -22,24 +25,29 @@ __all__ = [
|
||||
|
||||
|
||||
async def _resolve_group(target_identifier: str, bot: Bot) -> list[str | None]:
|
||||
"""解析 GROUP 类型的执行目标。"""
|
||||
return [target_identifier]
|
||||
|
||||
|
||||
async def _resolve_tag(target_identifier: str, bot: Bot) -> list[str | None]:
|
||||
"""解析 TAG 类型的标签执行目标。"""
|
||||
result = await tag_manager.resolve_tag_to_group_ids(target_identifier)
|
||||
return result # type: ignore
|
||||
|
||||
|
||||
async def _resolve_user(target_identifier: str, bot: Bot) -> list[str | None]:
|
||||
"""解析 USER 类型的执行目标。"""
|
||||
return [target_identifier]
|
||||
|
||||
|
||||
async def _resolve_all_groups(target_identifier: str, bot: Bot) -> list[str | None]:
|
||||
"""解析 ALL_GROUPS 类型的全群执行目标。"""
|
||||
result = await tag_manager.resolve_tag_to_group_ids("@all", bot=bot)
|
||||
return result
|
||||
|
||||
|
||||
async def _resolve_global_or_user(target_identifier: str, bot: Bot) -> list[str | None]:
|
||||
"""解析 GLOBAL 类型的全局执行目标。"""
|
||||
return [None]
|
||||
|
||||
|
||||
@@ -66,8 +74,6 @@ class ScheduleTargeter:
|
||||
返回:
|
||||
list[ScheduledJob]: 符合过滤条件的任务列表。
|
||||
"""
|
||||
from .repository import ScheduleRepository
|
||||
|
||||
query = ScheduleRepository.filter(**self._filters)
|
||||
return await query.all()
|
||||
|
||||
@@ -147,9 +153,6 @@ class ScheduleTargeter:
|
||||
返回:
|
||||
tuple[int, str]: (成功移除的任务数量, 操作结果消息)。
|
||||
"""
|
||||
from .engine import APSchedulerAdapter
|
||||
from .repository import ScheduleRepository
|
||||
|
||||
schedules = await self._get_schedules()
|
||||
if not schedules:
|
||||
target_desc = self._generate_target_description()
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BaseTrigger(BaseModel):
|
||||
"""触发器配置的基类"""
|
||||
|
||||
trigger_type: str = Field(..., exclude=True)
|
||||
|
||||
|
||||
class CronTrigger(BaseTrigger):
|
||||
"""Cron 触发器配置"""
|
||||
|
||||
trigger_type: Literal["cron"] = "cron" # type: ignore
|
||||
year: int | str | None = None
|
||||
month: int | str | None = None
|
||||
day: int | str | None = None
|
||||
week: int | str | None = None
|
||||
day_of_week: int | str | None = None
|
||||
hour: int | str | None = None
|
||||
minute: int | str | None = None
|
||||
second: int | str | None = None
|
||||
start_date: datetime | str | None = None
|
||||
end_date: datetime | str | None = None
|
||||
timezone: str | None = None
|
||||
jitter: int | None = None
|
||||
|
||||
|
||||
class IntervalTrigger(BaseTrigger):
|
||||
"""Interval 触发器配置"""
|
||||
|
||||
trigger_type: Literal["interval"] = "interval" # type: ignore
|
||||
weeks: int = 0
|
||||
days: int = 0
|
||||
hours: int = 0
|
||||
minutes: int = 0
|
||||
seconds: int = 0
|
||||
start_date: datetime | str | None = None
|
||||
end_date: datetime | str | None = None
|
||||
timezone: str | None = None
|
||||
jitter: int | None = None
|
||||
|
||||
|
||||
class DateTrigger(BaseTrigger):
|
||||
"""Date 触发器配置"""
|
||||
|
||||
trigger_type: Literal["date"] = "date" # type: ignore
|
||||
run_date: datetime | str
|
||||
timezone: str | None = None
|
||||
|
||||
|
||||
class Trigger:
|
||||
"""
|
||||
一个用于创建类型安全触发器配置的工厂类。
|
||||
提供了流畅的、具备IDE自动补全功能的API。
|
||||
|
||||
使用示例:
|
||||
from zhenxun.services.scheduler import Trigger
|
||||
|
||||
@scheduler.job(trigger=Trigger.cron(hour=8))
|
||||
async def my_task():
|
||||
...
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def cron(**kwargs) -> CronTrigger:
|
||||
"""创建一个 Cron 触发器配置。"""
|
||||
return CronTrigger(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def interval(**kwargs) -> IntervalTrigger:
|
||||
"""创建一个 Interval 触发器配置。"""
|
||||
return IntervalTrigger(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def date(**kwargs) -> DateTrigger:
|
||||
"""创建一个 Date 触发器配置。"""
|
||||
return DateTrigger(**kwargs)
|
||||
@@ -4,56 +4,101 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from zhenxun.utils.pydantic_compat import model_validate
|
||||
|
||||
|
||||
class TargetType(str, Enum):
|
||||
"""定时任务执行目标类型枚举"""
|
||||
|
||||
GLOBAL = "GLOBAL"
|
||||
"""全局"""
|
||||
ALL_GROUPS = "ALL_GROUPS"
|
||||
"""所有群组"""
|
||||
GROUP = "GROUP"
|
||||
"""群组"""
|
||||
USER = "USER"
|
||||
"""用户"""
|
||||
TAG = "TAG"
|
||||
"""标签"""
|
||||
|
||||
|
||||
class BaseTrigger(BaseModel):
|
||||
"""触发器配置的基类"""
|
||||
|
||||
trigger_type: str = Field(..., exclude=True)
|
||||
"""触发器类型"""
|
||||
|
||||
|
||||
class CronTrigger(BaseTrigger):
|
||||
"""Cron 触发器配置"""
|
||||
|
||||
trigger_type: Literal["cron"] = "cron" # type: ignore
|
||||
"""触发器类型"""
|
||||
year: int | str | None = None
|
||||
"""年"""
|
||||
month: int | str | None = None
|
||||
"""月"""
|
||||
day: int | str | None = None
|
||||
"""日"""
|
||||
week: int | str | None = None
|
||||
"""周"""
|
||||
day_of_week: int | str | None = None
|
||||
"""星期几"""
|
||||
hour: int | str | None = None
|
||||
"""小时"""
|
||||
minute: int | str | None = None
|
||||
"""分钟"""
|
||||
second: int | str | None = None
|
||||
"""秒"""
|
||||
start_date: datetime | str | None = None
|
||||
"""开始日期"""
|
||||
end_date: datetime | str | None = None
|
||||
"""结束日期"""
|
||||
timezone: str | None = None
|
||||
"""时区"""
|
||||
jitter: int | None = None
|
||||
"""运行抖动时间"""
|
||||
|
||||
|
||||
class IntervalTrigger(BaseTrigger):
|
||||
"""Interval 触发器配置"""
|
||||
|
||||
trigger_type: Literal["interval"] = "interval" # type: ignore
|
||||
"""触发器类型"""
|
||||
weeks: int = 0
|
||||
"""周数"""
|
||||
days: int = 0
|
||||
"""天数"""
|
||||
hours: int = 0
|
||||
"""小时数"""
|
||||
minutes: int = 0
|
||||
"""分钟数"""
|
||||
seconds: int = 0
|
||||
"""秒数"""
|
||||
start_date: datetime | str | None = None
|
||||
"""开始日期"""
|
||||
end_date: datetime | str | None = None
|
||||
"""结束日期"""
|
||||
timezone: str | None = None
|
||||
"""时区"""
|
||||
jitter: int | None = None
|
||||
"""运行抖动时间"""
|
||||
|
||||
|
||||
class DateTrigger(BaseTrigger):
|
||||
"""Date 触发器配置"""
|
||||
|
||||
trigger_type: Literal["date"] = "date" # type: ignore
|
||||
"""触发器类型"""
|
||||
run_date: datetime | str
|
||||
"""运行日期"""
|
||||
timezone: str | None = None
|
||||
"""时区"""
|
||||
|
||||
|
||||
class Trigger:
|
||||
@@ -83,18 +128,18 @@ class ExecutionOptions(BaseModel):
|
||||
封装定时任务的执行策略,包括重试和回调。
|
||||
"""
|
||||
|
||||
jitter: int | None = Field(None, description="触发时间抖动(秒)")
|
||||
spread: int | None = Field(
|
||||
None, description="(并发模式)多目标执行的最大分散延迟(秒)"
|
||||
)
|
||||
interval: int | None = Field(
|
||||
None, description="多目标执行的固定间隔(秒),设置后将强制串行执行"
|
||||
)
|
||||
concurrency_policy: Literal["ALLOW", "SKIP", "QUEUE"] = Field(
|
||||
"ALLOW", description="并发策略"
|
||||
)
|
||||
jitter: int | None = Field(None)
|
||||
"""触发时间抖动(秒)"""
|
||||
spread: int | None = Field(None)
|
||||
"""(并发模式)多目标执行的最大分散延迟(秒)"""
|
||||
interval: int | None = Field(None)
|
||||
"""多目标执行的固定间隔(秒),设置后将强制串行执行"""
|
||||
concurrency_policy: Literal["ALLOW", "SKIP", "QUEUE"] = Field("ALLOW")
|
||||
"""并发策略"""
|
||||
retries: int = 0
|
||||
"""重试次数"""
|
||||
retry_delay_seconds: int = 30
|
||||
"""重试延迟时间(秒)"""
|
||||
|
||||
|
||||
class ScheduleContext(BaseModel):
|
||||
@@ -102,12 +147,20 @@ class ScheduleContext(BaseModel):
|
||||
定时任务执行上下文,可通过依赖注入获取。
|
||||
"""
|
||||
|
||||
schedule_id: int = Field(..., description="数据库中的任务ID")
|
||||
plugin_name: str = Field(..., description="任务所属的插件名称")
|
||||
bot_id: str | None = Field(None, description="执行任务的Bot ID")
|
||||
platform_scope: str | None = Field(None, description="执行任务的细粒度平台作用域")
|
||||
group_id: str | None = Field(None, description="当前执行实例的目标群组ID")
|
||||
job_kwargs: dict = Field(default_factory=dict, description="任务配置的参数")
|
||||
schedule_id: int = Field(...)
|
||||
"""数据库中的任务ID"""
|
||||
plugin_name: str = Field(...)
|
||||
"""任务所属的插件名称"""
|
||||
bot_id: str | None = Field(None)
|
||||
"""执行任务的Bot ID"""
|
||||
platform_scope: str | None = Field(None)
|
||||
"""执行任务的细粒度平台作用域"""
|
||||
group_id: str | None = Field(None)
|
||||
"""当前执行实例的目标群组ID"""
|
||||
user_id: str | None = None
|
||||
"""当前执行实例的目标用户ID(私聊场景)"""
|
||||
job_kwargs: dict = Field(default_factory=dict)
|
||||
"""任务配置的参数"""
|
||||
|
||||
|
||||
class ExecutionPolicy(BaseModel):
|
||||
@@ -116,13 +169,50 @@ class ExecutionPolicy(BaseModel):
|
||||
"""
|
||||
|
||||
retries: int = 0
|
||||
"""重试次数"""
|
||||
retry_delay_seconds: int = 30
|
||||
"""重试延迟时间(秒)"""
|
||||
retry_backoff: bool = False
|
||||
"""是否使用退避算法延迟重试"""
|
||||
retry_on_exceptions: list[type[Exception]] | None = None
|
||||
"""触发重试的异常类型列表"""
|
||||
on_success_callback: Callable[[ScheduleContext, Any], Awaitable[None]] | None = None
|
||||
"""任务执行成功后的回调函数"""
|
||||
on_failure_callback: (
|
||||
Callable[[ScheduleContext, Exception], Awaitable[None]] | None
|
||||
) = None
|
||||
"""任务执行失败后的回调函数"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
|
||||
class JobConfig(BaseModel):
|
||||
"""
|
||||
定时任务参数配置聚合实体 (Parameter Object)。
|
||||
封装了除核心身份标识(插件名、目标类型)之外的所有调度配置。
|
||||
"""
|
||||
|
||||
trigger: BaseTrigger
|
||||
"""触发器配置"""
|
||||
job_kwargs: dict[str, Any] = Field(default_factory=dict)
|
||||
"""任务执行参数"""
|
||||
bot_id: str | None = None
|
||||
"""绑定的 Bot ID"""
|
||||
name: str | None = None
|
||||
"""定时任务名称"""
|
||||
created_by: str | None = None
|
||||
"""任务创建者标识"""
|
||||
required_permission: int = 5
|
||||
"""执行任务所需的权限等级"""
|
||||
source: str = "USER"
|
||||
"""任务来源"""
|
||||
is_one_off: bool = False
|
||||
"""是否为一次性任务"""
|
||||
execution_options: ExecutionOptions = Field(
|
||||
default_factory=lambda: model_validate(ExecutionOptions, {})
|
||||
)
|
||||
"""执行控制选项配置"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
@@ -132,10 +222,15 @@ class ScheduledJobDeclaration(BaseModel):
|
||||
"""用于在启动时声明默认定时任务的内部数据模型"""
|
||||
|
||||
plugin_name: str
|
||||
"""插件名称"""
|
||||
group_id: str | None
|
||||
"""绑定的群组 ID"""
|
||||
bot_id: str | None
|
||||
"""绑定的 Bot ID"""
|
||||
trigger: BaseTrigger
|
||||
"""触发器配置"""
|
||||
job_kwargs: dict[str, Any]
|
||||
"""任务执行参数"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
@@ -145,8 +240,11 @@ class EphemeralJobDeclaration(BaseModel):
|
||||
"""用于在启动时声明临时任务的内部数据模型"""
|
||||
|
||||
plugin_name: str
|
||||
"""插件名称"""
|
||||
func: Callable[..., Awaitable[Any]]
|
||||
"""临时任务执行的异步函数"""
|
||||
trigger: BaseTrigger
|
||||
"""触发器配置"""
|
||||
|
||||
class Config:
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
Reference in New Issue
Block a user