173 lines
7.3 KiB
Python
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),
|
|
)
|