Files
CC-Switch/src-tauri/src/commands/failover.rs
T
Jason a187380d6f fix(failover): switch to P1 immediately when enabling auto failover
Previously, enabling auto failover kept using the current provider until
the first failure, causing inconsistency when the current provider was
not in the failover queue. When stopping proxy, the restored config
would not match user expectations.

New behavior:
- Enable auto failover = immediately switch to queue P1
- Subsequent routing follows queue order (P1→P2→...)
- Auto-add current provider to queue if queue is empty

Changes:
- Add switch_proxy_target() for hot-switching during proxy mode
- Update provider_router to use queue order when failover enabled
- Sync tray menu Auto click with the same logic
- Update UI tooltips to reflect new semantics
- Add tests for queue-only routing scenario
2026-01-21 11:27:44 +08:00

172 lines
5.1 KiB
Rust

//! 故障转移队列命令
//!
//! 管理代理模式下的故障转移队列(基于 providers 表的 in_failover_queue 字段)
use crate::database::FailoverQueueItem;
use crate::provider::Provider;
use crate::store::AppState;
use std::str::FromStr;
use tauri::Emitter;
/// 获取故障转移队列
#[tauri::command]
pub async fn get_failover_queue(
state: tauri::State<'_, AppState>,
app_type: String,
) -> Result<Vec<FailoverQueueItem>, String> {
state
.db
.get_failover_queue(&app_type)
.map_err(|e| e.to_string())
}
/// 获取可添加到故障转移队列的供应商(不在队列中的)
#[tauri::command]
pub async fn get_available_providers_for_failover(
state: tauri::State<'_, AppState>,
app_type: String,
) -> Result<Vec<Provider>, String> {
state
.db
.get_available_providers_for_failover(&app_type)
.map_err(|e| e.to_string())
}
/// 添加供应商到故障转移队列
#[tauri::command]
pub async fn add_to_failover_queue(
state: tauri::State<'_, AppState>,
app_type: String,
provider_id: String,
) -> Result<(), String> {
state
.db
.add_to_failover_queue(&app_type, &provider_id)
.map_err(|e| e.to_string())
}
/// 从故障转移队列移除供应商
#[tauri::command]
pub async fn remove_from_failover_queue(
state: tauri::State<'_, AppState>,
app_type: String,
provider_id: String,
) -> Result<(), String> {
state
.db
.remove_from_failover_queue(&app_type, &provider_id)
.map_err(|e| e.to_string())
}
/// 获取指定应用的自动故障转移开关状态(从 proxy_config 表读取)
#[tauri::command]
pub async fn get_auto_failover_enabled(
state: tauri::State<'_, AppState>,
app_type: String,
) -> Result<bool, String> {
state
.db
.get_proxy_config_for_app(&app_type)
.await
.map(|config| config.auto_failover_enabled)
.map_err(|e| e.to_string())
}
/// 设置指定应用的自动故障转移开关状态(写入 proxy_config 表)
///
/// 注意:关闭故障转移时不会清除队列,队列内容会保留供下次开启时使用
#[tauri::command]
pub async fn set_auto_failover_enabled(
app: tauri::AppHandle,
state: tauri::State<'_, AppState>,
app_type: String,
enabled: bool,
) -> Result<(), String> {
log::info!(
"[Failover] Setting auto_failover_enabled: app_type='{app_type}', enabled={enabled}"
);
// 强一致语义:开启故障转移后立即切到队列 P1(并确保队列非空)
//
// 说明:
// - 仅在 enabled=true 时执行“切到 P1”
// - 若队列为空,则尝试把“当前供应商”自动加入队列作为 P1,避免用户在 UI 上陷入死锁(无法先加队列再开启)
let p1_provider_id = if enabled {
let mut queue = state
.db
.get_failover_queue(&app_type)
.map_err(|e| e.to_string())?;
if queue.is_empty() {
let app_enum = crate::app_config::AppType::from_str(&app_type)
.map_err(|_| format!("无效的应用类型: {app_type}"))?;
let current_id = crate::settings::get_effective_current_provider(&state.db, &app_enum)
.map_err(|e| e.to_string())?;
let Some(current_id) = current_id else {
return Err("故障转移队列为空,且未设置当前供应商,无法开启故障转移".to_string());
};
state
.db
.add_to_failover_queue(&app_type, &current_id)
.map_err(|e| e.to_string())?;
queue = state
.db
.get_failover_queue(&app_type)
.map_err(|e| e.to_string())?;
}
queue
.first()
.map(|item| item.provider_id.clone())
.ok_or_else(|| "故障转移队列为空,无法开启故障转移".to_string())?
} else {
String::new()
};
// 读取当前配置
let mut config = state
.db
.get_proxy_config_for_app(&app_type)
.await
.map_err(|e| e.to_string())?;
// 更新 auto_failover_enabled 字段
config.auto_failover_enabled = enabled;
// 写回数据库
state
.db
.update_proxy_config_for_app(config)
.await
.map_err(|e| e.to_string())?;
// 开启后立即切到 P1:更新 is_current + 本地 settings + Live 备份(接管模式下)
if enabled {
state
.proxy_service
.switch_proxy_target(&app_type, &p1_provider_id)
.await?;
// 发射 provider-switched 事件(让前端刷新当前供应商)
let event_data = serde_json::json!({
"appType": app_type,
"providerId": p1_provider_id,
"source": "failoverEnabled"
});
let _ = app.emit("provider-switched", event_data);
}
// 刷新托盘菜单,确保状态同步
if let Ok(new_menu) = crate::tray::create_tray_menu(&app, &state) {
if let Some(tray) = app.tray_by_id("main") {
let _ = tray.set_menu(Some(new_menu));
}
}
Ok(())
}