ForcePilot/backend/package/yuxi/channels/adapters/qqbot/tools/remind.py
Kris 552aef767c feat(qqbot): 实现QQ机器人适配器完整功能模块
新增QQ Bot适配器完整代码栈,包含:
1. 基础适配器入口与工具类封装
2. 会话管理、重试队列与流量控制
3. 命令系统与内置指令(ping/help/status等)
4. 富媒体消息处理与格式转换
5. 引用存储与审批管理
6. 凭证备份与会话持久化
7. 健康检查与交互回调系统
2026-05-12 00:48:04 +08:00

288 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="提醒 IDremove 操作时需要)")
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"