ForcePilot/backend/package/yuxi/channels/adapters/qqbot/credential_backup.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

136 lines
4.7 KiB
Python

from __future__ import annotations
import json
import logging
import os
import tempfile
import time
from dataclasses import dataclass, field
logger = logging.getLogger(__name__)
DEFAULT_BACKUP_DIR = os.path.join(tempfile.gettempdir(), "yuxi_qqbot_credentials")
@dataclass
class CredentialSnapshot:
app_id: str = ""
app_secret: str = ""
access_token: str = ""
expires_at: float = 0
token_obtained_at: float = 0
session_id: str = ""
sandbox: bool = False
metadata: dict = field(default_factory=dict)
def is_valid(self) -> bool:
return bool(self.app_id and self.app_secret)
def token_expired(self) -> bool:
if not self.access_token or not self.expires_at:
return True
return time.monotonic() > self.expires_at - 300
class CredentialBackup:
def __init__(self, app_id: str, backup_dir: str | None = None):
self._app_id = app_id
self._backup_dir = backup_dir or DEFAULT_BACKUP_DIR
self._backup_path = os.path.join(self._backup_dir, f"{app_id}.json")
def save(self, snapshot: CredentialSnapshot) -> bool:
try:
os.makedirs(self._backup_dir, exist_ok=True)
data = {
"app_id": snapshot.app_id,
"app_secret": snapshot.app_secret,
"access_token": snapshot.access_token,
"expires_at": snapshot.expires_at,
"token_obtained_at": snapshot.token_obtained_at,
"session_id": snapshot.session_id,
"sandbox": snapshot.sandbox,
"metadata": snapshot.metadata,
"saved_at": time.time(),
}
tmp_path = self._backup_path + ".tmp"
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False)
os.replace(tmp_path, self._backup_path)
logger.info("CredentialBackup: saved snapshot for app_id=%s", self._app_id[:6] + "...")
return True
except OSError:
logger.exception("CredentialBackup: failed to save snapshot")
return False
def restore(self) -> CredentialSnapshot | None:
try:
if not os.path.exists(self._backup_path):
return None
with open(self._backup_path, encoding="utf-8") as f:
data = json.load(f)
snapshot = CredentialSnapshot(
app_id=data.get("app_id", ""),
app_secret=data.get("app_secret", ""),
access_token=data.get("access_token", ""),
expires_at=data.get("expires_at", 0),
token_obtained_at=data.get("token_obtained_at", 0),
session_id=data.get("session_id", ""),
sandbox=data.get("sandbox", False),
metadata=data.get("metadata", {}),
)
if not snapshot.is_valid():
logger.warning("CredentialBackup: restored snapshot is invalid for app_id=%s", self._app_id[:6] + "...")
return None
logger.info("CredentialBackup: restored snapshot for app_id=%s", self._app_id[:6] + "...")
return snapshot
except (OSError, json.JSONDecodeError, KeyError):
logger.exception("CredentialBackup: failed to restore snapshot")
return None
def clear(self) -> bool:
try:
if os.path.exists(self._backup_path):
os.remove(self._backup_path)
tmp_path = self._backup_path + ".tmp"
if os.path.exists(tmp_path):
os.remove(tmp_path)
logger.info("CredentialBackup: cleared backup for app_id=%s", self._app_id[:6] + "...")
return True
except OSError:
logger.exception("CredentialBackup: failed to clear backup")
return False
@staticmethod
def cleanup_expired(backup_dir: str | None = None, max_age_s: float = 86400 * 7) -> int:
directory = backup_dir or DEFAULT_BACKUP_DIR
removed = 0
if not os.path.exists(directory):
return 0
now = time.time()
try:
for filename in os.listdir(directory):
if not filename.endswith(".json"):
continue
filepath = os.path.join(directory, filename)
try:
stat = os.stat(filepath)
if now - stat.st_mtime > max_age_s:
os.remove(filepath)
removed += 1
logger.debug("CredentialBackup: removed expired backup %s", filename)
except OSError:
pass
except OSError:
logger.exception("CredentialBackup: cleanup failed")
return removed