├── __init__.py ├── requirements.txt ├── .gitignore ├── config.json.template ├── tools.py ├── README.md └── task_scheduler.py /__init__.py: -------------------------------------------------------------------------------- 1 | from .task_scheduler import * -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-dateutil 2 | apscheduler 3 | SQLAlchemy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tasks.db 2 | config.json 3 | __pycache__/ 4 | .venv/ 5 | .vscode/ 6 | *.pyc 7 | *.pyo 8 | *.pyd 9 | *.pyw 10 | *.pyz 11 | *.pyj 12 | -------------------------------------------------------------------------------- /config.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "command_prefix": "task", 3 | "allow_call_other_plugins": true, 4 | "custom_commands": [ 5 | { 6 | "key_word": "早报", 7 | "command_prefix": "$tool " 8 | }, 9 | { 10 | "key_word": "点歌", 11 | "command_prefix": "$" 12 | }, 13 | { 14 | "key_word": "任务列表", 15 | "command_prefix": "$task " 16 | } 17 | ], 18 | "max_workers": 20 19 | } 20 | -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | from config import conf 2 | from common.log import logger 3 | 4 | 5 | class WrappedChannelTools: 6 | # itchat 的 UserName 其实都是 id 7 | def __init__(self): 8 | channel_type = conf().get("channel_type", "wx") 9 | if channel_type == "wx": 10 | from lib import itchat 11 | self.channel = itchat 12 | self.channel_type = "wx" 13 | elif channel_type == "ntchat": 14 | try: 15 | from channel.wechatnt.ntchat_channel import wechatnt 16 | self.channel = wechatnt 17 | self.channel_type = "ntchat" 18 | except Exception as e: 19 | logger.error(f"未安装ntchat: {e}") 20 | else: 21 | raise ValueError(f"不支持的channel_type: {channel_type}") 22 | 23 | def get_user_id_by_name(self, name): 24 | id = None 25 | if self.channel_type == "wx": 26 | friends = self.channel.search_friends(name=name) 27 | if not friends: 28 | self.channel.get_friends(update=True) 29 | friends = self.channel.search_friends(name=name) 30 | if not friends: 31 | return None 32 | return friends[0].get("UserName") 33 | 34 | elif self.channel_type == "ntchat": 35 | pass 36 | 37 | raise ValueError(f"不支持的channel_type: {self.channel_type}") 38 | 39 | 40 | # 根据群名称获取群ID 41 | def get_group_id_by_name(self, name:str): 42 | if self.channel_type == "wx": 43 | groups = self.channel.search_chatrooms(name=name) 44 | if not groups: 45 | self.channel.get_chatrooms(update=True) 46 | groups = self.channel.search_chatrooms(name=name) 47 | if not groups: 48 | return None 49 | for group in groups: 50 | if group.get("NickName") == name: 51 | return group.get("UserName") 52 | return None 53 | elif self.channel_type == "ntchat": 54 | pass 55 | raise ValueError(f"不支持的channel_type: {self.channel_type}") -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TaskScheduler 2 | 3 | ## 简介 4 | 5 | `TaskScheduler` 是一个用于定时执行任务或调用其他插件的插件,适用于 [chatgpt-on-wechat](https://github.com/zhayujie/chatgpt-on-wechat)。它基于 `APScheduler` 库,提供了灵活的任务管理和调度功能。 6 | 7 | ## 功能特性 8 | 9 | - **定时任务**:支持基于 Cron 表达式、具体日期、周期性等多种方式触发任务。 10 | - **任务管理**:可以添加、取消和列出任务。 11 | - **插件调用**:支持在定时任务中调用其他插件的功能。 12 | - **群组支持**:可以在指定群聊中触发任务 13 | 14 | ## 安装与配置 15 | 16 | ### 安装 17 | 18 | 1. 将 `TaskScheduler` 插件文件夹放置在插件目录中。 19 | 2. 确保已安装所有的依赖: 20 | ```bash 21 | pip install -r plugins/TaskScheduler/requirements.txt 22 | ``` 23 | 24 | ### 配置 25 | 26 | 1. 在插件目录中创建 `config.json` 文件,配置示例如下: 27 | ```json 28 | { 29 | "max_workers": 30, 30 | "command_prefix": "task", 31 | "allow_call_other_plugins": true, 32 | "custom_commands": [ 33 | { 34 | "key_word": "早报", 35 | "command_prefix": "$tool " 36 | }, 37 | { 38 | "key_word": "点歌", 39 | "command_prefix": "$" 40 | }, 41 | { 42 | "key_word": "任务列表", 43 | "command_prefix": "$task " 44 | } 45 | ] 46 | } 47 | ``` 48 | 2. 配置说明: 49 | - `max_workers`:线程池的最大工作线程数。 50 | - `command_prefix`:触发任务的命令前缀。 51 | - `allow_call_other_plugins`:是否允许在任务中调用其他插件。 52 | - `custom_commands`:自定义命令配置,用于在任务中调用特定插件。 53 | 54 | ## 使用方法 55 | 56 | 命令与 [haikerapples/timetask](https://github.com/haikerapples/timetask) 基本相同 57 | 58 | ### 添加任务 59 | 60 | #### 命令格式 61 | 62 | - **Cron 表达式**:`$task cron[* * * * *] event_str` 63 | - **周期性任务**:`$task cycle time_str event_str` 64 | 65 | 可选参数: 66 | - `group[group_name]`:指定群聊 67 | 68 | #### 示例 69 | 70 | - 每天 8:30 执行任务: 71 | ``` 72 | $task 每天 08:30 提醒我开会 73 | ``` 74 | - 使用 Cron 表达式: 75 | ``` 76 | $task cron[30 8 * * *] 提醒我开会 77 | ``` 78 | - 指定群聊(暂时只支持 itchat): 79 | ``` 80 | $task 每天 08:30 提醒下班 group[工作群] 81 | ``` 82 | - 调用其他插件: 83 | ``` 84 | $task 每天 08:30 $tool 早报 85 | ``` 86 | 87 | ### 取消任务 88 | 89 | #### 命令格式 90 | 91 | ``` 92 | $task 取消任务 task_id 93 | ``` 94 | 95 | #### 示例 96 | 97 | ``` 98 | $task 取消任务 1234567 99 | ``` 100 | 101 | ### 列出任务 102 | 103 | #### 命令格式 104 | 105 | ``` 106 | $task 任务列表 107 | ``` 108 | 109 | 110 | ## 注意事项 111 | 112 | 必须合并 https://github.com/zhayujie/chatgpt-on-wechat/pull/2413 以及 https://github.com/zhayujie/chatgpt-on-wechat/pull/2407 才能保证正常工作,否则 `reloadp` 和 `scanp` 会造成任务重复执行。 113 | -------------------------------------------------------------------------------- /task_scheduler.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | import hashlib 3 | import time 4 | from typing import Optional 5 | from apscheduler.jobstores.base import JobLookupError 6 | from apscheduler.schedulers.background import BackgroundScheduler 7 | 8 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore 9 | from apscheduler.executors.pool import ThreadPoolExecutor 10 | from apscheduler.triggers.cron import CronTrigger 11 | from apscheduler.triggers.date import DateTrigger 12 | 13 | from channel.chat_channel import ChatChannel 14 | from channel.chat_message import ChatMessage 15 | from channel.channel_factory import create_channel 16 | import datetime 17 | import re 18 | import uuid 19 | 20 | from bridge.reply import Reply, ReplyType 21 | import plugins 22 | from common.log import logger 23 | from bridge.context import ContextType 24 | from plugins import * 25 | 26 | 27 | from .tools import WrappedChannelTools 28 | 29 | current_dir = os.path.dirname(os.path.abspath(__file__)) 30 | db_path = os.path.join(current_dir, "tasks.db") 31 | 32 | class CheckedThreadPoolExecutor(ThreadPoolExecutor): 33 | def _do_submit_job(self, job, run_times): 34 | if not self._check_conditions(job): 35 | print(f"Job {job.id} conditions not met, skipping") 36 | return 37 | 38 | return super()._do_submit_job(job, run_times) 39 | 40 | def _check_conditions(self, job): 41 | # 单例的 42 | if not plugin_manager.PluginManager().pconf['plugins']['TaskScheduler']['enabled']: 43 | return False 44 | 45 | msg:ChatMessage = job.args[3] 46 | channel = create_channel(conf().get("channel_type")) 47 | channel_tools = WrappedChannelTools() 48 | 49 | if isinstance(channel, ChatChannel): 50 | bot_user_id = channel.user_id 51 | if bot_user_id is None: 52 | return False 53 | logger.info("检查 id 是否变化") 54 | if msg.to_user_id != bot_user_id: 55 | # id 发生变化,尝试根据昵称更新(当然这是不可靠的, 因为昵称可以相同) 56 | logger.info("id 改变,尝试更新") 57 | try: 58 | if msg.is_group: 59 | group_id = channel_tools.get_group_id_by_name(msg.other_user_nickname) 60 | if group_id is None: 61 | raise ValueError(f"没有找到 {msg.other_user_nickname}") 62 | msg.from_user_id = group_id 63 | msg.other_user_id = group_id 64 | msg.to_user_id = bot_user_id 65 | # actual_user_id 就不更新了,反正也没用到 66 | else: 67 | friend_id = channel_tools.get_user_id_by_name(msg.other_user_nickname) 68 | if friend_id is None: 69 | raise ValueError(f"没有找到 {msg.other_user_nickname}") 70 | msg.from_user_id = friend_id 71 | msg.other_user_id = friend_id 72 | msg.to_user_id = bot_user_id 73 | job.args = (job.args[0], job.args[1], job.args[2], msg, job.args[4]) 74 | self._scheduler.modify_job(job.id, args=(job.args[0], job.args[1], job.args[2], msg, job.args[4])) 75 | except Exception as e: 76 | logger.error(f"更新任务 {job.id} 失败: {e}") 77 | else: 78 | logger.info("id 无变化,无需更新") 79 | return True 80 | 81 | @plugins.register( 82 | name="TaskScheduler", 83 | desire_priority=-1, 84 | namecn="计划任务", 85 | desc="定时执行任务或者调用其他插件", 86 | version="1.0", 87 | author="rikka", 88 | ) 89 | class TaskScheduler(Plugin): 90 | def __del__(self): 91 | logger.info("[TaskScheduler] 关闭 scheduler") 92 | self.scheduler.shutdown() 93 | 94 | def __init__(self): 95 | super().__init__() 96 | try: 97 | self.config = super().load_config() 98 | if not self.config: 99 | # 如果由插件去读 template, 不会写入全局配置, 此插件将无法正常工作 100 | # 因为 task_excute 只能读取全局配置 101 | raise ValueError("TaskScheduler 配置文件不存在") 102 | 103 | # self.handlers[Event.ON_HANDLE_CONTEXT] = weakref.WeakMethod(self.on_handle_context) 104 | self.handlers[Event.ON_HANDLE_CONTEXT] = self.on_handle_context 105 | 106 | jobstores = { 107 | "default": SQLAlchemyJobStore( 108 | url=f"sqlite:///{db_path}" 109 | ) # 使用 SQLite 存储任务,位于当前文件同级目录 110 | } 111 | executors = { 112 | "default": CheckedThreadPoolExecutor( 113 | max_workers=self.config.get("max_workers", 30) 114 | ) 115 | } 116 | self.scheduler = BackgroundScheduler( 117 | jobstores=jobstores, executors=executors 118 | ) 119 | 120 | # self.scheduler.add_listener(self.check_and_update_job, EVENT_JOB_SUBMITTED) 121 | self.scheduler.start() 122 | self.channel_tools = WrappedChannelTools() 123 | logger.info("[TaskScheduler] inited") 124 | except Exception as e: 125 | logger.error(f"[TaskScheduler] 初始化异常:{e}") 126 | raise ValueError("[TaskScheduler] init failed, ignore ") 127 | 128 | def generate_short_id(self): 129 | seed = f"{time.time()}{uuid.uuid4()}" 130 | sha1 = hashlib.sha1(seed.encode("utf-8")).hexdigest() 131 | return sha1[:7] 132 | 133 | def on_handle_context(self, e_context: EventContext): 134 | trigger_prefix = conf().get("plugin_trigger_prefix", "$") 135 | command_prefix = self.config.get("command_prefix", "time") 136 | channel_tools = WrappedChannelTools() 137 | 138 | if e_context["context"].type == ContextType.TEXT: 139 | content = e_context["context"].content 140 | if content.startswith(f"{trigger_prefix}{command_prefix}"): 141 | # 先拆分出 $time 和剩余部分 142 | _, remaining = content.split(f"{trigger_prefix}{command_prefix} ", 1) 143 | remaining = remaining.strip() 144 | 145 | operation = remaining.split(" ", 1)[0] 146 | if operation == "任务列表": 147 | self.get_task_list(e_context) 148 | elif operation == "取消任务": 149 | task_id = remaining.split(" ", 1)[1].strip() 150 | self.cancel_task(e_context, task_id) 151 | else: 152 | # 检查是否含有组名称, 必须以 group[组名称] 的形式出现, 且位于尾部 153 | match = re.search(r"group\[(.*?)\]$", remaining) 154 | if match: 155 | group_name = match.group(1) 156 | else: 157 | group_name = None 158 | # 解析剩余的部分 159 | remaining = remaining.replace(f"group[{group_name}]", "").strip() 160 | 161 | # 如果是 cron, 格式就是 cron[* * * * *] event_str 162 | if remaining.startswith("cron["): 163 | cron_end = remaining.find("]") 164 | if cron_end != -1: 165 | cron_part = remaining[: cron_end + 1] 166 | event_part = remaining[cron_end + 1 :].strip() 167 | parts = [cron_part, event_part] 168 | else: 169 | reply = Reply() 170 | reply.type = ReplyType.TEXT 171 | reply.content = "指令格式错误,cron 表达式不完整" 172 | e_context["reply"] = reply 173 | e_context.action = EventAction.BREAK_PASS 174 | logger.error("指令格式错误,cron 表达式不完整") 175 | return 176 | self.add_task( 177 | e_context, 178 | cycle=parts[0], 179 | event=parts[1], 180 | group_name=group_name, 181 | ) 182 | else: 183 | # 否则就是普通周期和时间, 格式就是 cycle time_str event_str 184 | parts = remaining.split(" ", 2) 185 | if len(parts) != 3: 186 | reply = Reply() 187 | reply.type = ReplyType.TEXT 188 | reply.content = "指令格式错误" 189 | e_context["reply"] = reply 190 | e_context.action = EventAction.BREAK_PASS 191 | logger.error("指令格式错误") 192 | return 193 | 194 | self.add_task( 195 | e_context, 196 | cycle=parts[0], 197 | time_str=parts[1], 198 | event=parts[2], 199 | group_name=group_name, 200 | ) 201 | 202 | # 解析事件与组 203 | def parse_event_and_group(self, event_str): 204 | match = re.search(r"group\[(.*?)\]", event_str) 205 | if match: 206 | group_name = match.group(1) 207 | event = re.sub(r"group\[.*?\]", "", event_str).strip() 208 | else: 209 | group_name = None 210 | event = event_str 211 | return event, group_name 212 | 213 | # 根据周期和时间生成 Trigger 214 | def get_trigger(self, cycle: str, time_str: Optional[str] = None): 215 | """ 216 | 根据周期和时间生成对应的 Trigger 217 | :param cycle: 表示周期的字符串 (例如: "每天", "2024-12-03", "cron[30 6 * * *]") 218 | :param time_str: 表示时间的字符串 (例如: "08:30") 219 | """ 220 | try: 221 | # 解析 Cron 表达式,例如 "cron[0 8 * * *]" 222 | if cycle.startswith("cron[") and cycle.endswith("]"): 223 | cron_expr = cycle[5:-1] 224 | return CronTrigger.from_crontab(cron_expr) 225 | 226 | # 解析具体日期周期,例如 "2024-12-03" 227 | if re.match(r"\d{4}-\d{2}-\d{2}", cycle): 228 | if not time_str: 229 | raise ValueError("必须提供时间字符串 time_str") 230 | date_obj = datetime.datetime.strptime(cycle, "%Y-%m-%d").date() 231 | time_obj = datetime.datetime.strptime(time_str, "%H:%M").time() 232 | run_date = datetime.datetime.combine(date_obj, time_obj) 233 | return DateTrigger(run_date=run_date) 234 | 235 | # 解析今天、明天、后天 236 | if cycle in ["今天", "明天", "后天"]: 237 | if not time_str: 238 | raise ValueError("必须提供时间字符串 time_str") 239 | offset_days = {"今天": 0, "明天": 1, "后天": 2}[cycle] 240 | base_date = datetime.datetime.today().date() + datetime.timedelta( 241 | days=offset_days 242 | ) 243 | time_obj = datetime.datetime.strptime(time_str, "%H:%M").time() 244 | run_date = datetime.datetime.combine(base_date, time_obj) 245 | return DateTrigger(run_date=run_date) 246 | 247 | # 解析每周指定日,例如 "每周一" 248 | if cycle.startswith("每周"): 249 | if not time_str: 250 | raise ValueError("必须提供时间字符串 time_str") 251 | weekday_name = cycle[2:] 252 | weekday_mapping = { 253 | "一": 0, 254 | "二": 1, 255 | "三": 2, 256 | "四": 3, 257 | "五": 4, 258 | "六": 5, 259 | "日": 6, 260 | } 261 | if weekday_name not in weekday_mapping: 262 | raise ValueError(f"无效的星期: {weekday_name}") 263 | weekday = weekday_mapping[weekday_name] 264 | hour, minute = map(int, time_str.split(":")) 265 | return CronTrigger(day_of_week=weekday, hour=hour, minute=minute) 266 | 267 | # 默认处理每日、工作日等周期 268 | if cycle == "每天": 269 | if not time_str: 270 | raise ValueError("必须提供时间字符串 time_str") 271 | hour, minute = map(int, time_str.split(":")) 272 | return CronTrigger(hour=hour, minute=minute) 273 | if cycle == "工作日": 274 | if not time_str: 275 | raise ValueError("必须提供时间字符串 time_str") 276 | hour, minute = map(int, time_str.split(":")) 277 | return CronTrigger(day_of_week="mon-fri", hour=hour, minute=minute) 278 | 279 | # 无法匹配的周期规则 280 | raise ValueError(f"无法解析周期: {cycle}") 281 | except Exception as e: 282 | raise ValueError(f"生成 Trigger 时出错: {e}") 283 | 284 | # 添加任务 285 | def add_task( 286 | self, 287 | e_context: EventContext, 288 | event: str, 289 | cycle: str, 290 | time_str: Optional[str] = None, 291 | group_name: Optional[str] = None, 292 | ): 293 | reply = Reply() 294 | reply.type = ReplyType.TEXT 295 | try: 296 | task_id = self.generate_short_id() 297 | # event, group_name = parse_event_and_group(event) 298 | trigger = self.get_trigger(cycle, time_str) 299 | _msg = e_context["context"]["msg"] 300 | msg = ChatMessage({}) 301 | msg.ctype = ContextType.TEXT 302 | msg.content = event 303 | 304 | # 在群聊中触发 305 | if _msg.is_group: 306 | # 提供 group_name 307 | if group_name is not None: 308 | group_id = self.channel_tools.get_group_id_by_name(group_name) 309 | if group_id is None: 310 | raise ValueError(f"没有找到 {group_name}") 311 | msg.actual_user_id = _msg.actual_user_id 312 | msg.actual_user_nickname = _msg.actual_user_nickname 313 | msg.from_user_id = group_id 314 | msg.from_user_nickname = group_name 315 | msg.other_user_id = group_id 316 | msg.other_user_nickname = group_name 317 | msg.is_group = True 318 | msg.to_user_id = _msg.to_user_id 319 | msg.to_user_nickname = _msg.to_user_nickname 320 | msg.is_at = True 321 | no_need_at = True 322 | # 没有提供 group_name 323 | else: 324 | msg.actual_user_id = _msg.actual_user_id 325 | msg.actual_user_nickname = _msg.actual_user_nickname 326 | msg.from_user_id = _msg.from_user_id 327 | msg.from_user_nickname = _msg.from_user_nickname 328 | msg.other_user_id = _msg.other_user_id 329 | msg.other_user_nickname = _msg.other_user_nickname 330 | msg.to_user_id = _msg.to_user_id 331 | msg.to_user_nickname = _msg.to_user_nickname 332 | msg.is_group = True 333 | msg.is_at = True 334 | no_need_at = False 335 | # 私聊中触发 336 | else: 337 | # 提供 group_name 338 | if group_name is not None: 339 | group_id = self.channel_tools.get_group_id_by_name(group_name) 340 | if group_id is None: 341 | raise ValueError(f"没有找到 {group_name}") 342 | 343 | msg.actual_user_id = _msg.from_user_id 344 | msg.actual_user_nickname = _msg.from_user_nickname 345 | msg.from_user_id = group_id 346 | msg.from_user_nickname = group_name 347 | msg.other_user_id = group_id 348 | msg.other_user_nickname = group_name 349 | msg.to_user_id = _msg.to_user_id 350 | msg.to_user_nickname = _msg.to_user_nickname 351 | msg.is_at = True 352 | msg.is_group = True 353 | no_need_at = True 354 | # 没有提供 group_name 355 | else: 356 | msg.from_user_id = _msg.from_user_id 357 | msg.from_user_nickname = _msg.from_user_nickname 358 | msg.other_user_id = _msg.other_user_id 359 | msg.other_user_nickname = _msg.other_user_nickname 360 | msg.to_user_id = _msg.to_user_id 361 | msg.to_user_nickname = _msg.to_user_nickname 362 | msg.is_group = False 363 | no_need_at = False 364 | 365 | self.scheduler.add_job( 366 | task_execute, 367 | trigger, 368 | id=task_id, 369 | args=( 370 | task_id, 371 | event, 372 | group_name, 373 | msg, 374 | no_need_at 375 | ), 376 | misfire_grace_time=60 377 | ) 378 | logger.info(f"任务添加成功,任务编号: {task_id}") 379 | reply.content = f"任务添加成功,任务编号: {task_id}" 380 | except Exception as e: 381 | logger.error(f"添加任务失败: {e}") 382 | reply.content = f"添加任务失败: {e}" 383 | e_context["reply"] = reply 384 | e_context.action = EventAction.BREAK_PASS 385 | 386 | # 取消任务 387 | def cancel_task(self, e_context: EventContext, task_id: str): 388 | reply = Reply() 389 | reply.type = ReplyType.TEXT 390 | try: 391 | self.scheduler.remove_job(task_id) 392 | logger.info(f"任务 {task_id} 已取消") 393 | reply.content = f"任务 {task_id} 已取消" 394 | except JobLookupError: 395 | logger.error(f"任务 {task_id} 不存在") 396 | reply.content = f"任务 {task_id} 不存在" 397 | e_context["reply"] = reply 398 | e_context.action = EventAction.BREAK_PASS 399 | 400 | # 列出任务 401 | def get_task_list(self, e_context: EventContext): 402 | reply = Reply() 403 | reply.type = ReplyType.TEXT 404 | results = [] 405 | jobs = self.scheduler.get_jobs() 406 | if jobs: 407 | logger.info("任务列表:") 408 | results.append("任务列表:") 409 | for i, job in enumerate(jobs): 410 | logger.info(f"任务编号: {job.id}") 411 | results.append(f"任务编号: {job.id}") 412 | logger.info(f"下次运行时间: {job.next_run_time}") 413 | results.append(f"下次运行时间: {job.next_run_time}") 414 | logger.info(f"任务内容: {job.args[1]}") 415 | results.append(f"任务内容: {job.args[1]}") 416 | if job.args[2]: 417 | logger.info(f"群组: {job.args[2]}") 418 | results.append(f"群组: {job.args[2]}") 419 | if i != len(jobs) - 1: 420 | logger.info("--------------------") 421 | results.append("--------------------") 422 | else: 423 | logger.info("没有任务") 424 | results.append("没有任务") 425 | reply.content = "\n".join(results) 426 | e_context["reply"] = reply 427 | e_context.action = EventAction.BREAK_PASS 428 | 429 | 430 | # 执行任务 431 | def task_execute( 432 | task_id: str, 433 | event: str, 434 | group_name: Optional[str], 435 | msg:ChatMessage, 436 | no_need_at 437 | ): 438 | logger.info(f"开始执行任务{task_id}: {event}, 组: {group_name}") 439 | prefix = conf().get("single_chat_prefix", [""]) 440 | 441 | 442 | if pconf('TaskScheduler').get('allow_call_other_plugins', True): 443 | call_other_plugins = True 444 | for custom_command in pconf('TaskScheduler').get('custom_commands', []): 445 | if event.startswith(custom_command['key_word']): 446 | event = f"{custom_command['command_prefix']}{event}" 447 | break 448 | 449 | content = f"{prefix[0]} {event}" if not msg.is_group else event 450 | content_dict = { 451 | "no_need_at": no_need_at, 452 | "isgroup": msg.is_group, 453 | "msg": msg, 454 | } 455 | 456 | # channel 是单例的 457 | channel = create_channel(conf().get("channel_type")) 458 | if isinstance(channel, ChatChannel): 459 | context = channel._compose_context( 460 | ContextType.TEXT, content, **content_dict 461 | ) 462 | try: 463 | if call_other_plugins: 464 | e_context = PluginManager().emit_event( 465 | EventContext( 466 | Event.ON_HANDLE_CONTEXT, 467 | {"channel": channel, "context": context, "reply": Reply()}, 468 | ) 469 | ) 470 | if e_context["reply"]: 471 | reply = e_context["reply"] 472 | if not reply.content: 473 | reply.type = ReplyType.TEXT 474 | reply.content = f"【执行定时任务】\n任务编号: {task_id}\n任务内容: {event}" 475 | if msg.is_group and reply.type == ReplyType.TEXT and not context.get('no_need_at', False): 476 | reply.content = f"@{msg.actual_user_nickname}\n{reply.content}" 477 | channel.send(reply, context) 478 | 479 | except Exception as e: 480 | logger.error(f"执行任务失败: {e}") 481 | reply = Reply() 482 | reply.type = ReplyType.TEXT 483 | reply.content = f"执行任务失败: {e}" 484 | if context: 485 | channel.send(reply, context) 486 | 487 | --------------------------------------------------------------------------------