mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
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
This commit is contained in:
@@ -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") {
|
||||
|
||||
@@ -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<Vec<Provider>, 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<String> = 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<String>,
|
||||
original_userprofile: Option<String>,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+34
-1
@@ -211,6 +211,28 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A
|
||||
if let Some(app_state) = app.try_state::<AppState>() {
|
||||
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}");
|
||||
|
||||
@@ -140,7 +140,8 @@ export function FailoverQueueManager({
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("proxy.failover.autoSwitchDescription", {
|
||||
defaultValue: "开启后,请求失败时自动切换到队列中的下一个供应商",
|
||||
defaultValue:
|
||||
"开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "フェイルオーバーキューに追加しました",
|
||||
|
||||
@@ -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": "已添加到故障转移队列",
|
||||
|
||||
@@ -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"],
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user