mirror of
https://github.com/zhenxun-org/zhenxun_bot.git
synced 2026-10-11 15:00:00 +08:00
♻️ refactor(scheduler): 重构定时任务系统并增强功能 (#2009)
检查bot是否运行正常 / bot check (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Sequential Lint and Type Check / ruff-call (push) Waiting to run
Sequential Lint and Type Check / pyright-call (push) Blocked by required conditions
Release Drafter / Update Release Draft (push) Waiting to run
Force Sync to Aliyun / sync (push) Waiting to run
Update Version / update-version (push) Waiting to run
检查bot是否运行正常 / bot check (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CodeQL Code Security Analysis / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
Sequential Lint and Type Check / ruff-call (push) Waiting to run
Sequential Lint and Type Check / pyright-call (push) Blocked by required conditions
Release Drafter / Update Release Draft (push) Waiting to run
Force Sync to Aliyun / sync (push) Waiting to run
Update Version / update-version (push) Waiting to run
* ♻️ refactor(scheduler): 重构定时任务系统并增强功能 - 【模型重命名】将 `ScheduleInfo` 模型及其数据库表重命名为 `ScheduledJob`,以提高语义清晰度。 - 【触发器抽象】引入 `Trigger` 工厂类,提供类型安全的 Cron、Interval 和 Date 触发器配置。 - 【执行策略】新增 `ExecutionPolicy` 模型,允许为定时任务定义重试策略、延迟、异常类型以及成功/失败回调。 - 【任务执行】重构任务执行逻辑,支持 NoneBot 的依赖注入,并根据 `ExecutionPolicy` 处理任务的重试和回调。 - 【临时任务】增加声明式和编程式的临时任务调度能力,支持非持久化任务在运行时动态创建和执行。 - 【管理命令】更新定时任务管理命令 (`schedule_admin`),使其适配新的 `ScheduledJob` 模型和参数验证逻辑。 - 【展示优化】改进定时任务列表和状态展示,使用新的触发器格式化逻辑和参数模型信息。 - 【重试装饰器】为 `Retry.api` 装饰器添加 `on_success` 回调,允许在任务成功执行后触发额外操作。 * 🚨 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
67a2560de4
commit
be86e0bb7f
@@ -7,10 +7,11 @@ from nonebot.permission import SUPERUSER
|
||||
from nonebot_plugin_alconna import AlconnaMatch, Arparma, Match, Query
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from zhenxun.models.schedule_info import ScheduleInfo
|
||||
from zhenxun.models.scheduled_job import ScheduledJob
|
||||
from zhenxun.services.scheduler import scheduler_manager
|
||||
from zhenxun.services.scheduler.targeter import ScheduleTargeter
|
||||
from zhenxun.utils.message import MessageUtils
|
||||
from zhenxun.utils.pydantic_compat import model_dump
|
||||
|
||||
from . import presenters
|
||||
from .commands import (
|
||||
@@ -149,7 +150,10 @@ async def handle_set(
|
||||
|
||||
job_kwargs = {}
|
||||
if kwargs_str.available:
|
||||
task_meta = scheduler_manager._registered_tasks[p_name]
|
||||
task_meta = scheduler_manager._registered_tasks.get(p_name)
|
||||
if not task_meta:
|
||||
await schedule_cmd.finish(f"插件 '{p_name}' 未注册。")
|
||||
|
||||
params_model = task_meta.get("model")
|
||||
if not (
|
||||
params_model
|
||||
@@ -168,11 +172,7 @@ async def handle_set(
|
||||
|
||||
validated_model = model_validate(raw_kwargs)
|
||||
|
||||
model_dump = getattr(validated_model, "model_dump", None)
|
||||
if not model_dump:
|
||||
await schedule_cmd.finish(f"插件 '{p_name}' 的参数模型不支持导出")
|
||||
|
||||
job_kwargs = model_dump()
|
||||
job_kwargs = model_dump(validated_model)
|
||||
except ValidationError as e:
|
||||
errors = [f" - {err['loc'][0]}: {err['msg']}" for err in e.errors()]
|
||||
await schedule_cmd.finish(
|
||||
@@ -220,7 +220,7 @@ async def handle_set(
|
||||
|
||||
@schedule_cmd.assign("删除")
|
||||
async def handle_delete(targeter: ScheduleTargeter = GetTargeter("删除")):
|
||||
schedules_to_remove: list[ScheduleInfo] = await targeter._get_schedules()
|
||||
schedules_to_remove: list[ScheduledJob] = await targeter._get_schedules()
|
||||
if not schedules_to_remove:
|
||||
await schedule_cmd.finish("没有找到可删除的任务。")
|
||||
|
||||
@@ -239,7 +239,7 @@ async def handle_delete(targeter: ScheduleTargeter = GetTargeter("删除")):
|
||||
|
||||
@schedule_cmd.assign("暂停")
|
||||
async def handle_pause(targeter: ScheduleTargeter = GetTargeter("暂停")):
|
||||
schedules_to_pause: list[ScheduleInfo] = await targeter._get_schedules()
|
||||
schedules_to_pause: list[ScheduledJob] = await targeter._get_schedules()
|
||||
if not schedules_to_pause:
|
||||
await schedule_cmd.finish("没有找到可暂停的任务。")
|
||||
|
||||
@@ -258,7 +258,7 @@ async def handle_pause(targeter: ScheduleTargeter = GetTargeter("暂停")):
|
||||
|
||||
@schedule_cmd.assign("恢复")
|
||||
async def handle_resume(targeter: ScheduleTargeter = GetTargeter("恢复")):
|
||||
schedules_to_resume: list[ScheduleInfo] = await targeter._get_schedules()
|
||||
schedules_to_resume: list[ScheduledJob] = await targeter._get_schedules()
|
||||
if not schedules_to_resume:
|
||||
await schedule_cmd.finish("没有找到可恢复的任务。")
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from zhenxun.models.schedule_info import ScheduleInfo
|
||||
from zhenxun.models.scheduled_job import ScheduledJob
|
||||
from zhenxun.services.scheduler import scheduler_manager
|
||||
from zhenxun.utils._image_template import ImageTemplate, RowStyle
|
||||
from zhenxun.utils.pydantic_compat import model_json_schema
|
||||
|
||||
|
||||
def _get_type_name(annotation) -> str:
|
||||
@@ -15,10 +17,17 @@ def _get_type_name(annotation) -> str:
|
||||
return str(annotation)
|
||||
|
||||
|
||||
def _format_trigger(schedule: dict) -> str:
|
||||
"""格式化触发器信息为可读字符串"""
|
||||
trigger_type = schedule.get("trigger_type")
|
||||
config = schedule.get("trigger_config")
|
||||
def _get_schedule_attr(schedule: ScheduledJob | dict, attr_name: str) -> Any:
|
||||
"""兼容地从字典或对象获取属性"""
|
||||
if isinstance(schedule, dict):
|
||||
return schedule.get(attr_name)
|
||||
return getattr(schedule, attr_name, None)
|
||||
|
||||
|
||||
def _format_trigger_info(schedule: ScheduledJob | dict) -> str:
|
||||
"""格式化触发器信息为可读字符串(兼容字典和对象)"""
|
||||
trigger_type = _get_schedule_attr(schedule, "trigger_type")
|
||||
config = _get_schedule_attr(schedule, "trigger_config")
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return f"配置错误: {config}"
|
||||
@@ -51,59 +60,15 @@ def _format_trigger(schedule: dict) -> str:
|
||||
return f"未知触发器类型: {trigger_type}"
|
||||
|
||||
|
||||
def _format_trigger_for_card(schedule_info: ScheduleInfo | dict) -> str:
|
||||
"""为信息卡片格式化触发器规则"""
|
||||
trigger_type = (
|
||||
schedule_info.get("trigger_type")
|
||||
if isinstance(schedule_info, dict)
|
||||
else schedule_info.trigger_type
|
||||
)
|
||||
config = (
|
||||
schedule_info.get("trigger_config")
|
||||
if isinstance(schedule_info, dict)
|
||||
else schedule_info.trigger_config
|
||||
)
|
||||
|
||||
if not isinstance(config, dict):
|
||||
return f"配置错误: {config}"
|
||||
|
||||
if trigger_type == "cron":
|
||||
hour = config.get("hour", "??")
|
||||
minute = config.get("minute", "??")
|
||||
try:
|
||||
hour_int = int(hour)
|
||||
minute_int = int(minute)
|
||||
return f"每天 {hour_int:02d}:{minute_int:02d}"
|
||||
except (ValueError, TypeError):
|
||||
return f"每天 {hour}:{minute}"
|
||||
elif trigger_type == "interval":
|
||||
units = {
|
||||
"weeks": "周",
|
||||
"days": "天",
|
||||
"hours": "小时",
|
||||
"minutes": "分钟",
|
||||
"seconds": "秒",
|
||||
}
|
||||
for unit, unit_name in units.items():
|
||||
if value := config.get(unit):
|
||||
return f"每 {value} {unit_name}"
|
||||
return "未知间隔"
|
||||
elif trigger_type == "date":
|
||||
run_date = config.get("run_date", "N/A")
|
||||
return f"特定时间 {run_date}"
|
||||
else:
|
||||
return f"未知规则: {trigger_type}"
|
||||
|
||||
|
||||
def _format_operation_result_card(
|
||||
title: str, schedule_info: ScheduleInfo, extra_info: list[str] | None = None
|
||||
title: str, schedule_info: ScheduledJob, extra_info: list[str] | None = None
|
||||
) -> str:
|
||||
"""
|
||||
生成一个标准的操作结果信息卡片。
|
||||
|
||||
参数:
|
||||
title: 卡片的标题 (例如 "✅ 成功暂停定时任务!")
|
||||
schedule_info: 相关的 ScheduleInfo 对象
|
||||
schedule_info: 相关的 ScheduledJob 对象
|
||||
extra_info: (可选) 额外的补充信息行
|
||||
"""
|
||||
target_desc = (
|
||||
@@ -120,7 +85,7 @@ def _format_operation_result_card(
|
||||
f"✓ 任务 ID: {schedule_info.id}",
|
||||
f"🖋 插件: {schedule_info.plugin_name}",
|
||||
f"🎯 目标: {target_desc}",
|
||||
f"⏰ 时间: {_format_trigger_for_card(schedule_info)}",
|
||||
f"⏰ 时间: {_format_trigger_info(schedule_info)}",
|
||||
]
|
||||
if extra_info:
|
||||
info_lines.extend(extra_info)
|
||||
@@ -128,27 +93,27 @@ def _format_operation_result_card(
|
||||
return "\n".join(info_lines)
|
||||
|
||||
|
||||
def format_pause_success(schedule_info: ScheduleInfo) -> str:
|
||||
def format_pause_success(schedule_info: ScheduledJob) -> str:
|
||||
"""格式化暂停成功的消息"""
|
||||
return _format_operation_result_card("✅ 成功暂停定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_resume_success(schedule_info: ScheduleInfo) -> str:
|
||||
def format_resume_success(schedule_info: ScheduledJob) -> str:
|
||||
"""格式化恢复成功的消息"""
|
||||
return _format_operation_result_card("▶️ 成功恢复定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_remove_success(schedule_info: ScheduleInfo) -> str:
|
||||
def format_remove_success(schedule_info: ScheduledJob) -> str:
|
||||
"""格式化删除成功的消息"""
|
||||
return _format_operation_result_card("❌ 成功删除定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_trigger_success(schedule_info: ScheduleInfo) -> str:
|
||||
def format_trigger_success(schedule_info: ScheduledJob) -> str:
|
||||
"""格式化手动触发成功的消息"""
|
||||
return _format_operation_result_card("🚀 成功手动触发定时任务!", schedule_info)
|
||||
|
||||
|
||||
def format_update_success(schedule_info: ScheduleInfo) -> str:
|
||||
def format_update_success(schedule_info: ScheduledJob) -> str:
|
||||
"""格式化更新成功的消息"""
|
||||
return _format_operation_result_card("🔄️ 成功更新定时任务配置!", schedule_info)
|
||||
|
||||
@@ -174,7 +139,7 @@ def _format_params(schedule_status: dict) -> str:
|
||||
|
||||
|
||||
async def format_schedule_list_as_image(
|
||||
schedules: list[ScheduleInfo], title: str, current_page: int
|
||||
schedules: list[ScheduledJob], title: str, current_page: int
|
||||
):
|
||||
"""将任务列表格式化为图片"""
|
||||
page_size = 15
|
||||
@@ -204,7 +169,7 @@ async def format_schedule_list_as_image(
|
||||
s.get("bot_id") or "N/A",
|
||||
s["group_id"] or "全局",
|
||||
s["next_run_time"],
|
||||
_format_trigger(s),
|
||||
_format_trigger_info(s),
|
||||
_format_params(s),
|
||||
get_status_text(s["is_enabled"]),
|
||||
]
|
||||
@@ -235,7 +200,7 @@ def format_single_status_message(status: dict) -> str:
|
||||
f"▫️ 目标: {status['group_id'] or '全局'}",
|
||||
f"▫️ 状态: {'✔️ 已启用' if status['is_enabled'] else '⏸️ 已暂停'}",
|
||||
f"▫️ 下次运行: {status['next_run_time']}",
|
||||
f"▫️ 触发规则: {_format_trigger(status)}",
|
||||
f"▫️ 触发规则: {_format_trigger_info(status)}",
|
||||
f"▫️ 任务参数: {_format_params(status)}",
|
||||
]
|
||||
return "\n".join(info_lines)
|
||||
@@ -260,11 +225,12 @@ async def format_plugins_list() -> str:
|
||||
and isinstance(params_model, type)
|
||||
and issubclass(params_model, BaseModel)
|
||||
):
|
||||
model_fields = getattr(params_model, "model_fields", None)
|
||||
if model_fields:
|
||||
schema = model_json_schema(params_model)
|
||||
properties = schema.get("properties", {})
|
||||
if properties:
|
||||
param_info_str = "参数: " + ", ".join(
|
||||
f"{field_name}({_get_type_name(field_info.annotation)})"
|
||||
for field_name, field_info in model_fields.items()
|
||||
f"{field_name}({prop.get('type', 'any')})"
|
||||
for field_name, prop in properties.items()
|
||||
)
|
||||
elif params_model:
|
||||
param_info_str = "⚠️ 参数模型配置错误"
|
||||
|
||||
Reference in New Issue
Block a user