From bca0997afa11500df7a0cab84af8bec4afe5f530 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 17 Dec 2025 11:03:49 +0800 Subject: [PATCH] fix(proxy): harden crash recovery with fallback detection - Set takeover flag before writing proxy config to fix race condition where crash during takeover left Live configs corrupted but flag unset - Add fallback detection by checking for placeholder tokens in Live configs when backups exist but flag is false (handles legacy/edge cases) - Improve error handling with proper rollback at each stage of startup - Clean up stale backups when Live configs are not in takeover state to avoid long-term storage of sensitive tokens --- src-tauri/src/database/dao/proxy.rs | 11 +++ src-tauri/src/lib.rs | 50 +++++++++---- src-tauri/src/services/proxy.rs | 111 +++++++++++++++++++++++++--- 3 files changed, 146 insertions(+), 26 deletions(-) diff --git a/src-tauri/src/database/dao/proxy.rs b/src-tauri/src/database/dao/proxy.rs index 1e6344b6..5aacc11d 100644 --- a/src-tauri/src/database/dao/proxy.rs +++ b/src-tauri/src/database/dao/proxy.rs @@ -321,6 +321,17 @@ impl Database { Ok(()) } + /// 检查是否存在任意 Live 配置备份 + pub async fn has_any_live_backup(&self) -> Result { + let conn = lock_conn!(self.conn); + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM proxy_live_backup", [], |row| { + row.get(0) + }) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(count > 0) + } + /// 获取 Live 配置备份 pub async fn get_live_backup(&self, app_type: &str) -> Result, AppError> { let conn = lock_conn!(self.conn); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 75894186..21c3fa60 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -531,23 +531,41 @@ pub fn run() { let state = app_handle.state::(); // 1. 检测异常退出并恢复 Live 配置 - match state.db.is_live_takeover_active().await { - Ok(true) => { - // 接管标志为 true 但代理未运行 → 上次异常退出 - if !state.proxy_service.is_running().await { - log::warn!("检测到上次异常退出,正在恢复 Live 配置..."); - if let Err(e) = state.proxy_service.recover_from_crash().await { - log::error!("恢复 Live 配置失败: {e}"); - } else { - log::info!("Live 配置已从异常退出中恢复"); - } + let is_proxy_running = state.proxy_service.is_running().await; + if !is_proxy_running { + let takeover_flag = match state.db.is_live_takeover_active().await { + Ok(active) => active, + Err(e) => { + log::error!("检查接管状态失败: {e}"); + false + } + }; + + let has_backups = match state.db.has_any_live_backup().await { + Ok(v) => v, + Err(e) => { + log::error!("检查 Live 备份失败: {e}"); + false + } + }; + + // 兜底检测:旧版本/极端窗口期可能出现“标志未写入,但 Live 已被写成占位符”的残留状态。 + // 只有在存在备份时才检查占位符,避免误判覆盖用户正常配置。 + let live_taken_over = + has_backups && state.proxy_service.detect_takeover_in_live_configs(); + + if takeover_flag || live_taken_over { + log::warn!("检测到上次异常退出或残留接管状态,正在恢复 Live 配置..."); + if let Err(e) = state.proxy_service.recover_from_crash().await { + log::error!("恢复 Live 配置失败: {e}"); + } else { + log::info!("Live 配置已从异常退出中恢复"); + } + } else if has_backups { + // 备份残留但 Live 未处于接管状态:清理敏感备份,避免长期存储 Token + if let Err(e) = state.db.delete_all_live_backups().await { + log::warn!("清理残留 Live 备份失败: {e}"); } - } - Ok(false) => { - // 正常状态,无需恢复 - } - Err(e) => { - log::error!("检查接管状态失败: {e}"); } } diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 61d50c84..c75e07cb 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -84,16 +84,38 @@ impl ProxyService { self.backup_live_configs().await?; // 2. 同步 Live 配置中的 Token 到数据库(确保代理能读到最新的 Token) - self.sync_live_to_providers().await?; + if let Err(e) = self.sync_live_to_providers().await { + // 同步失败时尚未写入接管配置,但备份可能包含敏感信息,尽量清理 + if let Err(clean_err) = self.db.delete_all_live_backups().await { + log::warn!("清理 Live 备份失败: {clean_err}"); + } + return Err(e); + } - // 3. 接管各应用的 Live 配置(写入代理地址,清空 Token) - self.takeover_live_configs().await?; + // 3. 在写入接管配置之前先落盘接管标志: + // 这样即使在接管过程中断电/kill,下次启动也能检测到并自动恢复。 + if let Err(e) = self.db.set_live_takeover_active(true).await { + if let Err(clean_err) = self.db.delete_all_live_backups().await { + log::warn!("清理 Live 备份失败: {clean_err}"); + } + return Err(format!("设置接管状态失败: {e}")); + } - // 4. 设置接管状态 - self.db - .set_live_takeover_active(true) - .await - .map_err(|e| format!("设置接管状态失败: {e}"))?; + // 4. 接管各应用的 Live 配置(写入代理地址,清空 Token) + if let Err(e) = self.takeover_live_configs().await { + // 接管失败(可能是部分写入),尝试恢复原始配置;若恢复失败则保留标志与备份,等待下次启动自动恢复。 + log::error!("接管 Live 配置失败,尝试恢复原始配置: {e}"); + match self.restore_live_configs().await { + Ok(()) => { + let _ = self.db.set_live_takeover_active(false).await; + let _ = self.db.delete_all_live_backups().await; + } + Err(restore_err) => { + log::error!("恢复原始配置失败,将保留备份以便下次启动恢复: {restore_err}"); + } + } + return Err(e); + } // 5. 启动代理服务器 match self.start().await { @@ -101,8 +123,15 @@ impl ProxyService { Err(e) => { // 启动失败,恢复原始配置 log::error!("代理启动失败,尝试恢复原始配置: {e}"); - let _ = self.restore_live_configs().await; - let _ = self.db.set_live_takeover_active(false).await; + match self.restore_live_configs().await { + Ok(()) => { + let _ = self.db.set_live_takeover_active(false).await; + let _ = self.db.delete_all_live_backups().await; + } + Err(restore_err) => { + log::error!("恢复原始配置失败,将保留备份以便下次启动恢复: {restore_err}"); + } + } Err(e) } } @@ -517,6 +546,68 @@ impl ProxyService { Ok(()) } + /// 检测 Live 配置是否处于“被接管”的残留状态 + /// + /// 用于兜底处理:当数据库标志未写入成功(或旧版本遗留)但 Live 文件已经写成代理占位符时, + /// 启动流程可以据此触发恢复逻辑。 + pub fn detect_takeover_in_live_configs(&self) -> bool { + if let Ok(config) = self.read_claude_live() { + if Self::is_claude_live_taken_over(&config) { + return true; + } + } + + if let Ok(config) = self.read_codex_live() { + if Self::is_codex_live_taken_over(&config) { + return true; + } + } + + if let Ok(config) = self.read_gemini_live() { + if Self::is_gemini_live_taken_over(&config) { + return true; + } + } + + false + } + + fn is_claude_live_taken_over(config: &Value) -> bool { + let env = match config.get("env").and_then(|v| v.as_object()) { + Some(env) => env, + None => return false, + }; + + for key in [ + "ANTHROPIC_AUTH_TOKEN", + "ANTHROPIC_API_KEY", + "OPENROUTER_API_KEY", + "OPENAI_API_KEY", + ] { + if env.get(key).and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER) { + return true; + } + } + + false + } + + fn is_codex_live_taken_over(config: &Value) -> bool { + let auth = match config.get("auth").and_then(|v| v.as_object()) { + Some(auth) => auth, + None => return false, + }; + auth.get("OPENAI_API_KEY").and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER) + } + + fn is_gemini_live_taken_over(config: &Value) -> bool { + let env = match config.get("env").and_then(|v| v.as_object()) { + Some(env) => env, + None => return false, + }; + env.get("GEMINI_API_KEY").and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER) + } + /// 从供应商配置更新 Live 备份(用于代理模式下的热切换) /// /// 与 backup_live_configs() 不同,此方法从供应商的 settings_config 生成备份,