From f6e8d656cebc57bfbb6dc1733f04125410119e2d Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 22 Dec 2025 16:41:34 +0800 Subject: [PATCH] refactor(failover): merge failover_queue table into providers - Add in_failover_queue field to providers table - Remove standalone failover_queue table and related indexes - Simplify queue ordering by reusing sort_index field - Remove reorder_failover_queue and set_failover_item_enabled commands - Update frontend to use simplified FailoverQueueItem type --- src-tauri/src/commands/failover.rs | 28 +-- src-tauri/src/commands/proxy.rs | 12 +- src-tauri/src/commands/stream_check.rs | 4 +- src-tauri/src/database/dao/failover.rs | 177 +++------------ src-tauri/src/database/dao/providers.rs | 30 ++- src-tauri/src/database/schema.rs | 49 +++-- src-tauri/src/database/tests.rs | 1 + src-tauri/src/deeplink/provider.rs | 1 + src-tauri/src/lib.rs | 2 - src-tauri/src/provider.rs | 5 + src-tauri/src/proxy/provider_router.rs | 65 +++--- src-tauri/src/proxy/providers/claude.rs | 1 + src-tauri/src/proxy/providers/codex.rs | 1 + src-tauri/src/proxy/providers/gemini.rs | 1 + src-tauri/src/proxy/providers/mod.rs | 1 + src-tauri/src/proxy/providers/transform.rs | 1 + src/components/providers/ProviderList.tsx | 53 +---- src/components/proxy/FailoverQueueManager.tsx | 201 +++--------------- src/components/proxy/ProxyPanel.tsx | 33 ++- src/lib/api/failover.ts | 21 -- src/lib/query/failover.ts | 89 +------- src/types.ts | 2 + src/types/proxy.ts | 4 +- 23 files changed, 172 insertions(+), 610 deletions(-) diff --git a/src-tauri/src/commands/failover.rs b/src-tauri/src/commands/failover.rs index cd8b9a8f2..83558514c 100644 --- a/src-tauri/src/commands/failover.rs +++ b/src-tauri/src/commands/failover.rs @@ -1,6 +1,6 @@ //! 故障转移队列命令 //! -//! 管理代理模式下的故障转移队列 +//! 管理代理模式下的故障转移队列(基于 providers 表的 in_failover_queue 字段) use crate::database::FailoverQueueItem; use crate::provider::Provider; @@ -56,32 +56,6 @@ pub async fn remove_from_failover_queue( .map_err(|e| e.to_string()) } -/// 重新排序故障转移队列 -#[tauri::command] -pub async fn reorder_failover_queue( - state: tauri::State<'_, AppState>, - app_type: String, - provider_ids: Vec, -) -> Result<(), String> { - state - .db - .reorder_failover_queue(&app_type, &provider_ids) - .map_err(|e| e.to_string()) -} - -/// 设置故障转移队列项的启用状态 -#[tauri::command] -pub async fn set_failover_item_enabled( - state: tauri::State<'_, AppState>, - app_type: String, - provider_id: String, - enabled: bool, -) -> Result<(), String> { - state - .db - .set_failover_item_enabled(&app_type, &provider_id, enabled) - .map_err(|e| e.to_string()) -} /// 获取指定应用的自动故障转移开关状态 #[tauri::command] pub async fn get_auto_failover_enabled( diff --git a/src-tauri/src/commands/proxy.rs b/src-tauri/src/commands/proxy.rs index 918e1ea81..27ce31c00 100644 --- a/src-tauri/src/commands/proxy.rs +++ b/src-tauri/src/commands/proxy.rs @@ -156,18 +156,18 @@ pub async fn reset_circuit_breaker( .get_failover_queue(&app_type) .map_err(|e| e.to_string())?; - // 找到恢复的供应商和当前供应商在队列中的位置 + // 找到恢复的供应商和当前供应商在队列中的位置(使用 sort_index) let restored_order = queue .iter() - .find(|item| item.provider_id == provider_id && item.enabled) - .map(|item| item.queue_order); + .find(|item| item.provider_id == provider_id) + .and_then(|item| item.sort_index); let current_order = queue .iter() - .find(|item| item.provider_id == current_id && item.enabled) - .map(|item| item.queue_order); + .find(|item| item.provider_id == current_id) + .and_then(|item| item.sort_index); - // 如果恢复的供应商优先级更高(queue_order 更小),则切换 + // 如果恢复的供应商优先级更高(sort_index 更小),则切换 if let (Some(restored), Some(current)) = (restored_order, current_order) { if restored < current { log::info!( diff --git a/src-tauri/src/commands/stream_check.rs b/src-tauri/src/commands/stream_check.rs index 13ed33074..218cdfab5 100644 --- a/src-tauri/src/commands/stream_check.rs +++ b/src-tauri/src/commands/stream_check.rs @@ -52,9 +52,7 @@ pub async fn stream_check_all_providers( } if let Ok(queue) = state.db.get_failover_queue(app_type.as_str()) { for item in queue { - if item.enabled { - ids.insert(item.provider_id); - } + ids.insert(item.provider_id); } } Some(ids) diff --git a/src-tauri/src/database/dao/failover.rs b/src-tauri/src/database/dao/failover.rs index 35009ea4c..e9cc31e65 100644 --- a/src-tauri/src/database/dao/failover.rs +++ b/src-tauri/src/database/dao/failover.rs @@ -1,36 +1,32 @@ //! 故障转移队列 DAO //! -//! 管理代理模式下的故障转移队列 +//! 管理代理模式下的故障转移队列(基于 providers 表的 in_failover_queue 字段) use crate::database::{lock_conn, Database}; use crate::error::AppError; use crate::provider::Provider; use serde::{Deserialize, Serialize}; -use std::time::{SystemTime, UNIX_EPOCH}; -/// 故障转移队列条目 +/// 故障转移队列条目(简化版,用于前端展示) #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct FailoverQueueItem { pub provider_id: String, pub provider_name: String, - pub queue_order: i32, - pub enabled: bool, - pub created_at: i64, + pub sort_index: Option, } impl Database { - /// 获取故障转移队列(按 queue_order 排序) + /// 获取故障转移队列(按 sort_index 排序) pub fn get_failover_queue(&self, app_type: &str) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn .prepare( - "SELECT fq.provider_id, p.name, fq.queue_order, fq.enabled, fq.created_at - FROM failover_queue fq - JOIN providers p ON fq.provider_id = p.id AND fq.app_type = p.app_type - WHERE fq.app_type = ?1 - ORDER BY fq.queue_order ASC", + "SELECT id, name, sort_index + FROM providers + WHERE app_type = ?1 AND in_failover_queue = 1 + ORDER BY COALESCE(sort_index, 999999), id ASC", ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -39,9 +35,7 @@ impl Database { Ok(FailoverQueueItem { provider_id: row.get(0)?, provider_name: row.get(1)?, - queue_order: row.get(2)?, - enabled: row.get(3)?, - created_at: row.get(4)?, + sort_index: row.get(2)?, }) }) .map_err(|e| AppError::Database(e.to_string()))? @@ -53,43 +47,23 @@ impl Database { /// 获取故障转移队列中的供应商(完整 Provider 信息,按顺序) pub fn get_failover_providers(&self, app_type: &str) -> Result, AppError> { - let queue = self.get_failover_queue(app_type)?; let all_providers = self.get_all_providers(app_type)?; - let mut result = Vec::new(); - for item in queue { - if item.enabled { - if let Some(provider) = all_providers.get(&item.provider_id) { - result.push(provider.clone()); - } - } - } + let result: Vec = all_providers + .into_values() + .filter(|p| p.in_failover_queue) + .collect(); Ok(result) } - /// 添加供应商到故障转移队列末尾 + /// 添加供应商到故障转移队列 pub fn add_to_failover_queue(&self, app_type: &str, provider_id: &str) -> Result<(), AppError> { let conn = lock_conn!(self.conn); - // 获取当前最大 queue_order - let max_order: i32 = conn - .query_row( - "SELECT COALESCE(MAX(queue_order), 0) FROM failover_queue WHERE app_type = ?1", - [app_type], - |row| row.get(0), - ) - .map_err(|e| AppError::Database(e.to_string()))?; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs() as i64; - conn.execute( - "INSERT OR IGNORE INTO failover_queue (app_type, provider_id, queue_order, enabled, created_at) - VALUES (?1, ?2, ?3, 1, ?4)", - rusqlite::params![app_type, provider_id, max_order + 1, now], + "UPDATE providers SET in_failover_queue = 1 WHERE id = ?1 AND app_type = ?2", + rusqlite::params![provider_id, app_type], ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -104,104 +78,12 @@ impl Database { ) -> Result<(), AppError> { let conn = lock_conn!(self.conn); - // 获取被删除项的 queue_order - let removed_order: Option = conn - .query_row( - "SELECT queue_order FROM failover_queue WHERE app_type = ?1 AND provider_id = ?2", - [app_type, provider_id], - |row| row.get(0), - ) - .ok(); - - // 删除该项 conn.execute( - "DELETE FROM failover_queue WHERE app_type = ?1 AND provider_id = ?2", - [app_type, provider_id], + "UPDATE providers SET in_failover_queue = 0 WHERE id = ?1 AND app_type = ?2", + rusqlite::params![provider_id, app_type], ) .map_err(|e| AppError::Database(e.to_string()))?; - // 重新排序后面的项(填补空隙) - if let Some(order) = removed_order { - conn.execute( - "UPDATE failover_queue - SET queue_order = queue_order - 1 - WHERE app_type = ?1 AND queue_order > ?2", - rusqlite::params![app_type, order], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - } - - Ok(()) - } - - /// 重新排序故障转移队列 - /// provider_ids: 按新顺序排列的 provider_id 列表 - pub fn reorder_failover_queue( - &self, - app_type: &str, - provider_ids: &[String], - ) -> Result<(), AppError> { - let conn = lock_conn!(self.conn); - - // 使用事务确保原子性 - conn.execute("BEGIN TRANSACTION", []) - .map_err(|e| AppError::Database(e.to_string()))?; - - let result = (|| { - for (index, provider_id) in provider_ids.iter().enumerate() { - conn.execute( - "UPDATE failover_queue - SET queue_order = ?3 - WHERE app_type = ?1 AND provider_id = ?2", - rusqlite::params![app_type, provider_id, (index + 1) as i32], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - } - Ok(()) - })(); - - match result { - Ok(_) => { - conn.execute("COMMIT", []) - .map_err(|e| AppError::Database(e.to_string()))?; - Ok(()) - } - Err(e) => { - conn.execute("ROLLBACK", []).ok(); - Err(e) - } - } - } - - /// 设置故障转移队列中供应商的启用状态 - pub fn set_failover_item_enabled( - &self, - app_type: &str, - provider_id: &str, - enabled: bool, - ) -> Result<(), AppError> { - let conn = lock_conn!(self.conn); - - let rows_affected = conn - .execute( - "UPDATE failover_queue SET enabled = ?3 WHERE app_type = ?1 AND provider_id = ?2", - rusqlite::params![app_type, provider_id, enabled], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - - if rows_affected == 0 { - log::warn!( - "set_failover_item_enabled: 未找到匹配记录 app_type={app_type}, provider_id={provider_id}" - ); - return Err(AppError::Database(format!( - "未找到故障转移队列项: app_type={app_type}, provider_id={provider_id}" - ))); - } - - log::info!( - "set_failover_item_enabled: 已更新 app_type={app_type}, provider_id={provider_id}, enabled={enabled}" - ); - Ok(()) } @@ -209,8 +91,11 @@ impl Database { pub fn clear_failover_queue(&self, app_type: &str) -> Result<(), AppError> { let conn = lock_conn!(self.conn); - conn.execute("DELETE FROM failover_queue WHERE app_type = ?1", [app_type]) - .map_err(|e| AppError::Database(e.to_string()))?; + conn.execute( + "UPDATE providers SET in_failover_queue = 0 WHERE app_type = ?1", + [app_type], + ) + .map_err(|e| AppError::Database(e.to_string()))?; Ok(()) } @@ -223,15 +108,15 @@ impl Database { ) -> Result { let conn = lock_conn!(self.conn); - let count: i32 = conn + let in_queue: bool = conn .query_row( - "SELECT COUNT(*) FROM failover_queue WHERE app_type = ?1 AND provider_id = ?2", - [app_type, provider_id], + "SELECT in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2", + rusqlite::params![provider_id, app_type], |row| row.get(0), ) - .map_err(|e| AppError::Database(e.to_string()))?; + .unwrap_or(false); - Ok(count > 0) + Ok(in_queue) } /// 获取可添加到故障转移队列的供应商(不在队列中的) @@ -240,14 +125,10 @@ impl Database { app_type: &str, ) -> Result, AppError> { let all_providers = self.get_all_providers(app_type)?; - let queue = self.get_failover_queue(app_type)?; - - let queue_ids: std::collections::HashSet<_> = - queue.iter().map(|item| &item.provider_id).collect(); let available: Vec = all_providers .into_values() - .filter(|p| !queue_ids.contains(&p.id)) + .filter(|p| !p.in_failover_queue) .collect(); Ok(available) diff --git a/src-tauri/src/database/dao/providers.rs b/src-tauri/src/database/dao/providers.rs index 1ba3683c5..7a78edc48 100644 --- a/src-tauri/src/database/dao/providers.rs +++ b/src-tauri/src/database/dao/providers.rs @@ -17,7 +17,7 @@ impl Database { ) -> Result, AppError> { let conn = lock_conn!(self.conn); let mut stmt = conn.prepare( - "SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta + "SELECT id, name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue FROM providers WHERE app_type = ?1 ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC" ).map_err(|e| AppError::Database(e.to_string()))?; @@ -35,6 +35,7 @@ impl Database { let icon: Option = row.get(8)?; let icon_color: Option = row.get(9)?; let meta_str: String = row.get(10)?; + let in_failover_queue: bool = row.get(11)?; let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null); @@ -54,6 +55,7 @@ impl Database { meta: Some(meta), icon, icon_color, + in_failover_queue, }, )) }) @@ -129,7 +131,7 @@ impl Database { ) -> Result, AppError> { let conn = lock_conn!(self.conn); let result = conn.query_row( - "SELECT name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta + "SELECT name, settings_config, website_url, category, created_at, sort_index, notes, icon, icon_color, meta, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2", params![id, app_type], |row| { @@ -143,6 +145,7 @@ impl Database { let icon: Option = row.get(7)?; let icon_color: Option = row.get(8)?; let meta_str: String = row.get(9)?; + let in_failover_queue: bool = row.get(10)?; let settings_config = serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null); let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default(); @@ -159,6 +162,7 @@ impl Database { meta: Some(meta), icon, icon_color, + in_failover_queue, }) }, ); @@ -184,17 +188,18 @@ impl Database { let mut meta_clone = provider.meta.clone().unwrap_or_default(); let endpoints = std::mem::take(&mut meta_clone.custom_endpoints); - // 检查是否存在(用于判断新增/更新,以及保留 is_current) - let existing: Option = tx + // 检查是否存在(用于判断新增/更新,以及保留 is_current 和 in_failover_queue) + let existing: Option<(bool, bool)> = tx .query_row( - "SELECT is_current FROM providers WHERE id = ?1 AND app_type = ?2", + "SELECT is_current, in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2", params![provider.id, app_type], - |row| row.get(0), + |row| Ok((row.get(0)?, row.get(1)?)), ) .ok(); let is_update = existing.is_some(); - let is_current = existing.unwrap_or(false); + let (is_current, in_failover_queue) = + existing.unwrap_or((false, provider.in_failover_queue)); if is_update { // 更新模式:使用 UPDATE 避免触发 ON DELETE CASCADE @@ -210,8 +215,9 @@ impl Database { icon = ?8, icon_color = ?9, meta = ?10, - is_current = ?11 - WHERE id = ?12 AND app_type = ?13", + is_current = ?11, + in_failover_queue = ?12 + WHERE id = ?13 AND app_type = ?14", params![ provider.name, serde_json::to_string(&provider.settings_config).unwrap(), @@ -224,6 +230,7 @@ impl Database { provider.icon_color, serde_json::to_string(&meta_clone).unwrap(), is_current, + in_failover_queue, provider.id, app_type, ], @@ -234,8 +241,8 @@ impl Database { tx.execute( "INSERT INTO providers ( id, app_type, name, settings_config, website_url, category, - created_at, sort_index, notes, icon, icon_color, meta, is_current - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + created_at, sort_index, notes, icon, icon_color, meta, is_current, in_failover_queue + ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", params![ provider.id, app_type, @@ -250,6 +257,7 @@ impl Database { provider.icon_color, serde_json::to_string(&meta_clone).unwrap(), is_current, + in_failover_queue, ], ) .map_err(|e| AppError::Database(e.to_string()))?; diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 134397a40..2321e4a26 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -31,6 +31,7 @@ impl Database { icon_color TEXT, meta TEXT NOT NULL DEFAULT '{}', is_current BOOLEAN NOT NULL DEFAULT 0, + in_failover_queue BOOLEAN NOT NULL DEFAULT 0, PRIMARY KEY (id, app_type) )", [], @@ -312,29 +313,13 @@ impl Database { [], ); - // 14. Failover Queue 表 (故障转移队列) - conn.execute( - "CREATE TABLE IF NOT EXISTS failover_queue ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - app_type TEXT NOT NULL, - provider_id TEXT NOT NULL, - queue_order INTEGER NOT NULL, - enabled INTEGER NOT NULL DEFAULT 1, - created_at INTEGER NOT NULL, - UNIQUE (app_type, provider_id), - FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE - )", + // 为故障转移队列创建索引(基于 providers 表) + // 注意:此索引依赖 in_failover_queue 列,对于旧数据库会在迁移时创建 + let _ = conn.execute( + "CREATE INDEX IF NOT EXISTS idx_providers_failover + ON providers(app_type, in_failover_queue, sort_index)", [], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - - // 为故障转移队列创建索引 - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_failover_queue_order - ON failover_queue(app_type, queue_order)", - [], - ) - .map_err(|e| AppError::Database(e.to_string()))?; + ); Ok(()) } @@ -472,6 +457,26 @@ impl Database { Self::add_column_if_missing(conn, "providers", "limit_daily_usd", "TEXT")?; Self::add_column_if_missing(conn, "providers", "limit_monthly_usd", "TEXT")?; Self::add_column_if_missing(conn, "providers", "provider_type", "TEXT")?; + Self::add_column_if_missing( + conn, + "providers", + "in_failover_queue", + "BOOLEAN NOT NULL DEFAULT 0", + )?; + + // 删除旧的 failover_queue 表(如果存在) + conn.execute("DROP INDEX IF EXISTS idx_failover_queue_order", []) + .map_err(|e| AppError::Database(format!("删除 failover_queue 索引失败: {e}")))?; + conn.execute("DROP TABLE IF EXISTS failover_queue", []) + .map_err(|e| AppError::Database(format!("删除 failover_queue 表失败: {e}")))?; + + // 创建 failover 索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_providers_failover + ON providers(app_type, in_failover_queue, sort_index)", + [], + ) + .map_err(|e| AppError::Database(format!("创建 failover 索引失败: {e}")))?; // proxy_request_logs 表(包含所有字段) conn.execute( diff --git a/src-tauri/src/database/tests.rs b/src-tauri/src/database/tests.rs index 44bf304ac..74172fe58 100644 --- a/src-tauri/src/database/tests.rs +++ b/src-tauri/src/database/tests.rs @@ -245,6 +245,7 @@ fn dry_run_validates_schema_compatibility() { meta: None, icon: None, icon_color: None, + in_failover_queue: false, }, ); diff --git a/src-tauri/src/deeplink/provider.rs b/src-tauri/src/deeplink/provider.rs index 76fce86c1..79ceb3aff 100644 --- a/src-tauri/src/deeplink/provider.rs +++ b/src-tauri/src/deeplink/provider.rs @@ -132,6 +132,7 @@ pub(crate) fn build_provider_from_request( meta, icon: request.icon.clone(), icon_color: None, + in_failover_queue: false, }; Ok(provider) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3c1453484..8f536b16e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -708,8 +708,6 @@ pub fn run() { commands::get_available_providers_for_failover, commands::add_to_failover_queue, commands::remove_from_failover_queue, - commands::reorder_failover_queue, - commands::set_failover_item_enabled, commands::get_auto_failover_enabled, commands::set_auto_failover_enabled, // Usage statistics diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 966b284c5..8c082fe68 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -36,6 +36,10 @@ pub struct Provider { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "iconColor")] pub icon_color: Option, + /// 是否加入故障转移队列 + #[serde(default)] + #[serde(rename = "inFailoverQueue")] + pub in_failover_queue: bool, } impl Provider { @@ -58,6 +62,7 @@ impl Provider { meta: None, icon: None, icon_color: None, + in_failover_queue: false, } } } diff --git a/src-tauri/src/proxy/provider_router.rs b/src-tauri/src/proxy/provider_router.rs index 6fba778ba..04eead5f3 100644 --- a/src-tauri/src/proxy/provider_router.rs +++ b/src-tauri/src/proxy/provider_router.rs @@ -34,7 +34,6 @@ impl ProviderRouter { /// - 故障转移开启时:完全按照故障转移队列顺序返回,忽略当前供应商设置 pub async fn select_providers(&self, app_type: &str) -> Result, AppError> { let mut result = Vec::new(); - let all_providers = self.db.get_all_providers(app_type)?; // 检查该应用的自动故障转移开关是否开启 let failover_key = format!("auto_failover_enabled_{app_type}"); @@ -61,42 +60,34 @@ impl ProviderRouter { }; if auto_failover_enabled { - // 故障转移开启:完全按照队列顺序,忽略当前供应商 - let queue = self.db.get_failover_queue(app_type)?; + // 故障转移开启:使用 in_failover_queue 标记的供应商,按 sort_index 排序 + let failover_providers = self.db.get_failover_providers(app_type)?; log::info!( "[{}] Failover enabled, using queue order ({} items)", app_type, - queue.len() + failover_providers.len() ); - for item in queue { - // 跳过禁用的队列项 - if !item.enabled { - continue; - } + for provider in failover_providers { + // 检查熔断器状态 + let circuit_key = format!("{}:{}", app_type, provider.id); + let breaker = self.get_or_create_circuit_breaker(&circuit_key).await; - // 获取供应商信息 - if let Some(provider) = all_providers.get(&item.provider_id) { - // 检查熔断器状态 - let circuit_key = format!("{}:{}", app_type, provider.id); - let breaker = self.get_or_create_circuit_breaker(&circuit_key).await; - - if breaker.is_available().await { - log::info!( - "[{}] Queue provider available: {} ({}) at position {}", - app_type, - provider.name, - provider.id, - item.queue_order - ); - result.push(provider.clone()); - } else { - log::debug!( - "[{}] Queue provider {} circuit breaker open, skipping", - app_type, - provider.name - ); - } + if breaker.is_available().await { + log::info!( + "[{}] Queue provider available: {} ({}) at sort_index {:?}", + app_type, + provider.name, + provider.id, + provider.sort_index + ); + result.push(provider); + } else { + log::debug!( + "[{}] Queue provider {} circuit breaker open, skipping", + app_type, + provider.name + ); } } } else { @@ -104,7 +95,7 @@ impl ProviderRouter { log::info!("[{app_type}] Failover disabled, using current provider only"); if let Some(current_id) = self.db.get_current_provider(app_type)? { - if let Some(current) = all_providers.get(¤t_id) { + if let Some(current) = self.db.get_provider_by_id(¤t_id, app_type)? { let circuit_key = format!("{}:{}", app_type, current.id); let breaker = self.get_or_create_circuit_breaker(&circuit_key).await; @@ -115,7 +106,7 @@ impl ProviderRouter { current.name, current.id ); - result.push(current.clone()); + result.push(current); } else { log::warn!( "[{}] Current provider {} circuit breaker open", @@ -320,10 +311,13 @@ mod tests { async fn test_failover_enabled_uses_queue_order() { let db = Arc::new(Database::memory().unwrap()); - let provider_a = + // 设置 sort_index 来控制顺序:b=1, a=2 + let mut provider_a = Provider::with_id("a".to_string(), "Provider A".to_string(), json!({}), None); - let provider_b = + provider_a.sort_index = Some(2); + 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(); @@ -338,6 +332,7 @@ 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"); } diff --git a/src-tauri/src/proxy/providers/claude.rs b/src-tauri/src/proxy/providers/claude.rs index ff7a55921..ba6254252 100644 --- a/src-tauri/src/proxy/providers/claude.rs +++ b/src-tauri/src/proxy/providers/claude.rs @@ -270,6 +270,7 @@ mod tests { meta: None, icon: None, icon_color: None, + in_failover_queue: false, } } diff --git a/src-tauri/src/proxy/providers/codex.rs b/src-tauri/src/proxy/providers/codex.rs index 22c33c75a..d60ee0d6c 100644 --- a/src-tauri/src/proxy/providers/codex.rs +++ b/src-tauri/src/proxy/providers/codex.rs @@ -174,6 +174,7 @@ mod tests { meta: None, icon: None, icon_color: None, + in_failover_queue: false, } } diff --git a/src-tauri/src/proxy/providers/gemini.rs b/src-tauri/src/proxy/providers/gemini.rs index 1b4ef3593..646d237f7 100644 --- a/src-tauri/src/proxy/providers/gemini.rs +++ b/src-tauri/src/proxy/providers/gemini.rs @@ -250,6 +250,7 @@ mod tests { meta: None, icon: None, icon_color: None, + in_failover_queue: false, } } diff --git a/src-tauri/src/proxy/providers/mod.rs b/src-tauri/src/proxy/providers/mod.rs index ee59c0078..64ae8ff27 100644 --- a/src-tauri/src/proxy/providers/mod.rs +++ b/src-tauri/src/proxy/providers/mod.rs @@ -209,6 +209,7 @@ mod tests { meta: None, icon: None, icon_color: None, + in_failover_queue: false, } } diff --git a/src-tauri/src/proxy/providers/transform.rs b/src-tauri/src/proxy/providers/transform.rs index 141506fd5..1efbb0956 100644 --- a/src-tauri/src/proxy/providers/transform.rs +++ b/src-tauri/src/proxy/providers/transform.rs @@ -394,6 +394,7 @@ mod tests { meta: None, icon: None, icon_color: None, + in_failover_queue: false, } } diff --git a/src/components/providers/ProviderList.tsx b/src/components/providers/ProviderList.tsx index 443fd1f02..7f64e9065 100644 --- a/src/components/providers/ProviderList.tsx +++ b/src/components/providers/ProviderList.tsx @@ -17,9 +17,8 @@ import { useFailoverQueue, useAddToFailoverQueue, useRemoveFromFailoverQueue, - useReorderFailoverQueue, } from "@/lib/query/failover"; -import { useCallback, useEffect, useRef } from "react"; +import { useCallback } from "react"; interface ProviderListProps { providers: Record; @@ -67,22 +66,16 @@ export function ProviderList({ const { data: failoverQueue } = useFailoverQueue(appId); const addToQueue = useAddToFailoverQueue(); const removeFromQueue = useRemoveFromFailoverQueue(); - const reorderQueue = useReorderFailoverQueue(); // 联动状态:只有当前应用开启代理接管且故障转移开启时才启用故障转移模式 const isFailoverModeActive = isProxyTakeover === true && isAutoFailoverEnabled === true; - // 防止重复调用的 ref - const lastReorderRef = useRef(""); - - // 计算供应商在故障转移队列中的优先级 + // 计算供应商在故障转移队列中的优先级(基于 sortIndex 排序) const getFailoverPriority = useCallback( (providerId: string): number | undefined => { if (!isFailoverModeActive || !failoverQueue) return undefined; - // 只计算已启用的供应商的优先级 - const enabledQueue = failoverQueue.filter((item) => item.enabled); - const index = enabledQueue.findIndex( + const index = failoverQueue.findIndex( (item) => item.providerId === providerId, ); return index >= 0 ? index + 1 : undefined; @@ -94,9 +87,7 @@ export function ProviderList({ const isInFailoverQueue = useCallback( (providerId: string): boolean => { if (!isFailoverModeActive || !failoverQueue) return false; - return failoverQueue.some( - (item) => item.providerId === providerId && item.enabled, - ); + return failoverQueue.some((item) => item.providerId === providerId); }, [isFailoverModeActive, failoverQueue], ); @@ -113,42 +104,6 @@ export function ProviderList({ [appId, addToQueue, removeFromQueue], ); - // 当拖拽排序后,同步故障转移队列顺序 - useEffect(() => { - if (!isFailoverModeActive || !failoverQueue || failoverQueue.length === 0) - return; - - // 获取当前在队列中且已启用的供应商 ID 列表(按显示顺序) - const enabledProviderIds = sortedProviders - .filter((p) => isInFailoverQueue(p.id)) - .map((p) => p.id); - - if (enabledProviderIds.length === 0) return; - - // 生成唯一标识防止重复调用 - const orderKey = enabledProviderIds.join(","); - if (orderKey === lastReorderRef.current) return; - - // 检查顺序是否需要更新 - const currentOrder = failoverQueue - .filter((item) => item.enabled) - .sort((a, b) => a.queueOrder - b.queueOrder) - .map((item) => item.providerId) - .join(","); - - if (orderKey !== currentOrder) { - lastReorderRef.current = orderKey; - reorderQueue.mutate({ appType: appId, providerIds: enabledProviderIds }); - } - }, [ - sortedProviders, - isFailoverModeActive, - failoverQueue, - isInFailoverQueue, - appId, - reorderQueue, - ]); - const handleTest = (provider: Provider) => { checkProvider(provider.id, provider.name); }; diff --git a/src/components/proxy/FailoverQueueManager.tsx b/src/components/proxy/FailoverQueueManager.tsx index dbc6434fd..5dc7120a2 100644 --- a/src/components/proxy/FailoverQueueManager.tsx +++ b/src/components/proxy/FailoverQueueManager.tsx @@ -2,37 +2,14 @@ * 故障转移队列管理组件 * * 允许用户管理代理模式下的故障转移队列,支持: - * - 拖拽排序 * - 添加/移除供应商 - * - 启用/禁用队列项 + * - 队列顺序基于首页供应商列表的 sort_index */ -import { useState, useCallback, useMemo } from "react"; +import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { CSS } from "@dnd-kit/utilities"; -import { DndContext, closestCenter } from "@dnd-kit/core"; -import { - SortableContext, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, - type DragEndEvent, -} from "@dnd-kit/core"; -import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { toast } from "sonner"; -import { - GripVertical, - Plus, - Trash2, - Loader2, - Info, - AlertTriangle, -} from "lucide-react"; +import { Plus, Trash2, Loader2, Info, AlertTriangle } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -51,8 +28,6 @@ import { useAvailableProvidersForFailover, useAddToFailoverQueue, useRemoveFromFailoverQueue, - useReorderFailoverQueue, - useSetFailoverItemEnabled, useAutoFailoverEnabled, useSetAutoFailoverEnabled, } from "@/lib/query/failover"; @@ -85,65 +60,12 @@ export function FailoverQueueManager({ // Mutations const addToQueue = useAddToFailoverQueue(); const removeFromQueue = useRemoveFromFailoverQueue(); - const reorderQueue = useReorderFailoverQueue(); - const setItemEnabled = useSetFailoverItemEnabled(); // 切换故障转移开关 const handleToggleFailover = (enabled: boolean) => { setFailoverEnabled.mutate({ appType, enabled }); }; - // 拖拽配置 - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { distance: 8 }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); - - // 排序后的队列 - const sortedQueue = useMemo(() => { - if (!queue) return []; - return [...queue].sort((a, b) => a.queueOrder - b.queueOrder); - }, [queue]); - - // 处理拖拽结束 - const handleDragEnd = useCallback( - async (event: DragEndEvent) => { - const { active, over } = event; - if (!over || active.id === over.id || !sortedQueue) return; - - const oldIndex = sortedQueue.findIndex( - (item) => item.providerId === active.id, - ); - const newIndex = sortedQueue.findIndex( - (item) => item.providerId === over.id, - ); - - if (oldIndex === -1 || newIndex === -1) return; - - const reordered = arrayMove(sortedQueue, oldIndex, newIndex); - const providerIds = reordered.map((item) => item.providerId); - - try { - await reorderQueue.mutateAsync({ appType, providerIds }); - toast.success( - t("proxy.failoverQueue.reorderSuccess", "队列顺序已更新"), - { closeButton: true }, - ); - } catch (error) { - toast.error( - t("proxy.failoverQueue.reorderFailed", "更新顺序失败") + - ": " + - String(error), - ); - } - }, - [sortedQueue, appType, reorderQueue, t], - ); - // 添加供应商到队列 const handleAddProvider = async () => { if (!selectedProviderId) return; @@ -182,19 +104,6 @@ export function FailoverQueueManager({ } }; - // 切换启用状态 - const handleToggleEnabled = async (providerId: string, enabled: boolean) => { - try { - await setItemEnabled.mutateAsync({ appType, providerId, enabled }); - } catch (error) { - toast.error( - t("proxy.failoverQueue.toggleFailed", "状态更新失败") + - ": " + - String(error), - ); - } - }; - if (isQueueLoading) { return (
@@ -248,7 +157,7 @@ export function FailoverQueueManager({ {t( "proxy.failoverQueue.info", - "当前激活的供应商始终优先。当请求失败时,系统会按队列顺序依次尝试其他供应商。", + "队列顺序与首页供应商列表顺序一致。当请求失败时,系统会按顺序依次尝试队列中的供应商。", )} @@ -299,7 +208,7 @@ export function FailoverQueueManager({
{/* 队列列表 */} - {sortedQueue.length === 0 ? ( + {!queue || queue.length === 0 ? (

{t( @@ -309,39 +218,26 @@ export function FailoverQueueManager({

) : ( - - item.providerId)} - strategy={verticalListSortingStrategy} - > -
- {sortedQueue.map((item, index) => ( - - ))} -
-
-
+
+ {queue.map((item, index) => ( + + ))} +
)} {/* 队列说明 */} - {sortedQueue.length > 0 && ( + {queue && queue.length > 0 && (

{t( - "proxy.failoverQueue.dragHint", - "拖拽供应商可调整故障转移顺序,序号越小优先级越高。", + "proxy.failoverQueue.orderHint", + "队列顺序与首页供应商列表顺序一致,可在首页拖拽调整顺序。", )}

)} @@ -349,65 +245,29 @@ export function FailoverQueueManager({ ); } -interface SortableQueueItemProps { +interface QueueItemProps { item: FailoverQueueItem; index: number; disabled: boolean; - onToggleEnabled: (providerId: string, enabled: boolean) => void; onRemove: (providerId: string) => void; isRemoving: boolean; - isToggling: boolean; } -function SortableQueueItem({ +function QueueItem({ item, index, disabled, - onToggleEnabled, onRemove, isRemoving, - isToggling, -}: SortableQueueItemProps) { +}: QueueItemProps) { const { t } = useTranslation(); - const { - setNodeRef, - attributes, - listeners, - transform, - transition, - isDragging, - } = useSortable({ id: item.providerId, disabled }); - - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; return (
- {/* 拖拽手柄 */} - - {/* 序号 */}
{index + 1} @@ -415,24 +275,11 @@ function SortableQueueItem({ {/* 供应商名称 */}
- + {item.providerName}
- {/* 启用开关 */} - onToggleEnabled(item.providerId, checked)} - disabled={disabled || isToggling} - aria-label={t("proxy.failoverQueue.toggleEnabled", "启用/禁用")} - /> - {/* 删除按钮 */}