diff --git a/src-tauri/src/commands/failover.rs b/src-tauri/src/commands/failover.rs index 8269f5fc3..d0aad3900 100644 --- a/src-tauri/src/commands/failover.rs +++ b/src-tauri/src/commands/failover.rs @@ -5,6 +5,8 @@ use crate::database::FailoverQueueItem; use crate::provider::Provider; use crate::store::AppState; +use std::str::FromStr; +use tauri::Emitter; /// 获取故障转移队列 #[tauri::command] @@ -84,6 +86,47 @@ pub async fn set_auto_failover_enabled( "[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, ¤t_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 @@ -101,6 +144,22 @@ pub async fn set_auto_failover_enabled( .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") { diff --git a/src-tauri/src/proxy/provider_router.rs b/src-tauri/src/proxy/provider_router.rs index 5f3a315fd..cf9f0748c 100644 --- a/src-tauri/src/proxy/provider_router.rs +++ b/src-tauri/src/proxy/provider_router.rs @@ -2,11 +2,13 @@ //! //! 负责选择和管理代理目标供应商,实现智能故障转移 +use crate::app_config::AppType; use crate::database::Database; use crate::error::AppError; use crate::provider::Provider; use crate::proxy::circuit_breaker::{AllowResult, CircuitBreaker, CircuitBreakerConfig}; use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; @@ -31,7 +33,7 @@ impl ProviderRouter { /// /// 返回按优先级排序的可用供应商列表: /// - 故障转移关闭时:仅返回当前供应商 - /// - 故障转移开启时:完全按照故障转移队列顺序返回,忽略当前供应商设置 + /// - 故障转移开启时:仅使用故障转移队列,按队列顺序依次尝试(P1 → P2 → ...) pub async fn select_providers(&self, app_type: &str) -> Result, AppError> { let mut result = Vec::new(); let mut total_providers = 0usize; @@ -47,12 +49,25 @@ impl ProviderRouter { }; if auto_failover_enabled { - // 故障转移开启:使用 in_failover_queue 标记的供应商,按 sort_index 排序 - let failover_providers = self.db.get_failover_providers(app_type)?; - total_providers = failover_providers.len(); + // 故障转移开启:仅按队列顺序依次尝试(P1 → P2 → ...) + let all_providers = self.db.get_all_providers(app_type)?; - for provider in failover_providers { - let circuit_key = format!("{}:{}", app_type, provider.id); + // 使用 DAO 返回的排序结果,确保和前端展示一致 + let ordered_ids: Vec = self + .db + .get_failover_queue(app_type)? + .into_iter() + .map(|item| item.provider_id) + .collect(); + + total_providers = ordered_ids.len(); + + for provider_id in ordered_ids { + let Some(provider) = all_providers.get(&provider_id).cloned() else { + continue; + }; + + let circuit_key = format!("{app_type}:{}", provider.id); let breaker = self.get_or_create_circuit_breaker(&circuit_key).await; if breaker.is_available().await { @@ -63,7 +78,16 @@ impl ProviderRouter { } } else { // 故障转移关闭:仅使用当前供应商,跳过熔断器检查 - if let Some(current_id) = self.db.get_current_provider(app_type)? { + let current_id = AppType::from_str(app_type) + .ok() + .and_then(|app_enum| { + crate::settings::get_effective_current_provider(&self.db, &app_enum) + .ok() + .flatten() + }) + .or_else(|| self.db.get_current_provider(app_type).ok().flatten()); + + if let Some(current_id) = current_id { if let Some(current) = self.db.get_provider_by_id(¤t_id, app_type)? { total_providers = 1; result.push(current); @@ -239,9 +263,53 @@ mod tests { use super::*; use crate::database::Database; use serde_json::json; + use serial_test::serial; + use std::env; + use tempfile::TempDir; + + struct TempHome { + #[allow(dead_code)] + dir: TempDir, + original_home: Option, + original_userprofile: Option, + } + + impl TempHome { + fn new() -> Self { + let dir = TempDir::new().expect("failed to create temp home"); + let original_home = env::var("HOME").ok(); + let original_userprofile = env::var("USERPROFILE").ok(); + + env::set_var("HOME", dir.path()); + env::set_var("USERPROFILE", dir.path()); + crate::settings::reload_settings().expect("reload settings"); + + Self { + dir, + original_home, + original_userprofile, + } + } + } + + impl Drop for TempHome { + fn drop(&mut self) { + match &self.original_home { + Some(value) => env::set_var("HOME", value), + None => env::remove_var("HOME"), + } + + match &self.original_userprofile { + Some(value) => env::set_var("USERPROFILE", value), + None => env::remove_var("USERPROFILE"), + } + } + } #[tokio::test] + #[serial] async fn test_provider_router_creation() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); let router = ProviderRouter::new(db); @@ -250,7 +318,9 @@ mod tests { } #[tokio::test] + #[serial] async fn test_failover_disabled_uses_current_provider() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); let provider_a = @@ -271,7 +341,9 @@ mod tests { } #[tokio::test] - async fn test_failover_enabled_uses_queue_order() { + #[serial] + async fn test_failover_enabled_uses_queue_order_ignoring_current() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); // 设置 sort_index 来控制顺序:b=1, a=2 @@ -298,13 +370,45 @@ mod tests { let providers = router.select_providers("claude").await.unwrap(); assert_eq!(providers.len(), 2); - // 按 sort_index 排序:b(1) 在前,a(2) 在后 + // 故障转移开启时:仅按队列顺序选择(忽略当前供应商) assert_eq!(providers[0].id, "b"); assert_eq!(providers[1].id, "a"); } #[tokio::test] + #[serial] + async fn test_failover_enabled_uses_queue_only_even_if_current_not_in_queue() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().unwrap()); + + let provider_a = + Provider::with_id("a".to_string(), "Provider A".to_string(), json!({}), None); + let mut provider_b = + Provider::with_id("b".to_string(), "Provider B".to_string(), json!({}), None); + provider_b.sort_index = Some(1); + + db.save_provider("claude", &provider_a).unwrap(); + db.save_provider("claude", &provider_b).unwrap(); + db.set_current_provider("claude", "a").unwrap(); + + // 只把 b 加入故障转移队列(模拟“当前供应商不在队列里”的常见配置) + db.add_to_failover_queue("claude", "b").unwrap(); + + let mut config = db.get_proxy_config_for_app("claude").await.unwrap(); + config.auto_failover_enabled = true; + db.update_proxy_config_for_app(config).await.unwrap(); + + let router = ProviderRouter::new(db.clone()); + let providers = router.select_providers("claude").await.unwrap(); + + assert_eq!(providers.len(), 1); + assert_eq!(providers[0].id, "b"); + } + + #[tokio::test] + #[serial] async fn test_select_providers_does_not_consume_half_open_permit() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); db.update_circuit_breaker_config(&CircuitBreakerConfig { @@ -345,7 +449,9 @@ mod tests { } #[tokio::test] + #[serial] async fn test_release_permit_neutral_frees_half_open_slot() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); // 配置熔断器:1 次失败即熔断,0 秒超时立即进入 HalfOpen diff --git a/src-tauri/src/proxy/server.rs b/src-tauri/src/proxy/server.rs index bfd13ee2d..1a75083a9 100644 --- a/src-tauri/src/proxy/server.rs +++ b/src-tauri/src/proxy/server.rs @@ -190,6 +190,18 @@ impl ProxyServer { status } + /// 更新某个应用类型当前“目标供应商”(用于 UI 展示 active_targets) + /// + /// 注意:这不代表该供应商一定已经处理过请求,而是用于“热切换/启用故障转移立即切 P1” + /// 等场景下,让 UI 能立刻反映最新目标。 + pub async fn set_active_target(&self, app_type: &str, provider_id: &str, provider_name: &str) { + let mut current_providers = self.state.current_providers.write().await; + current_providers.insert( + app_type.to_string(), + (provider_id.to_string(), provider_name.to_string()), + ); + } + fn build_router(&self) -> Router { let cors = CorsLayer::new() .allow_origin(Any) diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 823bbfce8..b22043ad2 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -1530,7 +1530,10 @@ impl ProxyService { app_type: &str, provider_id: &str, ) -> Result<(), String> { - // 更新数据库中的 is_current 标记 + // 代理模式切换供应商(热切换): + // - 更新 SSOT(数据库 is_current) + // - 同步本地 settings(设备级 current_provider_*) + // - 若该应用正处于接管模式,则同步更新 Live 备份(用于停止代理时恢复) let app_type_enum = AppType::from_str(app_type).map_err(|_| format!("无效的应用类型: {app_type}"))?; @@ -1538,6 +1541,35 @@ impl ProxyService { .set_current_provider(app_type_enum.as_str(), provider_id) .map_err(|e| format!("更新当前供应商失败: {e}"))?; + // 同步本地 settings(设备级优先) + crate::settings::set_current_provider(&app_type_enum, Some(provider_id)) + .map_err(|e| format!("更新本地当前供应商失败: {e}"))?; + + // 仅在确实处于接管状态时才更新 Live 备份,避免无接管时误写覆盖 Live + let has_backup = self + .db + .get_live_backup(app_type_enum.as_str()) + .await + .ok() + .flatten() + .is_some(); + let live_taken_over = self.detect_takeover_in_live_config_for_app(&app_type_enum); + + if let Ok(Some(provider)) = self.db.get_provider_by_id(provider_id, app_type) { + // 同步更新 Live 备份(用于 stop_with_restore 恢复) + if has_backup || live_taken_over { + self.update_live_backup_from_provider(app_type, &provider) + .await?; + } + + // 同步更新 ProxyStatus.active_targets(用于 UI 立即反映切换目标) + if let Some(server) = self.server.read().await.as_ref() { + server + .set_active_target(app_type_enum.as_str(), &provider.id, &provider.name) + .await; + } + } + log::info!("代理模式:已切换 {app_type} 的目标供应商为 {provider_id}"); Ok(()) } @@ -2058,4 +2090,66 @@ model = "gpt-5.1-codex" "should not add ANTHROPIC_AUTH_TOKEN when absent" ); } + + #[tokio::test] + #[serial] + async fn switch_proxy_target_updates_live_backup_when_taken_over() { + let _home = TempHome::new(); + crate::settings::reload_settings().expect("reload settings"); + + let db = Arc::new(Database::memory().expect("init db")); + let service = ProxyService::new(db.clone()); + + let provider_a = Provider::with_id( + "a".to_string(), + "A".to_string(), + json!({ + "env": { + "ANTHROPIC_API_KEY": "a-key" + } + }), + None, + ); + let provider_b = Provider::with_id( + "b".to_string(), + "B".to_string(), + json!({ + "env": { + "ANTHROPIC_API_KEY": "b-key" + } + }), + None, + ); + db.save_provider("claude", &provider_a) + .expect("save provider a"); + db.save_provider("claude", &provider_b) + .expect("save provider b"); + db.set_current_provider("claude", "a") + .expect("set current provider"); + + // 模拟“已接管”状态:存在 Live 备份(内容不重要,会被热切换更新) + db.save_live_backup("claude", "{\"env\":{}}") + .await + .expect("seed live backup"); + + service + .switch_proxy_target("claude", "b") + .await + .expect("switch proxy target"); + + // 断言:本地 settings 的 current provider 已同步 + assert_eq!( + crate::settings::get_current_provider(&AppType::Claude).as_deref(), + Some("b") + ); + + // 断言:Live 备份已更新为目标供应商配置(用于 stop_with_restore 恢复) + let backup = db + .get_live_backup("claude") + .await + .expect("get live backup") + .expect("backup exists"); + let expected = serde_json::to_string(&provider_b.settings_config).expect("serialize"); + assert_eq!(backup.original_config, expected); + } } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 10b85fa0c..a3c151bf9 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -211,6 +211,28 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A if let Some(app_state) = app.try_state::() { let app_type_str = app_type.as_str(); + // 强一致语义:Auto 模式开启后立即切到队列 P1(P1→P2→...) + // 若队列为空,则尝试把“当前供应商”自动加入队列作为 P1,避免用户陷入无法开启的死锁。 + let mut queue = app_state.db.get_failover_queue(app_type_str)?; + if queue.is_empty() { + let current_id = + crate::settings::get_effective_current_provider(&app_state.db, app_type)?; + let Some(current_id) = current_id else { + return Err(AppError::Message( + "故障转移队列为空,且未设置当前供应商,无法启用 Auto 模式".to_string(), + )); + }; + app_state + .db + .add_to_failover_queue(app_type_str, ¤t_id)?; + queue = app_state.db.get_failover_queue(app_type_str)?; + } + + let p1_provider_id = queue + .first() + .map(|item| item.provider_id.clone()) + .ok_or_else(|| AppError::Message("故障转移队列为空,无法启用 Auto 模式".to_string()))?; + // 真正启用 failover:启动代理服务 + 执行接管 + 开启 auto_failover let proxy_service = &app_state.proxy_service; @@ -238,6 +260,16 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A .db .set_proxy_flags_sync(app_type_str, true, true)?; + // 3.1) 立即切到队列 P1(热切换:不写 Live,仅更新 DB/settings/备份) + if let Err(e) = futures::executor::block_on( + proxy_service.switch_proxy_target(app_type_str, &p1_provider_id), + ) { + log::error!("[Tray] Auto 模式切换到队列 P1 失败: {e}"); + return Err(AppError::Message(format!( + "Auto 模式切换到队列 P1 失败: {e}" + ))); + } + // 4) 更新托盘菜单 if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) { if let Some(tray) = app.tray_by_id("main") { @@ -249,7 +281,8 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A let event_data = serde_json::json!({ "appType": app_type_str, "proxyEnabled": true, - "autoFailoverEnabled": true + "autoFailoverEnabled": true, + "providerId": p1_provider_id }); if let Err(e) = app.emit("proxy-flags-changed", event_data.clone()) { log::error!("发射 proxy-flags-changed 事件失败: {e}"); diff --git a/src/components/proxy/FailoverQueueManager.tsx b/src/components/proxy/FailoverQueueManager.tsx index 5dc7120a2..9d56475dc 100644 --- a/src/components/proxy/FailoverQueueManager.tsx +++ b/src/components/proxy/FailoverQueueManager.tsx @@ -140,7 +140,8 @@ export function FailoverQueueManager({

