From 5b1b71728dcbd5958af055faf0e2afeab56c941d Mon Sep 17 00:00:00 2001 From: jesxion Date: Mon, 13 Apr 2026 12:42:47 +0800 Subject: [PATCH] =?UTF-8?q?Phase=202:=20=E4=BB=BB=E5=8A=A1=E8=B0=83?= =?UTF-8?q?=E5=BA=A6=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增文件: - config.tasks.yaml: 任务配置示例 - src/core/scheduler.py: 任务调度器 功能: - Cron 表达式定时任务 - 变量替换: {{date}}, {{time}}, {{weekday}}, {{datetime}} - 支持启用/禁用/立即执行 - 支持一次性任务 Controller 改动: - 添加 switch_chat() 切换聊天功能 Engine 改动: - 集成 TaskScheduler - start/stop 时自动启动/停止调度器 - 支持 get_tasks, enable_task, disable_task, run_task_now --- config.tasks.yaml | 51 ++++ requirements.txt | 3 + src/core/__pycache__/engine.cpython-311.pyc | Bin 21835 -> 27612 bytes src/core/engine.py | 101 +++++++ src/core/scheduler.py | 266 ++++++++++++++++++ .../__pycache__/controller.cpython-311.pyc | Bin 15729 -> 18277 bytes src/wechat/controller.py | 58 ++++ 7 files changed, 479 insertions(+) create mode 100644 config.tasks.yaml create mode 100644 src/core/scheduler.py diff --git a/config.tasks.yaml b/config.tasks.yaml new file mode 100644 index 0000000..eb19f99 --- /dev/null +++ b/config.tasks.yaml @@ -0,0 +1,51 @@ +# ============================================ +# 自动化任务配置 +# ============================================ +# 支持定时任务,主动向指定联系人/群发送消息 +# +# 字段说明: +# name: 任务名称(唯一标识) +# target: 目标联系人/群名称(必须精确匹配聊天列表中的名称) +# schedule: Cron 表达式(分 时 日 月 周) +# message: 发送的消息内容,支持变量替换 +# enabled: 是否启用 +# once: 是否一次性任务(执行后自动禁用) +# +# 变量替换: +# {{date}} - 当前日期 (2024-01-15) +# {{time}} - 当前时间 (14:30) +# {{datetime}} - 当前日期时间 +# {{weekday}} - 星期几 +# {{weather}} - 天气(需要接入天气 API) +# ============================================ + +tasks: + # 示例:每天早安提醒 + # - name: "早安提醒" + # target: "尾巴~" + # schedule: "0 9 * * *" # 每天9:00 + # message: "早上好!今天{{date}},{{weekday}},天气晴朗~" + # enabled: false + # once: false + + # 示例:每周五提醒提交周报 + # - name: "周报提醒" + # target: "工作群" + # schedule: "0 18 * * 5" # 每周五18:00 + # message: "📋 提醒:今天记得提交周报~" + # enabled: false + + # 示例:每小时心跳检测 + # - name: "心跳检测" + # target: "文件传输助手" + # schedule: "0 */1 * * *" # 每小时整点 + # message: "[心跳检测] {{datetime}}" + # enabled: false + + # 示例:一次性任务 + # - name: "生日祝福" + # target: "张三" + # schedule: "0 9 15 6 *" # 6月15日9:00(仅执行一次) + # message: "生日快乐!🎂" + # enabled: false + # once: true diff --git a/requirements.txt b/requirements.txt index c189091..d28b25d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,9 @@ openai>=1.0.0 # 自动化控制(备选) pyautogui>=0.9.54 + +# 任务调度 +APScheduler>=3.10.0 pyyaml>=6.0 pillow>=9.0.0 diff --git a/src/core/__pycache__/engine.cpython-311.pyc b/src/core/__pycache__/engine.cpython-311.pyc index 62456eaf4d018cd9f19c0a32fe30196a0aad44cd..fd1fabc9af4c5d0b4d24dc42568fd90eddb69069 100644 GIT binary patch delta 8774 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 delta 4324 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 bool: + """发送消息的回调""" + # 切换到目标聊天 + if not self.wechat.switch_chat(target): + logger.error(f"切换聊天失败: {target}") + return False + time.sleep(0.5) # 等待切换完成 + # 发送消息 + return self.wechat.send_text(message) + + self.scheduler.start_tasks(send_callback) + logger.info("任务调度器已启动") + except Exception as e: + logger.error(f"启动任务调度器失败: {e}") + + def _stop_scheduler(self): + """停止任务调度器""" + try: + self.scheduler.stop() + logger.info("任务调度器已停止") + except Exception as e: + logger.error(f"停止任务调度器失败: {e}") + + def _on_task_result(self, result: TaskResult): + """任务执行结果回调""" + logger.info(f"任务完成: {result.task_name} -> {result.target}, 成功: {result.success}") + self._emit("on_task_result", { + "task_name": result.task_name, + "target": result.target, + "message": result.message, + "success": result.success, + "error": result.error, + }) + + def get_tasks(self) -> List[Dict]: + """获取所有任务""" + return self.scheduler.list_tasks() + + def enable_task(self, name: str) -> bool: + """启用任务""" + return self.scheduler.enable_task(name) + + def disable_task(self, name: str) -> bool: + """禁用任务""" + return self.scheduler.disable_task(name) + + def run_task_now(self, name: str) -> bool: + """立即执行任务""" + def send_callback(target: str, message: str) -> bool: + if not self.wechat.switch_chat(target): + return False + time.sleep(0.5) + return self.wechat.send_text(message) + + return self.scheduler.run_task_now(name, send_callback) + + def reload_tasks(self) -> int: + """重新加载任务""" + self.scheduler.stop() + self.scheduler.load_tasks() + + def send_callback(target: str, message: str) -> bool: + if not self.wechat.switch_chat(target): + return False + time.sleep(0.5) + return self.wechat.send_text(message) + + self.scheduler.start() + self.scheduler.start_tasks(send_callback) + return len(self.scheduler.tasks) + + # ==================== 消息处理 ==================== + def _run_loop(self): """主循环""" poll_interval = self.config.wechat.poll_interval diff --git a/src/core/scheduler.py b/src/core/scheduler.py new file mode 100644 index 0000000..fa5742a --- /dev/null +++ b/src/core/scheduler.py @@ -0,0 +1,266 @@ +""" +任务调度器 +支持 cron 表达式定时任务 +""" + +import logging +import threading +import time +from dataclasses import dataclass, field +from datetime import datetime +from typing import List, Optional, Callable, Dict + +import yaml +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger + +logger = logging.getLogger(__name__) + + +@dataclass +class Task: + """任务定义""" + name: str + target: str # 目标联系人/群 + schedule: str # Cron 表达式 + message: str + enabled: bool = False + once: bool = False # 是否一次性任务 + + def __post_init__(self): + self._last_run: Optional[datetime] = None + + def should_run(self) -> bool: + """检查任务是否应该执行""" + if not self.enabled: + return False + + # 一次性任务检查 + if self.once and self._last_run is not None: + return False + + return True + + +@dataclass +class TaskResult: + """任务执行结果""" + task_name: str + target: str + success: bool + message: str + error: Optional[str] = None + + +class TaskScheduler: + """任务调度器""" + + def __init__(self, config_path: str = "config.tasks.yaml"): + self.config_path = config_path + self.tasks: Dict[str, Task] = {} + self._scheduler = BackgroundScheduler(timezone="Asia/Shanghai") + self._lock = threading.Lock() + self._callbacks: List[Callable] = [] + + # 变量替换函数 + self._var_funcs = { + "date": lambda: datetime.now().strftime("%Y-%m-%d"), + "time": lambda: datetime.now().strftime("%H:%M"), + "datetime": lambda: datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "weekday": lambda: ["周一", "周二", "周三", "周四", "周五", "周六", "周日"][datetime.now().weekday()], + } + + def load_tasks(self) -> int: + """加载任务配置""" + try: + with open(self.config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + + task_list = config.get("tasks", []) + + with self._lock: + self.tasks.clear() + for task_data in task_list: + task = Task( + name=task_data.get("name", ""), + target=task_data.get("target", ""), + schedule=task_data.get("schedule", ""), + message=task_data.get("message", ""), + enabled=task_data.get("enabled", False), + once=task_data.get("once", False), + ) + if task.name: + self.tasks[task.name] = task + + logger.info(f"加载了 {len(self.tasks)} 个任务") + return len(self.tasks) + + except FileNotFoundError: + logger.warning(f"任务配置文件不存在: {self.config_path}") + return 0 + except Exception as e: + logger.error(f"加载任务配置失败: {e}") + return 0 + + def start(self): + """启动调度器""" + self._scheduler.start() + logger.info("任务调度器已启动") + + def stop(self): + """停止调度器""" + self._scheduler.shutdown(wait=False) + logger.info("任务调度器已停止") + + def start_tasks(self, send_callback: Callable[[str, str], bool]): + """启动所有已启用的任务 + + Args: + send_callback: 发送消息回调,签名: (target, message) -> bool + """ + with self._lock: + for task_name, task in self.tasks.items(): + if not task.enabled: + continue + + try: + self._add_job(task, send_callback) + logger.info(f"启动任务: {task_name} ({task.schedule}) -> {task.target}") + except Exception as e: + logger.error(f"启动任务失败 {task_name}: {e}") + + def _add_job(self, task: Task, send_callback: Callable): + """添加一个调度任务""" + def job(): + self._execute_task(task, send_callback) + + # 解析 cron 表达式 + parts = task.schedule.split() + if len(parts) == 5: + minute, hour, day, month, day_of_week = parts + else: + logger.error(f"Cron 表达式格式错误: {task.schedule}") + return + + trigger = CronTrigger( + minute=minute, + hour=hour, + day=day, + month=month, + day_of_week=day_of_week, + timezone="Asia/Shanghai" + ) + + self._scheduler.add_job( + func=job, + trigger=trigger, + name=task.name, + id=task.name, + replace_existing=True, + ) + + def _execute_task(self, task: Task, send_callback: Callable[[str, str], bool]): + """执行任务""" + logger.info(f"执行任务: {task.name} -> {task.target}") + + try: + # 变量替换 + message = self._replace_vars(task.message) + + # 发送消息 + success = send_callback(task.target, message) + + # 更新执行时间 + task._last_run = datetime.now() + + # 一次性任务执行后禁用 + if task.once: + task.enabled = False + logger.info(f"一次性任务执行完毕,已禁用: {task.name}") + + # 触发回调 + result = TaskResult( + task_name=task.name, + target=task.target, + success=success, + message=message, + ) + self._emit_result(result) + + except Exception as e: + logger.error(f"执行任务失败 {task.name}: {e}") + result = TaskResult( + task_name=task.name, + target=task.target, + success=False, + message=task.message, + error=str(e), + ) + self._emit_result(result) + + def _replace_vars(self, text: str) -> str: + """替换变量""" + for var_name, var_func in self._var_funcs.items(): + placeholder = f"{{{{{var_name}}}}}" + if placeholder in text: + text = text.replace(placeholder, var_func()) + return text + + def _emit_result(self, result: TaskResult): + """触发结果回调""" + for callback in self._callbacks: + try: + callback(result) + except Exception as e: + logger.error(f"任务结果回调失败: {e}") + + def on_task_result(self, callback: Callable[[TaskResult], None]): + """注册任务结果回调""" + self._callbacks.append(callback) + + def get_task(self, name: str) -> Optional[Task]: + """获取任务""" + return self.tasks.get(name) + + def enable_task(self, name: str) -> bool: + """启用任务""" + task = self.tasks.get(name) + if task: + task.enabled = True + logger.info(f"任务已启用: {name}") + return True + return False + + def disable_task(self, name: str) -> bool: + """禁用任务""" + task = self.tasks.get(name) + if task: + task.enabled = False + # 移除调度任务 + self._scheduler.remove_job(name) + logger.info(f"任务已禁用: {name}") + return True + return False + + def run_task_now(self, name: str, send_callback: Callable[[str, str], bool]) -> bool: + """立即执行任务""" + task = self.tasks.get(name) + if task: + self._execute_task(task, send_callback) + return True + return False + + def list_tasks(self) -> List[Dict]: + """列出所有任务""" + return [ + { + "name": t.name, + "target": t.target, + "schedule": t.schedule, + "message": t.message, + "enabled": t.enabled, + "once": t.once, + "last_run": t._last_run.strftime("%Y-%m-%d %H:%M:%S") if t._last_run else None, + } + for t in self.tasks.values() + ] diff --git a/src/wechat/__pycache__/controller.cpython-311.pyc b/src/wechat/__pycache__/controller.cpython-311.pyc index 7aab6307ac9f5e46a40f774948272655d67a7875..d3da0eb21f584dc4df069973e20379353ac909a5 100644 GIT binary patch delta 2187 zcmbu9ZA?>F7{|})?Ja$|rMF0-g$rCLLQ8n5wg?tBqatV_;-bs6nPJGS6N0F>7f}LZ zE-a$r?7U_IsiQg+bj}y53DL#qqQUITTGoVyjKmKs=%>|WW_+4Erv=S)OP1{9^mm{0 zJkL4j`QLj^pLa&SfQh4OwGyDF6@F>HU2-$gOcq!m0B3;yOpcDO3sX@du0A33$$5v*IqyJC9ulF)A_a81EpM zrF;pt&tdpP$fSyEtUrlbRwwC`isvn+)9HyU1OkcTi>-xNvN(7NiToCdtS(hNx-!Ke z;fSl!OA_Qmzj*7GbV>M^r?d6qAGx#yg45ZCti@?dDQ_+>Zc(I>3eW{w#O{@>F5#*? zzRgm8H_=6i4-!*wY2DH;DW~9+pGyo&VNeBje$-8N!7kF5Ag(p#$PWU3BO1ujAUE;v zQt)1ckOai{bXgKkCEnMi6hMIAX@6rIZ3IHft7Snh;0XwJfHr9dI7I9K9dJ=r0tBzi zRlilPtV_07E{)6mpXHVR_wtagSItL>E0*hEB378jX|{s5w3)gSge386T{b~?C5XP2 zsilhUgijK1E*G;p;F4~$w*5Lq8z{@%!v_n25+xUwg<+FYL}a(r0HE;jxo z+Gj?(fom4p@eM03nDcn&Z(S%ZFiZFr9Fq&pLaVERckw6C@MCMN{k&#IwSHfl(AXr@ zHM<4A8HF}K!-u#%86jSHyt4R)c6M9OjC?&R8|SxcQPQ0E%G|MgNL@V7&Q`xQH!^bb z>8*=))||)}p&9&H3>sq)V>QBz+U*h=T&VsA!F5RB_v1h-M(j4mE@A~9R`y^Xz=&N; zdHiAu$T6dIyBhhXJq^3|9h#w<;<_4Iv9Q>3ZFmxo@h*Z}j_~UJrK7=&%H7c{p!xW- zbTS{fuulAX_)7)r;YY0qy7Hg zfYBZ_ZVMT=dFsN9X__gXVv2)|CB#@f8^Y{z-!9()->wlw6p%Vo3rf3L zrf{0EPBGSr&H&S64Kj`p12--$x-W&oEI{nUUq8p@y&sB``BRw)Pqz- zh^h!s72#!iPpSB_WMdCLke{tnY}9}ssCB7T`QYJlh?xhG>tVjK%0@g~A;Gp4V%w%i z_9OK=O$`esGa+UcPMu_xHATc^0ZyGPgxD@(k^NYs+ysfohK!As#N$d5*-hx!zG`Bc z?JVTPaoeBpx_Gm+4DJ!1m0m>lsa2(LzxZfXi~JT!HbZ_~EGyd%1<_Mxlf_(F;*Vut znY}2=t1!-Ee1q{F#tn>djN2G@#NP63Nsh!nT7DB6%SnT8YZRcz*tXc?O$gAx-Tpp= qlm2TJZn(uk!n7#(XFiYlfB%=L8~;Czr>s?g2MT7biFl9?5&r@-vvaoq delta 383 zcmaFb$M~^|Z#ge77XuLd%Dt0muD+2^lT}NBk%3`4Ln=cQV+unQQwn1ga|%-wOA2!o zYl>V7YbskBQwm!PYZQBm{NzMdO-6;u-K>_3@{^CUmMU|masp+gT3Di_QlwiLqNGz~ zfTT={ERd8<;R$BYRNS1)=B37%zIlg^G9z0b(B`84%@1`;nHlG74maG*sI~wkR0bk! zK!g>DumuvDEJgYt_8br~f3l#A_+)?M^OJAcE@sr4Txa)>amnObdviv;$y@E00@=|H zri^BjCpu(ttORLhELuL9*)f9AYI2OD2^&~QZt^t8X)3cp#&Ls)1t4M(h*%0DmV<~@ zAY%RGP$xyE|BRa}oHjCU_HhYiWZbiPk!vxdMF0z<)CUIa bool: + """切换到指定聊天 + + Args: + chat_name: 聊天名称(联系人/群名称) + + Returns: + 是否切换成功 + """ + try: + # 确保窗口有焦点 + self.main_window.set_focus() + time.sleep(0.1) + + # 方式1: 在搜索框搜索 + search_box = self.main_window.window(class_name="Edit", title_re=".*搜索.*") + if search_box: + search_box.set_edit_text("") + search_box.set_edit_text(chat_name) + time.sleep(0.3) + + # 尝试点击第一个搜索结果 + try: + # 查找联系人列表中的项 + contact_list = self.main_window.window(class_name="ContactList") + items = contact_list.items() + for item in items[:5]: # 只看前5个 + if chat_name in item.text(): + item.click() + time.sleep(0.3) + logger.info(f"切换聊天成功: {chat_name}") + return True + except Exception: + pass + + # 清空搜索框 + search_box.set_edit_text("") + + # 方式2: 直接在聊天列表查找(如果滚动可见) + try: + chat_list = self.main_window.window(class_name="ChatList") + items = chat_list.items() + for item in items: + if chat_name in item.text(): + item.click() + time.sleep(0.3) + logger.info(f"切换聊天成功: {chat_name}") + return True + except Exception: + pass + + logger.warning(f"未找到聊天: {chat_name}") + return False + + except Exception as e: + logger.error(f"切换聊天失败: {e}") + return False + def is_connected(self) -> bool: """检查是否已连接""" return self._connected and self.app is not None