新增QQ Bot适配器完整代码栈,包含: 1. 基础适配器入口与工具类封装 2. 会话管理、重试队列与流量控制 3. 命令系统与内置指令(ping/help/status等) 4. 富媒体消息处理与格式转换 5. 引用存储与审批管理 6. 凭证备份与会话持久化 7. 健康检查与交互回调系统
288 lines
8.9 KiB
Python
288 lines
8.9 KiB
Python
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"
|