import base64 import json import os from datetime import datetime, timedelta from typing import Dict, Any, List, Optional, Tuple from loguru import logger from pathlib import Path from base.plugin_common.message_plugin_interface import MessagePluginInterface from base.plugin_common.plugin_interface import PluginStatus from db.connection import DBConnectionManager from db.task_db import TaskDBOperator from utils.decorator.async_job import async_job from utils.decorator.plugin_decorators import plugin_stats_decorator from utils.robot_cmd.robot_command import PermissionStatus, GroupBotManager from wechat_ipad import WechatAPIClient from wechat_ipad.models.appmsg_xml import LINK_XML_NORMAL class MessagePushTask(MessagePluginInterface): """消息推送任务插件""" # 功能权限常量 FEATURE_KEY = "MESSAGE_PUSH" FEATURE_DESCRIPTION = "📢 消息推送功能 [推送, 消息推送, 定时推送]" @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 "liu.wei" @property def command_prefix(self) -> Optional[str]: return "" # 不需要前缀,直接匹配命令 @property def commands(self) -> List[str]: return self._commands def __init__(self): super().__init__() self.feature = self.register_feature() self.db = None self._task_execution_records = {} # 用于记录任务执行状态 def initialize(self, context: Dict[str, Any]) -> bool: """初始化插件""" self.LOG = logger self.LOG.debug(f"正在初始化 {self.name} 插件...") # 保存上下文对象 self.event_system = context.get("event_system") self.db_manager = DBConnectionManager.get_instance() # 初始化组件 self.db = TaskDBOperator(self.db_manager) # 初始化数据库表 if not self.db.init_tables(): self.LOG.error("初始化数据库表失败") return False async_job.every_seconds(5)(self.process_scheduled_tasks) # 加载配置 self._commands = self._config.get("MessagePush", {}).get("command", ["推送", "消息推送"]) self.command_format = self._config.get("MessagePush", {}).get("command-format", "推送 消息内容") self.enable = self._config.get("MessagePush", {}).get("enable", True) self.LOG.debug(f"[{self.name}] 插件初始化完成,指令:{self._commands}") return True def start(self) -> bool: """启动插件""" self.LOG.info(f"[{self.name}] 插件已启动") self.status = PluginStatus.RUNNING return True def stop(self) -> bool: """停止插件""" self.LOG.info(f"[{self.name}] 插件已停止") self.status = PluginStatus.STOPPED return True def can_process(self, message: Dict[str, Any]) -> bool: """检查是否可以处理该消息""" if not self.enable: return False content = str(message.get("content", "")).strip() command = content.split(" ")[0] return command in self._commands @plugin_stats_decorator(plugin_name="消息推送任务") async def process_message(self, message: Dict[str, Any]) -> Tuple[bool, Optional[str]]: """处理消息""" content = str(message.get("content", "")).strip() self.LOG.debug(f"插件执行: {self.name}:{content}") command = content.split(" ")[0] sender = message.get("sender") roomid = message.get("roomid", "") gbm: GroupBotManager = message.get("gbm") bot: WechatAPIClient = message.get("bot") # 检查命令格式 if len(content.split(" ")) == 1: await bot.send_text_message((roomid if roomid else sender), f"❌命令格式错误!\n{self.command_format}", sender) return False, "命令格式错误" # 检查权限 if roomid and gbm.get_group_permission(roomid, self.feature) == PermissionStatus.DISABLED: return False, "没有权限" return False, None def _get_execution_key(self, task_id: str, hour: int, minute: int) -> str: """生成任务执行记录的唯一键""" now = datetime.now() return f"{task_id}_{now.date()}_{hour}_{minute}" async def process_scheduled_tasks(self): """处理定时任务""" try: # 获取待执行的任务 tasks = self.db.get_scheduled_tasks() if not tasks: return for task in tasks: try: self.LOG.debug(f"开始处理定时任务: {task['task_id']}") # 检查任务是否应该执行 now = datetime.now() schedule_time = task['schedule_time'] # 对于重复任务,检查是否到达执行时间点 if task['schedule_type'] == 'recurring': recurring_time = task.get('recurring_time') if recurring_time: try: # 处理 timedelta 对象 if isinstance(recurring_time, timedelta): total_seconds = int(recurring_time.total_seconds()) hour = total_seconds // 3600 minute = (total_seconds % 3600) // 60 second = total_seconds % 60 else: # 处理字符串格式 time_parts = str(recurring_time).split(':') if len(time_parts) == 3: hour, minute, second = map(int, time_parts) else: hour, minute = map(int, time_parts) second = 0 # 检查当前时间是否在目标时间的前后5秒内 target_time = now.replace(hour=hour, minute=minute, second=second, microsecond=0) time_diff = abs((now - target_time).total_seconds()) if time_diff > 5: # 允许5秒的误差 continue # 检查是否已经在这个时间点执行过 execution_key = self._get_execution_key(task['task_id'], hour, minute) if execution_key in self._task_execution_records: self.LOG.debug(f"任务 {task['task_id']} 今天已经执行过,跳过") continue # 记录执行状态 self._task_execution_records[execution_key] = now except (ValueError, AttributeError) as e: self.LOG.error(f"无效的执行时间格式: {recurring_time}, 错误: {e}") continue # 更新任务状态为执行中 self.db.update_task(task['task_id'], {'status': 'running'}) # 获取任务内容 content_text = task.get('content_text') content_image = task.get('content_image') content_link = task.get('content_link') content_miniprogram = task.get('content_miniprogram') content_voice = task.get('content_voice') # 语音消息 content_video = task.get('content_video') # 视频消息 # 记录任务开始执行 self.db.log_task_action({ 'log_id': f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}", 'task_id': task['task_id'], 'action': 'update', 'user_id': task['creator_id'], 'changes': {'status': 'running'} }) # 发送消息到目标群组 if not self.bot: raise Exception("机器人实例未初始化") # 发送链接消息,提前进行数据整理,上传缩略图 if content_link: # content_link json 读取内容 link_data = json.loads(content_link) thumburl_path = link_data.get('thumburl', '') thumburl_wx = "" if thumburl_path and os.path.exists(thumburl_path): # Read local image file with open(thumburl_path, 'rb') as image_file: # Convert image to base64 base64_string = base64.b64encode(image_file.read()).decode('utf-8') # Call upload method with base64 string thumburl_wx = await self.bot.friend_circle_upload(base64=base64_string) else: print(f"Image file not found at: {thumburl_path}") success_count = 0 fail_count = 0 groups = task.get('groups', []) try: groups = json.loads(groups) except json.JSONDecodeError: raise Exception("无发送清单") for group_id in groups: try: # 发送文本消息 if content_text: await self.bot.send_text_message(group_id, content_text) # 发送图片消息 if content_image: await self.bot.send_image_message(group_id, Path(content_image)) # 发送链接消息 if content_link: # content_link json 读取内容 link_data = json.loads(content_link) xml_content = f"{LINK_XML_NORMAL}".format(title=link_data.get('title', ''), des=link_data.get('des', ''), url=link_data.get('url', ''), thumburl=thumburl_wx ) await self.bot.send_link_xml_message(xml_content, group_id) # 发送语音消息 if content_voice: voice_path = Path(task['content_voice']) # 根据文件扩展名确定类型 voice_type = 'wav' if voice_path.suffix.lower() == '.wav' else 'mp3' await self.bot.send_voice_message(group_id, Path(content_voice), voice_type) # 发送视频消息 if content_video: await self.bot.send_video_message(group_id, Path(content_video)) success_count += 1 except Exception as e: self.LOG.error(f"发送消息到群组 {group_id} 失败: {e}") fail_count += 1 # 更新任务状态 if task['schedule_type'] == 'recurring': # 检查是否超过重复结束时间 recurring_end = task.get('recurring_end') if recurring_end and now > recurring_end: # 如果超过结束时间,标记为已完成 status = 'completed' else: # 如果未超过结束时间,根据发送结果设置状态 if fail_count == 0: status = 'scheduled' # 保持为已调度状态 else: status = 'failed' # 任何失败都标记为失败 else: # 非重复任务的状态处理 if fail_count == 0: status = 'completed' else: status = 'failed' # 如果是重复任务且未超过结束时间,先计算下次执行时间 next_time = None if task['schedule_type'] == 'recurring' and status != 'completed': next_time = self._calculate_next_schedule_time( task['schedule_time'], task['recurring_interval'], task['recurring_end'], task['recurring_time'], task['weekly_days'], task['monthly_day'] ) if next_time: self.LOG.info(f"计算任务 {task['task_id']} 的下次执行时间为: {next_time}") else: # 如果没有下次执行时间,标记为已完成 status = 'completed' self.LOG.info(f"任务 {task['task_id']} 没有下次执行时间,标记为已完成") # 更新任务状态和下次执行时间 update_data = {'status': status} if next_time: update_data['schedule_time'] = next_time self.db.update_task(task['task_id'], update_data) # 记录任务完成 self.db.log_task_action({ 'log_id': f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}", 'task_id': task['task_id'], 'action': 'update', 'user_id': task['creator_id'], 'changes': update_data }) self.LOG.info(f"定时任务 {task['task_id']} 处理完成,状态: {status}, 下次执行时间: {next_time}") except Exception as e: self.LOG.error(f"处理定时任务 {task['task_id']} 失败: {e}") # 更新任务状态为失败 self.db.update_task(task['task_id'], {'status': 'failed'}) # 记录错误日志 self.db.log_task_action({ 'log_id': f"log_{datetime.now().strftime('%Y%m%d%H%M%S')}", 'task_id': task['task_id'], 'action': 'update', 'user_id': task['creator_id'], 'changes': {'status': 'failed', 'error': str(e)} }) except Exception as e: self.LOG.error(f"处理定时任务出错: {e}") def _calculate_next_schedule_time(self, schedule_time: datetime, recurring_interval: str, recurring_end: datetime, recurring_time: str, weekly_days: str = None, monthly_day: int = None) -> Optional[datetime]: """计算下次执行时间""" try: # 解析执行时间 if recurring_time: try: # 处理 timedelta 对象 if isinstance(recurring_time, timedelta): total_seconds = int(recurring_time.total_seconds()) hour = total_seconds // 3600 minute = (total_seconds % 3600) // 60 second = total_seconds % 60 else: # 处理字符串格式 time_parts = str(recurring_time).split(':') if len(time_parts) == 3: hour, minute, second = map(int, time_parts) else: hour, minute = map(int, time_parts) second = 0 except (ValueError, AttributeError) as e: self.LOG.error(f"无效的执行时间格式: {recurring_time}, 错误: {e}") return None else: hour, minute = schedule_time.hour, schedule_time.minute second = 0 # 获取当前时间 now = datetime.now() # 如果已经超过结束时间,返回None if recurring_end and now > recurring_end: return None # 根据重复间隔计算下次执行时间 if recurring_interval == 'daily': # 每天执行 next_time = now.replace(hour=hour, minute=minute, second=second, microsecond=0) if next_time <= now: next_time = next_time + timedelta(days=1) elif recurring_interval == 'weekly': # 每周执行 try: if isinstance(weekly_days, str): weekly_days = json.loads(weekly_days) if not weekly_days: return None # 获取当前是周几(0-6,0是周一) current_weekday = now.weekday() # 将数据库中的周几(1-7)转换为代码中的周几(0-6) weekly_days = [int(day) - 1 for day in weekly_days] weekly_days.sort() # 找到下一个执行日 next_weekday = None for day in weekly_days: if day > current_weekday: next_weekday = day break # 如果本周没有下一个执行日,取下周的第一个执行日 if next_weekday is None: next_weekday = weekly_days[0] # 直接计算到下周的指定日期 days_ahead = (7 - current_weekday) + next_weekday else: days_ahead = next_weekday - current_weekday # 计算下次执行时间 next_time = now.replace(hour=hour, minute=minute, second=second, microsecond=0) + timedelta( days=days_ahead) # 如果计算出的时间小于等于当前时间,说明计算有误,需要再加一周 if next_time <= now: next_time = next_time + timedelta(days=7) self.LOG.info( f"每周任务计算:当前周几={current_weekday}, 下次执行周几={next_weekday}, 天数差={days_ahead}, 下次执行时间={next_time}") except (json.JSONDecodeError, ValueError, IndexError) as e: self.LOG.error(f"处理每周执行日失败: {e}") return None elif recurring_interval == 'monthly': # 每月执行 try: if not monthly_day: return None # 获取当前日期 current_day = now.day current_month = now.month current_year = now.year # 如果当前日期已经过了执行日,移到下个月 if current_day >= monthly_day: if current_month == 12: next_month = 1 next_year = current_year + 1 else: next_month = current_month + 1 next_year = current_year else: next_month = current_month next_year = current_year # 处理无效日期(如2月30日) try: next_time = datetime(next_year, next_month, monthly_day, hour, minute, second) except ValueError: # 如果日期无效,使用该月的最后一天 if next_month == 12: last_day = 31 else: last_day = (datetime(next_year, next_month + 1, 1) - timedelta(days=1)).day next_time = datetime(next_year, next_month, last_day, hour, minute, second) except Exception as e: self.LOG.error(f"处理每月执行日失败: {e}") return None else: return None # 检查是否超过结束时间 if recurring_end and next_time > recurring_end: return None return next_time except Exception as e: self.LOG.error(f"计算下次执行时间失败: {e}") return None