mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
b642ef0633
- Forwarder buffers non-streaming bodies and primes streaming first chunk before signaling success, so body timeouts and SSE first-chunk failures route through the circuit breaker instead of being recorded as success on response-header arrival - Atomic enable-failover: switch to P1 before persisting the flag, and roll back auto-added queue entries when the switch is rejected (e.g. official providers) - Hot-reload circuit breaker config on per-app proxy config change instead of waiting for a proxy restart - FailoverToggle / FailoverQueueManager / AutoFailoverConfigPanel require proxy takeover for the active app; the backend command also rejects enabling when takeover is off - ProviderHealthBadge consumes the backend is_healthy flag instead of hardcoding the 5-failure threshold Cleanup: - impl From<&AppProxyConfig> for CircuitBreakerConfig and use it from the command layer - Collapse three identical TabsContent blocks into a single map
183 lines
5.5 KiB
Rust
183 lines
5.5 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}"
|
||
);
|
||
|
||
// 读取当前配置
|
||
let mut config = state
|
||
.db
|
||
.get_proxy_config_for_app(&app_type)
|
||
.await
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
if enabled && !config.enabled {
|
||
return Err("需要先启用该应用的代理接管,再开启故障转移".to_string());
|
||
}
|
||
|
||
// 队列为空时把当前供应商自动加入作为 P1,避免用户陷入"必须先加队列才能开启"的死锁
|
||
let mut auto_added_provider_id: Option<String> = None;
|
||
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, ¤t_id)
|
||
.map_err(|e| e.to_string())?;
|
||
auto_added_provider_id = Some(current_id);
|
||
|
||
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()
|
||
};
|
||
|
||
// 开启前先切到 P1。只有切换成功后才写入 auto_failover_enabled=true,
|
||
// 避免 P1 不可切换(例如 official provider)时留下“开关已开但目标未切”的脏状态。
|
||
if enabled {
|
||
if let Err(e) = state
|
||
.proxy_service
|
||
.switch_proxy_target(&app_type, &p1_provider_id)
|
||
.await
|
||
{
|
||
if let Some(provider_id) = auto_added_provider_id {
|
||
let _ = state.db.remove_from_failover_queue(&app_type, &provider_id);
|
||
}
|
||
return Err(e);
|
||
}
|
||
}
|
||
|
||
// 更新 auto_failover_enabled 字段
|
||
config.auto_failover_enabled = enabled;
|
||
|
||
// 写回数据库
|
||
state
|
||
.db
|
||
.update_proxy_config_for_app(config)
|
||
.await
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
if enabled {
|
||
// 发射 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(crate::tray::TRAY_ID) {
|
||
let _ = tray.set_menu(Some(new_menu));
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|