插件定时能力扩展:接入天气/群总结/百科问答/成员画像并补齐周月触发器编辑
- 将 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:
@@ -57,6 +57,8 @@
|
||||
<el-option label="每天固定时间" value="at_times"></el-option>
|
||||
<el-option label="固定间隔(秒)" value="every_seconds"></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-form-item>
|
||||
<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-input-number v-model="editForm.seconds" :min="1"></el-input-number>
|
||||
</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-option label="周一" :value="0"></el-option>
|
||||
<el-option label="周二" :value="1"></el-option>
|
||||
@@ -76,7 +78,7 @@
|
||||
<el-option label="周日" :value="6"></el-option>
|
||||
</el-select>
|
||||
</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-form-item>
|
||||
<el-form-item label="目标范围">
|
||||
@@ -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() {
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user