系统业务任务插件化迁移:下沉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:
@@ -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 = """
|
||||
|
||||
6
plugins/daily_news/__init__.py
Normal file
6
plugins/daily_news/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import DailyNewsPlugin
|
||||
|
||||
|
||||
def get_plugin():
|
||||
"""返回每日新闻插件实例。"""
|
||||
return DailyNewsPlugin()
|
||||
153
plugins/daily_news/main.py
Normal file
153
plugins/daily_news/main.py
Normal 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")
|
||||
6
plugins/daily_ranking/__init__.py
Normal file
6
plugins/daily_ranking/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import DailyRankingPlugin
|
||||
|
||||
|
||||
def get_plugin():
|
||||
"""返回每日排行插件实例。"""
|
||||
return DailyRankingPlugin()
|
||||
123
plugins/daily_ranking/main.py
Normal file
123
plugins/daily_ranking/main.py
Normal 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,
|
||||
},
|
||||
}
|
||||
6
plugins/epic_free/__init__.py
Normal file
6
plugins/epic_free/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import EpicFreePlugin
|
||||
|
||||
|
||||
def get_plugin():
|
||||
"""返回 Epic 播报插件实例。"""
|
||||
return EpicFreePlugin()
|
||||
128
plugins/epic_free/main.py
Normal file
128
plugins/epic_free/main.py
Normal 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,
|
||||
},
|
||||
}
|
||||
6
plugins/sehuatang_push/__init__.py
Normal file
6
plugins/sehuatang_push/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .main import SehuatangPushPlugin
|
||||
|
||||
|
||||
def get_plugin():
|
||||
"""返回涩图推送插件实例。"""
|
||||
return SehuatangPushPlugin()
|
||||
127
plugins/sehuatang_push/main.py
Normal file
127
plugins/sehuatang_push/main.py
Normal 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,
|
||||
},
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
35
robot.py
35
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:
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user