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

- 将 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

@@ -57,6 +57,8 @@
<el-option label="每天固定时间" value="at_times"></el-option> <el-option label="每天固定时间" value="at_times"></el-option>
<el-option label="固定间隔(秒)" value="every_seconds"></el-option> <el-option label="固定间隔(秒)" value="every_seconds"></el-option>
<el-option label="每周固定时间" value="every_weekday_time"></el-option> <el-option label="每周固定时间" value="every_weekday_time"></el-option>
<el-option label="每周固定时间(兼容)" value="every_week_time"></el-option>
<el-option label="每月最后一天" value="every_month_last_day_time"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="editForm.trigger_type === 'at_times'" label="时间列表"> <el-form-item v-if="editForm.trigger_type === 'at_times'" label="时间列表">
@@ -65,7 +67,7 @@
<el-form-item v-if="editForm.trigger_type === 'every_seconds'" label="间隔秒"> <el-form-item v-if="editForm.trigger_type === 'every_seconds'" label="间隔秒">
<el-input-number v-model="editForm.seconds" :min="1"></el-input-number> <el-input-number v-model="editForm.seconds" :min="1"></el-input-number>
</el-form-item> </el-form-item>
<el-form-item v-if="editForm.trigger_type === 'every_weekday_time'" label="星期"> <el-form-item v-if="editForm.trigger_type === 'every_weekday_time' || editForm.trigger_type === 'every_week_time'" label="星期">
<el-select v-model="editForm.weekday"> <el-select v-model="editForm.weekday">
<el-option label="周一" :value="0"></el-option> <el-option label="周一" :value="0"></el-option>
<el-option label="周二" :value="1"></el-option> <el-option label="周二" :value="1"></el-option>
@@ -76,7 +78,7 @@
<el-option label="周日" :value="6"></el-option> <el-option label="周日" :value="6"></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item v-if="editForm.trigger_type === 'every_weekday_time'" label="时间"> <el-form-item v-if="editForm.trigger_type === 'every_weekday_time' || editForm.trigger_type === 'every_week_time' || editForm.trigger_type === 'every_month_last_day_time'" label="时间">
<el-input v-model="editForm.time_str" placeholder="10:00"></el-input> <el-input v-model="editForm.time_str" placeholder="10:00"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="目标范围"> <el-form-item label="目标范围">
@@ -236,9 +238,12 @@ new Vue({
if (this.editForm.trigger_type === 'every_seconds') { if (this.editForm.trigger_type === 'every_seconds') {
return { seconds: Number(this.editForm.seconds || 60) } 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') } 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 {} return {}
}, },
buildTargetConfig() { buildTargetConfig() {

View File

@@ -4,7 +4,6 @@ from typing import Dict, Any, List, Optional, Tuple
from loguru import logger 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.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus from base.plugin_common.plugin_interface import PluginStatus
from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.decorator.plugin_decorators import plugin_stats_decorator
@@ -60,7 +59,6 @@ class GameTaskPlugin(MessagePluginInterface):
self.LOG = logger self.LOG = logger
# 注册功能权限 # 注册功能权限
self.feature = self.register_feature() self.feature = self.register_feature()
async_job.at_times(["17:58"])(self.run_random_task_assignment)
def initialize(self, context: Dict[str, Any]) -> bool: def initialize(self, context: Dict[str, Any]) -> bool:
"""初始化插件""" """初始化插件"""
@@ -185,6 +183,43 @@ class GameTaskPlugin(MessagePluginInterface):
self.LOG.error(f"处理消息出错: {e}") self.LOG.error(f"处理消息出错: {e}")
return False, 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: async def _handle_join_game(self, sender: str, roomid: str, wx_nick_name: str) -> None:
"""处理加入游戏请求""" """处理加入游戏请求"""
try: try:
@@ -535,20 +570,34 @@ class GameTaskPlugin(MessagePluginInterface):
sender sender
) )
async def run_random_task_assignment(self) -> None: async def run_random_task_assignment(self, target_groups: Optional[List[str]] = None) -> Dict[str, int]:
"""定时任务:整点触发排除23:00-08:00""" """定时任务:随机发题,排除 23:00-08:00
Args:
target_groups: 指定目标群列表;为空时按原逻辑扫描全部游戏群。
Returns:
dict: 执行统计信息。
"""
current_hour = datetime.now().hour current_hour = datetime.now().hour
if current_hour >= 23 or current_hour < 9: if current_hour >= 23 or current_hour < 9:
self.LOG.info(f"当前时间 {current_hour}:00 在23:00-08:00区间跳过任务发放") self.LOG.info(f"当前时间 {current_hour}:00 在23:00-08:00区间跳过任务发放")
return return {"candidate_groups": 0, "success_groups": 0, "failed_groups": 0}
try: try:
# 获取所有群聊 # 获取所有群聊
groups = self.encyclopedia_db.get_all_groups() 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: 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: if GroupBotManager.get_group_permission(group,self.feature) == PermissionStatus.DISABLED:
continue continue
candidate_groups += 1
# 获取群内所有玩家 # 获取群内所有玩家
players = self.encyclopedia_db.get_all_players_in_group(group) players = self.encyclopedia_db.get_all_players_in_group(group)
@@ -583,8 +632,17 @@ class GameTaskPlugin(MessagePluginInterface):
f"🌼 积分:{score}\n" f"🌼 积分:{score}\n"
f"🌈 抢答格式:/a {active_task_id} 答案", f"🌈 抢答格式:/a {active_task_id} 答案",
[holder_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: except Exception as e:
self.LOG.error(f"定时任务出错: {e}") self.LOG.error(f"定时任务出错: {e}")
return {"candidate_groups": 0, "success_groups": 0, "failed_groups": 1}
# 解析JSON # 解析JSON
def extract_content(self, data_string): def extract_content(self, data_string):

View File

@@ -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.message_plugin_interface import MessagePluginInterface
from base.plugin_common.plugin_interface import PluginStatus from base.plugin_common.plugin_interface import PluginStatus
from plugins.member_context.service import MemberContextService from plugins.member_context.service import MemberContextService
from utils.decorator.async_job import async_job
class MemberContextPlugin(MessagePluginInterface): class MemberContextPlugin(MessagePluginInterface):
@@ -44,39 +43,10 @@ class MemberContextPlugin(MessagePluginInterface):
super().__init__() super().__init__()
self.feature = self.register_feature() self.feature = self.register_feature()
self.service: Optional[MemberContextService] = None self.service: Optional[MemberContextService] = None
self._job_registered = False
def initialize(self, context: Dict[str, Any]) -> bool: def initialize(self, context: Dict[str, Any]) -> bool:
self.LOG.debug(f"正在初始化 {self.name} 插件...") self.LOG.debug(f"正在初始化 {self.name} 插件...")
self.service = MemberContextService(context["db_manager"], self._config) 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} 插件初始化完成") self.LOG.debug(f"{self.name} 插件初始化完成")
return True return True
@@ -95,3 +65,98 @@ class MemberContextPlugin(MessagePluginInterface):
async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]:
return False, None 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)}}

