插件定时能力扩展:接入天气/群总结/百科问答/成员画像并补齐周月触发器编辑

- 将 weather、message_summary、game_task、member_context 从硬编码 async_job 注册迁移为插件调度能力(get_schedule_actions/run_scheduled_action)\n- 保持原有默认时间与默认启用行为,新增执行统计结果用于后台日志展示\n- 为群总结与天气推送增加目标群范围适配,支持按后台配置选择 all/白名单/单群执行\n- 成员交互摘要支持日/周/月三类动作接入调度中心,兼容指定群与全量群刷新\n- 后台插件调度页面新增 every_week_time 与 every_month_last_day_time 的编辑支持
This commit is contained in:
liuwei
2026-04-16 15:49:02 +08:00
parent 184999b175
commit 1166323ab5
5 changed files with 333 additions and 66 deletions

View File

@@ -4,7 +4,7 @@ import os
import json
import asyncio
import datetime
from typing import Dict, Any, List, Optional, Tuple
from typing import Dict, Any, List, Optional, Set, Tuple
from base.plugin_common.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus
@@ -13,7 +13,6 @@ from utils.decorator.plugin_decorators import plugin_stats_decorator
from utils.robot_cmd.robot_command import Feature, PermissionStatus, GroupBotManager
from utils.decorator.points_decorator import plugin_points_cost
from wechat_ipad import WechatAPIClient
from utils.decorator.async_job import async_job
# ================= Redis 管理器 =================
@@ -114,8 +113,6 @@ class WeatherPlugin(MessagePluginInterface):
self.feature = self.register_feature()
self.redis_manager = None
self._config = {}
# 使用统一的异步定时任务系统,避免手写休眠循环
async_job.at_times(["08:00"])(self._execute_daily_push)
self.bot: WechatAPIClient = None
def initialize(self, context: Dict[str, Any]) -> bool:
@@ -230,10 +227,56 @@ class WeatherPlugin(MessagePluginInterface):
await self.bot.send_text_message(roomid or sender, weather_text, sender)
return True, "发送成功"
def get_schedule_actions(self) -> List[Dict[str, Any]]:
"""声明天气插件支持的可调度动作。"""
return [
{
"action_key": "daily_push",
"name": "天气订阅日报推送",
"description": "按订阅关系推送天气日报,可限定目标群范围",
"trigger_type": "at_times",
"trigger_config": {"time_list": ["08: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 != "daily_push":
return {
"success": False,
"summary": f"不支持的动作: {action_key}",
"detail": {"action_key": action_key},
}
# 调度中心会按 target_scope 解析目标群,这里仅做最终过滤。
target_groups = context.get("target_groups") or []
target_group_set = {str(g).strip() for g in target_groups if str(g).strip()} or None
result = await self._execute_daily_push(allowed_group_ids=target_group_set)
return {
"success": bool(result.get("failed_targets", 0) == 0),
"summary": (
f"天气推送完成: 目标{result.get('total_targets', 0)}"
f"成功{result.get('success_targets', 0)},失败{result.get('failed_targets', 0)}"
),
"detail": result,
}
# ================= 定时任务系统 =================
async def _execute_daily_push(self):
"""执行全量推送 (基于 ID 聚合)"""
async def _execute_daily_push(self, allowed_group_ids: Optional[Set[str]] = None) -> Dict[str, int]:
"""执行全量推送 (基于 ID 聚合)
Args:
allowed_group_ids: 允许发送的群ID集合为 None 时表示不过滤群范围。
Returns:
dict: 推送统计信息,便于后台调度日志展示。
"""
self.LOG.info("🚀 [Weather] 开始执行每日推送任务...")
if not self.bot:
@@ -241,7 +284,8 @@ class WeatherPlugin(MessagePluginInterface):
subs = self.redis_manager.get_all_subscriptions()
self.LOG.info(f"📋 [Weather] 共获取到 {len(subs)} 条订阅记录")
if not subs: return
if not subs:
return {"cities": 0, "total_targets": 0, "success_targets": 0, "failed_targets": 0}
# 1. 按 [city_id] 聚合 (真正的去重)
# 结构: {"101250101": {"name": "长沙", "users": [...]}, ...}
@@ -260,6 +304,10 @@ class WeatherPlugin(MessagePluginInterface):
self.LOG.info(f"🏙️ [Weather] 聚合为 {len(agg_map)} 个城市待处理: {[info['name'] for info in agg_map.values()]}")
# 2. 遍历 ID 获取天气
total_targets = 0
success_targets = 0
failed_targets = 0
for city_id, info in agg_map.items():
try:
city_name = info["name"]
@@ -287,6 +335,12 @@ class WeatherPlugin(MessagePluginInterface):
for user in user_list:
room_id = user.get('room_id')
sender_id = user.get('sender_id')
# 若指定了目标群范围,仅向范围内群聊推送;私聊订阅在此模式下不发送。
if allowed_group_ids is not None:
if not room_id:
continue
if room_id not in allowed_group_ids:
continue
target_id = room_id if room_id else sender_id
if target_id not in target_map:
target_map[target_id] = {
@@ -298,6 +352,7 @@ class WeatherPlugin(MessagePluginInterface):
target_map[target_id]['mentions'].add(sender_id)
self.LOG.info(f"📤 [Weather] 准备向 {len(target_map)} 个目标(群/人) 发送推送")
total_targets += len(target_map)
for target_id, info in target_map.items():
await asyncio.sleep(0.5)
@@ -310,10 +365,13 @@ class WeatherPlugin(MessagePluginInterface):
# 私聊:直接发送文本
await self.bot.send_text_message(target_id, final_msg)
self.LOG.info(f"✅ [Weather] 推送成功 -> {target_id}")
success_targets += 1
else:
self.LOG.error(f"❌ [Weather] Bot未就绪无法推送给 -> {target_id}")
failed_targets += 1
except Exception as send_e:
self.LOG.error(f"❌ [Weather] 推送给 {target_id} 失败: {send_e}")
failed_targets += 1
else:
self.LOG.warning(f"⚠️ [Weather] 城市 {city_name} 分析后无推送内容(可能是数据缺失)")
@@ -323,6 +381,13 @@ class WeatherPlugin(MessagePluginInterface):
except Exception as e:
self.LOG.error(f"❌ [Weather] 处理城市ID {city_id} 发生异常: {e}")
return {
"cities": len(agg_map),
"total_targets": total_targets,
"success_targets": success_targets,
"failed_targets": failed_targets,
}
# ================= 核心分析算法 (逻辑不变) =================
def _analyze_weather_change(self, city_name: str, api_data: dict, history_data: Optional[dict]) -> str: