Files
AskaEth d48ef65b35 v0.3: SQLite persistence + ProjectService + dependency resolution
- New: services/sensu_db.py (SQLite, 5 tables)
- New: services/project_service.py (project registry, port allocation, dep resolution)
- Integrated into main.py (step 11.6)
- Tests: 23/23 passing (15 original + 8 new)
2026-06-10 18:59:25 +08:00

120 lines
4.8 KiB
Python

#!/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