diff --git a/package.json b/package.json index af4cac3a5..bb717650c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, -"version": "0.17.6", +"version": "0.17.7", "type": "module", "repository": { "type": "git", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6772ebd54..cfbce5452 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3377,7 +3377,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.17.6" +version = "0.17.7" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f66f4f8b8..8882077a7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.17.6" +version = "0.17.7" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" diff --git a/src-tauri/src/commands/provider_pool_cmd.rs b/src-tauri/src/commands/provider_pool_cmd.rs index 150cf1713..59f0ae68a 100644 --- a/src-tauri/src/commands/provider_pool_cmd.rs +++ b/src-tauri/src/commands/provider_pool_cmd.rs @@ -493,6 +493,7 @@ pub fn update_provider_pool_credential( request.check_health, request.check_model_name, request.not_supported_models, + request.new_proxy_url, )? }; @@ -548,7 +549,7 @@ pub fn toggle_provider_pool_credential( ) -> Result { pool_service .0 - .update_credential(&db, &uuid, None, Some(is_disabled), None, None, None) + .update_credential(&db, &uuid, None, Some(is_disabled), None, None, None, None) } /// 重置凭证计数器 diff --git a/src-tauri/src/database/dao/provider_pool.rs b/src-tauri/src/database/dao/provider_pool.rs index 240a3c924..58972348b 100644 --- a/src-tauri/src/database/dao/provider_pool.rs +++ b/src-tauri/src/database/dao/provider_pool.rs @@ -18,7 +18,7 @@ impl ProviderPoolDao { "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, check_health, check_model_name, not_supported_models, usage_count, error_count, last_used, last_error_time, last_error_message, last_health_check_time, - last_health_check_model, created_at, updated_at, source + last_health_check_model, created_at, updated_at, source, proxy_url FROM provider_pool_credentials ORDER BY provider_type, created_at ASC", )?; @@ -41,7 +41,7 @@ impl ProviderPoolDao { "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, check_health, check_model_name, not_supported_models, usage_count, error_count, last_used, last_error_time, last_error_message, last_health_check_time, - last_health_check_model, created_at, updated_at, source + last_health_check_model, created_at, updated_at, source, proxy_url FROM provider_pool_credentials WHERE provider_type = ?1 ORDER BY created_at ASC", @@ -67,7 +67,7 @@ impl ProviderPoolDao { "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, check_health, check_model_name, not_supported_models, usage_count, error_count, last_used, last_error_time, last_error_message, last_health_check_time, - last_health_check_model, created_at, updated_at, source + last_health_check_model, created_at, updated_at, source, proxy_url FROM provider_pool_credentials WHERE uuid = ?1", )?; @@ -89,7 +89,7 @@ impl ProviderPoolDao { "SELECT uuid, provider_type, credential_data, name, is_healthy, is_disabled, check_health, check_model_name, not_supported_models, usage_count, error_count, last_used, last_error_time, last_error_message, last_health_check_time, - last_health_check_model, created_at, updated_at, source + last_health_check_model, created_at, updated_at, source, proxy_url FROM provider_pool_credentials WHERE name = ?1", )?; @@ -131,8 +131,8 @@ impl ProviderPoolDao { (uuid, provider_type, credential_data, name, is_healthy, is_disabled, check_health, check_model_name, not_supported_models, usage_count, error_count, last_used, last_error_time, last_error_message, last_health_check_time, - last_health_check_model, created_at, updated_at, source) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19)", + last_health_check_model, created_at, updated_at, source, proxy_url) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20)", params![ cred.uuid, cred.provider_type.to_string(), @@ -153,6 +153,7 @@ impl ProviderPoolDao { cred.created_at.timestamp(), cred.updated_at.timestamp(), source_str, + cred.proxy_url, ], )?; Ok(()) @@ -171,7 +172,7 @@ impl ProviderPoolDao { is_disabled = ?6, check_health = ?7, check_model_name = ?8, not_supported_models = ?9, usage_count = ?10, error_count = ?11, last_used = ?12, last_error_time = ?13, last_error_message = ?14, - last_health_check_time = ?15, last_health_check_model = ?16, updated_at = ?17 + last_health_check_time = ?15, last_health_check_model = ?16, updated_at = ?17, proxy_url = ?18 WHERE uuid = ?1", params![ cred.uuid, @@ -191,6 +192,7 @@ impl ProviderPoolDao { cred.last_health_check_time.map(|t| t.timestamp()), cred.last_health_check_model, cred.updated_at.timestamp(), + cred.proxy_url, ], )?; Ok(()) @@ -305,6 +307,7 @@ impl ProviderPoolDao { let created_at_ts: i64 = row.get(16)?; let updated_at_ts: i64 = row.get(17)?; let source_str: Option = row.get(18).ok(); + let proxy_url: Option = row.get(19).ok(); let provider_type: PoolProviderType = provider_type_str.parse().unwrap_or(PoolProviderType::Kiro); @@ -351,6 +354,7 @@ impl ProviderPoolDao { .unwrap_or_default(), cached_token: None, // 从 get_token_cache 单独获取 source, + proxy_url, }) } diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 030b65f50..381c47a2b 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -157,5 +157,122 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> { [], ); + // Migration: 添加代理URL字段 - 使用重建表结构的方式 + migrate_add_proxy_url_column(conn)?; + Ok(()) } + +/// 迁移:添加proxy_url列到provider_pool_credentials表 +/// 使用重建表结构的方式确保数据完整性 +fn migrate_add_proxy_url_column(conn: &Connection) -> Result<(), rusqlite::Error> { + // 检查是否已经存在proxy_url列 + let mut stmt = conn.prepare("PRAGMA table_info(provider_pool_credentials)")?; + let column_info: Vec = stmt + .query_map([], |row| { + let column_name: String = row.get(1)?; + Ok(column_name) + })? + .collect::, _>>()?; + + // 如果proxy_url列已存在,跳过迁移 + if column_info.contains(&"proxy_url".to_string()) { + return Ok(()); + } + + tracing::info!("开始迁移:添加proxy_url列到provider_pool_credentials表"); + + // 开始事务 + conn.execute("BEGIN TRANSACTION", [])?; + + let migration_result = (|| -> Result<(), rusqlite::Error> { + // 1. 备份现有数据 + conn.execute( + "CREATE TABLE provider_pool_credentials_backup AS + SELECT * FROM provider_pool_credentials", + [], + )?; + + // 2. 删除原表 + conn.execute("DROP TABLE provider_pool_credentials", [])?; + + // 3. 重建表结构(包含proxy_url列) + conn.execute( + "CREATE TABLE provider_pool_credentials ( + uuid TEXT PRIMARY KEY, + provider_type TEXT NOT NULL, + credential_data TEXT NOT NULL, + name TEXT, + is_healthy INTEGER DEFAULT 1, + is_disabled INTEGER DEFAULT 0, + check_health INTEGER DEFAULT 1, + check_model_name TEXT, + not_supported_models TEXT, + usage_count INTEGER DEFAULT 0, + error_count INTEGER DEFAULT 0, + last_used INTEGER, + last_error_time INTEGER, + last_error_message TEXT, + last_health_check_time INTEGER, + last_health_check_model TEXT, + proxy_url TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + cached_access_token TEXT, + cached_refresh_token TEXT, + token_expiry_time TEXT, + last_refresh_time TEXT, + refresh_error_count INTEGER DEFAULT 0, + last_refresh_error TEXT, + source TEXT DEFAULT 'manual' + )", + [], + )?; + + // 4. 恢复数据(proxy_url默认为NULL) + conn.execute( + "INSERT INTO provider_pool_credentials ( + uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, + error_count, last_used, last_error_time, last_error_message, + last_health_check_time, last_health_check_model, proxy_url, + created_at, updated_at, cached_access_token, cached_refresh_token, + token_expiry_time, last_refresh_time, refresh_error_count, + last_refresh_error, source + ) SELECT + uuid, provider_type, credential_data, name, is_healthy, is_disabled, + check_health, check_model_name, not_supported_models, usage_count, + error_count, last_used, last_error_time, last_error_message, + last_health_check_time, last_health_check_model, NULL as proxy_url, + created_at, updated_at, cached_access_token, cached_refresh_token, + token_expiry_time, last_refresh_time, refresh_error_count, + last_refresh_error, source + FROM provider_pool_credentials_backup", + [], + )?; + + // 5. 重建索引 + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_provider_pool_type ON provider_pool_credentials(provider_type)", + [], + )?; + + // 6. 删除备份表 + conn.execute("DROP TABLE provider_pool_credentials_backup", [])?; + + Ok(()) + })(); + + match migration_result { + Ok(()) => { + conn.execute("COMMIT", [])?; + tracing::info!("proxy_url列迁移成功完成"); + Ok(()) + } + Err(e) => { + conn.execute("ROLLBACK", [])?; + tracing::error!("proxy_url列迁移失败,已回滚: {}", e); + Err(e) + } + } +} diff --git a/src-tauri/src/models/provider_pool_model.rs b/src-tauri/src/models/provider_pool_model.rs index 187167ad7..4029f4db4 100644 --- a/src-tauri/src/models/provider_pool_model.rs +++ b/src-tauri/src/models/provider_pool_model.rs @@ -228,6 +228,8 @@ pub struct ProviderCredential { /// 凭证来源(手动添加/导入/私有) #[serde(default)] pub source: CredentialSource, + /// 代理 URL(可覆盖全局代理设置) + pub proxy_url: Option, } fn default_true() -> bool { @@ -259,6 +261,7 @@ impl ProviderCredential { updated_at: now, cached_token: None, source: CredentialSource::Manual, + proxy_url: None, } } @@ -526,6 +529,8 @@ pub struct CredentialDisplay { pub base_url: Option, /// API Key 凭证的完整 api_key(仅用于 OpenAI/Claude API Key 类型,用于编辑) pub api_key: Option, + /// 凭证级代理 URL(可覆盖全局代理设置) + pub proxy_url: Option, } /// 获取凭证类型字符串 @@ -623,6 +628,7 @@ impl From<&ProviderCredential> for CredentialDisplay { source: cred.source, base_url: get_base_url(&cred.credential), api_key: get_api_key(&cred.credential), + proxy_url: cred.proxy_url.clone(), } } } @@ -664,7 +670,7 @@ pub struct AddCredentialRequest { pub check_model_name: Option, } -/// 更新凭证的请求结构 +/// 更新凭证请求 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateCredentialRequest { pub name: Option, @@ -680,6 +686,8 @@ pub struct UpdateCredentialRequest { pub new_base_url: Option, /// API Key 相关:新的 api_key(仅适用于 API Key 凭证) pub new_api_key: Option, + /// 新的代理 URL(可覆盖全局代理设置) + pub new_proxy_url: Option, } pub type ProviderPools = HashMap>; @@ -747,6 +755,7 @@ mod tests { updated_at: Utc::now(), cached_token: None, source: CredentialSource::Manual, + proxy_url: None, }; assert!(!cred.supports_model("claude-opus")); @@ -780,6 +789,7 @@ mod tests { updated_at: Utc::now(), cached_token: None, source: CredentialSource::Manual, + proxy_url: None, }; // Exact match exclusion @@ -815,6 +825,7 @@ mod tests { updated_at: Utc::now(), cached_token: None, source: CredentialSource::Manual, + proxy_url: None, }; // Prefix wildcard exclusion @@ -854,6 +865,7 @@ mod tests { updated_at: Utc::now(), cached_token: None, source: CredentialSource::Manual, + proxy_url: None, }; // Contains wildcard exclusion @@ -890,6 +902,7 @@ mod tests { updated_at: Utc::now(), cached_token: None, source: CredentialSource::Manual, + proxy_url: None, }; // Excluded by not_supported_models (exact match) @@ -927,6 +940,7 @@ mod tests { updated_at: Utc::now(), cached_token: None, source: CredentialSource::Manual, + proxy_url: None, }; // All models should be supported since not_supported_models is empty diff --git a/src-tauri/src/services/provider_pool_service.rs b/src-tauri/src/services/provider_pool_service.rs index 2f9692071..65e4244a8 100644 --- a/src-tauri/src/services/provider_pool_service.rs +++ b/src-tauri/src/services/provider_pool_service.rs @@ -131,6 +131,7 @@ impl ProviderPoolService { check_health: Option, check_model_name: Option, not_supported_models: Option>, + proxy_url: Option, ) -> Result { let conn = db.lock().map_err(|e| e.to_string())?; let mut cred = ProviderPoolDao::get_by_uuid(&conn, uuid) @@ -154,6 +155,10 @@ impl ProviderPoolService { if let Some(models) = not_supported_models { cred.not_supported_models = models; } + // 处理 proxy_url:空字符串表示清除,None 表示不修改 + if let Some(p) = proxy_url { + cred.proxy_url = if p.is_empty() { None } else { Some(p) }; + } cred.updated_at = Utc::now(); ProviderPoolDao::update(&conn, &cred).map_err(|e| e.to_string())?; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2f3e70bde..104e4565f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ProxyCast", -"version": "0.17.6", +"version": "0.17.7", "identifier": "com.proxycast.app", "build": { "beforeDevCommand": "npm run dev",