{t("proxy.failover.autoSwitchDescription", { - defaultValue: "开启后,请求失败时自动切换到队列中的下一个供应商", + defaultValue: + "开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商", })}

diff --git a/src/components/proxy/FailoverToggle.tsx b/src/components/proxy/FailoverToggle.tsx index 9ec4c0222..9e6e23121 100644 --- a/src/components/proxy/FailoverToggle.tsx +++ b/src/components/proxy/FailoverToggle.tsx @@ -39,11 +39,11 @@ export function FailoverToggle({ className, activeApp }: FailoverToggleProps) { const tooltipText = isEnabled ? t("failover.tooltip.enabled", { app: appLabel, - defaultValue: `${appLabel} 故障转移已启用\n自动切换到下一个可用供应商`, + defaultValue: `${appLabel} 故障转移已启用\n按队列优先级(P1→P2→...)选择供应商`, }) : t("failover.tooltip.disabled", { app: appLabel, - defaultValue: `启用 ${appLabel} 故障转移\n当当前供应商失败时自动切换`, + defaultValue: `启用 ${appLabel} 故障转移\n将立即切换到队列 P1,并在失败时自动切换到下一个`, }); return ( diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index 84e5858b0..eda4692a8 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -50,8 +50,8 @@ export function ProxyPanel() { } }, [globalConfig]); - // 获取所有三个应用类型的故障转移队列(不包含当前供应商) - // 当前供应商始终优先,队列仅用于失败后的备用顺序 + // 获取所有三个应用类型的故障转移队列 + // 启用自动故障转移后,将按队列优先级(P1→P2→...)选择供应商 const { data: claudeQueue = [] } = useFailoverQueue("claude"); const { data: codexQueue = [] } = useFailoverQueue("codex"); const { data: geminiQueue = [] } = useFailoverQueue("gemini"); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index bfccd61cc..63568863d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1145,8 +1145,8 @@ "tooltip": "Failover priority {{priority}}" }, "tooltip": { - "enabled": "{{app}} failover enabled\nAutomatically switch to next available provider", - "disabled": "Enable {{app}} failover\nAutomatically switch when current provider fails" + "enabled": "{{app}} failover enabled\nRequests follow queue priority (P1→P2→...)", + "disabled": "Enable {{app}} failover\nSwitches to queue P1 immediately, then falls back on failures" } }, "proxy": { @@ -1248,15 +1248,18 @@ }, "switchFailed": "Switch failed: {{error}}", "failover": { - "proxyRequired": "Proxy service must be started to configure failover" + "proxyRequired": "Proxy service must be started to configure failover", + "autoSwitch": "Auto Failover", + "autoSwitchDescription": "When enabled, switches to queue P1 immediately and automatically tries the next provider in the queue on failures" }, "failoverQueue": { "title": "Failover Queue", "description": "Manage failover order for each app's providers", - "info": "The current active provider always takes priority. When requests fail, the system will try other providers in queue order.", + "info": "When auto failover is enabled, requests follow the queue priority order (P1 first). On failures, the system will try the next provider in the queue.", "selectProvider": "Select a provider to add to queue", "noAvailableProviders": "No providers available to add", "empty": "Failover queue is empty. Add providers to enable automatic failover.", + "orderHint": "Queue order matches the provider list order on the Home page. Reorder providers on the Home page to change priority.", "dragHint": "Drag providers to adjust failover order. Lower numbers have higher priority.", "toggleEnabled": "Enable/Disable", "addSuccess": "Added to failover queue", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 7784a9104..29172b74d 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1145,8 +1145,8 @@ "tooltip": "フェイルオーバー優先度 {{priority}}" }, "tooltip": { - "enabled": "{{app}} フェイルオーバーが有効\n次の利用可能なプロバイダーに自動切り替え", - "disabled": "{{app}} フェイルオーバーを有効にする\n現在のプロバイダーが失敗した場合に自動切り替え" + "enabled": "{{app}} フェイルオーバーが有効\nキューの優先度(P1→P2→...)で使用します", + "disabled": "{{app}} フェイルオーバーを有効にする\nキューの P1 に即時切替し、失敗時は次を順に試行します" } }, "proxy": { @@ -1243,15 +1243,18 @@ }, "switchFailed": "切り替えに失敗しました: {{error}}", "failover": { - "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります" + "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります", + "autoSwitch": "自動フェイルオーバー", + "autoSwitchDescription": "有効にするとキューの P1 に即時切り替え、リクエスト失敗時はキュー内の次のプロバイダーを自動で試行します" }, "failoverQueue": { "title": "フェイルオーバーキュー", "description": "各アプリのプロバイダーのフェイルオーバー順序を管理します", - "info": "現在アクティブなプロバイダーが常に優先されます。リクエストが失敗すると、システムはキュー順に他のプロバイダーを試行します。", + "info": "自動フェイルオーバーを有効にすると、キューの優先度順(P1 優先)でプロバイダーを使用します。失敗時はキュー内の次のプロバイダーを順に試行します。", "selectProvider": "キューに追加するプロバイダーを選択", "noAvailableProviders": "追加できるプロバイダーがありません", "empty": "フェイルオーバーキューが空です。自動フェイルオーバーを有効にするにはプロバイダーを追加してください。", + "orderHint": "キューの順序はホームのプロバイダー一覧の順序と一致します。ホームでドラッグして順序を変更できます。", "dragHint": "ドラッグでフェイルオーバー順序を調整します。番号が小さいほど優先度が高くなります。", "toggleEnabled": "有効/無効", "addSuccess": "フェイルオーバーキューに追加しました", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d36f1638a..8c9bb049b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1145,8 +1145,8 @@ "tooltip": "故障转移优先级 {{priority}}" }, "tooltip": { - "enabled": "{{app}} 故障转移已启用\n自动切换到下一个可用供应商", - "disabled": "启用 {{app}} 故障转移\n当当前供应商失败时自动切换" + "enabled": "{{app}} 故障转移已启用\n按队列优先级(P1→P2→...)选择供应商", + "disabled": "启用 {{app}} 故障转移\n将立即切换到队列 P1,并在失败时自动切换到下一个" } }, "proxy": { @@ -1248,15 +1248,18 @@ }, "switchFailed": "切换失败: {{error}}", "failover": { - "proxyRequired": "需要先启动代理服务才能配置故障转移" + "proxyRequired": "需要先启动代理服务才能配置故障转移", + "autoSwitch": "自动故障转移", + "autoSwitchDescription": "开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商" }, "failoverQueue": { "title": "故障转移队列", "description": "管理各应用的供应商故障转移顺序", - "info": "当前激活的供应商始终优先。当请求失败时,系统会按队列顺序依次尝试其他供应商。", + "info": "启用自动故障转移后,将按队列优先级选择供应商(P1 优先)。当请求失败时,系统会按队列顺序依次尝试下一个供应商。", "selectProvider": "选择供应商添加到队列", "noAvailableProviders": "没有可添加的供应商", "empty": "故障转移队列为空。添加供应商以启用自动故障转移。", + "orderHint": "队列顺序与首页供应商列表顺序一致,可在首页拖拽调整顺序。", "dragHint": "拖拽供应商可调整故障转移顺序,序号越小优先级越高。", "toggleEnabled": "启用/禁用", "addSuccess": "已添加到故障转移队列", diff --git a/src/lib/query/failover.ts b/src/lib/query/failover.ts index 10049eb80..a2a8be7c8 100644 --- a/src/lib/query/failover.ts +++ b/src/lib/query/failover.ts @@ -268,6 +268,21 @@ export function useSetAutoFailoverEnabled() { queryClient.invalidateQueries({ queryKey: ["autoFailoverEnabled", variables.appType], }); + // 启用/关闭故障转移可能触发: + // - 立即切到队列 P1(当前供应商变化) + // - 队列为空时自动把当前供应商加入队列(队列内容变化) + queryClient.invalidateQueries({ + queryKey: ["failoverQueue", variables.appType], + }); + queryClient.invalidateQueries({ + queryKey: ["availableProvidersForFailover", variables.appType], + }); + queryClient.invalidateQueries({ + queryKey: ["providers", variables.appType], + }); + queryClient.invalidateQueries({ + queryKey: ["proxyStatus"], + }); }, }); }