diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bff29df22..208665965 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -144,6 +144,18 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compression" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d10e4f991a553474232bc0a31799f6d24b034a84c0971d80d2e2f78b2e576e40" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.13.3" @@ -805,6 +817,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "compression-codecs" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00828ba6fd27b45a448e57dbfe84f1029d4c9f26b368157e9a448a5f49a2ec2a" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -5993,13 +6023,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags 2.10.0", "bytes", + "futures-core", "futures-util", "http", "http-body", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower 0.5.2", "tower-layer", "tower-service", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ea9035ffe..9ee261706 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -29,7 +29,7 @@ axum-server = { version = "0.7", features = ["tls-rustls"] } rustls-pemfile = "2" tower = "0.4" tower-http = { version = "0.5", features = ["limit", "cors"] } -reqwest = { version = "0.12", features = ["json", "stream"] } +reqwest = { version = "0.12", features = ["json", "stream", "gzip", "brotli", "deflate"] } uuid = { version = "1", features = ["v4"] } chrono = { version = "0.4", features = ["serde"] } dirs = "5" diff --git a/src-tauri/src/app/commands/config.rs b/src-tauri/src/app/commands/config.rs index 7fd2138b6..ad3b1449e 100644 --- a/src-tauri/src/app/commands/config.rs +++ b/src-tauri/src/app/commands/config.rs @@ -182,3 +182,158 @@ pub async fn set_endpoint_provider( ); Ok(provider_display.to_string()) } + +/// 根据 API Key Provider 更新环境变量 +/// +/// 当用户在 API Server 页面选择一个 API Key Provider 时调用 +/// 会更新 ~/.claude/settings.json 和 shell 配置文件中的环境变量 +#[tauri::command] +pub async fn update_provider_env_vars( + logs: tauri::State<'_, LogState>, + provider_type: String, + api_host: String, + api_key: Option, +) -> Result<(), String> { + use crate::services::live_sync::write_env_to_shell_config; + use serde_json::{json, Value}; + use std::fs; + + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + + // 根据 provider_type 确定要更新的环境变量 + // 参考 Claude Code 文档:https://code.claude.com/docs/en/llm-gateway + let env_vars: Vec<(String, String)> = match provider_type.to_lowercase().as_str() { + // Anthropic 兼容类型 - 包括大多数第三方 Provider + // 如 DeepSeek、智谱、MiniMax、OpenRouter、AiHubMix 等 + "anthropic" | "new-api" | "gateway" => { + let mut vars = vec![("ANTHROPIC_BASE_URL".to_string(), api_host.clone())]; + if let Some(key) = api_key { + vars.push(("ANTHROPIC_AUTH_TOKEN".to_string(), key)); + } + vars + } + // OpenAI 兼容类型 - 用于 Codex 等 + "openai" | "openai-response" => { + let mut vars = vec![("OPENAI_BASE_URL".to_string(), api_host.clone())]; + if let Some(key) = api_key { + vars.push(("OPENAI_API_KEY".to_string(), key)); + } + vars + } + // Gemini 类型 + "gemini" => { + let mut vars = vec![("GEMINI_API_BASE_URL".to_string(), api_host.clone())]; + if let Some(key) = api_key { + vars.push(("GEMINI_API_KEY".to_string(), key)); + } + vars + } + // Azure OpenAI 类型 + "azure-openai" => { + let mut vars = vec![("AZURE_OPENAI_BASE_URL".to_string(), api_host.clone())]; + if let Some(key) = api_key { + vars.push(("AZURE_OPENAI_API_KEY".to_string(), key)); + } + vars + } + // Google Vertex AI 类型 + "vertexai" => { + let mut vars = vec![("ANTHROPIC_VERTEX_BASE_URL".to_string(), api_host.clone())]; + if let Some(key) = api_key { + vars.push(("GOOGLE_APPLICATION_CREDENTIALS".to_string(), key)); + } + vars + } + // AWS Bedrock 类型 + "aws-bedrock" => { + let vars = vec![ + ("ANTHROPIC_BEDROCK_BASE_URL".to_string(), api_host.clone()), + ("CLAUDE_CODE_USE_BEDROCK".to_string(), "1".to_string()), + ]; + // Bedrock 通常使用 AWS 凭证,不需要单独的 API Key + vars + } + // Ollama 本地部署 + "ollama" => { + let vars = vec![("OLLAMA_BASE_URL".to_string(), api_host.clone())]; + vars + } + _ => { + // 未知类型,默认使用 ANTHROPIC_BASE_URL(因为大多数第三方 Provider 都是 Anthropic 兼容的) + logs.write().await.add( + "info", + &format!( + "Provider 类型 '{}' 使用默认 ANTHROPIC_BASE_URL", + provider_type + ), + ); + let mut vars = vec![("ANTHROPIC_BASE_URL".to_string(), api_host.clone())]; + if let Some(key) = api_key { + vars.push(("ANTHROPIC_AUTH_TOKEN".to_string(), key)); + } + vars + } + }; + + // 1. 更新 ~/.claude/settings.json + let claude_dir = home.join(".claude"); + let settings_path = claude_dir.join("settings.json"); + + // 确保目录存在 + if !claude_dir.exists() { + fs::create_dir_all(&claude_dir).map_err(|e| e.to_string())?; + } + + // 读取现有配置 + let mut settings: Value = if settings_path.exists() { + let content = fs::read_to_string(&settings_path).map_err(|e| e.to_string())?; + serde_json::from_str(&content).unwrap_or(json!({})) + } else { + json!({}) + }; + + // 更新 env 字段 + let settings_obj = settings.as_object_mut().ok_or("Invalid settings format")?; + if !settings_obj.contains_key("env") { + settings_obj.insert("env".to_string(), json!({})); + } + + if let Some(env_obj) = settings_obj.get_mut("env").and_then(|v| v.as_object_mut()) { + for (key, value) in &env_vars { + env_obj.insert(key.clone(), json!(value)); + } + } + + // 写入配置文件 + let content = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; + fs::write(&settings_path, content).map_err(|e| e.to_string())?; + + // 2. 更新 shell 配置文件 + if let Err(e) = write_env_to_shell_config(&env_vars) { + logs.write() + .await + .add("warn", &format!("写入 shell 配置文件失败: {}", e)); + // 不中断流程 + } + + logs.write().await.add( + "info", + &format!( + "已更新 {} 环境变量: {}", + provider_type, + env_vars + .iter() + .map(|(k, _)| k.as_str()) + .collect::>() + .join(", ") + ), + ); + + tracing::info!( + "[CONFIG] Provider 环境变量已更新: type={}, api_host={}", + provider_type, + api_host + ); + + Ok(()) +} diff --git a/src-tauri/src/commands/plugin_cmd.rs b/src-tauri/src/commands/plugin_cmd.rs index d05ceb89e..f2539bbe3 100644 --- a/src-tauri/src/commands/plugin_cmd.rs +++ b/src-tauri/src/commands/plugin_cmd.rs @@ -175,12 +175,40 @@ pub struct PluginUIInfo { /// 尝试读取 plugin.json 文件并解析为 PluginManifest fn read_plugin_manifest(install_path: &Path) -> Option { let manifest_path = install_path.join("plugin.json"); + tracing::debug!( + "read_plugin_manifest: install_path={:?}, manifest_path={:?}, exists={}", + install_path, + manifest_path, + manifest_path.exists() + ); + if !manifest_path.exists() { + tracing::debug!("read_plugin_manifest: manifest file does not exist"); return None; } - let content = std::fs::read_to_string(&manifest_path).ok()?; - serde_json::from_str(&content).ok() + match std::fs::read_to_string(&manifest_path) { + Ok(content) => { + tracing::debug!( + "read_plugin_manifest: file content length={}", + content.len() + ); + match serde_json::from_str(&content) { + Ok(manifest) => { + tracing::debug!("read_plugin_manifest: parsed successfully"); + Some(manifest) + } + Err(e) => { + tracing::error!("read_plugin_manifest: JSON parse error: {}", e); + None + } + } + } + Err(e) => { + tracing::error!("read_plugin_manifest: read file error: {}", e); + None + } + } } /// 获取带有 UI 配置的已安装插件列表 @@ -322,7 +350,17 @@ pub async fn read_plugin_manifest_cmd( let plugins_dir = manager.plugins_dir(); let plugin_path = plugins_dir.join(&plugin_id); - Ok(read_plugin_manifest(&plugin_path)) + tracing::debug!( + "read_plugin_manifest_cmd: plugin_id={}, plugins_dir={:?}, plugin_path={:?}", + plugin_id, + plugins_dir, + plugin_path + ); + + let result = read_plugin_manifest(&plugin_path); + tracing::debug!("read_plugin_manifest_cmd: result={:?}", result.is_some()); + + Ok(result) } /// 启动插件 UI(用于 binary 类型插件) diff --git a/src-tauri/src/providers/claude_custom.rs b/src-tauri/src/providers/claude_custom.rs index 23d4264a9..f46d42e15 100644 --- a/src-tauri/src/providers/claude_custom.rs +++ b/src-tauri/src/providers/claude_custom.rs @@ -29,6 +29,9 @@ fn create_http_client() -> Client { .connect_timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(600)) // 10 分钟总超时,支持长时间流式响应 .tcp_keepalive(Duration::from_secs(60)) // TCP keepalive 保持连接活跃 + .gzip(true) // 自动解压 gzip 响应 + .brotli(true) // 自动解压 brotli 响应 + .deflate(true) // 自动解压 deflate 响应 .build() .unwrap_or_else(|_| Client::new()) } diff --git a/src-tauri/src/providers/openai_custom.rs b/src-tauri/src/providers/openai_custom.rs index 99b8f4e54..a6e2794c8 100644 --- a/src-tauri/src/providers/openai_custom.rs +++ b/src-tauri/src/providers/openai_custom.rs @@ -3,6 +3,7 @@ use crate::models::openai::ChatCompletionRequest; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::error::Error; +use std::time::Duration; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OpenAICustomConfig { @@ -16,11 +17,24 @@ pub struct OpenAICustomProvider { pub client: Client, } +/// 创建配置好的 HTTP 客户端 +fn create_http_client() -> Client { + Client::builder() + .connect_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(600)) // 10 分钟总超时 + .tcp_keepalive(Duration::from_secs(60)) + .gzip(true) // 自动解压 gzip 响应 + .brotli(true) // 自动解压 brotli 响应 + .deflate(true) // 自动解压 deflate 响应 + .build() + .unwrap_or_else(|_| Client::new()) +} + impl Default for OpenAICustomProvider { fn default() -> Self { Self { config: OpenAICustomConfig::default(), - client: Client::new(), + client: create_http_client(), } } } @@ -38,7 +52,7 @@ impl OpenAICustomProvider { base_url, enabled: true, }, - client: Client::new(), + client: create_http_client(), } }