From d6ed95078c17ec5f6666a932ce7bbf1e95ba7909 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Wed, 24 Dec 2025 23:42:27 +0800 Subject: [PATCH] refactor(database): migrate proxy_config to per-app three-row structure Replace singleton proxy_config table with app_type primary key structure, allowing independent proxy settings for Claude, Codex, and Gemini. Add GlobalProxyConfig queries and per-app config management in DAO layer. --- src-tauri/src/database/dao/proxy.rs | 324 +++++++++++++---- src-tauri/src/database/dao/settings.rs | 22 +- src-tauri/src/database/schema.rs | 470 +++++++++++++------------ src/lib/api/proxy.ts | 89 +++++ src/lib/query/proxy.ts | 222 ++++++++++++ 5 files changed, 839 insertions(+), 288 deletions(-) create mode 100644 src/lib/api/proxy.ts create mode 100644 src/lib/query/proxy.ts diff --git a/src-tauri/src/database/dao/proxy.rs b/src-tauri/src/database/dao/proxy.rs index 7f5c24be2..886fbc528 100644 --- a/src-tauri/src/database/dao/proxy.rs +++ b/src-tauri/src/database/dao/proxy.rs @@ -8,69 +8,251 @@ use crate::proxy::types::*; use super::super::{lock_conn, Database}; impl Database { - // ==================== Proxy Config ==================== + // ==================== Global Proxy Config ==================== - /// 获取代理配置 + /// 获取全局代理配置(统一字段) + /// + /// 从 claude 行读取(三行镜像一致) + pub async fn get_global_proxy_config(&self) -> Result { + // 使用 block 限制 conn 的作用域,避免跨 await 持有锁 + let result = { + let conn = lock_conn!(self.conn); + conn.query_row( + "SELECT proxy_enabled, listen_address, listen_port, enable_logging + FROM proxy_config WHERE app_type = 'claude'", + [], + |row| { + Ok(GlobalProxyConfig { + proxy_enabled: row.get::<_, i32>(0)? != 0, + listen_address: row.get(1)?, + listen_port: row.get::<_, i32>(2)? as u16, + enable_logging: row.get::<_, i32>(3)? != 0, + }) + }, + ) + }; + // conn 已在 block 结束时释放 + + match result { + Ok(config) => Ok(config), + Err(rusqlite::Error::QueryReturnedNoRows) => { + // 如果不存在,创建默认配置 + self.init_proxy_config_rows().await?; + Ok(GlobalProxyConfig { + proxy_enabled: false, + listen_address: "127.0.0.1".to_string(), + listen_port: 5000, + enable_logging: true, + }) + } + Err(e) => Err(AppError::Database(e.to_string())), + } + } + + /// 更新全局代理配置(镜像写三行) + pub async fn update_global_proxy_config( + &self, + config: GlobalProxyConfig, + ) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + conn.execute( + "UPDATE proxy_config SET + proxy_enabled = ?1, + listen_address = ?2, + listen_port = ?3, + enable_logging = ?4, + updated_at = datetime('now')", + rusqlite::params![ + if config.proxy_enabled { 1 } else { 0 }, + config.listen_address, + config.listen_port as i32, + if config.enable_logging { 1 } else { 0 }, + ], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + Ok(()) + } + + /// 获取应用级代理配置 + pub async fn get_proxy_config_for_app( + &self, + app_type: &str, + ) -> Result { + // 使用 block 限制 conn 的作用域,避免跨 await 持有锁 + let app_type_owned = app_type.to_string(); + let result = { + let conn = lock_conn!(self.conn); + conn.query_row( + "SELECT app_type, enabled, auto_failover_enabled, + max_retries, streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout, + circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds, + circuit_error_rate_threshold, circuit_min_requests + FROM proxy_config WHERE app_type = ?1", + [app_type], + |row| { + Ok(AppProxyConfig { + app_type: row.get(0)?, + enabled: row.get::<_, i32>(1)? != 0, + auto_failover_enabled: row.get::<_, i32>(2)? != 0, + max_retries: row.get::<_, i32>(3)? as u32, + streaming_first_byte_timeout: row.get::<_, i32>(4)? as u32, + streaming_idle_timeout: row.get::<_, i32>(5)? as u32, + non_streaming_timeout: row.get::<_, i32>(6)? as u32, + circuit_failure_threshold: row.get::<_, i32>(7)? as u32, + circuit_success_threshold: row.get::<_, i32>(8)? as u32, + circuit_timeout_seconds: row.get::<_, i32>(9)? as u32, + circuit_error_rate_threshold: row.get(10)?, + circuit_min_requests: row.get::<_, i32>(11)? as u32, + }) + }, + ) + }; + // conn 已在 block 结束时释放 + + match result { + Ok(config) => Ok(config), + Err(rusqlite::Error::QueryReturnedNoRows) => { + // 如果不存在,创建默认配置 + self.init_proxy_config_rows().await?; + Ok(AppProxyConfig { + app_type: app_type_owned, + enabled: false, + auto_failover_enabled: false, + max_retries: 3, + streaming_first_byte_timeout: 30, + streaming_idle_timeout: 60, + non_streaming_timeout: 300, + circuit_failure_threshold: 5, + circuit_success_threshold: 2, + circuit_timeout_seconds: 60, + circuit_error_rate_threshold: 0.5, + circuit_min_requests: 10, + }) + } + Err(e) => Err(AppError::Database(e.to_string())), + } + } + + /// 更新应用级代理配置 + pub async fn update_proxy_config_for_app( + &self, + config: AppProxyConfig, + ) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + conn.execute( + "UPDATE proxy_config SET + enabled = ?2, + auto_failover_enabled = ?3, + max_retries = ?4, + streaming_first_byte_timeout = ?5, + streaming_idle_timeout = ?6, + non_streaming_timeout = ?7, + circuit_failure_threshold = ?8, + circuit_success_threshold = ?9, + circuit_timeout_seconds = ?10, + circuit_error_rate_threshold = ?11, + circuit_min_requests = ?12, + updated_at = datetime('now') + WHERE app_type = ?1", + rusqlite::params![ + config.app_type, + if config.enabled { 1 } else { 0 }, + if config.auto_failover_enabled { 1 } else { 0 }, + config.max_retries as i32, + config.streaming_first_byte_timeout as i32, + config.streaming_idle_timeout as i32, + config.non_streaming_timeout as i32, + config.circuit_failure_threshold as i32, + config.circuit_success_threshold as i32, + config.circuit_timeout_seconds as i32, + config.circuit_error_rate_threshold, + config.circuit_min_requests as i32, + ], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + + Ok(()) + } + + /// 初始化 proxy_config 表的三行数据 + async fn init_proxy_config_rows(&self) -> Result<(), AppError> { + let conn = lock_conn!(self.conn); + + for app_type in &["claude", "codex", "gemini"] { + conn.execute( + "INSERT OR IGNORE INTO proxy_config (app_type) VALUES (?1)", + [app_type], + ) + .map_err(|e| AppError::Database(e.to_string()))?; + } + + Ok(()) + } + + // ==================== Legacy Proxy Config (兼容旧代码) ==================== + + /// 获取代理配置(兼容旧接口,返回 claude 行的配置) pub async fn get_proxy_config(&self) -> Result { - // 在一个作用域内获取锁并查询,确保锁在await之前释放 + // 使用 block 限制 conn 的作用域,避免跨 await 持有锁 let result = { let conn = lock_conn!(self.conn); conn.query_row( "SELECT listen_address, listen_port, max_retries, - request_timeout, enable_logging, live_takeover_active, + enable_logging, streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout - FROM proxy_config WHERE id = 1", + FROM proxy_config WHERE app_type = 'claude'", [], |row| { Ok(ProxyConfig { listen_address: row.get(0)?, listen_port: row.get::<_, i32>(1)? as u16, max_retries: row.get::<_, i32>(2)? as u8, - request_timeout: row.get::<_, i32>(3)? as u64, - enable_logging: row.get::<_, i32>(4)? != 0, - live_takeover_active: row.get::<_, i32>(5).unwrap_or(0) != 0, - streaming_first_byte_timeout: row.get::<_, i32>(6).unwrap_or(30) as u64, - streaming_idle_timeout: row.get::<_, i32>(7).unwrap_or(60) as u64, - non_streaming_timeout: row.get::<_, i32>(8).unwrap_or(300) as u64, + request_timeout: 300, // 废弃字段,返回默认值 + enable_logging: row.get::<_, i32>(3)? != 0, + live_takeover_active: false, // 废弃字段 + streaming_first_byte_timeout: row.get::<_, i32>(4).unwrap_or(30) as u64, + streaming_idle_timeout: row.get::<_, i32>(5).unwrap_or(60) as u64, + non_streaming_timeout: row.get::<_, i32>(6).unwrap_or(300) as u64, }) }, ) - }; // conn锁在这里释放 + }; + // conn 已在 block 结束时释放 match result { Ok(config) => Ok(config), Err(rusqlite::Error::QueryReturnedNoRows) => { - // 如果不存在,插入默认配置 - let default_config = ProxyConfig::default(); - self.update_proxy_config(default_config.clone()).await?; - Ok(default_config) + // 如果不存在,初始化默认配置 + self.init_proxy_config_rows().await?; + Ok(ProxyConfig::default()) } Err(e) => Err(AppError::Database(e.to_string())), } } - /// 更新代理配置 + /// 更新代理配置(兼容旧接口,更新所有三行的公共字段) pub async fn update_proxy_config(&self, config: ProxyConfig) -> Result<(), AppError> { let conn = lock_conn!(self.conn); + // 更新所有三行的公共字段 conn.execute( - "INSERT OR REPLACE INTO proxy_config - (id, enabled, listen_address, listen_port, max_retries, request_timeout, - enable_logging, live_takeover_active, target_app, - streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout, - created_at, updated_at) - VALUES (1, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, - COALESCE((SELECT created_at FROM proxy_config WHERE id = 1), datetime('now')), - datetime('now'))", + "UPDATE proxy_config SET + listen_address = ?1, + listen_port = ?2, + max_retries = ?3, + enable_logging = ?4, + streaming_first_byte_timeout = ?5, + streaming_idle_timeout = ?6, + non_streaming_timeout = ?7, + updated_at = datetime('now')", rusqlite::params![ - 0, // 已移除自动启用逻辑,保留列但固定为 0 config.listen_address, config.listen_port as i32, config.max_retries as i32, - config.request_timeout as i32, if config.enable_logging { 1 } else { 0 }, - if config.live_takeover_active { 1 } else { 0 }, - "claude", // 兼容旧字段,写入默认值 config.streaming_first_byte_timeout as i32, config.streaming_idle_timeout as i32, config.non_streaming_timeout as i32, @@ -81,27 +263,26 @@ impl Database { Ok(()) } - /// 设置 Live 接管状态(仅更新 proxy_config 表,兼容旧逻辑) - /// - /// 注意:此方法不会清除 settings 表中的 proxy_takeover_* 状态。 - /// settings 表的状态由 set_proxy_takeover_enabled 单独管理,用于跨重启保持状态。 - pub async fn set_live_takeover_active(&self, active: bool) -> Result<(), AppError> { - // 仅更新 proxy_config 表(兼容旧版本) - let conn = lock_conn!(self.conn); - conn.execute( - "UPDATE proxy_config SET live_takeover_active = ?1, updated_at = datetime('now') WHERE id = 1", - rusqlite::params![if active { 1 } else { 0 }], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - + /// 设置 Live 接管状态(兼容旧版本,更新 enabled 字段) + pub async fn set_live_takeover_active(&self, _active: bool) -> Result<(), AppError> { + // 不再使用此字段,由 enabled 字段替代 + // 保留空实现以兼容旧代码 Ok(()) } /// 检查是否处于 Live 接管模式 /// - /// v3.8.0+:以 settings 表中的 `proxy_takeover_{app_type}` 为真实来源 + /// 检查是否有任一 app 的 enabled = true pub async fn is_live_takeover_active(&self) -> Result { - self.has_any_proxy_takeover() + let conn = lock_conn!(self.conn); + let count: i64 = conn + .query_row( + "SELECT COUNT(*) FROM proxy_config WHERE enabled = 1", + [], + |row| row.get(0), + ) + .map_err(|e| AppError::Database(e.to_string()))?; + Ok(count > 0) } // ==================== Provider Health ==================== @@ -280,19 +461,22 @@ impl Database { Ok(()) } - // ==================== Circuit Breaker Config ==================== + // ==================== Circuit Breaker Config (Legacy Compatibility) ==================== - /// 获取熔断器配置 + /// 获取熔断器配置(兼容旧接口,从 claude 行读取) + /// + /// 熔断器配置已合并到 proxy_config 表,每 app 独立 + /// 此方法保留用于兼容旧代码,建议使用 get_proxy_config_for_app 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", + // 使用 block 限制 conn 的作用域,避免跨 await 持有锁 + let result = { + let conn = lock_conn!(self.conn); + conn.query_row( + "SELECT circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds, + circuit_error_rate_threshold, circuit_min_requests + FROM proxy_config WHERE app_type = 'claude'", [], |row| { Ok(crate::proxy::circuit_breaker::CircuitBreakerConfig { @@ -304,27 +488,39 @@ impl Database { }) }, ) - .map_err(|e| AppError::Database(e.to_string()))?; + }; + // conn 已在 block 结束时释放 - Ok(config) + match result { + Ok(config) => Ok(config), + Err(rusqlite::Error::QueryReturnedNoRows) => { + // 如果不存在,初始化默认配置 + self.init_proxy_config_rows().await?; + Ok(crate::proxy::circuit_breaker::CircuitBreakerConfig::default()) + } + Err(e) => Err(AppError::Database(e.to_string())), + } } - /// 更新熔断器配置 + /// 更新熔断器配置(兼容旧接口,更新所有三行) + /// + /// 熔断器配置已合并到 proxy_config 表 + /// 此方法保留用于兼容旧代码,建议使用 update_proxy_config_for_app 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", + "UPDATE proxy_config SET + circuit_failure_threshold = ?1, + circuit_success_threshold = ?2, + circuit_timeout_seconds = ?3, + circuit_error_rate_threshold = ?4, + circuit_min_requests = ?5, + updated_at = datetime('now')", rusqlite::params![ config.failure_threshold as i32, config.success_threshold as i32, diff --git a/src-tauri/src/database/dao/settings.rs b/src-tauri/src/database/dao/settings.rs index 68a5c68b8..65d2328f9 100644 --- a/src-tauri/src/database/dao/settings.rs +++ b/src-tauri/src/database/dao/settings.rs @@ -63,11 +63,13 @@ impl Database { } } - // --- 代理接管状态管理 --- + // --- 代理接管状态管理(已废弃,使用 proxy_config.enabled 替代)--- /// 获取指定应用的代理接管状态 /// - /// 使用 settings 表存储各应用的接管状态,key 格式: `proxy_takeover_{app_type}` + /// **已废弃**: 请使用 `proxy_config.enabled` 字段替代 + /// 此方法仅用于数据库迁移时读取旧数据 + #[deprecated(since = "3.9.0", note = "使用 get_proxy_config_for_app().enabled 替代")] pub fn get_proxy_takeover_enabled(&self, app_type: &str) -> Result { let key = format!("proxy_takeover_{app_type}"); match self.get_setting(&key)? { @@ -78,8 +80,11 @@ impl Database { /// 设置指定应用的代理接管状态 /// - /// - `true` = 开启代理接管 - /// - `false` = 关闭代理接管 + /// **已废弃**: 请使用 `proxy_config.enabled` 字段替代 + #[deprecated( + since = "3.9.0", + note = "使用 update_proxy_config_for_app() 修改 enabled 字段" + )] pub fn set_proxy_takeover_enabled( &self, app_type: &str, @@ -91,6 +96,9 @@ impl Database { } /// 检查是否有任一应用开启了代理接管 + /// + /// **已废弃**: 请使用 `is_live_takeover_active()` 替代 + #[deprecated(since = "3.9.0", note = "使用 is_live_takeover_active() 替代")] pub fn has_any_proxy_takeover(&self) -> Result { let conn = lock_conn!(self.conn); let count: i64 = conn @@ -104,6 +112,12 @@ impl Database { } /// 清除所有代理接管状态(将所有 proxy_takeover_* 设置为 false) + /// + /// **已废弃**: settings 表不再用于存储代理状态 + #[deprecated( + since = "3.9.0", + note = "使用 update_proxy_config_for_app() 清除各应用的 enabled 字段" + )] pub fn clear_all_proxy_takeover(&self) -> Result<(), AppError> { let conn = lock_conn!(self.conn); conn.execute( diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 5e63436bf..70ab96d68 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -55,47 +55,28 @@ impl Database { // 3. MCP Servers 表 conn.execute( "CREATE TABLE IF NOT EXISTS mcp_servers ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - server_config TEXT NOT NULL, - description TEXT, - homepage TEXT, - docs TEXT, - tags TEXT NOT NULL DEFAULT '[]', - enabled_claude BOOLEAN NOT NULL DEFAULT 0, - enabled_codex BOOLEAN NOT NULL DEFAULT 0, - enabled_gemini BOOLEAN NOT NULL DEFAULT 0 - )", + id TEXT PRIMARY KEY, name TEXT NOT NULL, server_config TEXT NOT NULL, + description TEXT, homepage TEXT, docs TEXT, tags TEXT NOT NULL DEFAULT '[]', + enabled_claude BOOLEAN NOT NULL DEFAULT 0, enabled_codex BOOLEAN NOT NULL DEFAULT 0, + enabled_gemini BOOLEAN NOT NULL DEFAULT 0 + )", [], ) .map_err(|e| AppError::Database(e.to_string()))?; // 4. Prompts 表 - conn.execute( - "CREATE TABLE IF NOT EXISTS prompts ( - id TEXT NOT NULL, - app_type TEXT NOT NULL, - name TEXT NOT NULL, - content TEXT NOT NULL, - description TEXT, - enabled BOOLEAN NOT NULL DEFAULT 1, - created_at INTEGER, - updated_at INTEGER, - PRIMARY KEY (id, app_type) - )", - [], - ) - .map_err(|e| AppError::Database(e.to_string()))?; + conn.execute("CREATE TABLE IF NOT EXISTS prompts ( + id TEXT NOT NULL, app_type TEXT NOT NULL, name TEXT NOT NULL, content TEXT NOT NULL, + description TEXT, enabled BOOLEAN NOT NULL DEFAULT 1, created_at INTEGER, updated_at INTEGER, + PRIMARY KEY (id, app_type) + )", []).map_err(|e| AppError::Database(e.to_string()))?; // 5. Skills 表 conn.execute( "CREATE TABLE IF NOT EXISTS skills ( - directory TEXT NOT NULL, - app_type TEXT NOT NULL, - installed BOOLEAN NOT NULL DEFAULT 0, - installed_at INTEGER NOT NULL DEFAULT 0, - PRIMARY KEY (directory, app_type) - )", + directory TEXT NOT NULL, app_type TEXT NOT NULL, installed BOOLEAN NOT NULL DEFAULT 0, + installed_at INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (directory, app_type) + )", [], ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -103,172 +84,124 @@ impl Database { // 6. Skill Repos 表 conn.execute( "CREATE TABLE IF NOT EXISTS skill_repos ( - owner TEXT NOT NULL, - name TEXT NOT NULL, - branch TEXT NOT NULL DEFAULT 'main', - enabled BOOLEAN NOT NULL DEFAULT 1, - PRIMARY KEY (owner, name) - )", + owner TEXT NOT NULL, name TEXT NOT NULL, branch TEXT NOT NULL DEFAULT 'main', + enabled BOOLEAN NOT NULL DEFAULT 1, PRIMARY KEY (owner, name) + )", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - // 7. Settings 表 (通用配置) + // 7. Settings 表 conn.execute( - "CREATE TABLE IF NOT EXISTS settings ( - key TEXT PRIMARY KEY, - value TEXT - )", + "CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - // 8. Proxy Config 表 (代理服务器配置) - // 代理配置表(单例) + // 8. Proxy Config 表(三行结构,app_type 主键) + conn.execute("CREATE TABLE IF NOT EXISTS proxy_config ( + app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')), + proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1', + listen_port INTEGER NOT NULL DEFAULT 5000, enable_logging INTEGER NOT NULL DEFAULT 1, + enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0, + max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30, + streaming_idle_timeout INTEGER NOT NULL DEFAULT 60, non_streaming_timeout INTEGER NOT NULL DEFAULT 300, + circuit_failure_threshold INTEGER NOT NULL DEFAULT 5, circuit_success_threshold INTEGER NOT NULL DEFAULT 2, + circuit_timeout_seconds INTEGER NOT NULL DEFAULT 60, circuit_error_rate_threshold REAL NOT NULL DEFAULT 0.5, + circuit_min_requests INTEGER NOT NULL DEFAULT 10, + created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) + )", []).map_err(|e| AppError::Database(e.to_string()))?; + + // 初始化三行数据(每应用不同默认值) conn.execute( - "CREATE TABLE IF NOT EXISTS proxy_config ( - id INTEGER PRIMARY KEY CHECK (id = 1), - enabled INTEGER NOT NULL DEFAULT 0, - listen_address TEXT NOT NULL DEFAULT '127.0.0.1', - listen_port INTEGER NOT NULL DEFAULT 5000, - max_retries INTEGER NOT NULL DEFAULT 3, - request_timeout INTEGER NOT NULL DEFAULT 300, - enable_logging INTEGER NOT NULL DEFAULT 1, - target_app TEXT NOT NULL DEFAULT 'claude', - streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30, - streaming_idle_timeout INTEGER NOT NULL DEFAULT 60, - non_streaming_timeout INTEGER NOT NULL DEFAULT 300, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')) - )", + "INSERT OR IGNORE INTO proxy_config (app_type, max_retries, + streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout, + circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds, + circuit_error_rate_threshold, circuit_min_requests) + VALUES ('claude', 6, 45, 90, 300, 8, 3, 90, 0.6, 15)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - - // 尝试添加 target_app 列(如果表已存在但缺少该列) - // 忽略 "duplicate column name" 错误 - let _ = conn.execute( - "ALTER TABLE proxy_config ADD COLUMN target_app TEXT NOT NULL DEFAULT 'claude'", - [], - ); - - // 9. Provider Health 表 (Provider健康状态) conn.execute( - "CREATE TABLE IF NOT EXISTS provider_health ( - provider_id TEXT NOT NULL, - app_type TEXT NOT NULL, - is_healthy INTEGER NOT NULL DEFAULT 1, - consecutive_failures INTEGER NOT NULL DEFAULT 0, - last_success_at TEXT, - last_failure_at TEXT, - last_error TEXT, - updated_at TEXT NOT NULL, - PRIMARY KEY (provider_id, app_type), - FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE - )", + "INSERT OR IGNORE INTO proxy_config (app_type, max_retries, + streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout, + circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds, + circuit_error_rate_threshold, circuit_min_requests) + VALUES ('codex', 3, 30, 60, 300, 5, 2, 60, 0.5, 10)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - - // 10. Proxy Request Logs 表 (详细请求日志) conn.execute( - "CREATE TABLE IF NOT EXISTS proxy_request_logs ( - request_id TEXT PRIMARY KEY, - provider_id TEXT NOT NULL, - app_type TEXT NOT NULL, - model TEXT NOT NULL, - input_tokens INTEGER NOT NULL DEFAULT 0, - output_tokens INTEGER NOT NULL DEFAULT 0, - cache_read_tokens INTEGER NOT NULL DEFAULT 0, - cache_creation_tokens INTEGER NOT NULL DEFAULT 0, - input_cost_usd TEXT NOT NULL DEFAULT '0', - output_cost_usd TEXT NOT NULL DEFAULT '0', - cache_read_cost_usd TEXT NOT NULL DEFAULT '0', - cache_creation_cost_usd TEXT NOT NULL DEFAULT '0', - total_cost_usd TEXT NOT NULL DEFAULT '0', - latency_ms INTEGER NOT NULL, - first_token_ms INTEGER, - duration_ms INTEGER, - status_code INTEGER NOT NULL, - error_message TEXT, - session_id TEXT, - provider_type TEXT, - is_streaming INTEGER NOT NULL DEFAULT 0, - cost_multiplier TEXT NOT NULL DEFAULT '1.0', - created_at INTEGER NOT NULL - )", + "INSERT OR IGNORE INTO proxy_config (app_type, max_retries, + streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout, + circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds, + circuit_error_rate_threshold, circuit_min_requests) + VALUES ('gemini', 5, 30, 60, 300, 5, 2, 60, 0.5, 10)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; + // 9. Provider Health 表 + conn.execute("CREATE TABLE IF NOT EXISTS provider_health ( + provider_id TEXT NOT NULL, app_type TEXT NOT NULL, is_healthy INTEGER NOT NULL DEFAULT 1, + consecutive_failures INTEGER NOT NULL DEFAULT 0, last_success_at TEXT, last_failure_at TEXT, + last_error TEXT, updated_at TEXT NOT NULL, + PRIMARY KEY (provider_id, app_type), + FOREIGN KEY (provider_id, app_type) REFERENCES providers(id, app_type) ON DELETE CASCADE + )", []).map_err(|e| AppError::Database(e.to_string()))?; + + // 10. Proxy Request Logs 表 + conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs ( + request_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, app_type TEXT NOT NULL, model TEXT NOT NULL, + input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, + cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0, + input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0', + cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0', + total_cost_usd TEXT NOT NULL DEFAULT '0', latency_ms INTEGER NOT NULL, first_token_ms INTEGER, + duration_ms INTEGER, status_code INTEGER NOT NULL, error_message TEXT, session_id TEXT, + provider_type TEXT, is_streaming INTEGER NOT NULL DEFAULT 0, + cost_multiplier TEXT NOT NULL DEFAULT '1.0', created_at INTEGER NOT NULL + )", []).map_err(|e| AppError::Database(e.to_string()))?; + + conn.execute("CREATE INDEX IF NOT EXISTS idx_request_logs_provider ON proxy_request_logs(provider_id, app_type)", []) + .map_err(|e| AppError::Database(e.to_string()))?; + conn.execute("CREATE INDEX IF NOT EXISTS idx_request_logs_created_at ON proxy_request_logs(created_at)", []) + .map_err(|e| AppError::Database(e.to_string()))?; conn.execute( - "CREATE INDEX IF NOT EXISTS idx_request_logs_provider - ON proxy_request_logs(provider_id, app_type)", + "CREATE INDEX IF NOT EXISTS idx_request_logs_model ON proxy_request_logs(model)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_request_logs_created_at - ON proxy_request_logs(created_at)", + "CREATE INDEX IF NOT EXISTS idx_request_logs_session ON proxy_request_logs(session_id)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_request_logs_model - ON proxy_request_logs(model)", + "CREATE INDEX IF NOT EXISTS idx_request_logs_status ON proxy_request_logs(status_code)", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_request_logs_session - ON proxy_request_logs(session_id)", - [], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - - conn.execute( - "CREATE INDEX IF NOT EXISTS idx_request_logs_status - ON proxy_request_logs(status_code)", - [], - ) - .map_err(|e| AppError::Database(e.to_string()))?; - - // 11. Model Pricing 表 (模型定价) + // 11. Model Pricing 表 conn.execute( "CREATE TABLE IF NOT EXISTS model_pricing ( - model_id TEXT PRIMARY KEY, - display_name TEXT NOT NULL, - input_cost_per_million TEXT NOT NULL, - output_cost_per_million TEXT NOT NULL, - cache_read_cost_per_million TEXT NOT NULL DEFAULT '0', - cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0' - )", + model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, + input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL, + cache_read_cost_per_million TEXT NOT NULL DEFAULT '0', + cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0' + )", [], ) .map_err(|e| AppError::Database(e.to_string()))?; - // 12. Stream Check Logs 表 (流式健康检查日志) - conn.execute( - "CREATE TABLE IF NOT EXISTS stream_check_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - provider_id TEXT NOT NULL, - provider_name TEXT NOT NULL, - app_type TEXT NOT NULL, - status TEXT NOT NULL, - success INTEGER NOT NULL, - message TEXT NOT NULL, - response_time_ms INTEGER, - http_status INTEGER, - model_used TEXT, - retry_count INTEGER DEFAULT 0, - tested_at INTEGER NOT NULL - )", - [], - ) - .map_err(|e| AppError::Database(e.to_string()))?; + // 12. Stream Check Logs 表 + conn.execute("CREATE TABLE IF NOT EXISTS stream_check_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, provider_id TEXT NOT NULL, provider_name TEXT NOT NULL, + app_type TEXT NOT NULL, status TEXT NOT NULL, success INTEGER NOT NULL, message TEXT NOT NULL, + response_time_ms INTEGER, http_status INTEGER, model_used TEXT, + retry_count INTEGER DEFAULT 0, tested_at INTEGER NOT NULL + )", []).map_err(|e| AppError::Database(e.to_string()))?; conn.execute( "CREATE INDEX IF NOT EXISTS idx_stream_check_logs_provider @@ -277,35 +210,13 @@ impl Database { ) .map_err(|e| AppError::Database(e.to_string()))?; - // 13. 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()))?; + // 注意:circuit_breaker_config 已合并到 proxy_config 表中 // 16. Proxy Live Backup 表 (Live 配置备份) conn.execute( "CREATE TABLE IF NOT EXISTS proxy_live_backup ( - app_type TEXT PRIMARY KEY, - original_config TEXT NOT NULL, - backed_up_at TEXT NOT NULL - )", + app_type TEXT PRIMARY KEY, original_config TEXT NOT NULL, backed_up_at TEXT NOT NULL + )", [], ) .map_err(|e| AppError::Database(e.to_string()))?; @@ -316,7 +227,7 @@ impl Database { [], ); - // 尝试添加超时配置列到 proxy_config 表(v3 新增) + // 尝试添加超时配置列到 proxy_config 表 let _ = conn.execute( "ALTER TABLE proxy_config ADD COLUMN streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30", [], @@ -492,7 +403,7 @@ impl Database { "BOOLEAN NOT NULL DEFAULT 0", )?; - // 添加代理超时配置字段(在 v2 迁移中直接添加) + // 添加代理超时配置字段 if Self::table_exists(conn, "proxy_config")? { Self::add_column_if_missing( conn, @@ -528,35 +439,18 @@ impl Database { ) .map_err(|e| AppError::Database(format!("创建 failover 索引失败: {e}")))?; - // proxy_request_logs 表(包含所有字段) - conn.execute( - "CREATE TABLE IF NOT EXISTS proxy_request_logs ( - request_id TEXT PRIMARY KEY, - provider_id TEXT NOT NULL, - app_type TEXT NOT NULL, - model TEXT NOT NULL, - input_tokens INTEGER NOT NULL DEFAULT 0, - output_tokens INTEGER NOT NULL DEFAULT 0, - cache_read_tokens INTEGER NOT NULL DEFAULT 0, - cache_creation_tokens INTEGER NOT NULL DEFAULT 0, - input_cost_usd TEXT NOT NULL DEFAULT '0', - output_cost_usd TEXT NOT NULL DEFAULT '0', - cache_read_cost_usd TEXT NOT NULL DEFAULT '0', - cache_creation_cost_usd TEXT NOT NULL DEFAULT '0', - total_cost_usd TEXT NOT NULL DEFAULT '0', - latency_ms INTEGER NOT NULL, - first_token_ms INTEGER, - duration_ms INTEGER, - status_code INTEGER NOT NULL, - error_message TEXT, - session_id TEXT, - provider_type TEXT, - is_streaming INTEGER NOT NULL DEFAULT 0, - cost_multiplier TEXT NOT NULL DEFAULT '1.0', - created_at INTEGER NOT NULL - )", - [], - )?; + // proxy_request_logs 表 + conn.execute("CREATE TABLE IF NOT EXISTS proxy_request_logs ( + request_id TEXT PRIMARY KEY, provider_id TEXT NOT NULL, app_type TEXT NOT NULL, model TEXT NOT NULL, + input_tokens INTEGER NOT NULL DEFAULT 0, output_tokens INTEGER NOT NULL DEFAULT 0, + cache_read_tokens INTEGER NOT NULL DEFAULT 0, cache_creation_tokens INTEGER NOT NULL DEFAULT 0, + input_cost_usd TEXT NOT NULL DEFAULT '0', output_cost_usd TEXT NOT NULL DEFAULT '0', + cache_read_cost_usd TEXT NOT NULL DEFAULT '0', cache_creation_cost_usd TEXT NOT NULL DEFAULT '0', + total_cost_usd TEXT NOT NULL DEFAULT '0', latency_ms INTEGER NOT NULL, first_token_ms INTEGER, + duration_ms INTEGER, status_code INTEGER NOT NULL, error_message TEXT, session_id TEXT, + provider_type TEXT, is_streaming INTEGER NOT NULL DEFAULT 0, + cost_multiplier TEXT NOT NULL DEFAULT '1.0', created_at INTEGER NOT NULL + )", [])?; // 为已存在的表添加新字段 Self::add_column_if_missing(conn, "proxy_request_logs", "provider_type", "TEXT")?; @@ -578,13 +472,11 @@ impl Database { // model_pricing 表 conn.execute( "CREATE TABLE IF NOT EXISTS model_pricing ( - model_id TEXT PRIMARY KEY, - display_name TEXT NOT NULL, - input_cost_per_million TEXT NOT NULL, - output_cost_per_million TEXT NOT NULL, - cache_read_cost_per_million TEXT NOT NULL DEFAULT '0', - cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0' - )", + model_id TEXT PRIMARY KEY, display_name TEXT NOT NULL, + input_cost_per_million TEXT NOT NULL, output_cost_per_million TEXT NOT NULL, + cache_read_cost_per_million TEXT NOT NULL DEFAULT '0', + cache_creation_cost_per_million TEXT NOT NULL DEFAULT '0' + )", [], )?; @@ -596,6 +488,144 @@ impl Database { // 重构 skills 表(添加 app_type 字段) Self::migrate_skills_table(conn)?; + // 重构 proxy_config 为三行结构(每应用独立配置) + Self::migrate_proxy_config_to_per_app(conn)?; + + Ok(()) + } + + /// 将 proxy_config 迁移为三行结构(每应用独立配置) + fn migrate_proxy_config_to_per_app(conn: &Connection) -> Result<(), AppError> { + // 检查是否已经是新表结构(幂等性) + if !Self::table_exists(conn, "proxy_config")? { + // 表不存在,跳过迁移(新安装) + return Ok(()); + } + + if Self::has_column(conn, "proxy_config", "app_type")? { + // 已经是三行结构,跳过迁移 + log::info!("proxy_config 已经是三行结构,跳过迁移"); + return Ok(()); + } + + // 读取旧配置 + let old_config = conn + .query_row( + "SELECT listen_address, listen_port, max_retries, enable_logging, + streaming_first_byte_timeout, streaming_idle_timeout, non_streaming_timeout + FROM proxy_config WHERE id = 1", + [], + |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, i32>(1)?, + row.get::<_, i32>(2)?, + row.get::<_, i32>(3)?, + row.get::<_, i32>(4).unwrap_or(30), + row.get::<_, i32>(5).unwrap_or(60), + row.get::<_, i32>(6).unwrap_or(300), + )) + }, + ) + .unwrap_or_else(|_| ("127.0.0.1".to_string(), 5000, 3, 1, 30, 60, 300)); + + let old_cb = conn.query_row( + "SELECT failure_threshold, success_threshold, timeout_seconds, error_rate_threshold, min_requests + FROM circuit_breaker_config WHERE id = 1", [], + |row| Ok((row.get::<_, i32>(0)?, row.get::<_, i32>(1)?, row.get::<_, i64>(2)?, + row.get::<_, f64>(3)?, row.get::<_, i32>(4)?)) + ).unwrap_or((5, 2, 60, 0.5, 10)); + + let get_bool = |key: &str| -> bool { + conn.query_row("SELECT value FROM settings WHERE key = ?", [key], |r| { + r.get::<_, String>(0) + }) + .map(|v| v == "true" || v == "1") + .unwrap_or(false) + }; + + let apps = [ + ( + "claude", + get_bool("proxy_takeover_claude"), + get_bool("auto_failover_enabled_claude"), + 6, + 45, + 90, + 8, + 3, + 90, + 0.6, + 15, + ), + ( + "codex", + get_bool("proxy_takeover_codex"), + get_bool("auto_failover_enabled_codex"), + 3, + old_config.4, + old_config.5, + old_cb.0, + old_cb.1, + old_cb.2, + old_cb.3, + old_cb.4, + ), + ( + "gemini", + get_bool("proxy_takeover_gemini"), + get_bool("auto_failover_enabled_gemini"), + 5, + old_config.4, + old_config.5, + old_cb.0, + old_cb.1, + old_cb.2, + old_cb.3, + old_cb.4, + ), + ]; + + // 创建新表 + conn.execute("DROP TABLE IF EXISTS proxy_config_new", [])?; + conn.execute("CREATE TABLE proxy_config_new ( + app_type TEXT PRIMARY KEY CHECK (app_type IN ('claude','codex','gemini')), + proxy_enabled INTEGER NOT NULL DEFAULT 0, listen_address TEXT NOT NULL DEFAULT '127.0.0.1', + listen_port INTEGER NOT NULL DEFAULT 5000, enable_logging INTEGER NOT NULL DEFAULT 1, + enabled INTEGER NOT NULL DEFAULT 0, auto_failover_enabled INTEGER NOT NULL DEFAULT 0, + max_retries INTEGER NOT NULL DEFAULT 3, streaming_first_byte_timeout INTEGER NOT NULL DEFAULT 30, + streaming_idle_timeout INTEGER NOT NULL DEFAULT 60, non_streaming_timeout INTEGER NOT NULL DEFAULT 300, + circuit_failure_threshold INTEGER NOT NULL DEFAULT 5, circuit_success_threshold INTEGER NOT NULL DEFAULT 2, + circuit_timeout_seconds INTEGER NOT NULL DEFAULT 60, circuit_error_rate_threshold REAL NOT NULL DEFAULT 0.5, + circuit_min_requests INTEGER NOT NULL DEFAULT 10, + created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) + )", [])?; + + // 插入三行配置 + for (app, takeover, failover, retries, fb, idle, cb_f, cb_s, cb_t, cb_r, cb_m) in apps { + conn.execute( + "INSERT INTO proxy_config_new (app_type, proxy_enabled, listen_address, listen_port, enable_logging, + enabled, auto_failover_enabled, max_retries, streaming_first_byte_timeout, streaming_idle_timeout, + non_streaming_timeout, circuit_failure_threshold, circuit_success_threshold, circuit_timeout_seconds, + circuit_error_rate_threshold, circuit_min_requests) + VALUES (?1, 0, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)", + rusqlite::params![app, old_config.0, old_config.1, old_config.3, + if takeover { 1 } else { 0 }, if failover { 1 } else { 0 }, + retries, fb, idle, old_config.6, cb_f, cb_s, cb_t, cb_r, cb_m] + ).map_err(|e| AppError::Database(format!("插入 {app} 配置失败: {e}")))?; + } + + // 替换表并清理 + conn.execute("DROP TABLE IF EXISTS proxy_config", [])?; + conn.execute("ALTER TABLE proxy_config_new RENAME TO proxy_config", [])?; + conn.execute("DROP TABLE IF EXISTS circuit_breaker_config", [])?; + conn.execute("DELETE FROM settings WHERE key LIKE 'proxy_takeover_%'", [])?; + conn.execute( + "DELETE FROM settings WHERE key LIKE 'auto_failover_enabled_%'", + [], + )?; + + log::info!("proxy_config 已迁移为三行结构"); Ok(()) } diff --git a/src/lib/api/proxy.ts b/src/lib/api/proxy.ts new file mode 100644 index 000000000..a3777ee65 --- /dev/null +++ b/src/lib/api/proxy.ts @@ -0,0 +1,89 @@ +import { invoke } from "@tauri-apps/api/core"; +import type { + ProxyConfig, + ProxyStatus, + ProxyServerInfo, + ProxyTakeoverStatus, + GlobalProxyConfig, + AppProxyConfig, +} from "@/types/proxy"; + +export const proxyApi = { + // ========== 代理服务器控制 API ========== + + // 启动代理服务器 + async startProxyServer(): Promise { + return invoke("start_proxy_server"); + }, + + // 停止代理服务器并恢复配置 + async stopProxyWithRestore(): Promise { + return invoke("stop_proxy_with_restore"); + }, + + // 获取代理服务器状态 + async getProxyStatus(): Promise { + return invoke("get_proxy_status"); + }, + + // 检查代理服务器是否正在运行 + async isProxyRunning(): Promise { + return invoke("is_proxy_running"); + }, + + // 检查是否处于接管模式 + async isLiveTakeoverActive(): Promise { + return invoke("is_live_takeover_active"); + }, + + // 代理模式下切换供应商 + async switchProxyProvider(appType: string, providerId: string): Promise { + return invoke("switch_proxy_provider", { appType, providerId }); + }, + + // ========== 接管状态 API ========== + + // 获取各应用接管状态 + async getProxyTakeoverStatus(): Promise { + return invoke("get_proxy_takeover_status"); + }, + + // 为指定应用开启/关闭接管 + async setProxyTakeoverForApp(appType: string, enabled: boolean): Promise { + return invoke("set_proxy_takeover_for_app", { appType, enabled }); + }, + + // ========== Legacy 代理配置 API (兼容) ========== + + // 获取代理配置(旧版 v2 兼容接口) + async getProxyConfig(): Promise { + return invoke("get_proxy_config"); + }, + + // 更新代理配置(旧版 v2 兼容接口) + async updateProxyConfig(config: ProxyConfig): Promise { + return invoke("update_proxy_config", { config }); + }, + + // ========== v3+ 全局/应用级配置 API ========== + + // 获取全局代理配置 + async getGlobalProxyConfig(): Promise { + return invoke("get_global_proxy_config"); + }, + + // 更新全局代理配置 + async updateGlobalProxyConfig(config: GlobalProxyConfig): Promise { + return invoke("update_global_proxy_config", { config }); + }, + + // 获取指定应用的代理配置 + async getProxyConfigForApp(appType: string): Promise { + return invoke("get_proxy_config_for_app", { appType }); + }, + + // 更新指定应用的代理配置 + async updateProxyConfigForApp(config: AppProxyConfig): Promise { + return invoke("update_proxy_config_for_app", { config }); + }, +}; diff --git a/src/lib/query/proxy.ts b/src/lib/query/proxy.ts new file mode 100644 index 000000000..4d26a2d8b --- /dev/null +++ b/src/lib/query/proxy.ts @@ -0,0 +1,222 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { proxyApi } from "@/lib/api/proxy"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; +import type { GlobalProxyConfig, AppProxyConfig } from "@/types/proxy"; + +// ========== 代理服务器状态 Hooks ========== + +/** + * 获取代理服务器状态 + */ +export function useProxyStatus() { + return useQuery({ + queryKey: ["proxyStatus"], + queryFn: () => proxyApi.getProxyStatus(), + refetchInterval: 5000, // 每 5 秒刷新一次 + }); +} + +/** + * 检查代理服务器是否运行 + */ +export function useIsProxyRunning() { + return useQuery({ + queryKey: ["proxyRunning"], + queryFn: () => proxyApi.isProxyRunning(), + refetchInterval: 2000, + }); +} + +/** + * 检查是否处于接管模式 + */ +export function useIsLiveTakeoverActive() { + return useQuery({ + queryKey: ["liveTakeoverActive"], + queryFn: () => proxyApi.isLiveTakeoverActive(), + refetchInterval: 2000, + }); +} + +/** + * 获取各应用接管状态 + */ +export function useProxyTakeoverStatus() { + return useQuery({ + queryKey: ["proxyTakeoverStatus"], + queryFn: () => proxyApi.getProxyTakeoverStatus(), + refetchInterval: 2000, + }); +} + +// ========== 代理服务器控制 Hooks ========== + +/** + * 启动代理服务器 + */ +export function useStartProxyServer() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => proxyApi.startProxyServer(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); + queryClient.invalidateQueries({ queryKey: ["proxyRunning"] }); + queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] }); + queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] }); + }, + }); +} + +/** + * 停止代理服务器 + */ +export function useStopProxyServer() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => proxyApi.stopProxyWithRestore(), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); + queryClient.invalidateQueries({ queryKey: ["proxyRunning"] }); + queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] }); + queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] }); + }, + }); +} + +/** + * 设置应用接管状态 + */ +export function useSetProxyTakeoverForApp() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ appType, enabled }: { appType: string; enabled: boolean }) => + proxyApi.setProxyTakeoverForApp(appType, enabled), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["proxyTakeoverStatus"] }); + queryClient.invalidateQueries({ queryKey: ["liveTakeoverActive"] }); + }, + }); +} + +/** + * 代理模式下切换供应商 + */ +export function useSwitchProxyProvider() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: ({ appType, providerId }: { appType: string; providerId: string }) => + proxyApi.switchProxyProvider(appType, providerId), + onSuccess: (_, variables) => { + queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); + queryClient.invalidateQueries({ queryKey: ["providers", variables.appType] }); + }, + onError: (error: Error) => { + toast.error(t("proxy.switchFailed", { error: error.message })); + }, + }); +} + +// ========== Legacy 代理配置 Hooks (兼容) ========== + +/** + * 获取代理配置(旧版) + */ +export function useProxyConfig() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + const { data: config, isLoading } = useQuery({ + queryKey: ["proxyConfig"], + queryFn: () => proxyApi.getProxyConfig(), + }); + + const updateMutation = useMutation({ + mutationFn: proxyApi.updateProxyConfig, + onSuccess: () => { + toast.success(t("proxy.settings.toast.saved"), { closeButton: true }); + queryClient.invalidateQueries({ queryKey: ["proxyConfig"] }); + queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); + }, + onError: (error: Error) => { + toast.error(t("proxy.settings.toast.saveFailed", { error: error.message })); + }, + }); + + return { + config, + isLoading, + updateConfig: updateMutation.mutateAsync, + isUpdating: updateMutation.isPending, + }; +} + +// ========== v3+ 全局/应用级配置 Hooks ========== + +/** + * 获取全局代理配置 + */ +export function useGlobalProxyConfig() { + return useQuery({ + queryKey: ["globalProxyConfig"], + queryFn: () => proxyApi.getGlobalProxyConfig(), + }); +} + +/** + * 更新全局代理配置 + */ +export function useUpdateGlobalProxyConfig() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (config: GlobalProxyConfig) => proxyApi.updateGlobalProxyConfig(config), + onSuccess: () => { + toast.success(t("proxy.settings.toast.saved"), { closeButton: true }); + queryClient.invalidateQueries({ queryKey: ["globalProxyConfig"] }); + queryClient.invalidateQueries({ queryKey: ["proxyConfig"] }); + queryClient.invalidateQueries({ queryKey: ["proxyStatus"] }); + }, + onError: (error: Error) => { + toast.error(t("proxy.settings.toast.saveFailed", { error: error.message })); + }, + }); +} + +/** + * 获取指定应用的代理配置 + */ +export function useAppProxyConfig(appType: string) { + return useQuery({ + queryKey: ["appProxyConfig", appType], + queryFn: () => proxyApi.getProxyConfigForApp(appType), + enabled: !!appType, + }); +} + +/** + * 更新指定应用的代理配置 + */ +export function useUpdateAppProxyConfig() { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: (config: AppProxyConfig) => proxyApi.updateProxyConfigForApp(config), + onSuccess: (_, variables) => { + toast.success(t("proxy.settings.toast.saved"), { closeButton: true }); + queryClient.invalidateQueries({ queryKey: ["appProxyConfig", variables.appType] }); + queryClient.invalidateQueries({ queryKey: ["proxyConfig"] }); + queryClient.invalidateQueries({ queryKey: ["circuitBreakerConfig"] }); + }, + onError: (error: Error) => { + toast.error(t("proxy.settings.toast.saveFailed", { error: error.message })); + }, + }); +}