from __future__ import annotations import asyncio import json import logging import os import re import time from dataclasses import dataclass, field from datetime import datetime from pydantic import BaseModel, Field from yuxi.agents.toolkits.registry import tool logger = logging.getLogger(__name__) DEFAULT_REMIND_DIR = os.path.join(os.path.dirname(__file__), "..", "remind_data") _CRON_PATTERN = re.compile( r"^(\*|[0-5]?\d)\s+(\*|[01]?\d|2[0-3])\s+(\*|[012]?\d|3[01])\s+(\*|1[012]?|[1-9])\s+(\*|[0-7])$" ) _RELATIVE_TIME_PATTERN = re.compile(r"(\d+)\s*(秒|分钟|小时|天|周|s|min|h|d|w)", re.IGNORECASE) class RemindInput(BaseModel): action: str = Field( description="操作类型: add(添加提醒), list(列出提醒), remove(删除提醒)", ) message: str = Field(default="", description="提醒内容(add 操作时需要)") time_spec: str = Field( default="", description="时间规格,支持: 相对时间(如 '30分钟', '1小时', '2天')、" "cron 表达式(如 '* 9 * * *' 每天9点)、" "绝对时间 ISO格式(如 '2026-05-12T14:30:00')", ) remind_id: str = Field(default="", description="提醒 ID(remove 操作时需要)") chat_id: str = Field(default="", description="目标聊天 ID(发送提醒的目标位置)") _REMIND_TOOL_GUIDE = """ 使用前请确保 QQ Bot 已配置 app_id 和 app_secret。 该工具允许 Agent 创建和管理 QQ Bot 的定时提醒,支持: - 创建提醒(相对时间 / cron / 绝对时间) - 列出所有活跃提醒 - 删除指定提醒 提醒到期时会通过 QQ Bot 发送消息到指定的聊天目标。 """.strip() @dataclass class Reminder: remind_id: str message: str trigger_at: float chat_id: str created_at: float = field(default_factory=time.time) cron_expr: str = "" def to_dict(self) -> dict: return { "remind_id": self.remind_id, "message": self.message, "trigger_at": self.trigger_at, "chat_id": self.chat_id, "created_at": self.created_at, "cron_expr": self.cron_expr, } @classmethod def from_dict(cls, data: dict) -> Reminder: return cls( remind_id=data["remind_id"], message=data["message"], trigger_at=data["trigger_at"], chat_id=data.get("chat_id", ""), created_at=data.get("created_at", time.time()), cron_expr=data.get("cron_expr", ""), ) class RemindStore: def __init__(self, store_dir: str | None = None): self._store_dir = store_dir or DEFAULT_REMIND_DIR self._store_path = os.path.join(self._store_dir, "reminders.json") self._reminders: dict[str, Reminder] = {} self._lock = asyncio.Lock() self._load() def _load(self) -> None: try: if os.path.exists(self._store_path): with open(self._store_path, encoding="utf-8") as f: data = json.load(f) for item in data: r = Reminder.from_dict(item) self._reminders[r.remind_id] = r logger.info("RemindStore: loaded %d reminders", len(self._reminders)) except (OSError, json.JSONDecodeError): logger.exception("RemindStore: failed to load") async def _save(self) -> None: try: os.makedirs(self._store_dir, exist_ok=True) data = [r.to_dict() for r in self._reminders.values()] tmp_path = self._store_path + ".tmp" with open(tmp_path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False) os.replace(tmp_path, self._store_path) except OSError: logger.exception("RemindStore: failed to save") async def add(self, message: str, trigger_at: float, chat_id: str, cron_expr: str = "") -> Reminder: async with self._lock: import uuid r = Reminder( remind_id=str(uuid.uuid4())[:8], message=message, trigger_at=trigger_at, chat_id=chat_id, cron_expr=cron_expr, ) self._reminders[r.remind_id] = r await self._save() return r async def list_all(self) -> list[Reminder]: async with self._lock: self._cleanup_expired() return list(self._reminders.values()) async def remove(self, remind_id: str) -> bool: async with self._lock: if remind_id in self._reminders: del self._reminders[remind_id] await self._save() return True return False def _cleanup_expired(self) -> None: now = time.time() expired = [rid for rid, r in self._reminders.items() if r.trigger_at < now - 3600] for rid in expired: del self._reminders[rid] async def get_due_reminders(self) -> list[Reminder]: async with self._lock: now = time.time() due = [r for r in self._reminders.values() if r.trigger_at <= now] return due _remind_store: RemindStore | None = None def _get_store() -> RemindStore: global _remind_store if _remind_store is None: _remind_store = RemindStore() return _remind_store def parse_time_spec(time_spec: str) -> tuple[float, str]: """解析时间规格,返回 (trigger_at, cron_expr)""" now = time.time() if not time_spec: return now + 3600, "" relative_match = _RELATIVE_TIME_PATTERN.fullmatch(time_spec.strip()) if relative_match: value = int(relative_match.group(1)) unit = relative_match.group(2).lower() multipliers = { "秒": 1, "s": 1, "分钟": 60, "min": 60, "小时": 3600, "h": 3600, "天": 86400, "d": 86400, "周": 604800, "w": 604800, } return now + value * multipliers.get(unit, 60), "" if _CRON_PATTERN.match(time_spec.strip()): return now + 60, time_spec.strip() try: dt = datetime.fromisoformat(time_spec) return dt.timestamp(), "" except (ValueError, TypeError): pass try: seconds = int(time_spec) return now + seconds, "" except ValueError: pass return now + 3600, "" @tool( category="qqbot", tags=["QQ机器人", "定时提醒"], display_name="QQ 定时提醒", config_guide=_REMIND_TOOL_GUIDE, ) async def qqbot_remind( action: str, message: str = "", time_spec: str = "", remind_id: str = "", chat_id: str = "", ) -> str: """QQ Bot 定时提醒工具,用于创建和管理定时提醒消息。 支持操作类型: - add: 添加提醒(需要 message 和 time_spec) - list: 列出所有活跃提醒 - remove: 删除指定提醒(需要 remind_id) 时间规格支持: - 相对时间: '30分钟', '1小时', '2天', '1周' - cron 表达式: '* 9 * * *' (每天9点) - 绝对时间 ISO: '2026-05-12T14:30:00' Args: action: 操作类型 (add/list/remove) message: 提醒内容 time_spec: 时间规格 remind_id: 提醒 ID(删除时需要) chat_id: 目标聊天 ID Returns: 操作结果描述 """ store = _get_store() if action == "add": if not message: return "错误:添加提醒需要提供 message(提醒内容)。" trigger_at, cron_expr = parse_time_spec(time_spec) if not chat_id: chat_id = os.environ.get("QQBOT_DEFAULT_CHAT_ID", "") r = await store.add(message, trigger_at, chat_id, cron_expr) dt = datetime.fromtimestamp(trigger_at) result = f"提醒已创建 (ID: {r.remind_id})\n内容: {message}\n触发时间: {dt.strftime('%Y-%m-%d %H:%M:%S')}" if cron_expr: result += f"\nCron: {cron_expr}" if chat_id: result += f"\n目标: {chat_id}" return result elif action == "list": reminders = await store.list_all() if not reminders: return "当前没有活跃的提醒。" lines = ["当前活跃的提醒:"] for r in reminders: dt = datetime.fromtimestamp(r.trigger_at) status = "已过期" if r.trigger_at < time.time() else "待触发" lines.append(f" [{r.remind_id}] {r.message} - {dt.strftime('%Y-%m-%d %H:%M:%S')} ({status})") return "\n".join(lines) elif action == "remove": if not remind_id: return "错误:删除提醒需要提供 remind_id。" removed = await store.remove(remind_id) if removed: return f"提醒 {remind_id} 已删除。" return f"未找到提醒 {remind_id}。" else: return "不支持的操作类型。支持的操作: add, list, remove"