feat: 持久化诊断历史并修复 Agent 命令执行
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""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),
|
||||
)
|
||||
Reference in New Issue
Block a user