From 2a4fcc46ff8f121e8a0da527514947a7603fb9a9 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Mon, 8 Dec 2025 10:56:31 +0800 Subject: [PATCH] feat(db): add circuit breaker config table and provider proxy target APIs Add database support for auto-failover feature: - Add circuit_breaker_config table for storing failover thresholds - Add get/update_circuit_breaker_config methods in proxy DAO - Add reset_provider_health method for manual recovery - Add set_proxy_target and get_proxy_targets methods in providers DAO for managing multi-provider failover configuration --- src-tauri/src/database/dao/providers.rs | 110 ++++++++++++++++++++++++ src-tauri/src/database/dao/proxy.rs | 77 +++++++++++++++++ src-tauri/src/database/schema.rs | 22 +++++ 3 files changed, 209 insertions(+) diff --git a/src-tauri/src/database/dao/providers.rs b/src-tauri/src/database/dao/providers.rs index c2fcc0b5e..63ccb70dc 100644 --- a/src-tauri/src/database/dao/providers.rs +++ b/src-tauri/src/database/dao/providers.rs @@ -357,6 +357,116 @@ impl Database { Ok(()) } + /// 设置单个供应商的代理目标状态(支持多个代理目标) + pub async fn set_proxy_target( + &self, + provider_id: &str, + app_type: &str, + enabled: bool, + ) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + conn.execute( + "UPDATE providers SET is_proxy_target = ?1 + WHERE id = ?2 AND app_type = ?3", + params![if enabled { 1 } else { 0 }, provider_id, app_type], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) + } + + /// 获取指定应用类型的所有代理目标供应商(按 sort_index 排序) + pub async fn get_proxy_targets( + &self, + app_type: &str, + ) -> 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, is_proxy_target + FROM providers WHERE app_type = ?1 AND is_proxy_target = 1 + ORDER BY COALESCE(sort_index, 999999), created_at ASC, id ASC" + ).map_err(|e| AppError::Database(e.to_string()))?; + + let provider_iter = stmt + .query_map(params![app_type], |row| { + let id: String = row.get(0)?; + let name: String = row.get(1)?; + let settings_config_str: String = row.get(2)?; + let website_url: Option = row.get(3)?; + let category: Option = row.get(4)?; + let created_at: Option = row.get(5)?; + let sort_index: Option = row.get(6)?; + let notes: Option = row.get(7)?; + let icon: Option = row.get(8)?; + let icon_color: Option = row.get(9)?; + let meta_str: String = row.get(10)?; + let is_proxy_target: bool = row.get(11)?; + + 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(); + + Ok(( + id, + Provider { + id: "".to_string(), + name, + settings_config, + website_url, + category, + created_at, + sort_index, + notes, + meta: Some(meta), + icon, + icon_color, + is_proxy_target: Some(is_proxy_target), + }, + )) + }) + .map_err(|e| AppError::Database(e.to_string()))?; + + let mut providers = IndexMap::new(); + for provider_res in provider_iter { + let (id, mut provider) = provider_res.map_err(|e| AppError::Database(e.to_string()))?; + provider.id = id.clone(); + + // 加载 endpoints + let mut stmt_endpoints = conn.prepare( + "SELECT url, added_at FROM provider_endpoints WHERE provider_id = ?1 AND app_type = ?2 ORDER BY added_at ASC, url ASC" + ).map_err(|e| AppError::Database(e.to_string()))?; + + let endpoints_iter = stmt_endpoints + .query_map(params![id, app_type], |row| { + let url: String = row.get(0)?; + let added_at: Option = row.get(1)?; + Ok(( + url, + crate::settings::CustomEndpoint { + url: "".to_string(), + added_at: added_at.unwrap_or(0), + last_used: None, + }, + )) + }) + .map_err(|e| AppError::Database(e.to_string()))?; + + let mut custom_endpoints = HashMap::new(); + for ep_res in endpoints_iter { + let (url, mut ep) = ep_res.map_err(|e| AppError::Database(e.to_string()))?; + ep.url = url.clone(); + custom_endpoints.insert(url, ep); + } + + if let Some(meta) = &mut provider.meta { + meta.custom_endpoints = custom_endpoints; + } + + providers.insert(id, provider); + } + + Ok(providers) + } + /// 获取所有活跃的代理目标 pub fn get_all_proxy_targets(&self) -> Result, AppError> { let conn = lock_conn!(self.conn); diff --git a/src-tauri/src/database/dao/proxy.rs b/src-tauri/src/database/dao/proxy.rs index 187415f8d..0c451512d 100644 --- a/src-tauri/src/database/dao/proxy.rs +++ b/src-tauri/src/database/dao/proxy.rs @@ -165,6 +165,25 @@ impl Database { Ok(()) } + /// 重置Provider健康状态 + pub async fn reset_provider_health( + &self, + provider_id: &str, + app_type: &str, + ) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + conn.execute( + "DELETE FROM provider_health WHERE provider_id = ?1 AND app_type = ?2", + rusqlite::params![provider_id, app_type], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + log::debug!("Reset health status for provider {provider_id} (app: {app_type})"); + + Ok(()) + } + // ==================== Proxy Usage (可选) ==================== /// 记录代理使用统计 @@ -241,4 +260,62 @@ impl Database { Ok(records) } + + // ==================== Circuit Breaker Config ==================== + + /// 获取熔断器配置 + pub async fn get_circuit_breaker_config( + &self, + ) -> Result { + let conn = lock_conn!(self.conn); + + let config = conn + .query_row( + "SELECT failure_threshold, success_threshold, timeout_seconds, + error_rate_threshold, min_requests + FROM circuit_breaker_config WHERE id = 1", + [], + |row| { + Ok(crate::proxy::circuit_breaker::CircuitBreakerConfig { + failure_threshold: row.get::<_, i32>(0)? as u32, + success_threshold: row.get::<_, i32>(1)? as u32, + timeout_seconds: row.get::<_, i64>(2)? as u64, + error_rate_threshold: row.get(3)?, + min_requests: row.get::<_, i32>(4)? as u32, + }) + }, + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + Ok(config) + } + + /// 更新熔断器配置 + pub async fn update_circuit_breaker_config( + &self, + config: &crate::proxy::circuit_breaker::CircuitBreakerConfig, + ) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + conn.execute( + "UPDATE circuit_breaker_config + SET failure_threshold = ?1, + success_threshold = ?2, + timeout_seconds = ?3, + error_rate_threshold = ?4, + min_requests = ?5, + updated_at = CURRENT_TIMESTAMP + WHERE id = 1", + rusqlite::params![ + config.failure_threshold as i32, + config.success_threshold as i32, + config.timeout_seconds as i64, + config.error_rate_threshold, + config.min_requests as i32, + ], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + Ok(()) + } } diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 11b399ad6..14ae7e58b 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -336,6 +336,28 @@ impl Database { ) .map_err(|e| AppError::Database(e.to_string()))?; + // 15. Circuit Breaker Config 表 (熔断器配置) + conn.execute( + "CREATE TABLE IF NOT EXISTS circuit_breaker_config ( + id INTEGER PRIMARY KEY CHECK (id = 1), + failure_threshold INTEGER NOT NULL DEFAULT 5, + success_threshold INTEGER NOT NULL DEFAULT 2, + timeout_seconds INTEGER NOT NULL DEFAULT 60, + error_rate_threshold REAL NOT NULL DEFAULT 0.5, + min_requests INTEGER NOT NULL DEFAULT 10, + updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP + )", + [], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + // 插入默认熔断器配置 + conn.execute( + "INSERT OR IGNORE INTO circuit_breaker_config (id) VALUES (1)", + [], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(()) }