from __future__ import annotations import json import logging import time from pathlib import Path from typing import Any from yuxi.channel.extensions.qqbot.types import SessionState logger = logging.getLogger(__name__) DEFAULT_SESSION_DIR = Path.home() / ".forcepilot" / "qqbot" / "sessions" SESSION_EXPIRE_SECONDS = 300 class SessionStore: def __init__(self, session_dir: Path | None = None): self._session_dir = session_dir or DEFAULT_SESSION_DIR self._session_dir.mkdir(parents=True, exist_ok=True) def _session_path(self, account_id: str) -> Path: safe_id = account_id.replace("/", "_").replace("\\", "_") return self._session_dir / f"{safe_id}.json" def save(self, account_id: str, state: SessionState) -> None: path = self._session_path(account_id) data = { "session_id": state.session_id, "last_seq": state.last_seq, "last_connected_at": state.last_connected_at, "account_id": state.account_id, "saved_at": time.time(), "app_id": state.app_id, } try: path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") logger.debug("Session saved for account '%s': session_id=%s", account_id, state.session_id) except OSError as e: logger.warning("Failed to save session for '%s': %s", account_id, e) def load(self, account_id: str) -> SessionState | None: path = self._session_path(account_id) if not path.exists(): return None try: data = json.loads(path.read_text(encoding="utf-8")) saved_at = data.get("saved_at", 0) if time.time() - saved_at > SESSION_EXPIRE_SECONDS: logger.debug("Session expired for account '%s'", account_id) return None return SessionState( session_id=data.get("session_id"), last_seq=data.get("last_seq"), last_connected_at=data.get("last_connected_at", 0), account_id=data.get("account_id", account_id), saved_at=saved_at, app_id=data.get("app_id"), ) except (json.JSONDecodeError, OSError) as e: logger.warning("Failed to load session for '%s': %s", account_id, e) return None def clear(self, account_id: str) -> None: path = self._session_path(account_id) try: path.unlink(missing_ok=True) except OSError: pass def clear_all(self) -> None: for path in self._session_dir.glob("*.json"): try: path.unlink() except OSError: pass