系统业务任务插件化迁移:下沉7项非刚需任务并接入平滑迁移

- 系统任务保留刚需三项:登录巡检、消息计数入库、媒体补偿处理;移除新闻/Epic/排行/PDF/秀人维护等业务型系统任务定义\n- 新增 daily_news、epic_free、daily_ranking、sehuatang_push 四个插件,将原系统业务任务改为插件可调度动作\n- 扩展 xiuren_image 插件调度动作,新增秀人下载、绅士R15下载、图片缓存更新三项维护任务\n- 新增系统任务到插件任务的幂等迁移逻辑:按旧 job_key 映射到插件 action,同步 trigger_type/trigger_config/enabled,并通过 payload 标记防止反复覆盖\n- 在 Robot 启动流程中接入迁移执行与重载,并清理已迁移的历史系统任务记录,避免后台双份维护\n- 扩展插件调度数据库操作:支持按 plugin_name + action_key 精确查询,便于迁移与对账
This commit is contained in:
liuwei
2026-04-16 16:05:59 +08:00
parent 1166323ab5
commit 9652c2594e
13 changed files with 748 additions and 73 deletions

View File

@@ -97,6 +97,24 @@ class PluginScheduleDBOperator(BaseDBOperator):
self._parse_json_field(row, "payload") self._parse_json_field(row, "payload")
return row 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: def upsert_default_schedule(self, data: Dict[str, Any]) -> bool:
try: try:
sql = """ sql = """

View File

@@ -0,0 +1,6 @@
from .main import DailyNewsPlugin
def get_plugin():
"""返回每日新闻插件实例。"""
return DailyNewsPlugin()

153
plugins/daily_news/main.py Normal file
View File

@@ -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")

View File

@@ -0,0 +1,6 @@
from .main import DailyRankingPlugin
def get_plugin():
"""返回每日排行插件实例。"""
return DailyRankingPlugin()

View File

@@ -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,
},
}

View File

@@ -0,0 +1,6 @@
from .main import EpicFreePlugin
def get_plugin():
"""返回 Epic 播报插件实例。"""
return EpicFreePlugin()

128
plugins/epic_free/main.py Normal file
View File

@@ -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,
},
}

View File

@@ -0,0 +1,6 @@
from .main import SehuatangPushPlugin
def get_plugin():
"""返回涩图推送插件实例。"""
return SehuatangPushPlugin()

View File

@@ -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,
},
}

View File

@@ -3,12 +3,15 @@ from pathlib import Path
from loguru import logger from loguru import logger
import os import os
import base64 import base64
import asyncio
from typing import Dict, Any, List, Optional, Tuple from typing import Dict, Any, List, Optional, Tuple
from db.connection import DBConnectionManager from db.connection import DBConnectionManager
from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus from base.plugin_common.plugin_interface import PluginStatus
from plugins.xiuren_image.images_cache import ImageCacheManager 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.plugin_decorators import plugin_stats_decorator
from utils.decorator.rate_limit_decorator import group_feature_rate_limit from utils.decorator.rate_limit_decorator import group_feature_rate_limit
from utils.revoke.message_auto_revoke import MessageAutoRevoke from utils.revoke.message_auto_revoke import MessageAutoRevoke
@@ -234,11 +237,96 @@ class XiurenImagePlugin(MessagePluginInterface):
"target_config": {}, "target_config": {},
"payload": {"max_per_group": 1}, "payload": {"max_per_group": 1},
"default_enabled": False, "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]: 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": if action_key != "daily_push":
return { return {
"success": False, "success": False,

View File

@@ -106,10 +106,17 @@ class Robot:
self.plugins = self.plugin_manager.load_all_plugins() self.plugins = self.plugin_manager.load_all_plugins()
# 热加载改为低频扫描:每 60 秒检查一次插件文件变动 # 热加载改为低频扫描:每 60 秒检查一次插件文件变动
self.plugin_manager.start_hot_reload_watcher(interval_seconds=60.0) 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 = SystemJobLoader(self, self.system_job_db)
self.system_job_loader.init_and_load() 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("插件系统初始化完成") self.LOG.debug("插件系统初始化完成")
@@ -127,6 +134,30 @@ class Robot:
GroupBotManager.admin_list = self.config.wx_config.get("admin", []) GroupBotManager.admin_list = self.config.wx_config.get("admin", [])
self.recent_msg_ids = deque(maxlen=20) 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): def init_wechat_ipad(self):
"""初始化wechat_ipad客户端""" """初始化wechat_ipad客户端"""
try: try:

View File

@@ -23,6 +23,60 @@ class PluginScheduleManager:
self.db.init_tables() self.db.init_tables()
self.reload_from_db() 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]]: def _get_plugin_actions(self) -> List[Dict[str, Any]]:
actions = [] actions = []
for plugin in self.plugin_manager.plugins.values(): for plugin in self.plugin_manager.plugins.values():

View File

@@ -3,8 +3,6 @@ from __future__ import annotations
from typing import Any, Awaitable, Callable, Dict, List from typing import Any, Awaitable, Callable, Dict, List
from loguru import logger
from db.system_job_db import SystemJobDBOperator from db.system_job_db import SystemJobDBOperator
from utils.decorator.async_job import async_job 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 读取。 调度时间、启停状态全部从数据库 t_system_jobs 读取。
""" """
return [ 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", "job_key": "message_count_to_db",
"name": "消息计数入库", "name": "消息计数入库",
@@ -40,38 +22,6 @@ def get_system_job_definitions(robot) -> List[Dict[str, Any]]:
"trigger_config": {"time_list": ["02:30"]}, "trigger_config": {"time_list": ["02:30"]},
"handler": robot.message_count_to_db, "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", "job_key": "login_check",
"name": "登录状态巡检", "name": "登录状态巡检",
@@ -80,14 +30,6 @@ def get_system_job_definitions(robot) -> List[Dict[str, Any]]:
"trigger_config": {"time_list": ["14:43"]}, "trigger_config": {"time_list": ["14:43"]},
"handler": robot.login_twice_auto_auth, "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", "job_key": "process_pending_images",
"name": "待下载图片补偿处理", "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]]: def _build_process_pending_images_handler(robot) -> Callable[[], Awaitable[None]]:
async def _handler(): async def _handler():
if hasattr(robot, "message_storage") and robot.message_storage: if hasattr(robot, "message_storage") and robot.message_storage: