mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 19:22:15 +08:00
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
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
//! 故障转移队列命令
|
//! 故障转移队列命令
|
||||||
//!
|
//!
|
||||||
//! 管理代理模式下的故障转移队列
|
//! 管理代理模式下的故障转移队列(基于 providers 表的 in_failover_queue 字段)
|
||||||
|
|
||||||
use crate::database::FailoverQueueItem;
|
use crate::database::FailoverQueueItem;
|
||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
@@ -56,32 +56,6 @@ pub async fn remove_from_failover_queue(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重新排序故障转移队列
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn reorder_failover_queue(
|
|
||||||
state: tauri::State<'_, AppState>,
|
|
||||||
app_type: String,
|
|
||||||
provider_ids: Vec<String>,
|
|
||||||
) -> 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]
|
#[tauri::command]
|
||||||
pub async fn get_auto_failover_enabled(
|
pub async fn get_auto_failover_enabled(
|
||||||
|
|||||||
@@ -156,18 +156,18 @@ pub async fn reset_circuit_breaker(
|
|||||||
.get_failover_queue(&app_type)
|
.get_failover_queue(&app_type)
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// 找到恢复的供应商和当前供应商在队列中的位置
|
// 找到恢复的供应商和当前供应商在队列中的位置(使用 sort_index)
|
||||||
let restored_order = queue
|
let restored_order = queue
|
||||||
.iter()
|
.iter()
|
||||||
.find(|item| item.provider_id == provider_id && item.enabled)
|
.find(|item| item.provider_id == provider_id)
|
||||||
.map(|item| item.queue_order);
|
.and_then(|item| item.sort_index);
|
||||||
|
|
||||||
let current_order = queue
|
let current_order = queue
|
||||||
.iter()
|
.iter()
|
||||||
.find(|item| item.provider_id == current_id && item.enabled)
|
.find(|item| item.provider_id == current_id)
|
||||||
.map(|item| item.queue_order);
|
.and_then(|item| item.sort_index);
|
||||||
|
|
||||||
// 如果恢复的供应商优先级更高(queue_order 更小),则切换
|
// 如果恢复的供应商优先级更高(sort_index 更小),则切换
|
||||||
if let (Some(restored), Some(current)) = (restored_order, current_order) {
|
if let (Some(restored), Some(current)) = (restored_order, current_order) {
|
||||||
if restored < current {
|
if restored < current {
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|||||||
@@ -52,9 +52,7 @@ pub async fn stream_check_all_providers(
|
|||||||
}
|
}
|
||||||
if let Ok(queue) = state.db.get_failover_queue(app_type.as_str()) {
|
if let Ok(queue) = state.db.get_failover_queue(app_type.as_str()) {
|
||||||
for item in queue {
|
for item in queue {
|
||||||
if item.enabled {
|
ids.insert(item.provider_id);
|
||||||
ids.insert(item.provider_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(ids)
|
Some(ids)
|
||||||
|
|||||||
@@ -1,36 +1,32 @@
|
|||||||
//! 故障转移队列 DAO
|
//! 故障转移队列 DAO
|
||||||
//!
|
//!
|
||||||
//! 管理代理模式下的故障转移队列
|
//! 管理代理模式下的故障转移队列(基于 providers 表的 in_failover_queue 字段)
|
||||||
|
|
||||||
use crate::database::{lock_conn, Database};
|
use crate::database::{lock_conn, Database};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
use crate::provider::Provider;
|
use crate::provider::Provider;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
/// 故障转移队列条目
|
/// 故障转移队列条目(简化版,用于前端展示)
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FailoverQueueItem {
|
pub struct FailoverQueueItem {
|
||||||
pub provider_id: String,
|
pub provider_id: String,
|
||||||
pub provider_name: String,
|
pub provider_name: String,
|
||||||
pub queue_order: i32,
|
pub sort_index: Option<usize>,
|
||||||
pub enabled: bool,
|
|
||||||
pub created_at: i64,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
/// 获取故障转移队列(按 queue_order 排序)
|
/// 获取故障转移队列(按 sort_index 排序)
|
||||||
pub fn get_failover_queue(&self, app_type: &str) -> Result<Vec<FailoverQueueItem>, AppError> {
|
pub fn get_failover_queue(&self, app_type: &str) -> Result<Vec<FailoverQueueItem>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
let mut stmt = conn
|
let mut stmt = conn
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT fq.provider_id, p.name, fq.queue_order, fq.enabled, fq.created_at
|
"SELECT id, name, sort_index
|
||||||
FROM failover_queue fq
|
FROM providers
|
||||||
JOIN providers p ON fq.provider_id = p.id AND fq.app_type = p.app_type
|
WHERE app_type = ?1 AND in_failover_queue = 1
|
||||||
WHERE fq.app_type = ?1
|
ORDER BY COALESCE(sort_index, 999999), id ASC",
|
||||||
ORDER BY fq.queue_order ASC",
|
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
@@ -39,9 +35,7 @@ impl Database {
|
|||||||
Ok(FailoverQueueItem {
|
Ok(FailoverQueueItem {
|
||||||
provider_id: row.get(0)?,
|
provider_id: row.get(0)?,
|
||||||
provider_name: row.get(1)?,
|
provider_name: row.get(1)?,
|
||||||
queue_order: row.get(2)?,
|
sort_index: row.get(2)?,
|
||||||
enabled: row.get(3)?,
|
|
||||||
created_at: row.get(4)?,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?
|
.map_err(|e| AppError::Database(e.to_string()))?
|
||||||
@@ -53,43 +47,23 @@ impl Database {
|
|||||||
|
|
||||||
/// 获取故障转移队列中的供应商(完整 Provider 信息,按顺序)
|
/// 获取故障转移队列中的供应商(完整 Provider 信息,按顺序)
|
||||||
pub fn get_failover_providers(&self, app_type: &str) -> Result<Vec<Provider>, AppError> {
|
pub fn get_failover_providers(&self, app_type: &str) -> Result<Vec<Provider>, AppError> {
|
||||||
let queue = self.get_failover_queue(app_type)?;
|
|
||||||
let all_providers = self.get_all_providers(app_type)?;
|
let all_providers = self.get_all_providers(app_type)?;
|
||||||
|
|
||||||
let mut result = Vec::new();
|
let result: Vec<Provider> = all_providers
|
||||||
for item in queue {
|
.into_values()
|
||||||
if item.enabled {
|
.filter(|p| p.in_failover_queue)
|
||||||
if let Some(provider) = all_providers.get(&item.provider_id) {
|
.collect();
|
||||||
result.push(provider.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 添加供应商到故障转移队列末尾
|
/// 添加供应商到故障转移队列
|
||||||
pub fn add_to_failover_queue(&self, app_type: &str, provider_id: &str) -> Result<(), AppError> {
|
pub fn add_to_failover_queue(&self, app_type: &str, provider_id: &str) -> Result<(), AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
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(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO failover_queue (app_type, provider_id, queue_order, enabled, created_at)
|
"UPDATE providers SET in_failover_queue = 1 WHERE id = ?1 AND app_type = ?2",
|
||||||
VALUES (?1, ?2, ?3, 1, ?4)",
|
rusqlite::params![provider_id, app_type],
|
||||||
rusqlite::params![app_type, provider_id, max_order + 1, now],
|
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
@@ -104,104 +78,12 @@ impl Database {
|
|||||||
) -> Result<(), AppError> {
|
) -> Result<(), AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
// 获取被删除项的 queue_order
|
|
||||||
let removed_order: Option<i32> = 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(
|
conn.execute(
|
||||||
"DELETE FROM failover_queue WHERE app_type = ?1 AND provider_id = ?2",
|
"UPDATE providers SET in_failover_queue = 0 WHERE id = ?1 AND app_type = ?2",
|
||||||
[app_type, provider_id],
|
rusqlite::params![provider_id, app_type],
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,8 +91,11 @@ impl Database {
|
|||||||
pub fn clear_failover_queue(&self, app_type: &str) -> Result<(), AppError> {
|
pub fn clear_failover_queue(&self, app_type: &str) -> Result<(), AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
conn.execute("DELETE FROM failover_queue WHERE app_type = ?1", [app_type])
|
conn.execute(
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
"UPDATE providers SET in_failover_queue = 0 WHERE app_type = ?1",
|
||||||
|
[app_type],
|
||||||
|
)
|
||||||
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -223,15 +108,15 @@ impl Database {
|
|||||||
) -> Result<bool, AppError> {
|
) -> Result<bool, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
|
|
||||||
let count: i32 = conn
|
let in_queue: bool = conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT COUNT(*) FROM failover_queue WHERE app_type = ?1 AND provider_id = ?2",
|
"SELECT in_failover_queue FROM providers WHERE id = ?1 AND app_type = ?2",
|
||||||
[app_type, provider_id],
|
rusqlite::params![provider_id, app_type],
|
||||||
|row| row.get(0),
|
|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,
|
app_type: &str,
|
||||||
) -> Result<Vec<Provider>, AppError> {
|
) -> Result<Vec<Provider>, AppError> {
|
||||||
let all_providers = self.get_all_providers(app_type)?;
|
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<Provider> = all_providers
|
let available: Vec<Provider> = all_providers
|
||||||
.into_values()
|
.into_values()
|
||||||
.filter(|p| !queue_ids.contains(&p.id))
|
.filter(|p| !p.in_failover_queue)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(available)
|
Ok(available)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ impl Database {
|
|||||||
) -> Result<IndexMap<String, Provider>, AppError> {
|
) -> Result<IndexMap<String, Provider>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
let mut stmt = conn.prepare(
|
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
|
FROM providers WHERE app_type = ?1
|
||||||
ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC"
|
ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC"
|
||||||
).map_err(|e| AppError::Database(e.to_string()))?;
|
).map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
@@ -35,6 +35,7 @@ impl Database {
|
|||||||
let icon: Option<String> = row.get(8)?;
|
let icon: Option<String> = row.get(8)?;
|
||||||
let icon_color: Option<String> = row.get(9)?;
|
let icon_color: Option<String> = row.get(9)?;
|
||||||
let meta_str: String = row.get(10)?;
|
let meta_str: String = row.get(10)?;
|
||||||
|
let in_failover_queue: bool = row.get(11)?;
|
||||||
|
|
||||||
let settings_config =
|
let settings_config =
|
||||||
serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
serde_json::from_str(&settings_config_str).unwrap_or(serde_json::Value::Null);
|
||||||
@@ -54,6 +55,7 @@ impl Database {
|
|||||||
meta: Some(meta),
|
meta: Some(meta),
|
||||||
icon,
|
icon,
|
||||||
icon_color,
|
icon_color,
|
||||||
|
in_failover_queue,
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
@@ -129,7 +131,7 @@ impl Database {
|
|||||||
) -> Result<Option<Provider>, AppError> {
|
) -> Result<Option<Provider>, AppError> {
|
||||||
let conn = lock_conn!(self.conn);
|
let conn = lock_conn!(self.conn);
|
||||||
let result = conn.query_row(
|
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",
|
FROM providers WHERE id = ?1 AND app_type = ?2",
|
||||||
params![id, app_type],
|
params![id, app_type],
|
||||||
|row| {
|
|row| {
|
||||||
@@ -143,6 +145,7 @@ impl Database {
|
|||||||
let icon: Option<String> = row.get(7)?;
|
let icon: Option<String> = row.get(7)?;
|
||||||
let icon_color: Option<String> = row.get(8)?;
|
let icon_color: Option<String> = row.get(8)?;
|
||||||
let meta_str: String = row.get(9)?;
|
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 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();
|
let meta: ProviderMeta = serde_json::from_str(&meta_str).unwrap_or_default();
|
||||||
@@ -159,6 +162,7 @@ impl Database {
|
|||||||
meta: Some(meta),
|
meta: Some(meta),
|
||||||
icon,
|
icon,
|
||||||
icon_color,
|
icon_color,
|
||||||
|
in_failover_queue,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -184,17 +188,18 @@ impl Database {
|
|||||||
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
let mut meta_clone = provider.meta.clone().unwrap_or_default();
|
||||||
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
let endpoints = std::mem::take(&mut meta_clone.custom_endpoints);
|
||||||
|
|
||||||
// 检查是否存在(用于判断新增/更新,以及保留 is_current)
|
// 检查是否存在(用于判断新增/更新,以及保留 is_current 和 in_failover_queue)
|
||||||
let existing: Option<bool> = tx
|
let existing: Option<(bool, bool)> = tx
|
||||||
.query_row(
|
.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],
|
params![provider.id, app_type],
|
||||||
|row| row.get(0),
|
|row| Ok((row.get(0)?, row.get(1)?)),
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
let is_update = existing.is_some();
|
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 {
|
if is_update {
|
||||||
// 更新模式:使用 UPDATE 避免触发 ON DELETE CASCADE
|
// 更新模式:使用 UPDATE 避免触发 ON DELETE CASCADE
|
||||||
@@ -210,8 +215,9 @@ impl Database {
|
|||||||
icon = ?8,
|
icon = ?8,
|
||||||
icon_color = ?9,
|
icon_color = ?9,
|
||||||
meta = ?10,
|
meta = ?10,
|
||||||
is_current = ?11
|
is_current = ?11,
|
||||||
WHERE id = ?12 AND app_type = ?13",
|
in_failover_queue = ?12
|
||||||
|
WHERE id = ?13 AND app_type = ?14",
|
||||||
params![
|
params![
|
||||||
provider.name,
|
provider.name,
|
||||||
serde_json::to_string(&provider.settings_config).unwrap(),
|
serde_json::to_string(&provider.settings_config).unwrap(),
|
||||||
@@ -224,6 +230,7 @@ impl Database {
|
|||||||
provider.icon_color,
|
provider.icon_color,
|
||||||
serde_json::to_string(&meta_clone).unwrap(),
|
serde_json::to_string(&meta_clone).unwrap(),
|
||||||
is_current,
|
is_current,
|
||||||
|
in_failover_queue,
|
||||||
provider.id,
|
provider.id,
|
||||||
app_type,
|
app_type,
|
||||||
],
|
],
|
||||||
@@ -234,8 +241,8 @@ impl Database {
|
|||||||
tx.execute(
|
tx.execute(
|
||||||
"INSERT INTO providers (
|
"INSERT INTO providers (
|
||||||
id, app_type, name, settings_config, website_url, category,
|
id, app_type, name, settings_config, website_url, category,
|
||||||
created_at, sort_index, notes, icon, icon_color, meta, is_current
|
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)",
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
||||||
params![
|
params![
|
||||||
provider.id,
|
provider.id,
|
||||||
app_type,
|
app_type,
|
||||||
@@ -250,6 +257,7 @@ impl Database {
|
|||||||
provider.icon_color,
|
provider.icon_color,
|
||||||
serde_json::to_string(&meta_clone).unwrap(),
|
serde_json::to_string(&meta_clone).unwrap(),
|
||||||
is_current,
|
is_current,
|
||||||
|
in_failover_queue,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.map_err(|e| AppError::Database(e.to_string()))?;
|
.map_err(|e| AppError::Database(e.to_string()))?;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ impl Database {
|
|||||||
icon_color TEXT,
|
icon_color TEXT,
|
||||||
meta TEXT NOT NULL DEFAULT '{}',
|
meta TEXT NOT NULL DEFAULT '{}',
|
||||||
is_current BOOLEAN NOT NULL DEFAULT 0,
|
is_current BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
in_failover_queue BOOLEAN NOT NULL DEFAULT 0,
|
||||||
PRIMARY KEY (id, app_type)
|
PRIMARY KEY (id, app_type)
|
||||||
)",
|
)",
|
||||||
[],
|
[],
|
||||||
@@ -312,29 +313,13 @@ impl Database {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 14. Failover Queue 表 (故障转移队列)
|
// 为故障转移队列创建索引(基于 providers 表)
|
||||||
conn.execute(
|
// 注意:此索引依赖 in_failover_queue 列,对于旧数据库会在迁移时创建
|
||||||
"CREATE TABLE IF NOT EXISTS failover_queue (
|
let _ = conn.execute(
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
"CREATE INDEX IF NOT EXISTS idx_providers_failover
|
||||||
app_type TEXT NOT NULL,
|
ON providers(app_type, in_failover_queue, sort_index)",
|
||||||
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
|
|
||||||
)",
|
|
||||||
[],
|
[],
|
||||||
)
|
);
|
||||||
.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(())
|
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_daily_usd", "TEXT")?;
|
||||||
Self::add_column_if_missing(conn, "providers", "limit_monthly_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", "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 表(包含所有字段)
|
// proxy_request_logs 表(包含所有字段)
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ fn dry_run_validates_schema_compatibility() {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ pub(crate) fn build_provider_from_request(
|
|||||||
meta,
|
meta,
|
||||||
icon: request.icon.clone(),
|
icon: request.icon.clone(),
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(provider)
|
Ok(provider)
|
||||||
|
|||||||
@@ -708,8 +708,6 @@ pub fn run() {
|
|||||||
commands::get_available_providers_for_failover,
|
commands::get_available_providers_for_failover,
|
||||||
commands::add_to_failover_queue,
|
commands::add_to_failover_queue,
|
||||||
commands::remove_from_failover_queue,
|
commands::remove_from_failover_queue,
|
||||||
commands::reorder_failover_queue,
|
|
||||||
commands::set_failover_item_enabled,
|
|
||||||
commands::get_auto_failover_enabled,
|
commands::get_auto_failover_enabled,
|
||||||
commands::set_auto_failover_enabled,
|
commands::set_auto_failover_enabled,
|
||||||
// Usage statistics
|
// Usage statistics
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ pub struct Provider {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
#[serde(rename = "iconColor")]
|
#[serde(rename = "iconColor")]
|
||||||
pub icon_color: Option<String>,
|
pub icon_color: Option<String>,
|
||||||
|
/// 是否加入故障转移队列
|
||||||
|
#[serde(default)]
|
||||||
|
#[serde(rename = "inFailoverQueue")]
|
||||||
|
pub in_failover_queue: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Provider {
|
impl Provider {
|
||||||
@@ -58,6 +62,7 @@ impl Provider {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ impl ProviderRouter {
|
|||||||
/// - 故障转移开启时:完全按照故障转移队列顺序返回,忽略当前供应商设置
|
/// - 故障转移开启时:完全按照故障转移队列顺序返回,忽略当前供应商设置
|
||||||
pub async fn select_providers(&self, app_type: &str) -> Result<Vec<Provider>, AppError> {
|
pub async fn select_providers(&self, app_type: &str) -> Result<Vec<Provider>, AppError> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let all_providers = self.db.get_all_providers(app_type)?;
|
|
||||||
|
|
||||||
// 检查该应用的自动故障转移开关是否开启
|
// 检查该应用的自动故障转移开关是否开启
|
||||||
let failover_key = format!("auto_failover_enabled_{app_type}");
|
let failover_key = format!("auto_failover_enabled_{app_type}");
|
||||||
@@ -61,42 +60,34 @@ impl ProviderRouter {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if auto_failover_enabled {
|
if auto_failover_enabled {
|
||||||
// 故障转移开启:完全按照队列顺序,忽略当前供应商
|
// 故障转移开启:使用 in_failover_queue 标记的供应商,按 sort_index 排序
|
||||||
let queue = self.db.get_failover_queue(app_type)?;
|
let failover_providers = self.db.get_failover_providers(app_type)?;
|
||||||
log::info!(
|
log::info!(
|
||||||
"[{}] Failover enabled, using queue order ({} items)",
|
"[{}] Failover enabled, using queue order ({} items)",
|
||||||
app_type,
|
app_type,
|
||||||
queue.len()
|
failover_providers.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
for item in queue {
|
for provider in failover_providers {
|
||||||
// 跳过禁用的队列项
|
// 检查熔断器状态
|
||||||
if !item.enabled {
|
let circuit_key = format!("{}:{}", app_type, provider.id);
|
||||||
continue;
|
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
|
||||||
}
|
|
||||||
|
|
||||||
// 获取供应商信息
|
if breaker.is_available().await {
|
||||||
if let Some(provider) = all_providers.get(&item.provider_id) {
|
log::info!(
|
||||||
// 检查熔断器状态
|
"[{}] Queue provider available: {} ({}) at sort_index {:?}",
|
||||||
let circuit_key = format!("{}:{}", app_type, provider.id);
|
app_type,
|
||||||
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
|
provider.name,
|
||||||
|
provider.id,
|
||||||
if breaker.is_available().await {
|
provider.sort_index
|
||||||
log::info!(
|
);
|
||||||
"[{}] Queue provider available: {} ({}) at position {}",
|
result.push(provider);
|
||||||
app_type,
|
} else {
|
||||||
provider.name,
|
log::debug!(
|
||||||
provider.id,
|
"[{}] Queue provider {} circuit breaker open, skipping",
|
||||||
item.queue_order
|
app_type,
|
||||||
);
|
provider.name
|
||||||
result.push(provider.clone());
|
);
|
||||||
} else {
|
|
||||||
log::debug!(
|
|
||||||
"[{}] Queue provider {} circuit breaker open, skipping",
|
|
||||||
app_type,
|
|
||||||
provider.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -104,7 +95,7 @@ impl ProviderRouter {
|
|||||||
log::info!("[{app_type}] Failover disabled, using current provider only");
|
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_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 circuit_key = format!("{}:{}", app_type, current.id);
|
||||||
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
|
let breaker = self.get_or_create_circuit_breaker(&circuit_key).await;
|
||||||
|
|
||||||
@@ -115,7 +106,7 @@ impl ProviderRouter {
|
|||||||
current.name,
|
current.name,
|
||||||
current.id
|
current.id
|
||||||
);
|
);
|
||||||
result.push(current.clone());
|
result.push(current);
|
||||||
} else {
|
} else {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"[{}] Current provider {} circuit breaker open",
|
"[{}] Current provider {} circuit breaker open",
|
||||||
@@ -320,10 +311,13 @@ mod tests {
|
|||||||
async fn test_failover_enabled_uses_queue_order() {
|
async fn test_failover_enabled_uses_queue_order() {
|
||||||
let db = Arc::new(Database::memory().unwrap());
|
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);
|
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::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_a).unwrap();
|
||||||
db.save_provider("claude", &provider_b).unwrap();
|
db.save_provider("claude", &provider_b).unwrap();
|
||||||
@@ -338,6 +332,7 @@ mod tests {
|
|||||||
let providers = router.select_providers("claude").await.unwrap();
|
let providers = router.select_providers("claude").await.unwrap();
|
||||||
|
|
||||||
assert_eq!(providers.len(), 2);
|
assert_eq!(providers.len(), 2);
|
||||||
|
// 按 sort_index 排序:b(1) 在前,a(2) 在后
|
||||||
assert_eq!(providers[0].id, "b");
|
assert_eq!(providers[0].id, "b");
|
||||||
assert_eq!(providers[1].id, "a");
|
assert_eq!(providers[1].id, "a");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ mod tests {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ mod tests {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ mod tests {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ mod tests {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ mod tests {
|
|||||||
meta: None,
|
meta: None,
|
||||||
icon: None,
|
icon: None,
|
||||||
icon_color: None,
|
icon_color: None,
|
||||||
|
in_failover_queue: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
useFailoverQueue,
|
useFailoverQueue,
|
||||||
useAddToFailoverQueue,
|
useAddToFailoverQueue,
|
||||||
useRemoveFromFailoverQueue,
|
useRemoveFromFailoverQueue,
|
||||||
useReorderFailoverQueue,
|
|
||||||
} from "@/lib/query/failover";
|
} from "@/lib/query/failover";
|
||||||
import { useCallback, useEffect, useRef } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
interface ProviderListProps {
|
interface ProviderListProps {
|
||||||
providers: Record<string, Provider>;
|
providers: Record<string, Provider>;
|
||||||
@@ -67,22 +66,16 @@ export function ProviderList({
|
|||||||
const { data: failoverQueue } = useFailoverQueue(appId);
|
const { data: failoverQueue } = useFailoverQueue(appId);
|
||||||
const addToQueue = useAddToFailoverQueue();
|
const addToQueue = useAddToFailoverQueue();
|
||||||
const removeFromQueue = useRemoveFromFailoverQueue();
|
const removeFromQueue = useRemoveFromFailoverQueue();
|
||||||
const reorderQueue = useReorderFailoverQueue();
|
|
||||||
|
|
||||||
// 联动状态:只有当前应用开启代理接管且故障转移开启时才启用故障转移模式
|
// 联动状态:只有当前应用开启代理接管且故障转移开启时才启用故障转移模式
|
||||||
const isFailoverModeActive =
|
const isFailoverModeActive =
|
||||||
isProxyTakeover === true && isAutoFailoverEnabled === true;
|
isProxyTakeover === true && isAutoFailoverEnabled === true;
|
||||||
|
|
||||||
// 防止重复调用的 ref
|
// 计算供应商在故障转移队列中的优先级(基于 sortIndex 排序)
|
||||||
const lastReorderRef = useRef<string>("");
|
|
||||||
|
|
||||||
// 计算供应商在故障转移队列中的优先级
|
|
||||||
const getFailoverPriority = useCallback(
|
const getFailoverPriority = useCallback(
|
||||||
(providerId: string): number | undefined => {
|
(providerId: string): number | undefined => {
|
||||||
if (!isFailoverModeActive || !failoverQueue) return undefined;
|
if (!isFailoverModeActive || !failoverQueue) return undefined;
|
||||||
// 只计算已启用的供应商的优先级
|
const index = failoverQueue.findIndex(
|
||||||
const enabledQueue = failoverQueue.filter((item) => item.enabled);
|
|
||||||
const index = enabledQueue.findIndex(
|
|
||||||
(item) => item.providerId === providerId,
|
(item) => item.providerId === providerId,
|
||||||
);
|
);
|
||||||
return index >= 0 ? index + 1 : undefined;
|
return index >= 0 ? index + 1 : undefined;
|
||||||
@@ -94,9 +87,7 @@ export function ProviderList({
|
|||||||
const isInFailoverQueue = useCallback(
|
const isInFailoverQueue = useCallback(
|
||||||
(providerId: string): boolean => {
|
(providerId: string): boolean => {
|
||||||
if (!isFailoverModeActive || !failoverQueue) return false;
|
if (!isFailoverModeActive || !failoverQueue) return false;
|
||||||
return failoverQueue.some(
|
return failoverQueue.some((item) => item.providerId === providerId);
|
||||||
(item) => item.providerId === providerId && item.enabled,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[isFailoverModeActive, failoverQueue],
|
[isFailoverModeActive, failoverQueue],
|
||||||
);
|
);
|
||||||
@@ -113,42 +104,6 @@ export function ProviderList({
|
|||||||
[appId, addToQueue, removeFromQueue],
|
[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) => {
|
const handleTest = (provider: Provider) => {
|
||||||
checkProvider(provider.id, provider.name);
|
checkProvider(provider.id, provider.name);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,37 +2,14 @@
|
|||||||
* 故障转移队列管理组件
|
* 故障转移队列管理组件
|
||||||
*
|
*
|
||||||
* 允许用户管理代理模式下的故障转移队列,支持:
|
* 允许用户管理代理模式下的故障转移队列,支持:
|
||||||
* - 拖拽排序
|
|
||||||
* - 添加/移除供应商
|
* - 添加/移除供应商
|
||||||
* - 启用/禁用队列项
|
* - 队列顺序基于首页供应商列表的 sort_index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useCallback, useMemo } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { toast } from "sonner";
|
||||||
import {
|
import { Plus, Trash2, Loader2, Info, AlertTriangle } from "lucide-react";
|
||||||
GripVertical,
|
|
||||||
Plus,
|
|
||||||
Trash2,
|
|
||||||
Loader2,
|
|
||||||
Info,
|
|
||||||
AlertTriangle,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
@@ -51,8 +28,6 @@ import {
|
|||||||
useAvailableProvidersForFailover,
|
useAvailableProvidersForFailover,
|
||||||
useAddToFailoverQueue,
|
useAddToFailoverQueue,
|
||||||
useRemoveFromFailoverQueue,
|
useRemoveFromFailoverQueue,
|
||||||
useReorderFailoverQueue,
|
|
||||||
useSetFailoverItemEnabled,
|
|
||||||
useAutoFailoverEnabled,
|
useAutoFailoverEnabled,
|
||||||
useSetAutoFailoverEnabled,
|
useSetAutoFailoverEnabled,
|
||||||
} from "@/lib/query/failover";
|
} from "@/lib/query/failover";
|
||||||
@@ -85,65 +60,12 @@ export function FailoverQueueManager({
|
|||||||
// Mutations
|
// Mutations
|
||||||
const addToQueue = useAddToFailoverQueue();
|
const addToQueue = useAddToFailoverQueue();
|
||||||
const removeFromQueue = useRemoveFromFailoverQueue();
|
const removeFromQueue = useRemoveFromFailoverQueue();
|
||||||
const reorderQueue = useReorderFailoverQueue();
|
|
||||||
const setItemEnabled = useSetFailoverItemEnabled();
|
|
||||||
|
|
||||||
// 切换故障转移开关
|
// 切换故障转移开关
|
||||||
const handleToggleFailover = (enabled: boolean) => {
|
const handleToggleFailover = (enabled: boolean) => {
|
||||||
setFailoverEnabled.mutate({ appType, enabled });
|
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 () => {
|
const handleAddProvider = async () => {
|
||||||
if (!selectedProviderId) return;
|
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) {
|
if (isQueueLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center p-8">
|
<div className="flex items-center justify-center p-8">
|
||||||
@@ -248,7 +157,7 @@ export function FailoverQueueManager({
|
|||||||
<AlertDescription className="text-sm">
|
<AlertDescription className="text-sm">
|
||||||
{t(
|
{t(
|
||||||
"proxy.failoverQueue.info",
|
"proxy.failoverQueue.info",
|
||||||
"当前激活的供应商始终优先。当请求失败时,系统会按队列顺序依次尝试其他供应商。",
|
"队列顺序与首页供应商列表顺序一致。当请求失败时,系统会按顺序依次尝试队列中的供应商。",
|
||||||
)}
|
)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
@@ -299,7 +208,7 @@ export function FailoverQueueManager({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 队列列表 */}
|
{/* 队列列表 */}
|
||||||
{sortedQueue.length === 0 ? (
|
{!queue || queue.length === 0 ? (
|
||||||
<div className="rounded-lg border border-dashed border-muted-foreground/40 p-8 text-center">
|
<div className="rounded-lg border border-dashed border-muted-foreground/40 p-8 text-center">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
@@ -309,39 +218,26 @@ export function FailoverQueueManager({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<DndContext
|
<div className="space-y-2">
|
||||||
sensors={sensors}
|
{queue.map((item, index) => (
|
||||||
collisionDetection={closestCenter}
|
<QueueItem
|
||||||
onDragEnd={handleDragEnd}
|
key={item.providerId}
|
||||||
>
|
item={item}
|
||||||
<SortableContext
|
index={index}
|
||||||
items={sortedQueue.map((item) => item.providerId)}
|
disabled={disabled}
|
||||||
strategy={verticalListSortingStrategy}
|
onRemove={handleRemoveProvider}
|
||||||
>
|
isRemoving={removeFromQueue.isPending}
|
||||||
<div className="space-y-2">
|
/>
|
||||||
{sortedQueue.map((item, index) => (
|
))}
|
||||||
<SortableQueueItem
|
</div>
|
||||||
key={item.providerId}
|
|
||||||
item={item}
|
|
||||||
index={index}
|
|
||||||
disabled={disabled}
|
|
||||||
onToggleEnabled={handleToggleEnabled}
|
|
||||||
onRemove={handleRemoveProvider}
|
|
||||||
isRemoving={removeFromQueue.isPending}
|
|
||||||
isToggling={setItemEnabled.isPending}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</SortableContext>
|
|
||||||
</DndContext>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 队列说明 */}
|
{/* 队列说明 */}
|
||||||
{sortedQueue.length > 0 && (
|
{queue && queue.length > 0 && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
"proxy.failoverQueue.dragHint",
|
"proxy.failoverQueue.orderHint",
|
||||||
"拖拽供应商可调整故障转移顺序,序号越小优先级越高。",
|
"队列顺序与首页供应商列表顺序一致,可在首页拖拽调整顺序。",
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -349,65 +245,29 @@ export function FailoverQueueManager({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SortableQueueItemProps {
|
interface QueueItemProps {
|
||||||
item: FailoverQueueItem;
|
item: FailoverQueueItem;
|
||||||
index: number;
|
index: number;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
onToggleEnabled: (providerId: string, enabled: boolean) => void;
|
|
||||||
onRemove: (providerId: string) => void;
|
onRemove: (providerId: string) => void;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
isToggling: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function SortableQueueItem({
|
function QueueItem({
|
||||||
item,
|
item,
|
||||||
index,
|
index,
|
||||||
disabled,
|
disabled,
|
||||||
onToggleEnabled,
|
|
||||||
onRemove,
|
onRemove,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isToggling,
|
}: QueueItemProps) {
|
||||||
}: SortableQueueItemProps) {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
|
||||||
setNodeRef,
|
|
||||||
attributes,
|
|
||||||
listeners,
|
|
||||||
transform,
|
|
||||||
transition,
|
|
||||||
isDragging,
|
|
||||||
} = useSortable({ id: item.providerId, disabled });
|
|
||||||
|
|
||||||
const style = {
|
|
||||||
transform: CSS.Transform.toString(transform),
|
|
||||||
transition,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
|
||||||
style={style}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-3 rounded-lg border bg-card p-3 transition-colors",
|
"flex items-center gap-3 rounded-lg border bg-card p-3 transition-colors",
|
||||||
isDragging && "opacity-50 shadow-lg",
|
|
||||||
!item.enabled && "opacity-60",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* 拖拽手柄 */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cn(
|
|
||||||
"cursor-grab touch-none text-muted-foreground hover:text-foreground",
|
|
||||||
disabled && "cursor-not-allowed opacity-50",
|
|
||||||
)}
|
|
||||||
{...attributes}
|
|
||||||
{...listeners}
|
|
||||||
disabled={disabled}
|
|
||||||
aria-label={t("provider.dragHandle", "拖拽排序")}
|
|
||||||
>
|
|
||||||
<GripVertical className="h-5 w-5" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* 序号 */}
|
{/* 序号 */}
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-muted text-xs font-medium">
|
<div className="flex h-6 w-6 items-center justify-center rounded-full bg-muted text-xs font-medium">
|
||||||
{index + 1}
|
{index + 1}
|
||||||
@@ -415,24 +275,11 @@ function SortableQueueItem({
|
|||||||
|
|
||||||
{/* 供应商名称 */}
|
{/* 供应商名称 */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<span
|
<span className="text-sm font-medium truncate block">
|
||||||
className={cn(
|
|
||||||
"text-sm font-medium truncate block",
|
|
||||||
!item.enabled && "text-muted-foreground line-through",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.providerName}
|
{item.providerName}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 启用开关 */}
|
|
||||||
<Switch
|
|
||||||
checked={item.enabled}
|
|
||||||
onCheckedChange={(checked) => onToggleEnabled(item.providerId, checked)}
|
|
||||||
disabled={disabled || isToggling}
|
|
||||||
aria-label={t("proxy.failoverQueue.toggleEnabled", "启用/禁用")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 删除按钮 */}
|
{/* 删除按钮 */}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -147,13 +147,10 @@ export function ProxyPanel() {
|
|||||||
<ProviderQueueGroup
|
<ProviderQueueGroup
|
||||||
appType="claude"
|
appType="claude"
|
||||||
appLabel="Claude"
|
appLabel="Claude"
|
||||||
targets={claudeQueue
|
targets={claudeQueue.map((item) => ({
|
||||||
.filter((item) => item.enabled)
|
id: item.providerId,
|
||||||
.sort((a, b) => a.queueOrder - b.queueOrder)
|
name: item.providerName,
|
||||||
.map((item) => ({
|
}))}
|
||||||
id: item.providerId,
|
|
||||||
name: item.providerName,
|
|
||||||
}))}
|
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -163,13 +160,10 @@ export function ProxyPanel() {
|
|||||||
<ProviderQueueGroup
|
<ProviderQueueGroup
|
||||||
appType="codex"
|
appType="codex"
|
||||||
appLabel="Codex"
|
appLabel="Codex"
|
||||||
targets={codexQueue
|
targets={codexQueue.map((item) => ({
|
||||||
.filter((item) => item.enabled)
|
id: item.providerId,
|
||||||
.sort((a, b) => a.queueOrder - b.queueOrder)
|
name: item.providerName,
|
||||||
.map((item) => ({
|
}))}
|
||||||
id: item.providerId,
|
|
||||||
name: item.providerName,
|
|
||||||
}))}
|
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -179,13 +173,10 @@ export function ProxyPanel() {
|
|||||||
<ProviderQueueGroup
|
<ProviderQueueGroup
|
||||||
appType="gemini"
|
appType="gemini"
|
||||||
appLabel="Gemini"
|
appLabel="Gemini"
|
||||||
targets={geminiQueue
|
targets={geminiQueue.map((item) => ({
|
||||||
.filter((item) => item.enabled)
|
id: item.providerId,
|
||||||
.sort((a, b) => a.queueOrder - b.queueOrder)
|
name: item.providerName,
|
||||||
.map((item) => ({
|
}))}
|
||||||
id: item.providerId,
|
|
||||||
name: item.providerName,
|
|
||||||
}))}
|
|
||||||
status={status}
|
status={status}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -84,27 +84,6 @@ export const failoverApi = {
|
|||||||
return invoke("remove_from_failover_queue", { appType, providerId });
|
return invoke("remove_from_failover_queue", { appType, providerId });
|
||||||
},
|
},
|
||||||
|
|
||||||
// 重新排序故障转移队列
|
|
||||||
async reorderFailoverQueue(
|
|
||||||
appType: string,
|
|
||||||
providerIds: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
return invoke("reorder_failover_queue", { appType, providerIds });
|
|
||||||
},
|
|
||||||
|
|
||||||
// 设置故障转移队列项的启用状态
|
|
||||||
async setFailoverItemEnabled(
|
|
||||||
appType: string,
|
|
||||||
providerId: string,
|
|
||||||
enabled: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
return invoke("set_failover_item_enabled", {
|
|
||||||
appType,
|
|
||||||
providerId,
|
|
||||||
enabled,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取指定应用的自动故障转移开关状态
|
// 获取指定应用的自动故障转移开关状态
|
||||||
async getAutoFailoverEnabled(appType: string): Promise<boolean> {
|
async getAutoFailoverEnabled(appType: string): Promise<boolean> {
|
||||||
return invoke("get_auto_failover_enabled", { appType });
|
return invoke("get_auto_failover_enabled", { appType });
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ export function useAddToFailoverQueue() {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["availableProvidersForFailover", variables.appType],
|
queryKey: ["availableProvidersForFailover", variables.appType],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["providers", variables.appType],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -156,92 +159,8 @@ export function useRemoveFromFailoverQueue() {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["availableProvidersForFailover", variables.appType],
|
queryKey: ["availableProvidersForFailover", variables.appType],
|
||||||
});
|
});
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重新排序故障转移队列
|
|
||||||
*/
|
|
||||||
export function useReorderFailoverQueue() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({
|
|
||||||
appType,
|
|
||||||
providerIds,
|
|
||||||
}: {
|
|
||||||
appType: string;
|
|
||||||
providerIds: string[];
|
|
||||||
}) => failoverApi.reorderFailoverQueue(appType, providerIds),
|
|
||||||
onSuccess: (_, variables) => {
|
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: ["failoverQueue", variables.appType],
|
queryKey: ["providers", variables.appType],
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置故障转移队列项的启用状态
|
|
||||||
* 使用乐观更新(Optimistic Update)以提供即时反馈
|
|
||||||
*/
|
|
||||||
export function useSetFailoverItemEnabled() {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
return useMutation({
|
|
||||||
mutationFn: ({
|
|
||||||
appType,
|
|
||||||
providerId,
|
|
||||||
enabled,
|
|
||||||
}: {
|
|
||||||
appType: string;
|
|
||||||
providerId: string;
|
|
||||||
enabled: boolean;
|
|
||||||
}) => failoverApi.setFailoverItemEnabled(appType, providerId, enabled),
|
|
||||||
|
|
||||||
// 乐观更新:立即更新缓存中的数据
|
|
||||||
onMutate: async (variables) => {
|
|
||||||
// 取消正在进行的查询,防止覆盖乐观更新
|
|
||||||
await queryClient.cancelQueries({
|
|
||||||
queryKey: ["failoverQueue", variables.appType],
|
|
||||||
});
|
|
||||||
|
|
||||||
// 保存之前的数据以便回滚
|
|
||||||
const previousQueue = queryClient.getQueryData<
|
|
||||||
import("@/types/proxy").FailoverQueueItem[]
|
|
||||||
>(["failoverQueue", variables.appType]);
|
|
||||||
|
|
||||||
// 乐观地更新缓存
|
|
||||||
if (previousQueue) {
|
|
||||||
queryClient.setQueryData<import("@/types/proxy").FailoverQueueItem[]>(
|
|
||||||
["failoverQueue", variables.appType],
|
|
||||||
previousQueue.map((item) =>
|
|
||||||
item.providerId === variables.providerId
|
|
||||||
? { ...item, enabled: variables.enabled }
|
|
||||||
: item,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回上下文供 onError 使用
|
|
||||||
return { previousQueue };
|
|
||||||
},
|
|
||||||
|
|
||||||
// 错误时回滚
|
|
||||||
onError: (_error, variables, context) => {
|
|
||||||
if (context?.previousQueue) {
|
|
||||||
queryClient.setQueryData(
|
|
||||||
["failoverQueue", variables.appType],
|
|
||||||
context.previousQueue,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 无论成功失败,都重新获取最新数据以确保一致性
|
|
||||||
onSettled: (_, __, variables) => {
|
|
||||||
queryClient.invalidateQueries({
|
|
||||||
queryKey: ["failoverQueue", variables.appType],
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export interface Provider {
|
|||||||
// 图标配置
|
// 图标配置
|
||||||
icon?: string; // 图标名称(如 "openai", "anthropic")
|
icon?: string; // 图标名称(如 "openai", "anthropic")
|
||||||
iconColor?: string; // 图标颜色(Hex 格式,如 "#00A67E")
|
iconColor?: string; // 图标颜色(Hex 格式,如 "#00A67E")
|
||||||
|
// 是否加入故障转移队列
|
||||||
|
inFailoverQueue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppConfig {
|
export interface AppConfig {
|
||||||
|
|||||||
+1
-3
@@ -103,7 +103,5 @@ export interface ProxyUsageRecord {
|
|||||||
export interface FailoverQueueItem {
|
export interface FailoverQueueItem {
|
||||||
providerId: string;
|
providerId: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
queueOrder: number;
|
sortIndex?: number;
|
||||||
enabled: boolean;
|
|
||||||
createdAt: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user