import os import json import datetime import uuid import astrbot.api.star as star from astrbot.api.event import filter from apscheduler.schedulers.asyncio import AsyncIOScheduler from astrbot.api.event import AstrMessageEvent, MessageEventResult from astrbot.api import llm_tool, logger @star.register( name="astrbot-reminder", desc="使用 LLM 待办提醒", author="Soulter", version="0.0.1" ) class Main(star.Star): """使用 LLM 待办提醒。只需对 LLM 说想要提醒的事情和时间即可。比如:`之后每天这个时候都提醒我做多邻国`""" def __init__(self, context: star.Context) -> None: self.context = context self.scheduler = AsyncIOScheduler(timezone="Asia/Shanghai") # set and load config if not os.path.exists("data/astrbot-reminder.json"): with open("data/astrbot-reminder.json", "w", encoding="utf-8") as f: f.write("{}") with open("data/astrbot-reminder.json", "r", encoding="utf-8") as f: self.reminder_data = json.load(f) self._init_scheduler() self.scheduler.start() def _init_scheduler(self): """Initialize the scheduler.""" for group in self.reminder_data: for reminder in self.reminder_data[group]: if "id" not in reminder: id_ = str(uuid.uuid4()) reminder["id"] = id_ else: id_ = reminder["id"] if "datetime" in reminder: if self.check_is_outdated(reminder): continue self.scheduler.add_job( self._reminder_callback, id=id_, trigger="date", args=[group, reminder], run_date=datetime.datetime.strptime( reminder["datetime"], "%Y-%m-%d %H:%M" ), misfire_grace_time=60, ) elif "cron" in reminder: self.scheduler.add_job( self._reminder_callback, trigger="cron", id=id_, args=[group, reminder], misfire_grace_time=60, **self._parse_cron_expr(reminder["cron"]), ) def check_is_outdated(self, reminder: dict): """Check if the reminder is outdated.""" if "datetime" in reminder: return ( datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M") < datetime.datetime.now() ) return False async def _save_data(self): """Save the reminder data.""" with open("data/astrbot-reminder.json", "w", encoding="utf-8") as f: json.dump(self.reminder_data, f, ensure_ascii=False) def _parse_cron_expr(self, cron_expr: str): fields = cron_expr.split(" ") return { "minute": fields[0], "hour": fields[1], "day": fields[2], "month": fields[3], "day_of_week": fields[4], } @llm_tool("reminder") async def reminder_tool( self, event: AstrMessageEvent, text: str = None, datetime_str: str = None, cron_expression: str = None, human_readable_cron: str = None, ): """Call this function when user is asking for setting a reminder. Args: text(string): Must Required. The content of the reminder. datetime_str(string): Required when user's reminder is a single reminder. The datetime string of the reminder, Must format with %Y-%m-%d %H:%M cron_expression(string): Required when user's reminder is a repeated reminder. The cron expression of the reminder. human_readable_cron(string): Optional. The human readable cron expression of the reminder. """ if event.get_platform_name() == "qq_official": yield event.plain_result("reminder 暂不支持 QQ 官方机器人。") return if event.unified_msg_origin not in self.reminder_data: self.reminder_data[event.unified_msg_origin] = [] if not cron_expression and not datetime_str: raise ValueError( "The cron_expression and datetime_str cannot be both None." ) reminder_time = "" if not text: text = "未命名待办事项" if cron_expression: d = { "text": text, "cron": cron_expression, "cron_h": human_readable_cron, "id": str(uuid.uuid4()), } self.reminder_data[event.unified_msg_origin].append(d) self.scheduler.add_job( self._reminder_callback, "cron", id=d["id"], misfire_grace_time=60, **self._parse_cron_expr(cron_expression), args=[event.unified_msg_origin, d], ) if human_readable_cron: reminder_time = f"{human_readable_cron}(Cron: {cron_expression})" else: d = {"text": text, "datetime": datetime_str, "id": str(uuid.uuid4())} self.reminder_data[event.unified_msg_origin].append(d) datetime_scheduled = datetime.datetime.strptime( datetime_str, "%Y-%m-%d %H:%M" ) self.scheduler.add_job( self._reminder_callback, "date", id=d["id"], args=[event.unified_msg_origin, d], run_date=datetime_scheduled, misfire_grace_time=60, ) reminder_time = datetime_str await self._save_data() yield event.plain_result( "成功设置待办事项。\n内容: " + text + "\n时间: " + reminder_time + "\n\n使用 /reminder ls 查看所有待办事项。\n使用 /tool off reminder 关闭此功能。" ) @filter.command_group("reminder") def reminder(self): """The command group of the reminder.""" pass async def get_upcoming_reminders(self, unified_msg_origin: str): """Get upcoming reminders.""" reminders = self.reminder_data.get(unified_msg_origin, []) if not reminders: return [] now = datetime.datetime.now() upcoming_reminders = [ reminder for reminder in reminders if "datetime" not in reminder or datetime.datetime.strptime(reminder["datetime"], "%Y-%m-%d %H:%M") >= now ] return upcoming_reminders @reminder.command("ls") async def reminder_ls(self, event: AstrMessageEvent): """List upcoming reminders.""" reminders = await self.get_upcoming_reminders(event.unified_msg_origin) if not reminders: yield event.plain_result("没有正在进行的待办事项。") else: reminder_str = "正在进行的待办事项:\n" for i, reminder in enumerate(reminders): time_ = reminder.get("datetime", "") if not time_: cron_expr = reminder.get("cron", "") time_ = reminder.get("cron_h", "") + f"(Cron: {cron_expr})" reminder_str += f"{i + 1}. {reminder['text']} - {time_}\n" reminder_str += "\n使用 /reminder rm 删除待办事项。\n" yield event.plain_result(reminder_str) @reminder.command("rm") async def reminder_rm(self, event: AstrMessageEvent, index: int): """Remove a reminder by index.""" reminders = await self.get_upcoming_reminders(event.unified_msg_origin) if not reminders: yield event.plain_result("没有待办事项。") elif index < 1 or index > len(reminders): yield event.plain_result("索引越界。") else: reminder = reminders.pop(index - 1) job_id = reminder.get("id") # self.reminder_data[event.unified_msg_origin] = reminder users_reminders = self.reminder_data.get(event.unified_msg_origin, []) for i, r in enumerate(users_reminders): if r.get("id") == job_id: users_reminders.pop(i) try: self.scheduler.remove_job(job_id) except Exception as e: logger.error(f"Remove job error: {e}") yield event.plain_result( f"成功移除对应的待办事项。删除定时任务失败: {str(e)} 可能需要重启 AstrBot 以取消该提醒任务。" ) await self._save_data() yield event.plain_result("成功删除待办事项:\n" + reminder["text"]) async def _reminder_callback(self, unified_msg_origin: str, d: dict): """The callback function of the reminder.""" logger.info(f"Reminder Activated: {d['text']}, created by {unified_msg_origin}") await self.context.send_message( unified_msg_origin, MessageEventResult().message( "待办提醒: \n\n" + d["text"] + "\n时间: " + d.get("datetime", "") + d.get("cron_h", "") ), ) async def terminate(self): self.scheduler.shutdown() await self._save_data() logger.info("Reminder plugin terminated.")