From b0c24d64f0d99233cd276b86cad11d49ed1b6829 Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Mon, 17 Aug 2026 18:02:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(storage):=20=E6=B8=85=E7=90=86=E6=97=A7?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E9=81=97=E7=95=99=E7=9A=84=E7=A9=BA=20cli=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E5=B9=B6=E9=99=8D=E7=BA=A7=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 早期版本每次 WebSocket 连接都会新建一个 cli 通道空会话(CLI Session xxxxxxxx),长期运行积累大量空壳记录,导致会话列表冗长、网关日志逐条刷屏 - 新增 user_version=3 一次性迁移:删除无消息/话题/待办/技能事件的 cli 空会话,有真实内容的会话不受影响(事务包裹,含回归测试) - ws.rs 会话列表逐条日志由 INFO 降级为 debug,仅保留条数汇总 --- src/gateway/ws.rs | 12 ++++++++++-- src/storage/migrations.rs | 40 +++++++++++++++++++++++++++++++++++++++ src/storage/mod.rs | 1 + src/storage/tests.rs | 30 +++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/gateway/ws.rs b/src/gateway/ws.rs index a24214b..2e24409 100644 --- a/src/gateway/ws.rs +++ b/src/gateway/ws.rs @@ -238,9 +238,17 @@ async fn handle_socket(ws: WebSocket, state: Arc) { websocket_sessions.sort_by_key(|s| -(s.last_active_at)); } - tracing::info!("Sending {} sessions to client", websocket_sessions.len()); + tracing::info!( + session_count = websocket_sessions.len(), + "Sending session list to client" + ); for s in &websocket_sessions { - tracing::info!(" - {}: {} (channel: {})", s.id, s.title, s.channel_name); + tracing::debug!( + session_id = %s.id, + title = %s.title, + channel = %s.channel_name, + "Session list entry" + ); } let session_summaries: Vec = websocket_sessions diff --git a/src/storage/migrations.rs b/src/storage/migrations.rs index 5665d22..2d935db 100644 --- a/src/storage/migrations.rs +++ b/src/storage/migrations.rs @@ -383,6 +383,46 @@ fn repair_session_id_prefix_pollution_inner(conn: &Connection) -> Result<(), Sto Ok(()) } +/// 清理历史遗留的空 cli 会话(user_version 3,一次性迁移)。 +/// +/// 早期版本每次 WebSocket 连接建立都会新建一个 cli 通道空会话 +/// (自动标题 "CLI Session xxxxxxxx"),长期运行后 sessions 表积累大量 +/// 空壳记录:它们会被并入发送给前端的会话列表,也在网关日志中逐条打印。 +/// 本迁移删除没有任何消息、话题、待办、技能事件的 cli 会话(纯空壳), +/// 有真实内容的会话不受影响。事务包裹,通过 PRAGMA user_version 仅执行一次。 +pub(super) fn cleanup_legacy_empty_cli_sessions(conn: &mut Connection) -> Result<(), StorageError> { + const EMPTY_CLI_CLEANUP_VERSION: i64 = 3; + + let current_version: i64 = conn.query_row("PRAGMA user_version", [], |row| row.get(0))?; + if current_version >= EMPTY_CLI_CLEANUP_VERSION { + return Ok(()); + } + + let tx = conn.transaction()?; + let deleted = tx.execute( + "DELETE FROM sessions + WHERE channel_name = 'cli' + AND NOT EXISTS (SELECT 1 FROM messages m WHERE m.session_id = sessions.id) + AND NOT EXISTS (SELECT 1 FROM topics t WHERE t.session_id = sessions.id) + AND NOT EXISTS (SELECT 1 FROM todos d WHERE d.session_id = sessions.id) + AND NOT EXISTS (SELECT 1 FROM skill_events s WHERE s.session_id = sessions.id)", + [], + )?; + if deleted > 0 { + tracing::info!( + deleted_count = deleted, + "Cleaned up legacy empty cli sessions" + ); + } + tx.execute( + &format!("PRAGMA user_version = {EMPTY_CLI_CLEANUP_VERSION}"), + [], + )?; + tx.commit()?; + + Ok(()) +} + pub(super) fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> { let table_exists: bool = conn .query_row( diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 07247f5..5507144 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -238,6 +238,7 @@ impl SessionStore { ensure_todos_schema(&conn)?; ensure_pending_subagents_schema(&conn)?; repair_session_id_prefix_pollution(&mut conn)?; + cleanup_legacy_empty_cli_sessions(&mut conn)?; drop(conn); diff --git a/src/storage/tests.rs b/src/storage/tests.rs index bb12429..e2fe56d 100644 --- a/src/storage/tests.rs +++ b/src/storage/tests.rs @@ -794,3 +794,33 @@ fn test_repair_session_id_prefix_pollution() { assert_eq!(store.get_session("abc").unwrap().unwrap().message_count, 2); assert_eq!(store.list_sessions("websocket", false).unwrap().len(), 2); } + +#[test] +fn test_cleanup_legacy_empty_cli_sessions() { + let store = SessionStore::in_memory().unwrap(); + + // 空 cli 会话(旧版本遗留的空壳):应被删除 + let empty = store.create_cli_session(None).unwrap(); + // 有消息的 cli 会话:必须保留 + let with_data = store.create_cli_session(None).unwrap(); + store + .append_message(&with_data.id, &ChatMessage::user("hello")) + .unwrap(); + // 空 websocket 会话:不在 cli 清理范围内 + let ws = store.ensure_channel_session("websocket", "chat-1").unwrap(); + + let mut conn = store.pool.get().unwrap(); + // from_connection 已把 user_version 推到 3,回退到 2 模拟"尚未清理" + conn.execute("PRAGMA user_version = 2", []).unwrap(); + + super::migrations::cleanup_legacy_empty_cli_sessions(&mut conn).unwrap(); + + assert!(store.get_session(&empty.id).unwrap().is_none()); + assert!(store.get_session(&with_data.id).unwrap().is_some()); + assert!(store.get_session(&ws.id).unwrap().is_some()); + + // 幂等:版本守卫使第二次调用直接跳过,已有会话不受影响 + super::migrations::cleanup_legacy_empty_cli_sessions(&mut conn).unwrap(); + assert!(store.get_session(&with_data.id).unwrap().is_some()); + assert!(store.get_session(&ws.id).unwrap().is_some()); +}