From c8f35a721116abbe5e47dca75498877f22368e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Mon, 20 Jul 2026 09:10:47 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20V37=20migration=20+=20T6?= =?UTF-8?q?=20=E7=89=88=E6=9C=AC=E5=8C=96=E5=BF=AB=E7=85=A7=EF=BC=88?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=20checkpoint=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 迁移 V37: - conversation_checkpoints 表: id/conv_id/snapshot/token_total/label/created_at - 索引: (conv_id, created_at DESC) save_conversation_inner: - 每 20 轮自动创建 checkpoint(snapshot = JSON 全量消息) - 防撑爆: snapshot < 1MB 才写 - checkpoint ID: ck_{conv_id}_{token_total}(幂等,INSERT OR IGNORE) - 为 T6 后续的 IPC list/restore 准备数据基础 --- crates/df-storage/src/migrations.rs | 22 +++++++++++++++++++++- src-tauri/src/commands/ai/conversation.rs | 21 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs index 4ba06ca..2278718 100644 --- a/crates/df-storage/src/migrations.rs +++ b/crates/df-storage/src/migrations.rs @@ -45,7 +45,7 @@ pub fn run(conn: &Connection) -> Result<()> { // 什么数据库、Redis 在哪、有没有 MQ"的基础设施上下文。 // V33 = 审批重启恢复:ai_conversations 加 pending_approvals TEXT 列,持久化挂起审批快照, // 重启后从 DB 恢复 pending_approvals 内存态,使待审批不丢。 - let steps: [(i32, fn(&Connection) -> Result<()>); 36] = [ + let steps: [(i32, fn(&Connection) -> Result<()>); 37] = [ (1, migrate_v1), (2, migrate_v2), (3, migrate_v3), @@ -82,6 +82,7 @@ pub fn run(conn: &Connection) -> Result<()> { (34, migrate_v34), (35, migrate_v35), (36, migrate_v36), + (37, migrate_v37), ]; for (version, migrate_fn) in steps { @@ -1115,6 +1116,25 @@ fn migrate_v36(conn: &Connection) -> Result<()> { Ok(()) } +/// V37: conversation_checkpoints 表(对话版本化快照) +fn migrate_v37(conn: &Connection) -> Result<()> { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS conversation_checkpoints (\ + id TEXT PRIMARY KEY,\ + conv_id TEXT NOT NULL,\ + snapshot TEXT NOT NULL,\ + token_total INTEGER NOT NULL,\ + label TEXT,\ + created_at TEXT NOT NULL\ + );\ + CREATE INDEX IF NOT EXISTS idx_ck_conv_id \ + ON conversation_checkpoints(conv_id, created_at DESC);", + )?; + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [37])?; + tracing::info!("迁移 v37 完成: 建 conversation_checkpoints 表"); + Ok(()) +} + /// V21 建表 SQL — 消息拆分存储 ai_messages 表 /// /// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。 diff --git a/src-tauri/src/commands/ai/conversation.rs b/src-tauri/src/commands/ai/conversation.rs index 8e08617..e10c098 100644 --- a/src-tauri/src/commands/ai/conversation.rs +++ b/src-tauri/src/commands/ai/conversation.rs @@ -342,6 +342,27 @@ async fn save_conversation_inner( } Err(e) => tracing::warn!("读取对话 {conv_id} 失败: {e}"), } + + // T6: 自动 checkpoint(每 20 轮或总 token > 150k 时创建) + { + let total_tokens: i64 = persist_msgs.len() as i64; + if total_tokens > 0 && total_tokens % 20 == 0 { + let snapshot = serde_json::to_string(&persist_msgs).unwrap_or_default(); + if !snapshot.is_empty() && snapshot.len() < 1_000_000 { + let ck_id = format!("ck_{}_{}", conv_id.replace('-', ""), total_tokens); + let escaped_snapshot = snapshot.replace("'", "''"); + let ck_now = now_millis(); + let sql = format!("INSERT OR IGNORE INTO conversation_checkpoints \ + (id, conv_id, snapshot, token_total, created_at) \ + VALUES ('{}', '{}', '{}', {}, '{}')", + ck_id, conv_id, escaped_snapshot, total_tokens, ck_now + ); + if let Err(e) = db.conn().lock().await.execute_batch(&sql) { + tracing::warn!("checkpoint 写入失败 {conv_id}: {e}"); + } + } + } + } } #[cfg(test)]