From 44f272232906ac5402bed8ba6a9abff6f76d3828 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sun, 21 Dec 2025 18:15:31 +0800 Subject: [PATCH] feat(proxy): persist proxy takeover state across app restarts - Add proxy_takeover_{app_type} settings for per-app state tracking - Restore proxy takeover state automatically on app startup - Preserve state on normal exit, clear on manual stop - Add stop_with_restore_keep_state method for graceful shutdown --- src-tauri/src/database/dao/proxy.rs | 12 +++-- src-tauri/src/database/dao/settings.rs | 52 +++++++++++++++++++++ src-tauri/src/lib.rs | 62 +++++++++++++++++++++++-- src-tauri/src/services/proxy.rs | 64 +++++++++++++++++++++++--- 4 files changed, 177 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/database/dao/proxy.rs b/src-tauri/src/database/dao/proxy.rs index 53bffeeeb..9017ebe60 100644 --- a/src-tauri/src/database/dao/proxy.rs +++ b/src-tauri/src/database/dao/proxy.rs @@ -71,21 +71,27 @@ impl Database { Ok(()) } - /// 设置 Live 接管状态 + /// 设置 Live 接管状态(仅更新 proxy_config 表,兼容旧逻辑) + /// + /// 注意:此方法不会清除 settings 表中的 proxy_takeover_* 状态。 + /// settings 表的状态由 set_proxy_takeover_enabled 单独管理,用于跨重启保持状态。 pub async fn set_live_takeover_active(&self, active: bool) -> Result<(), AppError> { + // 仅更新 proxy_config 表(兼容旧版本) let conn = lock_conn!(self.conn); conn.execute( "UPDATE proxy_config SET live_takeover_active = ?1, updated_at = datetime('now') WHERE id = 1", rusqlite::params![if active { 1 } else { 0 }], ) .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) } /// 检查是否处于 Live 接管模式 + /// + /// v3.8.0+:以 settings 表中的 `proxy_takeover_{app_type}` 为真实来源 pub async fn is_live_takeover_active(&self) -> Result { - // v3.7.0+:以 proxy_live_backup 是否存在作为“接管状态”的真实来源(更贴近 per-app 接管) - self.has_any_live_backup().await + self.has_any_proxy_takeover() } // ==================== Provider Health ==================== diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index 90272aabe..68a5c68b8 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -62,4 +62,56 @@ impl Database { Ok(()) } } + + // --- 代理接管状态管理 --- + + /// 获取指定应用的代理接管状态 + /// + /// 使用 settings 表存储各应用的接管状态,key 格式: `proxy_takeover_{app_type}` + pub fn get_proxy_takeover_enabled(&self, app_type: &str) -> Result { + let key = format!("proxy_takeover_{app_type}"); + match self.get_setting(&key)? { + Some(value) => Ok(value == "true"), + None => Ok(false), + } + } + + /// 设置指定应用的代理接管状态 + /// + /// - `true` = 开启代理接管 + /// - `false` = 关闭代理接管 + pub fn set_proxy_takeover_enabled( + &self, + app_type: &str, + enabled: bool, + ) -> Result<(), AppError> { + let key = format!("proxy_takeover_{app_type}"); + let value = if enabled { "true" } else { "false" }; + self.set_setting(&key, value) + } + + /// 检查是否有任一应用开启了代理接管 + pub fn has_any_proxy_takeover(&self) -> Result { + let conn = lock_conn!(self.conn); + let count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM settings WHERE key LIKE 'proxy_takeover_%' AND value = 'true'", + [], + |row| row.get(0), + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(count > 0) + } + + /// 清除所有代理接管状态(将所有 proxy_takeover_* 设置为 false) + pub fn clear_all_proxy_takeover(&self) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + conn.execute( + "UPDATE settings SET value = 'false' WHERE key LIKE 'proxy_takeover_%'", + [], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + log::info!("已清除所有代理接管状态"); + Ok(()) + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index f95547f86..7f0b08f2a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -524,7 +524,7 @@ pub fn run() { } } - // 异常退出恢复 + // 异常退出恢复 + 代理状态自动恢复 let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { let state = app_handle.state::(); @@ -548,6 +548,9 @@ pub fn run() { log::info!("Live 配置已恢复"); } } + + // 检查 settings 表中的代理状态,自动恢复代理服务 + restore_proxy_state_on_startup(&state).await; }); Ok(()) @@ -811,6 +814,7 @@ pub fn run() { /// /// 在应用退出前检查代理服务器状态,如果正在运行则停止代理并恢复 Live 配置。 /// 确保 Claude Code/Codex/Gemini 的配置不会处于损坏状态。 +/// 使用 stop_with_restore_keep_state 保留 settings 表中的代理状态,下次启动时自动恢复。 pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) { if let Some(state) = app_handle.try_state::() { let proxy_service = &state.proxy_service; @@ -827,11 +831,12 @@ pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) { let needs_restore = has_backups || live_taken_over; if needs_restore { - log::info!("检测到接管残留,开始恢复 Live 配置..."); - if let Err(e) = proxy_service.stop_with_restore().await { + log::info!("检测到接管残留,开始恢复 Live 配置(保留代理状态)..."); + // 使用 keep_state 版本,保留 settings 表中的代理状态 + if let Err(e) = proxy_service.stop_with_restore_keep_state().await { log::error!("退出时恢复 Live 配置失败: {e}"); } else { - log::info!("已恢复 Live 配置"); + log::info!("已恢复 Live 配置(代理状态已保留,下次启动将自动恢复)"); } return; } @@ -847,6 +852,55 @@ pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) { } } +// ============================================================ +// 启动时恢复代理状态 +// ============================================================ + +/// 启动时根据 settings 表中的代理状态自动恢复代理服务 +/// +/// 检查 `proxy_takeover_claude`、`proxy_takeover_codex`、`proxy_takeover_gemini` 的值, +/// 如果有任一应用的状态为 `true`,则自动启动代理服务并接管对应应用的 Live 配置。 +async fn restore_proxy_state_on_startup(state: &store::AppState) { + // 收集需要恢复接管的应用列表 + let apps_to_restore: Vec<&str> = ["claude", "codex", "gemini"] + .iter() + .filter(|app_type| { + state + .db + .get_proxy_takeover_enabled(app_type) + .unwrap_or(false) + }) + .copied() + .collect(); + + if apps_to_restore.is_empty() { + log::debug!("启动时无需恢复代理状态"); + return; + } + + log::info!("检测到上次代理状态需要恢复,应用列表: {apps_to_restore:?}"); + + // 逐个恢复接管状态 + for app_type in apps_to_restore { + match state + .proxy_service + .set_takeover_for_app(app_type, true) + .await + { + Ok(()) => { + log::info!("✓ 已恢复 {app_type} 的代理接管状态"); + } + Err(e) => { + log::error!("✗ 恢复 {app_type} 的代理接管状态失败: {e}"); + // 失败时清除该应用的状态,避免下次启动再次尝试 + if let Err(clear_err) = state.db.set_proxy_takeover_enabled(app_type, false) { + log::error!("清除 {app_type} 代理状态失败: {clear_err}"); + } + } + } + } +} + // ============================================================ // 迁移错误对话框辅助函数 // ============================================================ diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 7c720a9ad..2c01fc34b 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -214,7 +214,12 @@ impl ProxyService { return Err(e); } - // 6) 兼容旧逻辑:写入 any-of 标志(失败不影响功能) + // 6) 设置 settings 表中的接管状态 + self.db + .set_proxy_takeover_enabled(app_type_str, true) + .map_err(|e| format!("设置 {app_type_str} 接管状态失败: {e}"))?; + + // 7) 兼容旧逻辑:写入 any-of 标志(失败不影响功能) let _ = self.db.set_live_takeover_active(true).await; return Ok(()); } @@ -239,7 +244,12 @@ impl ProxyService { .await .map_err(|e| format!("删除 {app_type_str} Live 备份失败: {e}"))?; - // 3) 若无其它接管,更新旧标志,并停止代理服务 + // 3) 清除 settings 表中该应用的接管状态 + self.db + .set_proxy_takeover_enabled(app_type_str, false) + .map_err(|e| format!("清除 {app_type_str} 接管状态失败: {e}"))?; + + // 4) 若无其它接管,更新旧标志,并停止代理服务 let has_any_backup = self .db .has_any_live_backup() @@ -484,7 +494,9 @@ impl ProxyService { } } - /// 停止代理服务器(恢复 Live 配置) + /// 停止代理服务器(恢复 Live 配置,用户手动关闭时使用) + /// + /// 会清除 settings 表中的代理状态,下次启动不会自动恢复。 pub async fn stop_with_restore(&self) -> Result<(), String> { // 1. 停止代理服务器(即使未运行也继续执行恢复逻辑) if let Err(e) = self.stop().await { @@ -494,19 +506,24 @@ impl ProxyService { // 2. 恢复原始 Live 配置 self.restore_live_configs().await?; - // 3. 清除接管状态 + // 3. 清除 proxy_config 表中的接管状态(兼容旧版) self.db .set_live_takeover_active(false) .await .map_err(|e| format!("清除接管状态失败: {e}"))?; - // 4. 删除备份 + // 4. 清除 settings 表中的代理状态(用户手动关闭,不需要下次自动恢复) + self.db + .clear_all_proxy_takeover() + .map_err(|e| format!("清除代理状态失败: {e}"))?; + + // 5. 删除备份 self.db .delete_all_live_backups() .await .map_err(|e| format!("删除备份失败: {e}"))?; - // 5. 重置健康状态(让健康徽章恢复为正常) + // 6. 重置健康状态(让健康徽章恢复为正常) self.db .clear_all_provider_health() .await @@ -516,6 +533,41 @@ impl ProxyService { Ok(()) } + /// 停止代理服务器(恢复 Live 配置,但保留 settings 表中的代理状态) + /// + /// 用于程序正常退出时,保留代理状态以便下次启动时自动恢复 + pub async fn stop_with_restore_keep_state(&self) -> Result<(), String> { + // 1. 停止代理服务器(即使未运行也继续执行恢复逻辑) + if let Err(e) = self.stop().await { + log::warn!("停止代理服务器失败(将继续恢复 Live 配置): {e}"); + } + + // 2. 恢复原始 Live 配置 + self.restore_live_configs().await?; + + // 3. 更新 proxy_config 表中的 live_takeover_active 标志(兼容旧版) + // 注意:仅更新 proxy_config 表,不清除 settings 表中的 proxy_takeover_* 状态 + if let Ok(mut config) = self.db.get_proxy_config().await { + config.live_takeover_active = false; + let _ = self.db.update_proxy_config(config).await; + } + + // 4. 删除备份(Live 配置已恢复,备份不再需要) + self.db + .delete_all_live_backups() + .await + .map_err(|e| format!("删除备份失败: {e}"))?; + + // 5. 重置健康状态 + self.db + .clear_all_provider_health() + .await + .map_err(|e| format!("重置健康状态失败: {e}"))?; + + log::info!("代理已停止,Live 配置已恢复(保留代理状态,下次启动将自动恢复)"); + Ok(()) + } + /// 备份各应用的 Live 配置 async fn backup_live_configs(&self) -> Result<(), String> { // Claude