"""中控 SQLite。""" from __future__ import annotations from pathlib import Path from threading import Lock from typing import Any from .config import get_control_settings _SCHEMA = """ CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, base_url TEXT NOT NULL UNIQUE, token_sealed TEXT NOT NULL DEFAULT '', created_at_ms INTEGER NOT NULL, updated_at_ms INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); """ _db: "ControlDB | None" = None _lock = Lock() def set_control_db(db: "ControlDB | None") -> None: global _db _db = db def get_control_db() -> "ControlDB": if _db is None: raise RuntimeError("control db not initialized") return _db class ControlDB: def __init__(self, path: Path | None = None) -> None: import sqlite3 import time settings = get_control_settings() self.path = path or settings.db_path self.path.parent.mkdir(parents=True, exist_ok=True) self._conn = sqlite3.connect(str(self.path), check_same_thread=False) self._conn.row_factory = sqlite3.Row self._conn.executescript(_SCHEMA) self._conn.commit() self._lock = Lock() # touch _ = time.time() def close(self) -> None: self._conn.close() def list_nodes(self) -> list[dict[str, Any]]: with self._lock: rows = self._conn.execute( "SELECT id, name, base_url, token_sealed, created_at_ms, updated_at_ms FROM nodes ORDER BY id" ).fetchall() return [dict(r) for r in rows] def get_node(self, node_id: int) -> dict[str, Any] | None: with self._lock: row = self._conn.execute( "SELECT id, name, base_url, token_sealed, created_at_ms, updated_at_ms FROM nodes WHERE id=?", (node_id,), ).fetchone() return dict(row) if row else None def create_node(self, name: str, base_url: str) -> dict[str, Any]: import time now = int(time.time() * 1000) name = name.strip() base_url = base_url.strip().rstrip("/") with self._lock: cur = self._conn.execute( "INSERT INTO nodes(name, base_url, token_sealed, created_at_ms, updated_at_ms) VALUES (?,?,?,?,?)", (name, base_url, "", now, now), ) self._conn.commit() nid = int(cur.lastrowid) return self.get_node(nid) # type: ignore[return-value] def update_node( self, node_id: int, *, name: str | None = None, base_url: str | None = None, token_sealed: str | None = None, ) -> dict[str, Any] | None: import time node = self.get_node(node_id) if not node: return None now = int(time.time() * 1000) new_name = name.strip() if name is not None else node["name"] new_url = base_url.strip().rstrip("/") if base_url is not None else node["base_url"] new_tok = token_sealed if token_sealed is not None else node["token_sealed"] with self._lock: self._conn.execute( "UPDATE nodes SET name=?, base_url=?, token_sealed=?, updated_at_ms=? WHERE id=?", (new_name, new_url, new_tok, now, node_id), ) self._conn.commit() return self.get_node(node_id) def delete_node(self, node_id: int) -> bool: with self._lock: cur = self._conn.execute("DELETE FROM nodes WHERE id=?", (node_id,)) self._conn.commit() return cur.rowcount > 0