From b8cf6ea152cd67d2dad5ce5935222d29a49ee4f2 Mon Sep 17 00:00:00 2001 From: slexce <2767145231@qq.com> Date: Tue, 10 Mar 2026 21:18:46 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20=E6=B7=BB=E5=8A=A0=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E5=88=9B=E5=BB=BA=E4=B8=8E=E6=8F=90?= =?UTF-8?q?=E9=86=92=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- nonebot_plugin_llmchat/__init__.py | 15 +- nonebot_plugin_llmchat/config.py | 5 + nonebot_plugin_llmchat/mcpclient.py | 31 +- nonebot_plugin_llmchat/scheduler.py | 707 ++++++++++++++++++++++++++++ 4 files changed, 754 insertions(+), 4 deletions(-) create mode 100644 nonebot_plugin_llmchat/scheduler.py diff --git a/nonebot_plugin_llmchat/__init__.py b/nonebot_plugin_llmchat/__init__.py index 4dad9fd..32eb4ed 100755 --- a/nonebot_plugin_llmchat/__init__.py +++ b/nonebot_plugin_llmchat/__init__.py @@ -31,6 +31,7 @@ from openai import AsyncOpenAI from .config import Config, PresetConfig from .mcpclient import MCPClient +from .scheduler import SchedulerManager require("nonebot_plugin_localstore") import nonebot_plugin_localstore as store @@ -483,13 +484,17 @@ async def process_messages(context_id: int, is_group: bool = True): tool_name, tool_args, group_id=event.group_id, - bot_id=str(event.self_id) + bot_id=str(event.self_id), + user_id=event.user_id, + is_group=True ) else: result = await mcp_client.call_tool( tool_name, tool_args, - bot_id=str(event.self_id) + bot_id=str(event.self_id), + user_id=event.user_id, + is_group=False ) new_messages.append({ @@ -856,6 +861,9 @@ async def load_state(): async def init_plugin(): logger.info("插件启动初始化") await load_state() + # 加载定时任务 + scheduler_manager = SchedulerManager.get_instance() + await scheduler_manager.load_tasks() # 每5分钟保存状态 scheduler.add_job(save_state, "interval", minutes=5) @@ -864,5 +872,8 @@ async def init_plugin(): async def cleanup_plugin(): logger.info("插件关闭清理") await save_state() + # 保存定时任务 + scheduler_manager = SchedulerManager.get_instance() + await scheduler_manager.save_tasks() # 销毁MCPClient单例 await MCPClient.destroy_instance() diff --git a/nonebot_plugin_llmchat/config.py b/nonebot_plugin_llmchat/config.py index 1a70a44..5a47ab7 100755 --- a/nonebot_plugin_llmchat/config.py +++ b/nonebot_plugin_llmchat/config.py @@ -55,6 +55,11 @@ class ScopedConfig(BaseModel): ) enable_private_chat: bool = Field(False, description="是否启用私聊功能") private_chat_preset: str = Field("off", description="私聊默认使用的预设名称") + scheduler_max_retry: int = Field(5, description="定时任务AI调用最大重试次数") + scheduler_default_reminder: str = Field( + "您设置的提醒时间到了:{description}", + description="AI调用失败时的默认提醒模板" + ) class Config(BaseModel): diff --git a/nonebot_plugin_llmchat/mcpclient.py b/nonebot_plugin_llmchat/mcpclient.py index 447750f..ab5a219 100644 --- a/nonebot_plugin_llmchat/mcpclient.py +++ b/nonebot_plugin_llmchat/mcpclient.py @@ -12,6 +12,7 @@ from nonebot import logger from .config import MCPServerConfig, transportType from .onebottools import OneBotTools +from .scheduler import SchedulerManager class MCPClient: @@ -39,6 +40,8 @@ class MCPClient: self._cache_initialized = False # 初始化OneBot工具 self.onebot_tools = OneBotTools() + # 初始化定时任务管理器 + self.scheduler_manager = SchedulerManager.get_instance() self._initialized = True logger.debug("MCPClient单例初始化成功") @@ -114,7 +117,7 @@ class MCPClient: ) read, write, session_callback = transport case _: - raise ValueError("Unknown transport type") + raise ValueError("Server config must have either url or command") self.session = await self.exit_stack.enter_async_context(ClientSession(read, write)) await self.session.initialize() @@ -164,10 +167,20 @@ class MCPClient: if is_group: # 群聊场景,包含OneBot工具和MCP工具 available_tools.extend(self.onebot_tools.get_available_tools()) + # 添加定时任务工具(群聊和私聊都可用) + available_tools.extend(self.scheduler_manager.get_available_tools()) logger.debug(f"获取可用工具列表,共{len(available_tools)}个工具") return available_tools - async def call_tool(self, tool_name: str, tool_args: dict, group_id: int | None = None, bot_id: str | None = None): + async def call_tool( + self, + tool_name: str, + tool_args: dict, + group_id: int | None = None, + bot_id: str | None = None, + user_id: int | None = None, + is_group: bool = True + ): """按需连接调用工具,调用后立即断开""" # 检查是否是OneBot内置工具 if tool_name.startswith("ob__"): @@ -176,6 +189,16 @@ class MCPClient: logger.info(f"调用OneBot工具[{tool_name}]") return await self.onebot_tools.call_tool(tool_name, tool_args, group_id, bot_id) + # 检查是否是定时任务工具 + if tool_name.startswith("scheduler__"): + context_id = group_id if is_group else user_id + if context_id is None or user_id is None: + return "定时任务工具需要提供context_id和user_id参数" + logger.info(f"调用定时任务工具[{tool_name}]") + return await self.scheduler_manager.call_tool( + tool_name, tool_args, context_id, is_group, user_id + ) + # 检查是否是MCP工具 if tool_name.startswith("mcp__"): # MCP工具处理:mcp__server_name__tool_name @@ -205,6 +228,10 @@ class MCPClient: if tool_name.startswith("ob__"): return self.onebot_tools.get_friendly_name(tool_name) + # 检查是否是定时任务工具 + if tool_name.startswith("scheduler__"): + return self.scheduler_manager.get_friendly_name(tool_name) + # 检查是否是MCP工具 if tool_name.startswith("mcp__"): # MCP工具处理:mcp__server_name__tool_name diff --git a/nonebot_plugin_llmchat/scheduler.py b/nonebot_plugin_llmchat/scheduler.py new file mode 100644 index 0000000..dfd79de --- /dev/null +++ b/nonebot_plugin_llmchat/scheduler.py @@ -0,0 +1,707 @@ +"""定时任务管理模块""" + +import asyncio +import json +import os +import uuid +from datetime import datetime, timedelta +from enum import Enum +from typing import Any + +import aiofiles +import httpx +from nonebot import get_bot, get_driver, logger, require +from nonebot.adapters.onebot.v11 import Bot, Message +from openai import AsyncOpenAI +from pydantic import BaseModel, Field + +require("nonebot_plugin_localstore") +import nonebot_plugin_localstore as store + +require("nonebot_plugin_apscheduler") +from nonebot_plugin_apscheduler import scheduler + + +class ScheduleType(str, Enum): + """定时任务类型""" + INTERVAL_MINUTES = "interval_minutes" # 每N分钟 + DAILY = "daily" # 每天指定时间 + WEEKLY = "weekly" # 每周指定天 + YEARLY = "yearly" # 每年指定日期 + ONCE = "once" # 一次性任务 + + +class ScheduledTask(BaseModel): + """定时任务模型""" + task_id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8]) + context_id: int # 群号或用户ID + is_group: bool # 是否群聊 + schedule_type: ScheduleType # 任务类型 + description: str # 任务描述(用于AI生成提醒) + creator_id: int # 创建者用户ID + created_at: datetime = Field(default_factory=datetime.now) + + # 调度参数 + interval_minutes: int | None = None # 间隔分钟数 + hour: int | None = None # 小时 (0-23) + minute: int | None = None # 分钟 (0-59) + day_of_week: int | None = None # 周几 (0-6, 0=周一) + month: int | None = None # 月份 (1-12) + day: int | None = None # 日期 (1-31) + + # 一次性任务 + trigger_time: datetime | None = None # 触发时间 + + +class SchedulerTools: + """定时任务工具定义""" + + def __init__(self): + self.tools = [ + { + "type": "function", + "function": { + "name": "scheduler__create_task", + "description": """创建一个定时提醒任务。支持以下类型: +- interval_minutes: 每隔N分钟提醒,需提供 interval_minutes (1-10080) +- daily: 每天指定时间提醒,需提供 hour (0-23) 和 minute (0-59) +- weekly: 每周指定天提醒,需提供 hour, minute, day_of_week (0=周一, 1=周二...6=周日) +- yearly: 每年指定日期提醒,需提供 month (1-12), day (1-31), hour, minute +- once: 一次性提醒,需提供 minutes_later 表示几分钟后触发 (1-525600)""", + "parameters": { + "type": "object", + "properties": { + "schedule_type": { + "type": "string", + "description": "任务类型", + "enum": ["interval_minutes", "daily", "weekly", "yearly", "once"] + }, + "description": { + "type": "string", + "description": "任务描述,将用于生成提醒信息" + }, + "interval_minutes": { + "type": "integer", + "description": "间隔分钟数,仅interval_minutes类型需要", + "minimum": 1, + "maximum": 10080 + }, + "hour": { + "type": "integer", + "description": "小时 (0-23)", + "minimum": 0, + "maximum": 23 + }, + "minute": { + "type": "integer", + "description": "分钟 (0-59)", + "minimum": 0, + "maximum": 59 + }, + "day_of_week": { + "type": "integer", + "description": "周几 (0=周一, 1=周二...6=周日)", + "minimum": 0, + "maximum": 6 + }, + "month": { + "type": "integer", + "description": "月份 (1-12)", + "minimum": 1, + "maximum": 12 + }, + "day": { + "type": "integer", + "description": "日期 (1-31)", + "minimum": 1, + "maximum": 31 + }, + "minutes_later": { + "type": "integer", + "description": "几分钟后触发,仅once类型需要", + "minimum": 1, + "maximum": 525600 + } + }, + "required": ["schedule_type", "description"] + } + } + }, + { + "type": "function", + "function": { + "name": "scheduler__list_tasks", + "description": "列出当前聊天的所有定时任务", + "parameters": { + "type": "object", + "properties": {}, + "required": [] + } + } + }, + { + "type": "function", + "function": { + "name": "scheduler__delete_task", + "description": "删除指定的定时任务", + "parameters": { + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "要删除的任务ID" + } + }, + "required": ["task_id"] + } + } + }, + { + "type": "function", + "function": { + "name": "scheduler__update_task", + "description": "更新定时任务的描述", + "parameters": { + "type": "object", + "properties": { + "task_id": { + "type": "string", + "description": "要更新的任务ID" + }, + "description": { + "type": "string", + "description": "新的任务描述" + } + }, + "required": ["task_id", "description"] + } + } + } + ] + + def get_available_tools(self) -> list[dict[str, Any]]: + """获取可用的工具列表""" + return self.tools + + def get_friendly_name(self, tool_name: str) -> str: + """获取工具的友好名称""" + friendly_names = { + "scheduler__create_task": "定时任务 - 创建任务", + "scheduler__list_tasks": "定时任务 - 列出任务", + "scheduler__delete_task": "定时任务 - 删除任务", + "scheduler__update_task": "定时任务 - 更新任务", + } + return friendly_names.get(tool_name, tool_name) + + +class SchedulerManager: + """定时任务管理器""" + + _instance = None + _initialized = False + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if self._initialized: + return + + self.tasks: dict[str, ScheduledTask] = {} + self.tools = SchedulerTools() + self.data_file = store.get_plugin_data_file("llmchat_scheduler_tasks.json") + self._initialized = True + logger.info("SchedulerManager 初始化完成") + + @classmethod + def get_instance(cls) -> "SchedulerManager": + """获取单例实例""" + if cls._instance is None: + cls._instance = cls() + return cls._instance + + async def load_tasks(self): + """从文件加载任务""" + logger.info(f"从文件加载定时任务: {self.data_file}") + if not os.path.exists(self.data_file): + logger.debug("定时任务文件不存在,跳过加载") + return + + try: + async with aiofiles.open(self.data_file, encoding="utf8") as f: + data = json.loads(await f.read()) + for task_id, task_data in data.items(): + # 转换字符串为datetime + if task_data.get("created_at"): + task_data["created_at"] = datetime.fromisoformat(task_data["created_at"]) + if task_data.get("trigger_time"): + task_data["trigger_time"] = datetime.fromisoformat(task_data["trigger_time"]) + self.tasks[task_id] = ScheduledTask(**task_data) + + logger.info(f"成功加载 {len(self.tasks)} 个定时任务") + # 注册所有任务到APScheduler + await self.register_all_jobs() + except Exception as e: + logger.error(f"加载定时任务失败: {e}") + + async def save_tasks(self): + """保存任务到文件""" + logger.info(f"保存定时任务到文件: {self.data_file}") + try: + data = {} + for task_id, task in self.tasks.items(): + task_dict = task.model_dump() + # 转换datetime为字符串 + if task_dict.get("created_at"): + task_dict["created_at"] = task_dict["created_at"].isoformat() + if task_dict.get("trigger_time"): + task_dict["trigger_time"] = task_dict["trigger_time"].isoformat() + data[task_id] = task_dict + + os.makedirs(os.path.dirname(self.data_file), exist_ok=True) + async with aiofiles.open(self.data_file, "w", encoding="utf8") as f: + await f.write(json.dumps(data, ensure_ascii=False, indent=2)) + logger.info(f"成功保存 {len(self.tasks)} 个定时任务") + except Exception as e: + logger.error(f"保存定时任务失败: {e}") + + def _validate_task_params(self, schedule_type: ScheduleType, **kwargs) -> str | None: + """校验任务参数,返回错误信息或None""" + if schedule_type == ScheduleType.INTERVAL_MINUTES: + interval = kwargs.get("interval_minutes") + if interval is None: + return "interval_minutes类型需要提供 interval_minutes 参数" + if not 1 <= interval <= 10080: + return "interval_minutes 必须在 1-10080 之间" + + elif schedule_type == ScheduleType.DAILY: + hour = kwargs.get("hour") + minute = kwargs.get("minute") + if hour is None or minute is None: + return "daily类型需要提供 hour 和 minute 参数" + if not 0 <= hour <= 23: + return "hour 必须在 0-23 之间" + if not 0 <= minute <= 59: + return "minute 必须在 0-59 之间" + + elif schedule_type == ScheduleType.WEEKLY: + hour = kwargs.get("hour") + minute = kwargs.get("minute") + day_of_week = kwargs.get("day_of_week") + if hour is None or minute is None or day_of_week is None: + return "weekly类型需要提供 hour, minute 和 day_of_week 参数" + if not 0 <= hour <= 23: + return "hour 必须在 0-23 之间" + if not 0 <= minute <= 59: + return "minute 必须在 0-59 之间" + if not 0 <= day_of_week <= 6: + return "day_of_week 必须在 0-6 之间 (0=周一)" + + elif schedule_type == ScheduleType.YEARLY: + hour = kwargs.get("hour") + minute = kwargs.get("minute") + month = kwargs.get("month") + day = kwargs.get("day") + if hour is None or minute is None or month is None or day is None: + return "yearly类型需要提供 hour, minute, month 和 day 参数" + if not 0 <= hour <= 23: + return "hour 必须在 0-23 之间" + if not 0 <= minute <= 59: + return "minute 必须在 0-59 之间" + if not 1 <= month <= 12: + return "month 必须在 1-12 之间" + if not 1 <= day <= 31: + return "day 必须在 1-31 之间" + + elif schedule_type == ScheduleType.ONCE: + minutes_later = kwargs.get("minutes_later") + if minutes_later is None: + return "once类型需要提供 minutes_later 参数" + if not 1 <= minutes_later <= 525600: + return "minutes_later 必须在 1-525600 之间" + + return None + + async def create_task( + self, + context_id: int, + is_group: bool, + creator_id: int, + schedule_type: str, + description: str, + **kwargs + ) -> tuple[bool, str]: + """创建定时任务""" + try: + stype = ScheduleType(schedule_type) + except ValueError: + return False, f"无效的任务类型: {schedule_type}" + + # 参数校验 + error = self._validate_task_params(stype, **kwargs) + if error: + return False, error + + # 计算一次性任务的触发时间 + trigger_time = None + if stype == ScheduleType.ONCE: + minutes_later = kwargs.get("minutes_later", 0) + trigger_time = datetime.now() + timedelta(minutes=minutes_later) + + # 创建任务 + task = ScheduledTask( + context_id=context_id, + is_group=is_group, + schedule_type=stype, + description=description, + creator_id=creator_id, + interval_minutes=kwargs.get("interval_minutes"), + hour=kwargs.get("hour"), + minute=kwargs.get("minute"), + day_of_week=kwargs.get("day_of_week"), + month=kwargs.get("month"), + day=kwargs.get("day"), + trigger_time=trigger_time + ) + + self.tasks[task.task_id] = task + + # 注册到APScheduler + self._register_job(task) + + # 保存 + await self.save_tasks() + + logger.info(f"创建定时任务成功: {task.task_id} - {description}") + return True, f"创建成功!任务ID: {task.task_id}" + + async def delete_task(self, task_id: str, context_id: int, is_group: bool) -> tuple[bool, str]: + """删除定时任务""" + if task_id not in self.tasks: + return False, f"任务不存在: {task_id}" + + task = self.tasks[task_id] + + # 检查权限:只能删除同一聊天的任务 + if task.context_id != context_id or task.is_group != is_group: + return False, "无法删除其他聊天的任务" + + # 从APScheduler移除 + job_id = f"scheduler_{task_id}" + if scheduler.get_job(job_id): + scheduler.remove_job(job_id) + + # 删除任务 + del self.tasks[task_id] + await self.save_tasks() + + logger.info(f"删除定时任务: {task_id}") + return True, f"任务 {task_id} 已删除" + + async def update_task( + self, + task_id: str, + context_id: int, + is_group: bool, + description: str + ) -> tuple[bool, str]: + """更新定时任务""" + if task_id not in self.tasks: + return False, f"任务不存在: {task_id}" + + task = self.tasks[task_id] + + # 检查权限 + if task.context_id != context_id or task.is_group != is_group: + return False, "无法更新其他聊天的任务" + + task.description = description + await self.save_tasks() + + logger.info(f"更新定时任务: {task_id}") + return True, f"任务 {task_id} 已更新" + + def list_tasks(self, context_id: int, is_group: bool) -> list[dict]: + """列出指定聊天的所有任务""" + result = [] + for task in self.tasks.values(): + if task.context_id == context_id and task.is_group == is_group: + task_info = { + "task_id": task.task_id, + "description": task.description, + "schedule_type": task.schedule_type.value, + "created_at": task.created_at.strftime("%Y-%m-%d %H:%M:%S") + } + + # 添加具体时间信息 + if task.schedule_type == ScheduleType.INTERVAL_MINUTES: + task_info["schedule"] = f"每 {task.interval_minutes} 分钟" + elif task.schedule_type == ScheduleType.DAILY: + task_info["schedule"] = f"每天 {task.hour:02d}:{task.minute:02d}" + elif task.schedule_type == ScheduleType.WEEKLY: + weekdays = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"] + task_info["schedule"] = f"每{weekdays[task.day_of_week]} {task.hour:02d}:{task.minute:02d}" + elif task.schedule_type == ScheduleType.YEARLY: + task_info["schedule"] = f"每年 {task.month}月{task.day}日 {task.hour:02d}:{task.minute:02d}" + elif task.schedule_type == ScheduleType.ONCE: + if task.trigger_time: + task_info["schedule"] = f"一次性: {task.trigger_time.strftime('%Y-%m-%d %H:%M:%S')}" + + result.append(task_info) + + return result + + def _register_job(self, task: ScheduledTask): + """注册单个任务到APScheduler""" + job_id = f"scheduler_{task.task_id}" + + # 移除已存在的同ID任务 + if scheduler.get_job(job_id): + scheduler.remove_job(job_id) + + if task.schedule_type == ScheduleType.INTERVAL_MINUTES: + scheduler.add_job( + self._trigger_task, + "interval", + minutes=task.interval_minutes, + id=job_id, + args=[task.task_id] + ) + + elif task.schedule_type == ScheduleType.DAILY: + scheduler.add_job( + self._trigger_task, + "cron", + hour=task.hour, + minute=task.minute, + id=job_id, + args=[task.task_id] + ) + + elif task.schedule_type == ScheduleType.WEEKLY: + # APScheduler的day_of_week: 0=周一...6=周日 + scheduler.add_job( + self._trigger_task, + "cron", + day_of_week=task.day_of_week, + hour=task.hour, + minute=task.minute, + id=job_id, + args=[task.task_id] + ) + + elif task.schedule_type == ScheduleType.YEARLY: + scheduler.add_job( + self._trigger_task, + "cron", + month=task.month, + day=task.day, + hour=task.hour, + minute=task.minute, + id=job_id, + args=[task.task_id] + ) + + elif task.schedule_type == ScheduleType.ONCE: + if task.trigger_time and task.trigger_time > datetime.now(): + scheduler.add_job( + self._trigger_task, + "date", + run_date=task.trigger_time, + id=job_id, + args=[task.task_id] + ) + + logger.debug(f"注册定时任务到APScheduler: {job_id}") + + async def register_all_jobs(self): + """注册所有任务到APScheduler""" + logger.info(f"注册 {len(self.tasks)} 个任务到APScheduler") + for task in self.tasks.values(): + self._register_job(task) + + async def _trigger_task(self, task_id: str): + """任务触发处理""" + if task_id not in self.tasks: + logger.warning(f"触发的任务不存在: {task_id}") + return + + task = self.tasks[task_id] + logger.info(f"定时任务触发: {task_id} - {task.description}") + + # 导入配置(避免循环导入) + from .config import ScopedConfig + from nonebot import get_plugin_config + from .config import Config + plugin_config = get_plugin_config(Config).llmchat + + # 获取Bot + try: + bots = list(get_driver().bots.values()) + if not bots: + logger.error("没有可用的Bot") + return + bot: Bot = bots[0] # type: ignore + except Exception as e: + logger.error(f"获取Bot失败: {e}") + return + + # 尝试调用AI生成提醒信息 + reminder_message = await self._generate_ai_reminder(task, plugin_config) + + # 发送消息 + try: + if task.is_group: + await bot.send_group_msg(group_id=task.context_id, message=Message(reminder_message)) + else: + await bot.send_private_msg(user_id=task.context_id, message=Message(reminder_message)) + logger.info(f"定时任务提醒发送成功: {task_id}") + except Exception as e: + logger.error(f"发送提醒消息失败: {e}") + + # 一次性任务触发后删除 + if task.schedule_type == ScheduleType.ONCE: + logger.info(f"删除一次性任务: {task_id}") + del self.tasks[task_id] + await self.save_tasks() + + async def _generate_ai_reminder(self, task: ScheduledTask, plugin_config) -> str: + """调用AI生成提醒信息""" + max_retry = plugin_config.scheduler_max_retry + default_reminder = plugin_config.scheduler_default_reminder.format(description=task.description) + + # 获取预设配置 + preset = None + if task.is_group: + from . import group_states + state = group_states.get(task.context_id) + if state and state.preset_name != "off": + for p in plugin_config.api_presets: + if p.name == state.preset_name: + preset = p + break + else: + from . import private_chat_states + state = private_chat_states.get(task.context_id) + if state and state.preset_name != "off": + for p in plugin_config.api_presets: + if p.name == state.preset_name: + preset = p + break + + if not preset: + # 没有配置预设,使用默认提醒 + logger.debug("没有可用的API预设,使用默认提醒") + return default_reminder + + # 构建AI请求 + system_prompt = f"""你是一个友好的提醒助手。用户设置了一个定时提醒任务,现在任务触发了。 +请根据任务描述生成一条简短、友好的提醒消息。 +要求: +- 消息要简洁,不要太长 +- 语气要友好、亲切 +- 可以适当使用语气词或颜文字 +- 不要有多余的解释,直接发送提醒内容""" + + user_prompt = f"任务描述:{task.description}" + + # 初始化OpenAI客户端 + if preset.proxy: + client = AsyncOpenAI( + base_url=preset.api_base, + api_key=preset.api_key, + timeout=plugin_config.request_timeout, + http_client=httpx.AsyncClient(proxy=preset.proxy), + ) + else: + client = AsyncOpenAI( + base_url=preset.api_base, + api_key=preset.api_key, + timeout=plugin_config.request_timeout, + ) + + for attempt in range(max_retry): + try: + response = await client.chat.completions.create( + model=preset.model_name, + max_tokens=256, + temperature=0.7, + messages=[ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt} + ] + ) + + content = response.choices[0].message.content + if content: + logger.debug(f"AI生成提醒成功: {content[:50]}...") + return content.strip() + except Exception as e: + logger.warning(f"AI生成提醒失败 (尝试 {attempt + 1}/{max_retry}): {e}") + if attempt < max_retry - 1: + await asyncio.sleep(1) + + # 重试失败,返回默认提醒 + logger.warning(f"AI生成提醒全部失败,使用默认提醒") + return default_reminder + + async def call_tool( + self, + tool_name: str, + tool_args: dict[str, Any], + context_id: int, + is_group: bool, + creator_id: int + ) -> str: + """调用定时任务工具""" + if tool_name == "scheduler__create_task": + success, message = await self.create_task( + context_id=context_id, + is_group=is_group, + creator_id=creator_id, + schedule_type=tool_args.get("schedule_type", ""), + description=tool_args.get("description", ""), + interval_minutes=tool_args.get("interval_minutes"), + hour=tool_args.get("hour"), + minute=tool_args.get("minute"), + day_of_week=tool_args.get("day_of_week"), + month=tool_args.get("month"), + day=tool_args.get("day"), + minutes_later=tool_args.get("minutes_later") + ) + return message + + elif tool_name == "scheduler__list_tasks": + tasks = self.list_tasks(context_id, is_group) + if not tasks: + return "当前没有定时任务" + return json.dumps(tasks, ensure_ascii=False, indent=2) + + elif tool_name == "scheduler__delete_task": + success, message = await self.delete_task( + task_id=tool_args.get("task_id", ""), + context_id=context_id, + is_group=is_group + ) + return message + + elif tool_name == "scheduler__update_task": + success, message = await self.update_task( + task_id=tool_args.get("task_id", ""), + context_id=context_id, + is_group=is_group, + description=tool_args.get("description", "") + ) + return message + + return f"未知的工具: {tool_name}" + + def get_available_tools(self) -> list[dict[str, Any]]: + """获取可用工具列表""" + return self.tools.get_available_tools() + + def get_friendly_name(self, tool_name: str) -> str: + """获取工具友好名称""" + return self.tools.get_friendly_name(tool_name)