diff --git a/db/plugin_schedule_db.py b/db/plugin_schedule_db.py index 479eb73..05f4ee1 100644 --- a/db/plugin_schedule_db.py +++ b/db/plugin_schedule_db.py @@ -97,6 +97,24 @@ class PluginScheduleDBOperator(BaseDBOperator): self._parse_json_field(row, "payload") return row + def get_schedule_by_plugin_action(self, plugin_name: str, action_key: str) -> Optional[Dict[str, Any]]: + """按插件名+动作键查询调度配置。""" + row = self.execute_query( + """ + SELECT * FROM t_plugin_schedules + WHERE plugin_name = %s AND action_key = %s + LIMIT 1 + """, + (plugin_name, action_key), + fetch_one=True, + ) + if not row: + return None + self._parse_json_field(row, "trigger_config") + self._parse_json_field(row, "target_config") + self._parse_json_field(row, "payload") + return row + def upsert_default_schedule(self, data: Dict[str, Any]) -> bool: try: sql = """ diff --git a/plugins/daily_news/__init__.py b/plugins/daily_news/__init__.py new file mode 100644 index 0000000..9f5cf80 --- /dev/null +++ b/plugins/daily_news/__init__.py @@ -0,0 +1,6 @@ +from .main import DailyNewsPlugin + + +def get_plugin(): + """返回每日新闻插件实例。""" + return DailyNewsPlugin() diff --git a/plugins/daily_news/main.py b/plugins/daily_news/main.py new file mode 100644 index 0000000..e5f2a39 --- /dev/null +++ b/plugins/daily_news/main.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +import asyncio +import base64 +from typing import Any, Dict, List, Optional, Tuple + +import requests + +from base.func_news import News +from base.plugin_common.message_plugin_interface import MessagePluginInterface +from base.plugin_common.plugin_interface import PluginStatus +from utils.robot_cmd.robot_command import GroupBotManager +from wechat_ipad.models.appmsg_xml import LINK_XML_NEWS + + +class DailyNewsPlugin(MessagePluginInterface): + """每日新闻定时插件。""" + + FEATURE_KEY = "DAILY_NEWS" + FEATURE_DESCRIPTION = "📰 每日新闻自动播报 [每日8:30定时发送]" + + @property + def name(self) -> str: + return "每日新闻" + + @property + def version(self) -> str: + return "1.0.0" + + @property + def description(self) -> str: + return "将百度新闻日报能力下沉为插件定时任务。" + + @property + def author(self) -> str: + return "ABOT Team" + + @property + def commands(self) -> List[str]: + # 该插件只负责后台调度,不处理前台命令。 + return [] + + @property + def feature_key(self) -> Optional[str]: + return self.FEATURE_KEY + + @property + def feature_description(self) -> Optional[str]: + return self.FEATURE_DESCRIPTION + + def __init__(self): + super().__init__() + self.feature = self.register_feature() + + def initialize(self, context: Dict[str, Any]) -> bool: + self.LOG.debug(f"正在初始化 {self.name} 插件...") + return True + + def start(self) -> bool: + self.status = PluginStatus.RUNNING + return True + + def stop(self) -> bool: + self.status = PluginStatus.STOPPED + return True + + def can_process(self, message: Dict[str, Any]) -> bool: + return False + + async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + return False, None + + def get_schedule_actions(self) -> List[Dict[str, Any]]: + """声明插件可调度动作。""" + return [ + { + "action_key": "baidu_news_daily_push", + "name": "百度新闻日报推送", + "description": "每天推送百度新闻文本、60秒新闻图和资讯卡片", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["08:30"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + } + ] + + async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: + if action_key != "baidu_news_daily_push": + return { + "success": False, + "summary": f"不支持的动作: {action_key}", + "detail": {"action_key": action_key}, + } + + if not self.bot: + return {"success": False, "summary": "bot 未注入", "detail": {}} + + target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] + if not target_groups: + # 兜底:当后台未指定范围时,直接按群功能开关收集目标群。 + target_groups = [ + gid for gid in GroupBotManager.get_group_list() + if GroupBotManager.get_group_permission(gid, self.feature).value == "enabled" + ] + + if not target_groups: + return {"success": False, "summary": "没有可推送目标群", "detail": {"target_count": 0}} + + try: + # 新闻抓取为同步逻辑,放入线程池避免阻塞调度主循环。 + text_news = await asyncio.to_thread(News().get_baidu_news) + image_url = await asyncio.to_thread(News().get_news_60s) + except Exception as e: + return {"success": False, "summary": f"新闻抓取失败: {e}", "detail": {"error": str(e)}} + + # 图片接口返回 URL,统一下载为 base64 再发送,兼容 wechat_ipad 图片发送接口。 + image_base64 = "" + if image_url: + try: + image_base64 = await asyncio.to_thread(self._download_image_as_base64, image_url) + except Exception as e: + self.LOG.warning(f"每日新闻图片下载失败,将仅发送文本和卡片: {e}") + + success_groups = [] + failed_groups = {} + for gid in target_groups: + try: + if text_news: + await self.bot.send_text_message(gid, text_news) + if image_base64: + await self.bot.send_image_message(gid, image_base64) + await self.bot.send_link_xml_message(LINK_XML_NEWS, gid) + success_groups.append(gid) + except Exception as e: + failed_groups[gid] = str(e) + + return { + "success": len(failed_groups) == 0, + "summary": f"每日新闻推送完成: 成功{len(success_groups)}群, 失败{len(failed_groups)}群", + "detail": { + "target_count": len(target_groups), + "success_groups": success_groups, + "failed_groups": failed_groups, + }, + } + + @staticmethod + def _download_image_as_base64(url: str) -> str: + """下载图片并转为 base64,便于统一发送。""" + resp = requests.get(url, timeout=15) + resp.raise_for_status() + return base64.b64encode(resp.content).decode("utf-8") diff --git a/plugins/daily_ranking/__init__.py b/plugins/daily_ranking/__init__.py new file mode 100644 index 0000000..6942c57 --- /dev/null +++ b/plugins/daily_ranking/__init__.py @@ -0,0 +1,6 @@ +from .main import DailyRankingPlugin + + +def get_plugin(): + """返回每日排行插件实例。""" + return DailyRankingPlugin() diff --git a/plugins/daily_ranking/main.py b/plugins/daily_ranking/main.py new file mode 100644 index 0000000..3d3c5da --- /dev/null +++ b/plugins/daily_ranking/main.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict, List, Optional, Tuple + +from base.plugin_common.message_plugin_interface import MessagePluginInterface +from base.plugin_common.plugin_interface import PluginStatus +from utils.robot_cmd.robot_command import GroupBotManager +from utils.wechat.message_to_db import MessageStorage + + +class DailyRankingPlugin(MessagePluginInterface): + """每日群消息排行推送插件。""" + + FEATURE_KEY = "DAILY_SUMMARY" + FEATURE_DESCRIPTION = "🕤 每日群发言总结 [每日9:30定时发送]" + + @property + def name(self) -> str: + return "每日排行" + + @property + def version(self) -> str: + return "1.0.0" + + @property + def description(self) -> str: + return "将群消息排行推送从系统任务迁移到插件任务。" + + @property + def author(self) -> str: + return "ABOT Team" + + @property + def commands(self) -> List[str]: + return [] + + @property + def feature_key(self) -> Optional[str]: + return self.FEATURE_KEY + + @property + def feature_description(self) -> Optional[str]: + return self.FEATURE_DESCRIPTION + + def __init__(self): + super().__init__() + self.feature = self.register_feature() + self.message_storage: Optional[MessageStorage] = None + + def initialize(self, context: Dict[str, Any]) -> bool: + # 与历史系统逻辑保持一致,直接复用 MessageStorage 的排行生成能力。 + self.message_storage = MessageStorage() + return True + + def start(self) -> bool: + self.status = PluginStatus.RUNNING + return True + + def stop(self) -> bool: + self.status = PluginStatus.STOPPED + return True + + def can_process(self, message: Dict[str, Any]) -> bool: + return False + + async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + return False, None + + def get_schedule_actions(self) -> List[Dict[str, Any]]: + return [ + { + "action_key": "daily_message_ranking_push", + "name": "群消息排行推送", + "description": "每天生成并发送群消息发言排行", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["09:30"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + } + ] + + async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: + if action_key != "daily_message_ranking_push": + return { + "success": False, + "summary": f"不支持的动作: {action_key}", + "detail": {"action_key": action_key}, + } + if not self.bot: + return {"success": False, "summary": "bot 未注入", "detail": {}} + if not self.message_storage: + return {"success": False, "summary": "message_storage 未初始化", "detail": {}} + + target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] + if not target_groups: + target_groups = [ + gid for gid in GroupBotManager.get_group_list() + if GroupBotManager.get_group_permission(gid, self.feature).value == "enabled" + ] + if not target_groups: + return {"success": False, "summary": "没有可推送目标群", "detail": {"target_count": 0}} + + success_groups = [] + failed_groups = {} + for gid in target_groups: + try: + ok, text = await self.message_storage.generate_and_send_ranking(gid, {}) + if ok and text: + await self.bot.send_text_message(gid, text) + success_groups.append(gid) + except Exception as e: + failed_groups[gid] = str(e) + + return { + "success": len(failed_groups) == 0, + "summary": f"每日排行推送完成: 成功{len(success_groups)}群, 失败{len(failed_groups)}群", + "detail": { + "target_count": len(target_groups), + "success_groups": success_groups, + "failed_groups": failed_groups, + }, + } diff --git a/plugins/epic_free/__init__.py b/plugins/epic_free/__init__.py new file mode 100644 index 0000000..6eede5e --- /dev/null +++ b/plugins/epic_free/__init__.py @@ -0,0 +1,6 @@ +from .main import EpicFreePlugin + + +def get_plugin(): + """返回 Epic 播报插件实例。""" + return EpicFreePlugin() diff --git a/plugins/epic_free/main.py b/plugins/epic_free/main.py new file mode 100644 index 0000000..a9ef96d --- /dev/null +++ b/plugins/epic_free/main.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +from typing import Any, Dict, List, Optional, Tuple + +from base.func_epic import get_free, is_friday +from base.plugin_common.message_plugin_interface import MessagePluginInterface +from base.plugin_common.plugin_interface import PluginStatus +from utils.robot_cmd.robot_command import GroupBotManager + + +class EpicFreePlugin(MessagePluginInterface): + """Epic 免费游戏自动播报插件。""" + + FEATURE_KEY = "EPIC" + FEATURE_DESCRIPTION = "📊 EPIC自动播报 [每周五自动发送]" + + @property + def name(self) -> str: + return "Epic播报" + + @property + def version(self) -> str: + return "1.0.0" + + @property + def description(self) -> str: + return "将 Epic 免费游戏播报从系统任务迁移到插件任务。" + + @property + def author(self) -> str: + return "ABOT Team" + + @property + def commands(self) -> List[str]: + return [] + + @property + def feature_key(self) -> Optional[str]: + return self.FEATURE_KEY + + @property + def feature_description(self) -> Optional[str]: + return self.FEATURE_DESCRIPTION + + def __init__(self): + super().__init__() + self.feature = self.register_feature() + + def initialize(self, context: Dict[str, Any]) -> bool: + return True + + def start(self) -> bool: + self.status = PluginStatus.RUNNING + return True + + def stop(self) -> bool: + self.status = PluginStatus.STOPPED + return True + + def can_process(self, message: Dict[str, Any]) -> bool: + return False + + async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + return False, None + + def get_schedule_actions(self) -> List[Dict[str, Any]]: + return [ + { + "action_key": "weekly_free_games_push", + "name": "Epic免费游戏推送", + "description": "每周五推送 Epic 当周免费游戏", + "trigger_type": "every_weekday_time", + "trigger_config": {"weekday": 4, "time_str": "10:00"}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {"force": False}, + "default_enabled": True, + } + ] + + async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: + if action_key != "weekly_free_games_push": + return { + "success": False, + "summary": f"不支持的动作: {action_key}", + "detail": {"action_key": action_key}, + } + if not self.bot: + return {"success": False, "summary": "bot 未注入", "detail": {}} + + payload = context.get("payload") or {} + force = bool(payload.get("force", False)) + if not force and not is_friday(): + # 非周五时默认跳过;手动触发可通过 payload.force 强制执行。 + return {"success": True, "summary": "今天不是周五,已跳过 Epic 播报", "detail": {"skipped": True}} + + target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] + if not target_groups: + target_groups = [ + gid for gid in GroupBotManager.get_group_list() + if GroupBotManager.get_group_permission(gid, self.feature).value == "enabled" + ] + if not target_groups: + return {"success": False, "summary": "没有可推送目标群", "detail": {"target_count": 0}} + + try: + text = get_free() + except Exception as e: + return {"success": False, "summary": f"获取 Epic 免费游戏失败: {e}", "detail": {"error": str(e)}} + + success_groups = [] + failed_groups = {} + for gid in target_groups: + try: + await self.bot.send_text_message(gid, text) + success_groups.append(gid) + except Exception as e: + failed_groups[gid] = str(e) + + return { + "success": len(failed_groups) == 0, + "summary": f"Epic播报完成: 成功{len(success_groups)}群, 失败{len(failed_groups)}群", + "detail": { + "target_count": len(target_groups), + "success_groups": success_groups, + "failed_groups": failed_groups, + "force": force, + }, + } diff --git a/plugins/sehuatang_push/__init__.py b/plugins/sehuatang_push/__init__.py new file mode 100644 index 0000000..66746c4 --- /dev/null +++ b/plugins/sehuatang_push/__init__.py @@ -0,0 +1,6 @@ +from .main import SehuatangPushPlugin + + +def get_plugin(): + """返回涩图推送插件实例。""" + return SehuatangPushPlugin() diff --git a/plugins/sehuatang_push/main.py b/plugins/sehuatang_push/main.py new file mode 100644 index 0000000..a69e086 --- /dev/null +++ b/plugins/sehuatang_push/main.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +import asyncio +from typing import Any, Dict, List, Optional, Tuple + +from base.plugin_common.message_plugin_interface import MessagePluginInterface +from base.plugin_common.plugin_interface import PluginStatus +from utils.sehuatang.shehuatang import pdf_file_path +from utils.sehuatang.shehuatang_undetected import pdf_file_path_undetected + + +class SehuatangPushPlugin(MessagePluginInterface): + """涩图 PDF 推送插件。""" + + FEATURE_KEY = "PDF_CAPABILITY" + FEATURE_DESCRIPTION = "📄 sehuatang PDF能力 [无]" + + @property + def name(self) -> str: + return "涩图推送" + + @property + def version(self) -> str: + return "1.0.0" + + @property + def description(self) -> str: + return "将 sehuatang PDF 推送从系统任务迁移到插件任务。" + + @property + def author(self) -> str: + return "ABOT Team" + + @property + def commands(self) -> List[str]: + return [] + + @property + def feature_key(self) -> Optional[str]: + return self.FEATURE_KEY + + @property + def feature_description(self) -> Optional[str]: + return self.FEATURE_DESCRIPTION + + def __init__(self): + super().__init__() + self.feature = self.register_feature() + + def initialize(self, context: Dict[str, Any]) -> bool: + return True + + def start(self) -> bool: + self.status = PluginStatus.RUNNING + return True + + def stop(self) -> bool: + self.status = PluginStatus.STOPPED + return True + + def can_process(self, message: Dict[str, Any]) -> bool: + return False + + async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: + return False, None + + def get_schedule_actions(self) -> List[Dict[str, Any]]: + return [ + { + "action_key": "daily_pdf_push", + "name": "涩图PDF推送", + "description": "每天生成并推送涩图 PDF 提醒", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["15:30"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {"at_user": "Jyunere"}, + "default_enabled": True, + } + ] + + async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: + if action_key != "daily_pdf_push": + return { + "success": False, + "summary": f"不支持的动作: {action_key}", + "detail": {"action_key": action_key}, + } + if not self.bot: + return {"success": False, "summary": "bot 未注入", "detail": {}} + + target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] + if not target_groups: + return {"success": False, "summary": "没有可推送目标群", "detail": {"target_count": 0}} + + payload = context.get("payload") or {} + at_user = str(payload.get("at_user", "Jyunere") or "Jyunere").strip() + + # 兼容历史逻辑:优先使用 undetected 方案,失败后回退普通抓取。 + try: + ok, path = await asyncio.to_thread(pdf_file_path_undetected) + if not ok: + ok, path = await asyncio.to_thread(pdf_file_path) + if not ok: + return {"success": False, "summary": "PDF 生成失败", "detail": {}} + except Exception as e: + return {"success": False, "summary": f"PDF 生成异常: {e}", "detail": {"error": str(e)}} + + success_groups = [] + failed_groups = {} + for gid in target_groups: + try: + # 历史系统任务仅发送提醒文本,这里保持一致,避免引入文件发送兼容风险。 + await self.bot.send_at_message(gid, f"98堂 PDF已就绪,请手动发送\n文件: {path}", [at_user]) + success_groups.append(gid) + except Exception as e: + failed_groups[gid] = str(e) + + return { + "success": len(failed_groups) == 0, + "summary": f"涩图推送完成: 成功{len(success_groups)}群, 失败{len(failed_groups)}群", + "detail": { + "target_count": len(target_groups), + "success_groups": success_groups, + "failed_groups": failed_groups, + "pdf_path": path, + }, + } diff --git a/plugins/xiuren_image/main.py b/plugins/xiuren_image/main.py index 1a83347..f1e2781 100644 --- a/plugins/xiuren_image/main.py +++ b/plugins/xiuren_image/main.py @@ -3,12 +3,15 @@ from pathlib import Path from loguru import logger import os import base64 +import asyncio from typing import Dict, Any, List, Optional, Tuple from db.connection import DBConnectionManager from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus from plugins.xiuren_image.images_cache import ImageCacheManager +from plugins.xiuren_image.meitu_dl import meitu_dowload_pub_pic +from plugins.xiuren_image.shenshi_r15 import run_daily_job from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.decorator.rate_limit_decorator import group_feature_rate_limit from utils.revoke.message_auto_revoke import MessageAutoRevoke @@ -234,11 +237,96 @@ class XiurenImagePlugin(MessagePluginInterface): "target_config": {}, "payload": {"max_per_group": 1}, "default_enabled": False, + }, + { + "action_key": "resource_xiuren_download", + "name": "秀人资源下载", + "description": "执行秀人资源下载维护任务", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["01:30"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + }, + { + "action_key": "resource_shenshi_r15_download", + "name": "绅士R15资源下载", + "description": "执行绅士R15资源下载维护任务", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["02:30"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + }, + { + "action_key": "resource_update_image_cache", + "name": "图片缓存更新", + "description": "扫描并更新图片缓存", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["05:00"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, } ] async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: """执行插件定时动作。""" + if action_key == "resource_xiuren_download": + try: + # 历史逻辑为同步下载任务,这里放到线程池执行,避免阻塞调度主循环。 + await asyncio.to_thread(meitu_dowload_pub_pic) + return { + "success": True, + "summary": "秀人资源下载任务执行完成", + "detail": {}, + } + except Exception as e: + return { + "success": False, + "summary": f"秀人资源下载失败: {e}", + "detail": {"error": str(e)}, + } + + if action_key == "resource_shenshi_r15_download": + try: + await asyncio.to_thread(run_daily_job) + return { + "success": True, + "summary": "绅士R15资源下载任务执行完成", + "detail": {}, + } + except Exception as e: + return { + "success": False, + "summary": f"绅士R15资源下载失败: {e}", + "detail": {"error": str(e)}, + } + + if action_key == "resource_update_image_cache": + try: + if not self.image_cache_manager: + return { + "success": False, + "summary": "缓存管理器未初始化", + "detail": {}, + } + await self.image_cache_manager.update_image_cache() + return { + "success": True, + "summary": "图片缓存更新完成", + "detail": {"image_folder": self.image_folder}, + } + except Exception as e: + return { + "success": False, + "summary": f"图片缓存更新失败: {e}", + "detail": {"error": str(e)}, + } + if action_key != "daily_push": return { "success": False, diff --git a/robot.py b/robot.py index 68fbba8..b230b9a 100644 --- a/robot.py +++ b/robot.py @@ -106,10 +106,17 @@ class Robot: self.plugins = self.plugin_manager.load_all_plugins() # 热加载改为低频扫描:每 60 秒检查一次插件文件变动 self.plugin_manager.start_hot_reload_watcher(interval_seconds=60.0) - self.plugin_schedule_manager = PluginScheduleManager(self.plugin_manager, self.plugin_schedule_db) - self.plugin_schedule_manager.init_and_load() self.system_job_loader = SystemJobLoader(self, self.system_job_db) self.system_job_loader.init_and_load() + self.plugin_schedule_manager = PluginScheduleManager(self.plugin_manager, self.plugin_schedule_db) + self.plugin_schedule_manager.init_and_load() + # 将历史业务型系统任务迁移到插件调度配置,避免升级后出现“任务丢失”。 + migration_result = self.plugin_schedule_manager.migrate_from_system_jobs(self.system_job_db) + if migration_result.get("migrated", 0) > 0: + self.LOG.info(f"系统任务迁移到插件任务完成: {migration_result}") + self.plugin_schedule_manager.reload_from_db() + # 迁移完成后,清理已下沉到插件层的系统任务,避免后台重复维护两套配置。 + self._cleanup_migrated_system_jobs() # 加载插件 self.LOG.debug("插件系统初始化完成") @@ -127,6 +134,30 @@ class Robot: GroupBotManager.admin_list = self.config.wx_config.get("admin", []) self.recent_msg_ids = deque(maxlen=20) + def _cleanup_migrated_system_jobs(self): + """清理已经迁移到插件层的历史系统任务键。""" + migrated_keys = [ + "news_baidu_report_auto", + "epic_free_games", + "message_ranking_push", + "sehuatang_pdf_push", + "xiuren_download", + "shenshi_r15_download", + "update_image_cache", + ] + removed = 0 + for job_key in migrated_keys: + try: + row = self.system_job_db.get_job(job_key) + if not row: + continue + if self.system_job_db.delete_job(job_key): + removed += 1 + except Exception as e: + self.LOG.warning(f"清理迁移系统任务失败: job_key={job_key}, error={e}") + if removed > 0: + self.LOG.info(f"已清理 {removed} 个历史系统任务配置(迁移至插件任务)") + def init_wechat_ipad(self): """初始化wechat_ipad客户端""" try: diff --git a/utils/plugin_schedule_manager.py b/utils/plugin_schedule_manager.py index 40e8846..2fa24b8 100644 --- a/utils/plugin_schedule_manager.py +++ b/utils/plugin_schedule_manager.py @@ -23,6 +23,60 @@ class PluginScheduleManager: self.db.init_tables() self.reload_from_db() + def migrate_from_system_jobs(self, system_job_db) -> Dict[str, int]: + """把历史系统任务配置迁移到插件任务表(幂等)。""" + # 迁移映射:旧 system_job_key -> (插件显示名, 插件动作 key) + migration_map = { + "news_baidu_report_auto": ("每日新闻", "baidu_news_daily_push"), + "epic_free_games": ("Epic播报", "weekly_free_games_push"), + "message_ranking_push": ("每日排行", "daily_message_ranking_push"), + "sehuatang_pdf_push": ("涩图推送", "daily_pdf_push"), + "xiuren_download": ("秀人图片", "resource_xiuren_download"), + "shenshi_r15_download": ("秀人图片", "resource_shenshi_r15_download"), + "update_image_cache": ("秀人图片", "resource_update_image_cache"), + } + + migrated = 0 + skipped = 0 + failed = 0 + for job_key, target in migration_map.items(): + plugin_name, action_key = target + try: + sys_row = system_job_db.get_job(job_key) + if not sys_row: + skipped += 1 + continue + schedule_row = self.db.get_schedule_by_plugin_action(plugin_name, action_key) + if not schedule_row: + skipped += 1 + continue + + # 通过 payload 标记是否已经迁移,避免每次启动覆盖用户后续修改。 + payload = schedule_row.get("payload") or {} + if payload.get("_migrated_from_system_job") == job_key: + skipped += 1 + continue + + payload["_migrated_from_system_job"] = job_key + updates = { + "trigger_type": sys_row.get("trigger_type", schedule_row.get("trigger_type")), + "trigger_config": sys_row.get("trigger_config") or schedule_row.get("trigger_config") or {}, + "enabled": bool(sys_row.get("enabled", 1)), + "payload": payload, + } + # 名称/描述尽量沿用用户在插件端的展示,但允许继承旧系统任务描述。 + if sys_row.get("description"): + updates["description"] = sys_row.get("description") + if self.db.update_schedule(int(schedule_row["id"]), updates): + migrated += 1 + else: + failed += 1 + except Exception as e: + failed += 1 + logger.error(f"系统任务迁移到插件任务失败: job_key={job_key}, error={e}") + + return {"migrated": migrated, "skipped": skipped, "failed": failed} + def _get_plugin_actions(self) -> List[Dict[str, Any]]: actions = [] for plugin in self.plugin_manager.plugins.values(): diff --git a/utils/system_jobs.py b/utils/system_jobs.py index 68ceb0a..42e2fe4 100644 --- a/utils/system_jobs.py +++ b/utils/system_jobs.py @@ -3,8 +3,6 @@ from __future__ import annotations from typing import Any, Awaitable, Callable, Dict, List -from loguru import logger - from db.system_job_db import SystemJobDBOperator from utils.decorator.async_job import async_job @@ -16,22 +14,6 @@ def get_system_job_definitions(robot) -> List[Dict[str, Any]]: 调度时间、启停状态全部从数据库 t_system_jobs 读取。 """ return [ - { - "job_key": "news_baidu_report_auto", - "name": "百度新闻日报", - "description": "每天 08:30 推送百度新闻", - "trigger_type": "at_times", - "trigger_config": {"time_list": ["08:30"]}, - "handler": robot.news_baidu_report_auto, - }, - { - "job_key": "epic_free_games", - "name": "Epic 免费游戏推送", - "description": "每周五 10:00 推送 Epic 免费游戏", - "trigger_type": "every_weekday_time", - "trigger_config": {"weekday": 4, "time_str": "10:00"}, - "handler": robot.send_epic_free_games, - }, { "job_key": "message_count_to_db", "name": "消息计数入库", @@ -40,38 +22,6 @@ def get_system_job_definitions(robot) -> List[Dict[str, Any]]: "trigger_config": {"time_list": ["02:30"]}, "handler": robot.message_count_to_db, }, - { - "job_key": "message_ranking_push", - "name": "群消息排行推送", - "description": "每天 09:30 生成并发送群消息排行", - "trigger_type": "at_times", - "trigger_config": {"time_list": ["09:30"]}, - "handler": robot.generate_and_send_ranking, - }, - { - "job_key": "sehuatang_pdf_push", - "name": "涩图 PDF 推送", - "description": "每天 15:30 生成并发送涩图 PDF", - "trigger_type": "at_times", - "trigger_config": {"time_list": ["15:30"]}, - "handler": robot.generate_sehuatang_pdf, - }, - { - "job_key": "xiuren_download", - "name": "秀人网下载任务", - "description": "每天 01:30 执行秀人网下载任务", - "trigger_type": "at_times", - "trigger_config": {"time_list": ["01:30"]}, - "handler": robot.xiu_ren_download_task, - }, - { - "job_key": "shenshi_r15_download", - "name": "绅士 R15 下载任务", - "description": "每天 02:30 执行绅士 R15 下载任务", - "trigger_type": "at_times", - "trigger_config": {"time_list": ["02:30"]}, - "handler": robot.shen_shi_download_task, - }, { "job_key": "login_check", "name": "登录状态巡检", @@ -80,14 +30,6 @@ def get_system_job_definitions(robot) -> List[Dict[str, Any]]: "trigger_config": {"time_list": ["14:43"]}, "handler": robot.login_twice_auto_auth, }, - { - "job_key": "update_image_cache", - "name": "图片缓存更新", - "description": "每天 05:00 扫描并更新图片缓存", - "trigger_type": "at_times", - "trigger_config": {"time_list": ["05:00"]}, - "handler": _build_image_cache_handler(robot), - }, { "job_key": "process_pending_images", "name": "待下载图片补偿处理", @@ -98,19 +40,6 @@ def get_system_job_definitions(robot) -> List[Dict[str, Any]]: }, ] - -def _build_image_cache_handler(robot) -> Callable[[], Awaitable[None]]: - async def _handler(): - from plugins.xiuren_image.images_cache import ImageCacheManager - - logger.info("开始执行图片缓存更新任务") - manager = ImageCacheManager("/mnt/nfs_share") - await manager.update_image_cache() - logger.info("图片缓存更新完成") - - return _handler - - def _build_process_pending_images_handler(robot) -> Callable[[], Awaitable[None]]: async def _handler(): if hasattr(robot, "message_storage") and robot.message_storage: