#!/usr/bin/env python3 """SenSu SQLite 持久化层 — 替代零散 JSON 文件""" import sqlite3, os, json, logging, threading from typing import Optional, Dict, List, Any logger = logging.getLogger(__name__) DEFAULT_DB_PATH = "data/sensu.db" class SenSuDB: def __init__(self, db_path: str = DEFAULT_DB_PATH): os.makedirs(os.path.dirname(db_path) or ".", exist_ok=True) self.db_path = db_path self._local = threading.local() self._init_schema() @property def conn(self): if not hasattr(self._local, "conn") or self._local.conn is None: self._local.conn = sqlite3.connect(self.db_path) self._local.conn.row_factory = sqlite3.Row self._local.conn.execute("PRAGMA journal_mode=WAL") self._local.conn.execute("PRAGMA foreign_keys=ON") return self._local.conn def _init_schema(self): c = self.conn c.executescript(""" CREATE TABLE IF NOT EXISTS plugins ( name TEXT PRIMARY KEY, version TEXT DEFAULT '0.1.0', status TEXT DEFAULT 'unloaded', config_json TEXT DEFAULT '{}', permissions_json TEXT DEFAULT '[]', depends_on TEXT DEFAULT '[]', project_path TEXT, project_port INTEGER, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS permissions ( plugin_name TEXT, permission TEXT, granted INTEGER DEFAULT 0, granted_at TEXT, PRIMARY KEY (plugin_name, permission) ); CREATE TABLE IF NOT EXISTS config_kv ( key TEXT PRIMARY KEY, value TEXT, updated_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS audit_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, plugin_name TEXT, action TEXT, detail TEXT, created_at TEXT DEFAULT (datetime('now')) ); """) c.commit() # -- Plugin CRUD -- def save_plugin(self, name: str, **kwargs): fields = ["name"] + list(kwargs.keys()) placeholders = ["?"] * len(fields) values = [name] + list(kwargs.values()) for k in ["config", "permissions", "depends_on"]: if k in kwargs and not isinstance(kwargs[k], str): kwargs[k] = json.dumps(kwargs[k]) idx = fields.index(k) values[idx] = kwargs[k] sql = f"INSERT OR REPLACE INTO plugins ({','.join(fields)}) VALUES ({','.join(placeholders)})" self.conn.execute("UPDATE plugins SET updated_at=datetime('now') WHERE name=?", [name]) self.conn.execute(sql, values) self.conn.commit() def get_plugin(self, name: str) -> Optional[Dict]: row = self.conn.execute("SELECT * FROM plugins WHERE name=?", [name]).fetchone() if not row: return None d = dict(row) for f in ["config_json", "permissions_json", "depends_on"]: if d.get(f): try: d[f.replace("_json","")] = json.loads(d.pop(f)) except: pass return d def list_plugins(self) -> List[Dict]: return [dict(r) for r in self.conn.execute("SELECT name,version,status,project_path,project_port FROM plugins").fetchall()] # -- Permissions -- def grant_permission(self, plugin: str, perm: str): self.conn.execute( "INSERT OR REPLACE INTO permissions(plugin_name,permission,granted,granted_at) VALUES(?,?,1,datetime('now'))", [plugin, perm]) self.conn.commit() def check_permission(self, plugin: str, perm: str) -> bool: r = self.conn.execute("SELECT granted FROM permissions WHERE plugin_name=? AND permission=?", [plugin, perm]).fetchone() return bool(r and r[0]) # -- Config -- def set_config(self, key: str, value: str): self.conn.execute("INSERT OR REPLACE INTO config_kv(key,value,updated_at) VALUES(?,?,datetime('now'))", [key, value]) self.conn.commit() def get_config(self, key: str, default=None) -> Optional[str]: r = self.conn.execute("SELECT value FROM config_kv WHERE key=?", [key]).fetchone() return r[0] if r else default # -- Audit -- def log_audit(self, plugin: str, action: str, detail: str = ""): self.conn.execute("INSERT INTO audit_log(plugin_name,action,detail) VALUES(?,?,?)", [plugin, action, detail]) self.conn.commit() def close(self): if hasattr(self._local, "conn") and self._local.conn: self._local.conn.close() self._local.conn = None