Files
k8smanager-cli/backend/history_store.py
T

173 lines
7.3 KiB
Python

"""SQLite 持久化诊断历史、AI 分析和 Agent 处理记录。"""
import json
import os
import sqlite3
import uuid
from datetime import datetime
from typing import Optional
def _now() -> str:
return datetime.now().isoformat(timespec="seconds")
class HistoryStore:
def __init__(self, path: str):
self.path = path
parent = os.path.dirname(os.path.abspath(path))
os.makedirs(parent, exist_ok=True)
self._init_db()
def _connect(self):
connection = sqlite3.connect(self.path, timeout=30)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA journal_mode=WAL")
connection.execute("PRAGMA foreign_keys=ON")
return connection
def _init_db(self):
with self._connect() as db:
db.executescript("""
CREATE TABLE IF NOT EXISTS diagnoses (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
namespace TEXT NOT NULL,
score INTEGER NOT NULL,
status TEXT NOT NULL,
critical_count INTEGER NOT NULL DEFAULT 0,
warning_count INTEGER NOT NULL DEFAULT 0,
elapsed_seconds REAL NOT NULL DEFAULT 0,
diagnosis_json TEXT NOT NULL,
report TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_diagnoses_created_at ON diagnoses(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_diagnoses_status ON diagnoses(status);
CREATE TABLE IF NOT EXISTS processing_runs (
id TEXT PRIMARY KEY,
diagnosis_id TEXT NOT NULL,
kind TEXT NOT NULL,
mode TEXT NOT NULL DEFAULT '',
question TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
started_at TEXT NOT NULL,
finished_at TEXT,
FOREIGN KEY(diagnosis_id) REFERENCES diagnoses(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_runs_diagnosis ON processing_runs(diagnosis_id, started_at);
CREATE TABLE IF NOT EXISTS processing_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL,
sequence INTEGER NOT NULL,
type TEXT NOT NULL,
created_at TEXT NOT NULL,
payload_json TEXT NOT NULL,
FOREIGN KEY(run_id) REFERENCES processing_runs(id) ON DELETE CASCADE,
UNIQUE(run_id, sequence)
);
""")
def save_diagnosis(self, diagnosis: dict, report: str) -> str:
record_id = uuid.uuid4().hex
created_at = diagnosis.get("timestamp") or _now()
with self._connect() as db:
db.execute(
"""INSERT INTO diagnoses
(id, created_at, namespace, score, status, critical_count, warning_count,
elapsed_seconds, diagnosis_json, report)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
record_id, created_at, diagnosis.get("namespace", "all"),
int(diagnosis.get("score", 0)), diagnosis.get("status", "unknown"),
int(diagnosis.get("critical_count", 0)), int(diagnosis.get("warning_count", 0)),
float(diagnosis.get("elapsed_seconds", 0)),
json.dumps(diagnosis, ensure_ascii=False), report,
),
)
return record_id
def list_diagnoses(self, status: str = "", namespace: str = "", limit: int = 100, offset: int = 0) -> list[dict]:
conditions = []
params: list = []
if status:
conditions.append("status = ?")
params.append(status)
if namespace:
conditions.append("namespace = ?")
params.append(namespace)
where = " WHERE " + " AND ".join(conditions) if conditions else ""
params.extend([max(1, min(limit, 500)), max(0, offset)])
with self._connect() as db:
rows = db.execute(
f"""SELECT id, created_at, namespace, score, status, critical_count,
warning_count, elapsed_seconds,
(SELECT COUNT(*) FROM processing_runs r WHERE r.diagnosis_id = diagnoses.id) AS run_count
FROM diagnoses{where}
ORDER BY created_at DESC, rowid DESC LIMIT ? OFFSET ?""",
params,
).fetchall()
return [dict(row) for row in rows]
def get_diagnosis(self, record_id: str) -> Optional[dict]:
with self._connect() as db:
row = db.execute("SELECT * FROM diagnoses WHERE id = ?", (record_id,)).fetchone()
if row is None:
return None
run_rows = db.execute(
"SELECT * FROM processing_runs WHERE diagnosis_id = ? ORDER BY started_at, rowid",
(record_id,),
).fetchall()
runs = []
for run_row in run_rows:
run = dict(run_row)
event_rows = db.execute(
"SELECT type, created_at, payload_json FROM processing_events WHERE run_id = ? ORDER BY sequence",
(run["id"],),
).fetchall()
run["events"] = [
{"type": event["type"], "created_at": event["created_at"], **json.loads(event["payload_json"])}
for event in event_rows
]
runs.append(run)
return {
"id": row["id"], "created_at": row["created_at"],
"diagnosis": json.loads(row["diagnosis_json"]),
"report": row["report"], "runs": runs,
}
def latest(self) -> Optional[dict]:
records = self.list_diagnoses(limit=1)
return self.get_diagnosis(records[0]["id"]) if records else None
def create_run(self, diagnosis_id: str, kind: str, mode: str = "", question: str = "") -> str:
run_id = uuid.uuid4().hex
with self._connect() as db:
db.execute(
"""INSERT INTO processing_runs
(id, diagnosis_id, kind, mode, question, status, started_at)
VALUES (?, ?, ?, ?, ?, 'running', ?)""",
(run_id, diagnosis_id, kind, mode, question, _now()),
)
return run_id
def append_event(self, run_id: str, event_type: str, payload: dict):
with self._connect() as db:
sequence = db.execute(
"SELECT COALESCE(MAX(sequence), 0) + 1 FROM processing_events WHERE run_id = ?",
(run_id,),
).fetchone()[0]
db.execute(
"""INSERT INTO processing_events (run_id, sequence, type, created_at, payload_json)
VALUES (?, ?, ?, ?, ?)""",
(run_id, sequence, event_type, _now(), json.dumps(payload, ensure_ascii=False)),
)
def finish_run(self, run_id: str, status: str, summary: str = ""):
with self._connect() as db:
db.execute(
"UPDATE processing_runs SET status = ?, summary = ?, finished_at = ? WHERE id = ?",
(status, summary, _now(), run_id),
)