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
This commit is contained in:
YoVinchen
2025-12-21 18:15:31 +08:00
parent f05904821c
commit 44f2722329
4 changed files with 177 additions and 13 deletions
+9 -3
View File
@@ -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<bool, AppError> {
// v3.7.0+:以 proxy_live_backup 是否存在作为“接管状态”的真实来源(更贴近 per-app 接管)
self.has_any_live_backup().await
self.has_any_proxy_takeover()
}
// ==================== Provider Health ====================
+52
View File
@@ -62,4 +62,56 @@ impl Database {
Ok(())
}
}
// --- 代理接管状态管理 ---
/// 获取指定应用的代理接管状态
///
/// 使用 settings 表存储各应用的接管状态,key 格式: `proxy_takeover_{app_type}`
pub fn get_proxy_takeover_enabled(&self, app_type: &str) -> Result<bool, AppError> {
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<bool, AppError> {
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(())
}
}
+58 -4
View File
@@ -524,7 +524,7 @@ pub fn run() {
}
}
// 异常退出恢复
// 异常退出恢复 + 代理状态自动恢复
let app_handle = app.handle().clone();
tauri::async_runtime::spawn(async move {
let state = app_handle.state::<AppState>();
@@ -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::<store::AppState>() {
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}");
}
}
}
}
}
// ============================================================
// 迁移错误对话框辅助函数
// ============================================================
+58 -6
View File
@@ -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