View File

@@ -12,7 +12,6 @@ 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 db.message_summary_db import MessageSummaryDBOperator from db.message_summary_db import MessageSummaryDBOperator
from utils.compress_chat_data import compress_chat_data 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.plugin_decorators import plugin_stats_decorator
from utils.decorator.points_decorator import plugin_points_cost from utils.decorator.points_decorator import plugin_points_cost
from utils.decorator.rate_limit_decorator import group_feature_rate_limit from utils.decorator.rate_limit_decorator import group_feature_rate_limit
@@ -73,8 +72,6 @@ class MessageSummaryPlugin(MessagePluginInterface):
self._auto_revoke = None self._auto_revoke = None
# 注册功能权限 # 注册功能权限
self.feature = self.register_feature() self.feature = self.register_feature()
# 注册定时任务每天早上9点总结昨天的聊天信息
async_job.at_times(["09:00"])(self.daily_summary_job)
def initialize(self, context: Dict[str, Any]) -> bool: def initialize(self, context: Dict[str, Any]) -> bool:
"""初始化插件""" """初始化插件"""
@@ -203,6 +200,49 @@ class MessageSummaryPlugin(MessagePluginInterface):
self.LOG.error(f"处理消息总结命令失败: {e}") self.LOG.error(f"处理消息总结命令失败: {e}")
return False, None 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( async def _async_generate_and_send_summary(
self, self,
chat_content: str, chat_content: str,
@@ -545,12 +585,24 @@ class MessageSummaryPlugin(MessagePluginInterface):
section = "\n".join(section_lines) section = "\n".join(section_lines)
return f"{section}\n\n{summary.strip()}" return f"{section}\n\n{summary.strip()}"
async def daily_summary_job(self): async def daily_summary_job(
"""定时任务每天早上9点总结昨天的聊天信息""" self,
target_groups: Optional[List[str]] = None,
min_message_count: int = 100,
) -> Dict[str, Any]:
"""定时任务:总结指定群在昨日的聊天信息并推送。
Args:
target_groups: 目标群列表;为空时自动取所有已开启权限的群。
min_message_count: 触发总结的最低消息条数阈值。
Returns:
dict: 执行统计信息,便于后台调度日志展示。
"""
try: try:
if not self.bot: if not self.bot:
self.LOG.warning("每日聊天总结任务跳过bot 尚未注入") self.LOG.warning("每日聊天总结任务跳过bot 尚未注入")
return return {"total_groups": 0, "sent_groups": 0, "failed_groups": 0, "skipped_groups": 0}
self.LOG.info("开始执行每日聊天总结任务") self.LOG.info("开始执行每日聊天总结任务")
# 计算昨天的时间范围 # 计算昨天的时间范围
@@ -561,24 +613,32 @@ class MessageSummaryPlugin(MessagePluginInterface):
self.LOG.info( self.LOG.info(
f"总结时间范围: {yesterday_start.strftime('%Y-%m-%d %H:%M:%S')}{yesterday_end.strftime('%Y-%m-%d %H:%M:%S')}") 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: enabled_groups = []
self.LOG.info("没有群聊启用群机器人,跳过定时总结") for group_id in all_groups:
return 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: if not enabled_groups:
self.LOG.info("没有群聊开启定时总结功能,跳过") self.LOG.info("没有群聊开启定时总结功能,跳过")
return return {"total_groups": 0, "sent_groups": 0, "failed_groups": 0, "skipped_groups": 0}
self.LOG.info(f"找到 {len(enabled_groups)} 个开启定时总结的群聊") self.LOG.info(f"找到 {len(enabled_groups)} 个开启定时总结的群聊")
sent_groups = 0
failed_groups = 0
skipped_groups = 0
# 为每个群生成总结 # 为每个群生成总结
for group_id in enabled_groups: for group_id in enabled_groups:
@@ -590,9 +650,10 @@ class MessageSummaryPlugin(MessagePluginInterface):
yesterday_end yesterday_end
) )
# 消息少于50条跳过总结 # 消息低于阈值时跳过,阈值可由后台 payload 配置。
if message_count < 100: if message_count < min_message_count:
self.LOG.info(f"{group_id} 昨天只有 {message_count} 条消息,不足50条,跳过总结") self.LOG.info(f"{group_id} 昨天只有 {message_count} 条消息,不足{min_message_count}条,跳过总结")
skipped_groups += 1
continue continue
self.LOG.info(f"{group_id} 昨天有 {message_count} 条消息,开始获取内容") self.LOG.info(f"{group_id} 昨天有 {message_count} 条消息,开始获取内容")
@@ -651,6 +712,7 @@ class MessageSummaryPlugin(MessagePluginInterface):
str(image_path), str(image_path),
) )
self.LOG.info(f"成功发送群 {group_name} 的昨日总结图片") self.LOG.info(f"成功发送群 {group_name} 的昨日总结图片")
sent_groups += 1
else: else:
# 图片生成失败,发送文本消息 # 图片生成失败,发送文本消息
if summary and len(summary.strip()) > 0: if summary and len(summary.strip()) > 0:
@@ -661,6 +723,7 @@ class MessageSummaryPlugin(MessagePluginInterface):
if summary.strip() == "生成总结时出错": if summary.strip() == "生成总结时出错":
await self._send_text_with_revoke(group_id, f"❌ [{yesterday.strftime('%Y-%m-%d')}] 聊天总结生成失败,请稍后再试", 5) await self._send_text_with_revoke(group_id, f"❌ [{yesterday.strftime('%Y-%m-%d')}] 聊天总结生成失败,请稍后再试", 5)
self.LOG.warning(f"{group_name} 的昨日总结生成失败,已发送可撤回失败提醒") self.LOG.warning(f"{group_name} 的昨日总结生成失败,已发送可撤回失败提醒")
failed_groups += 1
else: else:
await self.bot.send_text_message(group_id, summary) await self.bot.send_text_message(group_id, summary)
self._save_daily_summary_record( self._save_daily_summary_record(
@@ -673,18 +736,29 @@ class MessageSummaryPlugin(MessagePluginInterface):
None, None,
) )
self.LOG.info(f"成功发送群 {group_name} 的昨日总结文本") self.LOG.info(f"成功发送群 {group_name} 的昨日总结文本")
sent_groups += 1
else: else:
await self._send_text_with_revoke(group_id, f"❌ [{yesterday.strftime('%Y-%m-%d')}] 聊天总结生成失败,请稍后再试", 5) await self._send_text_with_revoke(group_id, f"❌ [{yesterday.strftime('%Y-%m-%d')}] 聊天总结生成失败,请稍后再试", 5)
self.LOG.warning(f"{group_name} 的昨日总结无有效内容,已发送可撤回失败提醒") self.LOG.warning(f"{group_name} 的昨日总结无有效内容,已发送可撤回失败提醒")
failed_groups += 1
# 避免请求过快 # 避免请求过快
await asyncio.sleep(2) await asyncio.sleep(2)
except Exception as e: except Exception as e:
self.LOG.error(f"为群 {group_id} 生成昨日总结失败: {e}", exc_info=True) self.LOG.error(f"为群 {group_id} 生成昨日总结失败: {e}", exc_info=True)
failed_groups += 1
continue continue
self.LOG.info("每日聊天总结任务执行完成") 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: except Exception as e:
self.LOG.error(f"每日聊天总结任务执行失败: {e}", exc_info=True) self.LOG.error(f"每日聊天总结任务执行失败: {e}", exc_info=True)
return {"total_groups": 0, "sent_groups": 0, "failed_groups": 1, "skipped_groups": 0}

View File

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