diff --git a/admin/dashboard/templates/plugin_schedules.html b/admin/dashboard/templates/plugin_schedules.html index a8d7604..d364dcd 100644 --- a/admin/dashboard/templates/plugin_schedules.html +++ b/admin/dashboard/templates/plugin_schedules.html @@ -57,6 +57,8 @@ + + @@ -65,7 +67,7 @@ - + @@ -76,7 +78,7 @@ - + @@ -236,9 +238,12 @@ new Vue({ if (this.editForm.trigger_type === 'every_seconds') { return { seconds: Number(this.editForm.seconds || 60) } } - if (this.editForm.trigger_type === 'every_weekday_time') { + if (this.editForm.trigger_type === 'every_weekday_time' || this.editForm.trigger_type === 'every_week_time') { return { weekday: Number(this.editForm.weekday || 0), time_str: String(this.editForm.time_str || '09:00') } } + if (this.editForm.trigger_type === 'every_month_last_day_time') { + return { time_str: String(this.editForm.time_str || '09:00') } + } return {} }, buildTargetConfig() { diff --git a/plugins/game_task/main.py b/plugins/game_task/main.py index 9cbca33..5c2a633 100644 --- a/plugins/game_task/main.py +++ b/plugins/game_task/main.py @@ -4,7 +4,6 @@ from typing import Dict, Any, List, Optional, Tuple from loguru import logger -from utils.decorator.async_job import async_job from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus from utils.decorator.plugin_decorators import plugin_stats_decorator @@ -60,7 +59,6 @@ class GameTaskPlugin(MessagePluginInterface): self.LOG = logger # 注册功能权限 self.feature = self.register_feature() - async_job.at_times(["17:58"])(self.run_random_task_assignment) def initialize(self, context: Dict[str, Any]) -> bool: """初始化插件""" @@ -185,6 +183,43 @@ class GameTaskPlugin(MessagePluginInterface): self.LOG.error(f"处理消息出错: {e}") return False, f"处理出错: {e}" + def get_schedule_actions(self) -> List[Dict[str, Any]]: + """声明百科问答插件支持的可调度动作。""" + return [ + { + "action_key": "random_task_assignment", + "name": "群随机发题", + "description": "在配置时间给目标群随机发放一条百科问答任务", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["17:58"]}, + "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 != "random_task_assignment": + return { + "success": False, + "summary": f"不支持的动作: {action_key}", + "detail": {"action_key": action_key}, + } + + target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] + result = await self.run_random_task_assignment(target_groups=target_groups) + return { + "success": bool(result.get("failed_groups", 0) == 0), + "summary": ( + f"发题完成: 候选{result.get('candidate_groups', 0)}群," + f"成功{result.get('success_groups', 0)}群,失败{result.get('failed_groups', 0)}群" + ), + "detail": result, + } + async def _handle_join_game(self, sender: str, roomid: str, wx_nick_name: str) -> None: """处理加入游戏请求""" try: @@ -535,20 +570,34 @@ class GameTaskPlugin(MessagePluginInterface): sender ) - async def run_random_task_assignment(self) -> None: - """定时任务:整点触发,排除23:00-08:00""" + async def run_random_task_assignment(self, target_groups: Optional[List[str]] = None) -> Dict[str, int]: + """定时任务:随机发题,排除 23:00-08:00。 + + Args: + target_groups: 指定目标群列表;为空时按原逻辑扫描全部游戏群。 + + Returns: + dict: 执行统计信息。 + """ current_hour = datetime.now().hour if current_hour >= 23 or current_hour < 9: self.LOG.info(f"当前时间 {current_hour}:00 在23:00-08:00区间,跳过任务发放") - return + return {"candidate_groups": 0, "success_groups": 0, "failed_groups": 0} try: # 获取所有群聊 groups = self.encyclopedia_db.get_all_groups() + target_group_set = {g for g in (target_groups or []) if g} + candidate_groups = 0 + success_groups = 0 + failed_groups = 0 for group in groups: + if target_group_set and group not in target_group_set: + continue # 检查权限 if GroupBotManager.get_group_permission(group,self.feature) == PermissionStatus.DISABLED: continue + candidate_groups += 1 # 获取群内所有玩家 players = self.encyclopedia_db.get_all_players_in_group(group) @@ -583,8 +632,17 @@ class GameTaskPlugin(MessagePluginInterface): f"🌼 积分:{score}\n" f"🌈 抢答格式:/a {active_task_id} 答案", [holder_id]) + success_groups += 1 + else: + failed_groups += 1 + return { + "candidate_groups": candidate_groups, + "success_groups": success_groups, + "failed_groups": failed_groups, + } except Exception as e: self.LOG.error(f"定时任务出错: {e}") + return {"candidate_groups": 0, "success_groups": 0, "failed_groups": 1} # 解析JSON def extract_content(self, data_string): diff --git a/plugins/member_context/main.py b/plugins/member_context/main.py index 4ccfb39..e541f4c 100644 --- a/plugins/member_context/main.py +++ b/plugins/member_context/main.py @@ -3,7 +3,6 @@ from typing import Dict, Any, Tuple, Optional, List from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus from plugins.member_context.service import MemberContextService -from utils.decorator.async_job import async_job class MemberContextPlugin(MessagePluginInterface): @@ -44,39 +43,10 @@ class MemberContextPlugin(MessagePluginInterface): super().__init__() self.feature = self.register_feature() self.service: Optional[MemberContextService] = None - self._job_registered = False def initialize(self, context: Dict[str, Any]) -> bool: self.LOG.debug(f"正在初始化 {self.name} 插件...") self.service = MemberContextService(context["db_manager"], self._config) - refresh_times = self._config.get("schedule", {}).get("refresh_times", []) - weekly_refresh_time = self._config.get("schedule", {}).get("weekly_refresh_time", "") - monthly_refresh_time = self._config.get("schedule", {}).get("monthly_refresh_time", "") - if not self._job_registered: - if refresh_times: - @async_job.at_times(refresh_times) - async def refresh_member_context_job(): - if self.service: - self.LOG.info("开始刷新成员交互摘要(日任务)") - self.service.refresh_all_chatrooms(enable_weekly_digest=False, enable_monthly_digest=False) - self.LOG.info("成员交互摘要刷新完成(日任务)") - - if weekly_refresh_time: - @async_job.every_week_time(weekday=6, time_str=weekly_refresh_time) - async def refresh_member_context_weekly_job(): - if self.service: - self.LOG.info("开始刷新成员交互摘要(周任务)") - self.service.refresh_all_chatrooms(enable_weekly_digest=True, enable_monthly_digest=False) - self.LOG.info("成员交互摘要刷新完成(周任务)") - - if monthly_refresh_time: - @async_job.every_month_last_day_time(monthly_refresh_time) - async def refresh_member_context_monthly_job(): - if self.service: - self.LOG.info("开始刷新成员交互摘要(月任务)") - self.service.refresh_all_chatrooms(enable_weekly_digest=False, enable_monthly_digest=True) - self.LOG.info("成员交互摘要刷新完成(月任务)") - self._job_registered = True self.LOG.debug(f"{self.name} 插件初始化完成") return True @@ -95,3 +65,98 @@ class MemberContextPlugin(MessagePluginInterface): 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]]: + """把成员画像日/周/月刷新动作声明给统一调度中心。""" + schedule_cfg = self._config.get("schedule", {}) or {} + refresh_times = schedule_cfg.get("refresh_times", []) or [] + weekly_refresh_time = str(schedule_cfg.get("weekly_refresh_time", "") or "").strip() + monthly_refresh_time = str(schedule_cfg.get("monthly_refresh_time", "") or "").strip() + + actions: List[Dict[str, Any]] = [] + if refresh_times: + actions.append( + { + "action_key": "daily_refresh", + "name": "成员画像日刷新", + "description": "刷新启用群的成员交互摘要(日任务)", + "trigger_type": "at_times", + "trigger_config": {"time_list": refresh_times}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + } + ) + if weekly_refresh_time: + actions.append( + { + "action_key": "weekly_refresh", + "name": "成员画像周刷新", + "description": "刷新启用群的成员交互摘要(周任务,含周摘要)", + "trigger_type": "every_week_time", + "trigger_config": {"weekday": 6, "time_str": weekly_refresh_time}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + } + ) + if monthly_refresh_time: + actions.append( + { + "action_key": "monthly_refresh", + "name": "成员画像月刷新", + "description": "刷新启用群的成员交互摘要(月任务,含月摘要)", + "trigger_type": "every_month_last_day_time", + "trigger_config": {"time_str": monthly_refresh_time}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {}, + "default_enabled": True, + } + ) + return actions + + async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: + """执行成员画像后台调度动作。""" + if not self.service: + return {"success": False, "summary": "服务未初始化", "detail": {}} + if action_key not in {"daily_refresh", "weekly_refresh", "monthly_refresh"}: + return {"success": False, "summary": f"不支持的动作: {action_key}", "detail": {"action_key": action_key}} + + # 兼容“指定群执行”的场景;若未指定则沿用全量刷新逻辑。 + target_groups = [str(g).strip() for g in (context.get("target_groups") or []) if str(g).strip()] + enable_weekly = action_key == "weekly_refresh" + enable_monthly = action_key == "monthly_refresh" + + try: + if target_groups: + groups = 0 + members = 0 + skipped = 0 + for group_id in target_groups: + result = self.service.refresh_group_contexts( + group_id, + enable_weekly_digest=enable_weekly, + enable_monthly_digest=enable_monthly, + ) + if result.get("disabled"): + continue + groups += 1 + members += int(result.get("refreshed", 0)) + skipped += int(result.get("skipped", 0)) + detail = {"groups": groups, "members": members, "skipped": skipped, "targeted": True} + else: + detail = self.service.refresh_all_chatrooms( + enable_weekly_digest=enable_weekly, + enable_monthly_digest=enable_monthly, + ) + detail["targeted"] = False + return { + "success": True, + "summary": f"成员画像刷新完成: 群{detail.get('groups', 0)},成员{detail.get('members', 0)}", + "detail": detail, + } + except Exception as e: + return {"success": False, "summary": f"执行异常: {e}", "detail": {"error": str(e)}} diff --git a/plugins/message_summary/main.py b/plugins/message_summary/main.py index 50da87b..d9793e7 100644 --- a/plugins/message_summary/main.py +++ b/plugins/message_summary/main.py @@ -12,7 +12,6 @@ from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus from db.message_summary_db import MessageSummaryDBOperator from utils.compress_chat_data import compress_chat_data -from utils.decorator.async_job import async_job from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.decorator.points_decorator import plugin_points_cost from utils.decorator.rate_limit_decorator import group_feature_rate_limit @@ -73,8 +72,6 @@ class MessageSummaryPlugin(MessagePluginInterface): self._auto_revoke = None # 注册功能权限 self.feature = self.register_feature() - # 注册定时任务:每天早上9点总结昨天的聊天信息 - async_job.at_times(["09:00"])(self.daily_summary_job) def initialize(self, context: Dict[str, Any]) -> bool: """初始化插件""" @@ -203,6 +200,49 @@ class MessageSummaryPlugin(MessagePluginInterface): self.LOG.error(f"处理消息总结命令失败: {e}") return False, None + def get_schedule_actions(self) -> List[Dict[str, Any]]: + """声明群总结插件支持的后台可配置定时动作。""" + return [ + { + "action_key": "daily_summary", + "name": "昨日群聊总结推送", + "description": "每天自动总结昨天群聊内容并发送到目标群", + "trigger_type": "at_times", + "trigger_config": {"time_list": ["09:00"]}, + "target_scope": "all_enabled_groups", + "target_config": {}, + "payload": {"min_messages": 100}, + # 保持与原先硬编码任务一致:默认启用。 + "default_enabled": True, + } + ] + + async def run_scheduled_action(self, action_key: str, context: Dict[str, Any]) -> Dict[str, Any]: + """执行群总结定时动作,返回结构化执行结果。""" + if action_key != "daily_summary": + return { + "success": False, + "summary": f"不支持的动作: {action_key}", + "detail": {"action_key": action_key}, + } + + payload = context.get("payload") or {} + # 使用后台可配置参数控制最低消息阈值,避免写死在代码里。 + min_messages = int(payload.get("min_messages", 100)) + target_groups = context.get("target_groups") or [] + result = await self.daily_summary_job( + target_groups=[str(g).strip() for g in target_groups if str(g).strip()], + min_message_count=min_messages, + ) + return { + "success": bool(result.get("failed_groups", 0) == 0), + "summary": ( + f"群总结完成: 目标{result.get('total_groups', 0)}群," + f"发送成功{result.get('sent_groups', 0)}群,失败{result.get('failed_groups', 0)}群" + ), + "detail": result, + } + async def _async_generate_and_send_summary( self, chat_content: str, @@ -545,12 +585,24 @@ class MessageSummaryPlugin(MessagePluginInterface): section = "\n".join(section_lines) return f"{section}\n\n{summary.strip()}" - async def daily_summary_job(self): - """定时任务:每天早上9点总结昨天的聊天信息""" + async def daily_summary_job( + self, + target_groups: Optional[List[str]] = None, + min_message_count: int = 100, + ) -> Dict[str, Any]: + """定时任务:总结指定群在昨日的聊天信息并推送。 + + Args: + target_groups: 目标群列表;为空时自动取所有已开启权限的群。 + min_message_count: 触发总结的最低消息条数阈值。 + + Returns: + dict: 执行统计信息,便于后台调度日志展示。 + """ try: if not self.bot: self.LOG.warning("每日聊天总结任务跳过:bot 尚未注入") - return + return {"total_groups": 0, "sent_groups": 0, "failed_groups": 0, "skipped_groups": 0} self.LOG.info("开始执行每日聊天总结任务") # 计算昨天的时间范围 @@ -561,24 +613,32 @@ class MessageSummaryPlugin(MessagePluginInterface): self.LOG.info( f"总结时间范围: {yesterday_start.strftime('%Y-%m-%d %H:%M:%S')} 至 {yesterday_end.strftime('%Y-%m-%d %H:%M:%S')}") - # 获取所有启用了群机器人的群聊 - all_groups = GroupBotManager.get_group_list() + # 调度动作支持“全部启用群”与“指定群”两种模式;指定群也会再做权限校验,防止误推送。 + if target_groups: + enabled_groups = [] + for group_id in target_groups: + if GroupBotManager.get_group_permission(group_id, self.feature) == PermissionStatus.ENABLED: + enabled_groups.append(group_id) + self.LOG.info(f"按指定目标群执行总结,输入={len(target_groups)},权限通过={len(enabled_groups)}") + else: + all_groups = GroupBotManager.get_group_list() + if not all_groups: + self.LOG.info("没有群聊启用群机器人,跳过定时总结") + return {"total_groups": 0, "sent_groups": 0, "failed_groups": 0, "skipped_groups": 0} - if not all_groups: - self.LOG.info("没有群聊启用群机器人,跳过定时总结") - return - - # 筛选出开启了总结功能的群聊 - enabled_groups = [] - for group_id in all_groups: - if GroupBotManager.get_group_permission(group_id, self.feature) == PermissionStatus.ENABLED: - enabled_groups.append(group_id) + enabled_groups = [] + for group_id in all_groups: + if GroupBotManager.get_group_permission(group_id, self.feature) == PermissionStatus.ENABLED: + enabled_groups.append(group_id) if not enabled_groups: self.LOG.info("没有群聊开启定时总结功能,跳过") - return + return {"total_groups": 0, "sent_groups": 0, "failed_groups": 0, "skipped_groups": 0} self.LOG.info(f"找到 {len(enabled_groups)} 个开启定时总结的群聊") + sent_groups = 0 + failed_groups = 0 + skipped_groups = 0 # 为每个群生成总结 for group_id in enabled_groups: @@ -590,9 +650,10 @@ class MessageSummaryPlugin(MessagePluginInterface): yesterday_end ) - # 消息少于50条,跳过总结 - if message_count < 100: - self.LOG.info(f"群 {group_id} 昨天只有 {message_count} 条消息,不足50条,跳过总结") + # 消息低于阈值时跳过,阈值可由后台 payload 配置。 + if message_count < min_message_count: + self.LOG.info(f"群 {group_id} 昨天只有 {message_count} 条消息,不足{min_message_count}条,跳过总结") + skipped_groups += 1 continue self.LOG.info(f"群 {group_id} 昨天有 {message_count} 条消息,开始获取内容") @@ -651,6 +712,7 @@ class MessageSummaryPlugin(MessagePluginInterface): str(image_path), ) self.LOG.info(f"成功发送群 {group_name} 的昨日总结图片") + sent_groups += 1 else: # 图片生成失败,发送文本消息 if summary and len(summary.strip()) > 0: @@ -661,6 +723,7 @@ class MessageSummaryPlugin(MessagePluginInterface): if summary.strip() == "生成总结时出错": await self._send_text_with_revoke(group_id, f"❌ [{yesterday.strftime('%Y-%m-%d')}] 聊天总结生成失败,请稍后再试", 5) self.LOG.warning(f"群 {group_name} 的昨日总结生成失败,已发送可撤回失败提醒") + failed_groups += 1 else: await self.bot.send_text_message(group_id, summary) self._save_daily_summary_record( @@ -673,18 +736,29 @@ class MessageSummaryPlugin(MessagePluginInterface): None, ) self.LOG.info(f"成功发送群 {group_name} 的昨日总结文本") + sent_groups += 1 else: await self._send_text_with_revoke(group_id, f"❌ [{yesterday.strftime('%Y-%m-%d')}] 聊天总结生成失败,请稍后再试", 5) self.LOG.warning(f"群 {group_name} 的昨日总结无有效内容,已发送可撤回失败提醒") + failed_groups += 1 # 避免请求过快 await asyncio.sleep(2) except Exception as e: self.LOG.error(f"为群 {group_id} 生成昨日总结失败: {e}", exc_info=True) + failed_groups += 1 continue self.LOG.info("每日聊天总结任务执行完成") + return { + "total_groups": len(enabled_groups), + "sent_groups": sent_groups, + "failed_groups": failed_groups, + "skipped_groups": skipped_groups, + "min_message_count": min_message_count, + } except Exception as e: self.LOG.error(f"每日聊天总结任务执行失败: {e}", exc_info=True) + return {"total_groups": 0, "sent_groups": 0, "failed_groups": 1, "skipped_groups": 0} diff --git a/plugins/weather/main.py b/plugins/weather/main.py index 0d3a024..0348984 100644 --- a/plugins/weather/main.py +++ b/plugins/weather/main.py @@ -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: