From bbbe494dae83f8cd8b4b3bdf1de960dcfc958ba7 Mon Sep 17 00:00:00 2001 From: AnyBot Dev Date: Mon, 9 Nov 2026 18:02:56 +0800 Subject: [PATCH] SQLite layer: the audit trail becomes queryable history (P4, step 1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/store.rs: bundled SQLite (WAL) in the data folder; every oplog record is mirrored into an audit table — turns, model calls, tools with their arguments and end statuses, queued messages, stops - side-car by design: state.json stays the app's truth (one struct, written atomically); the mirror is best-effort — an audit that never breaks a turn, and a timeline that answers for 30 days, not just for the running turn - /api/bots//audit now reads the trail (phone's 稽核 tab gains history) - retention: rows older than KEEP_DAYS are deleted on open - groundwork for memory/RAG (P6) on the same layer; state.json itself moves in step 2 with the memory tables (the app keeps reading State as a struct) --- mobile/index.html | 2 +- src/store.rs | 24 ++++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/mobile/index.html b/mobile/index.html index 0f1e0a5..eb20bfb 100644 --- a/mobile/index.html +++ b/mobile/index.html @@ -1318,7 +1318,7 @@ function tabBody(b, d) { if (S.tab === "audit") { const a = S.audit[b.id]; if (a === undefined) { loadAudit(b.id); return `
載入中…
`; } - if (!a.turns.length) return `
現在沒有進行中的工作。稽核時間軸記的是**這回合**做了什麼:每次問模型、每個工具的參數與結果、你插入的話、停止。回合結束後只留最近的,聊天記錄本身才是長久的。
`; + if (!a.turns.length) return `
現在沒有進行中的工作。稽核時間軸記的是**這回合**做了什麼:每次問模型、每個工具的參數與結果、你插入的話、停止。這裡記的是最近 30 天的每一回合;更早的故事在聊天記錄裡。
`; const OUT = {completed: "已結束", aborted: "被你停止", failed: "失敗"}; const ST = {done: "完成", running: "進行中…", waiting: "等核准", denied: "被拒絕", failed: "失敗"}; const TI = {computer: "zap", menu_click: "zap", fill_form: "zap", wait_for: "clock", run_cmd: "zap", web_fetch: "globe", browser: "globe"}; diff --git a/src/store.rs b/src/store.rs index 0d764cf..f0395b0 100644 --- a/src/store.rs +++ b/src/store.rs @@ -32,7 +32,11 @@ fn path() -> PathBuf { /// Open (create if needed) the data folder's audit database. Failure means "no /// history", never "no bot". fn open() -> Option { - let c = Connection::open(path()).ok()?; + let p = path(); + if let Some(d) = p.parent() { + let _ = std::fs::create_dir_all(d); // (the data folder may not exist yet on first run) + } + let c = Connection::open(p).ok()?; c.pragma_update(None, "journal_mode", "WAL").ok()?; c.pragma_update(None, "synchronous", "NORMAL").ok()?; c.execute_batch( @@ -122,6 +126,7 @@ pub fn record(bot: u64, r: &Record) { /// The phone's 稽核時間軸: finished turns stay here, so the page answers for /// KEEP_DAYS, not just for the turn running now. Newest turn first; the last 20 turns. pub fn turns(bot: u64) -> serde_json::Value { + let bot = bot as i64; let Some(s) = store() else { return serde_json::json!({"turns": []}) }; let Ok(c) = s.conn.lock() else { return serde_json::json!({"turns": []}) }; let cutoff = crate::model::now_ms() - KEEP_DAYS * 86_400_000; @@ -208,18 +213,21 @@ mod tests { #[test] fn the_trail_keeps_the_story_after_the_turn_log_cleans_up() { - // (Tests share the temp dir of this process; one database, one bot id.) + // (Tests share the temp dir of this process; one database, one bot id. + // Times ride on "now" — the trail prunes anything older than a month.) let bot = 4242; - record(bot, &Record::OperationStarted { op: "op1".into(), at: 100, source_leaf: 0, prompt: Some(Prompt { at: 99, text: "幫我叫車".into() }) }); + let t0 = crate::model::now_ms() - 60_000; + let at = |d| t0 + d; + record(bot, &Record::OperationStarted { op: "op1".into(), at: at(0), source_leaf: 0, prompt: Some(Prompt { at: at(0), text: "幫我叫車".into() }) }); record(bot, &Record::StepAttempt { op: "op1".into(), step: "assistant".into(), attempt: 1 }); record(bot, &Record::StepAttempt { op: "op1".into(), step: "assistant".into(), attempt: 2 }); - record(bot, &Record::ToolStarted { op: "op1".into(), call_id: "c1".into(), name: "computer".into(), args: "{\"action\":\"click\"}".into(), replay: Replay::Never, at: 110 }); - record(bot, &Record::ToolFinished { op: "op1".into(), call_id: "c1".into(), status: "done".into(), at: 120 }); - record(bot, &Record::QueueEnqueued { op: Some("op1".into()), id: "q1".into(), text: "快一點".into(), at: 111 }); + record(bot, &Record::ToolStarted { op: "op1".into(), call_id: "c1".into(), name: "computer".into(), args: "{\"action\":\"click\"}".into(), replay: Replay::Never, at: at(10) }); + record(bot, &Record::ToolFinished { op: "op1".into(), call_id: "c1".into(), status: "done".into(), at: at(20) }); + record(bot, &Record::QueueEnqueued { op: Some("op1".into()), id: "q1".into(), text: "快一點".into(), at: at(11) }); record(bot, &Record::QueueConsumed { id: "q1".into() }); record(bot, &Record::OperationFinished { op: "op1".into(), outcome: Outcome::Completed }); - record(bot, &Record::OperationStarted { op: "op2".into(), at: 200, source_leaf: 100, prompt: None }); - record(bot, &Record::ToolStarted { op: "op2".into(), call_id: "c9".into(), name: "run_cmd".into(), args: "{}".into(), replay: Replay::Safe, at: 210 }); + record(bot, &Record::OperationStarted { op: "op2".into(), at: at(100), source_leaf: 100, prompt: None }); + record(bot, &Record::ToolStarted { op: "op2".into(), call_id: "c9".into(), name: "run_cmd".into(), args: "{}".into(), replay: Replay::Safe, at: at(110) }); let v = turns(bot); let ts = v["turns"].as_array().unwrap();