diff --git a/src-tauri/src/auto_launch.rs b/src-tauri/src/auto_launch.rs index 26c1d5c3..c69ae46a 100644 --- a/src-tauri/src/auto_launch.rs +++ b/src-tauri/src/auto_launch.rs @@ -75,8 +75,7 @@ mod tests { #[cfg(target_os = "macos")] #[test] fn test_get_macos_app_bundle_path_valid() { - let exe_path = - std::path::Path::new("/Applications/CC Switch.app/Contents/MacOS/CC Switch"); + let exe_path = std::path::Path::new("/Applications/CC Switch.app/Contents/MacOS/CC Switch"); let result = get_macos_app_bundle_path(exe_path); assert_eq!( result, @@ -92,7 +91,9 @@ mod tests { let result = get_macos_app_bundle_path(exe_path); assert_eq!( result, - Some(std::path::PathBuf::from("/Users/test/My Apps/CC Switch.app")) + Some(std::path::PathBuf::from( + "/Users/test/My Apps/CC Switch.app" + )) ); } diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index 465e2819..9be6b747 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -3,10 +3,10 @@ use crate::app_config::AppType; use crate::init_status::InitErrorPayload; use crate::services::ProviderService; +use std::str::FromStr; use tauri::AppHandle; use tauri::State; use tauri_plugin_opener::OpenerExt; -use std::str::FromStr; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; @@ -304,7 +304,8 @@ pub async fn open_provider_terminal( let providers = ProviderService::list(state.inner(), app_type.clone()) .map_err(|e| format!("获取提供商列表失败: {e}"))?; - let provider = providers.get(&providerId) + let provider = providers + .get(&providerId) .ok_or_else(|| format!("提供商 {providerId} 不存在"))?; // 从提供商配置中提取环境变量 @@ -324,51 +325,43 @@ fn extract_env_vars_from_config( ) -> Vec<(String, String)> { let mut env_vars = Vec::new(); - if let Some(obj) = config.as_object() { - // Claude 使用 env 字段 - if let Some(env) = obj.get("env").and_then(|v| v.as_object()) { - for (key, value) in env { - if let Some(str_val) = value.as_str() { - env_vars.push((key.clone(), str_val.to_string())); - } + let Some(obj) = config.as_object() else { + return env_vars; + }; + + // 处理 env 字段(Claude/Gemini 通用) + if let Some(env) = obj.get("env").and_then(|v| v.as_object()) { + for (key, value) in env { + if let Some(str_val) = value.as_str() { + env_vars.push((key.clone(), str_val.to_string())); } } - // Codex 使用 auth 字段 + // 处理 base_url: 根据应用类型添加对应的环境变量 + let base_url_key = match app_type { + AppType::Claude => Some("ANTHROPIC_BASE_URL"), + AppType::Gemini => Some("GOOGLE_GEMINI_BASE_URL"), + _ => None, + }; + + if let Some(key) = base_url_key { + if let Some(url_str) = env.get(key).and_then(|v| v.as_str()) { + env_vars.push((key.to_string(), url_str.to_string())); + } + } + } + + // Codex 使用 auth 字段转换为 OPENAI_API_KEY + if *app_type == AppType::Codex { if let Some(auth) = obj.get("auth").and_then(|v| v.as_str()) { - match app_type { - AppType::Codex => { - env_vars.push(("OPENAI_API_KEY".to_string(), auth.to_string())); - } - _ => {} - } + env_vars.push(("OPENAI_API_KEY".to_string(), auth.to_string())); } + } - // Gemini 使用 API_KEY + // Gemini 使用 api_key 字段转换为 GEMINI_API_KEY + if *app_type == AppType::Gemini { if let Some(api_key) = obj.get("api_key").and_then(|v| v.as_str()) { - match app_type { - AppType::Gemini => { - env_vars.push(("GEMINI_API_KEY".to_string(), api_key.to_string())); - } - _ => {} - } - } - - // 提取 base_url(如果存在) - if let Some(env) = obj.get("env").and_then(|v| v.as_object()) { - if let Some(base_url) = env.get("ANTHROPIC_BASE_URL").or_else(|| env.get("GOOGLE_GEMINI_BASE_URL")) { - if let Some(url_str) = base_url.as_str() { - match app_type { - AppType::Claude => { - env_vars.push(("ANTHROPIC_BASE_URL".to_string(), url_str.to_string())); - } - AppType::Gemini => { - env_vars.push(("GOOGLE_GEMINI_BASE_URL".to_string(), url_str.to_string())); - } - _ => {} - } - } - } + env_vars.push(("GEMINI_API_KEY".to_string(), api_key.to_string())); } } @@ -377,180 +370,201 @@ fn extract_env_vars_from_config( /// 创建临时配置文件并启动 claude 终端 /// 使用 --settings 参数传入提供商特定的 API 配置 -fn launch_terminal_with_env(env_vars: Vec<(String, String)>, provider_id: &str) -> Result<(), String> { - use std::process::Command; - - // 创建临时配置文件,使用提供商ID和进程ID确保唯一性 +fn launch_terminal_with_env( + env_vars: Vec<(String, String)>, + provider_id: &str, +) -> Result<(), String> { let temp_dir = std::env::temp_dir(); - let config_file = temp_dir.join(format!("claude_{}_{}.json", provider_id, std::process::id())); + let config_file = temp_dir.join(format!( + "claude_{}_{}.json", + provider_id, + std::process::id() + )); - // 构建 claude 配置 JSON 格式 - let mut config_obj = serde_json::Map::new(); - let mut env_obj = serde_json::Map::new(); - - for (key, value) in &env_vars { - env_obj.insert(key.clone(), serde_json::Value::String(value.clone())); - } - - config_obj.insert("env".to_string(), serde_json::Value::Object(env_obj)); - - let config_json = serde_json::to_string_pretty(&config_obj) - .map_err(|e| format!("序列化配置失败: {e}"))?; - - // 写入临时配置文件 - std::fs::write(&config_file, config_json) - .map_err(|e| format!("写入配置文件失败: {e}"))?; + // 创建并写入配置文件 + write_claude_config(&config_file, &env_vars)?; // 转义配置文件路径用于 shell - let config_path_escaped = config_file.to_string_lossy() - .replace('\\', "\\\\") - .replace('"', "\\\"") - .replace('$', "\\$") - .replace(' ', "\\ "); + let config_path_escaped = escape_shell_path(&config_file); #[cfg(target_os = "macos")] { - // macOS: 使用 Terminal.app 启动 claude,使用包装脚本来处理清理 - let mut terminal_cmd = Command::new("osascript"); - terminal_cmd.arg("-e"); - - // 使用 bash 的 trap 来清理配置文件 - let config_path_for_script = config_file.to_string_lossy() - .replace('\\', "\\\\") - .replace('"', "\\\""); - - let shell_script = format!( - "bash -c 'trap \"rm -f \\\"{}\\\"\" EXIT; echo \"Using provider-specific claude config:\"; echo \"{}\"; claude --settings \"{}\"; exec bash --norc --noprofile'", - config_path_for_script, - config_path_escaped, - config_path_escaped - ); - - let script = format!( - r#"tell application "Terminal" - activate - do script "{}" - end tell"#, - shell_script.replace('\\', "\\\\").replace('"', "\\\"") - ); - - terminal_cmd.arg(&script); - - terminal_cmd - .spawn() - .map_err(|e| format!("启动 macOS 终端失败: {e}"))?; - + launch_macos_terminal(&config_file, &config_path_escaped)?; return Ok(()); } #[cfg(target_os = "linux")] { - // Linux: 尝试使用常见终端,使用包装脚本在 shell 退出时清理配置文件 - let terminals = [ - "gnome-terminal", "konsole", "xfce4-terminal", - "mate-terminal", "lxterminal", "alacritty", "kitty", - ]; - - let mut last_error = String::from("未找到可用的终端"); - - // 使用 bash 创建包装脚本来处理清理 - let config_path_for_bash = config_file.to_string_lossy(); - let shell_cmd = format!( - "bash -c 'trap \"rm -f \\\"{}\\\"\" EXIT; echo \"Using provider-specific claude config:\"; echo \"{}\"; claude --settings \"{}\"; exec bash --norc --noprofile'", - config_path_for_bash, config_path_escaped, config_path_escaped - ); - - for terminal in terminals { - // 检查终端是否存在 - if Command::new("which").arg(terminal).output().is_err() { - continue; - } - - // 不同的终端使用不同的参数格式 - let result = match terminal { - "gnome-terminal" | "mate-terminal" => { - Command::new(terminal) - .arg("--") - .arg("bash") - .arg("-c") - .arg(&shell_cmd) - .spawn() - } - _ => { - Command::new(terminal) - .arg("-e") - .arg("bash") - .arg("-c") - .arg(&shell_cmd) - .spawn() - } - }; - - match result { - Ok(_) => { - return Ok(()); - } - Err(e) => { - last_error = format!("启动 {} 失败: {}", terminal, e); - continue; - } - } - } - - // 如果所有终端都失败,清理配置文件 - let _ = std::fs::remove_file(&config_file); - return Err(last_error); + launch_linux_terminal(&config_file, &config_path_escaped)?; + Ok(()) } #[cfg(target_os = "windows")] { - use std::io::Write; - - // Windows: 创建临时批处理文件,并在执行完毕后清理配置文件 - let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id())); - - // 转义配置文件路径用于批处理 - let config_path_for_batch = config_file.to_string_lossy() - .to_string() - .replace('&', "^&"); - - let mut content = String::from("@echo off\n"); - content.push_str(&format!("echo Using provider-specific claude config:\n")); - content.push_str(&format!("echo {}\n", config_path_for_batch)); - content.push_str(&format!("claude --settings \"{}\"\n", config_path_for_batch)); - - // 在 claude 执行完毕后(无论成功与否),删除临时配置文件和批处理文件本身 - content.push_str(&format!("del \"{}\" >nul 2>&1\n", config_path_for_batch)); - content.push_str(&format!("del \"%%~f0\" >nul 2>&1\n")); // %%~f0 表示批处理文件自身 - - // 如果 claude 出错,暂停以便用户查看错误信息 - content.push_str("if errorlevel 1 (\n"); - content.push_str(" echo.\n"); - content.push_str(" echo Press any key to close...\n"); - content.push_str(" pause >nul\n"); - content.push_str(")\n"); - - std::fs::write(&bat_file, content) - .map_err(|e| format!("写入批处理文件失败: {e}"))?; - - // 启动新的 cmd 窗口执行批处理文件 - Command::new("cmd") - .args(["/C", "start", "cmd", "/C", &bat_file.to_string_lossy().to_string()]) - .creation_flags(CREATE_NO_WINDOW) - .spawn() - .map_err(|e| format!("启动 Windows 终端失败: {e}"))?; - + launch_windows_terminal(&temp_dir, &config_file)?; return Ok(()); } - // 这个代码在所有支持的平台上都不可达,因为前面的平台特定块都已经返回了 - // 使用 cfg 和 allow 来避免编译器警告和错误 - #[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] - #[allow(unreachable_code)] - { - Ok(()) - } - #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] Err("不支持的操作系统".to_string()) } + +/// 写入 claude 配置文件 +fn write_claude_config( + config_file: &std::path::Path, + env_vars: &[(String, String)], +) -> Result<(), String> { + let mut config_obj = serde_json::Map::new(); + let mut env_obj = serde_json::Map::new(); + + for (key, value) in env_vars { + env_obj.insert(key.clone(), serde_json::Value::String(value.clone())); + } + + config_obj.insert("env".to_string(), serde_json::Value::Object(env_obj)); + + let config_json = + serde_json::to_string_pretty(&config_obj).map_err(|e| format!("序列化配置失败: {e}"))?; + + std::fs::write(config_file, config_json).map_err(|e| format!("写入配置文件失败: {e}")) +} + +/// 转义 shell 路径 +fn escape_shell_path(path: &std::path::Path) -> String { + path.to_string_lossy() + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('$', "\\$") + .replace(' ', "\\ ") +} + +/// 生成 bash 包装脚本,用于清理临时文件 +fn generate_wrapper_script(config_path: &str, escaped_path: &str) -> String { + format!( + "bash -c 'trap \"rm -f \\\"{}\\\"\" EXIT; echo \"Using provider-specific claude config:\"; echo \"{}\"; claude --settings \"{}\"; exec bash --norc --noprofile'", + config_path, escaped_path, escaped_path + ) +} + +/// macOS: 使用 Terminal.app 启动 +#[cfg(target_os = "macos")] +fn launch_macos_terminal( + config_file: &std::path::Path, + config_path_escaped: &str, +) -> Result<(), String> { + use std::process::Command; + + let config_path_for_script = config_file.to_string_lossy().replace('\"', "\\\""); + + let shell_script = generate_wrapper_script(&config_path_for_script, config_path_escaped); + + let script = format!( + r#"tell application "Terminal" + activate + do script "{}" + end tell"#, + shell_script.replace('\"', "\\\"") + ); + + Command::new("osascript") + .arg("-e") + .arg(&script) + .spawn() + .map_err(|e| format!("启动 macOS 终端失败: {e}"))?; + + Ok(()) +} + +/// Linux: 尝试使用常见终端启动 +#[cfg(target_os = "linux")] +fn launch_linux_terminal( + config_file: &std::path::Path, + config_path_escaped: &str, +) -> Result<(), String> { + use std::process::Command; + + let terminals = [ + "gnome-terminal", + "konsole", + "xfce4-terminal", + "mate-terminal", + "lxterminal", + "alacritty", + "kitty", + ]; + + let config_path_for_bash = config_file.to_string_lossy(); + let shell_cmd = generate_wrapper_script(&config_path_for_bash, config_path_escaped); + + let mut last_error = String::from("未找到可用的终端"); + + for terminal in terminals { + // 检查终端是否存在 + if std::path::Path::new(&format!("/usr/bin/{}", terminal)).exists() + || std::path::Path::new(&format!("/bin/{}", terminal)).exists() + { + let result = match terminal { + "gnome-terminal" | "mate-terminal" => Command::new(terminal) + .arg("--") + .arg("bash") + .arg("-c") + .arg(&shell_cmd) + .spawn(), + _ => Command::new(terminal) + .arg("-e") + .arg("bash") + .arg("-c") + .arg(&shell_cmd) + .spawn(), + }; + + match result { + Ok(_) => return Ok(()), + Err(e) => { + last_error = format!("启动 {} 失败: {}", terminal, e); + } + } + } + } + + // 清理配置文件 + let _ = std::fs::remove_file(config_file); + Err(last_error) +} + +/// Windows: 创建临时批处理文件启动 +#[cfg(target_os = "windows")] +fn launch_windows_terminal( + temp_dir: &std::path::Path, + config_file: &std::path::Path, +) -> Result<(), String> { + use std::process::Command; + + let bat_file = temp_dir.join(format!("cc_switch_claude_{}.bat", std::process::id())); + let config_path_for_batch = config_file.to_string_lossy().replace('&', "^&"); + + let content = format!( + "@echo off +echo Using provider-specific claude config: +echo {} +claude --settings \"{}\" +del \"{}\" >nul 2>&1 +del \"%~f0\" >nul 2>&1 +if errorlevel 1 ( + echo. + echo Press any key to close... + pause >nul +)", + config_path_for_batch, config_path_for_batch, config_path_for_batch + ); + + std::fs::write(&bat_file, content).map_err(|e| format!("写入批处理文件失败: {e}"))?; + + Command::new("cmd") + .args(["/C", "start", "cmd", "/C", &bat_file.to_string_lossy()]) + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .map_err(|e| format!("启动 Windows 终端失败: {e}"))?; + + Ok(()) +} diff --git a/src-tauri/src/database/dao/universal_providers.rs b/src-tauri/src/database/dao/universal_providers.rs index 32c95500..a822e23e 100644 --- a/src-tauri/src/database/dao/universal_providers.rs +++ b/src-tauri/src/database/dao/universal_providers.rs @@ -12,22 +12,22 @@ const UNIVERSAL_PROVIDERS_KEY: &str = "universal_providers"; impl Database { /// 获取所有统一供应商 - pub fn get_all_universal_providers(&self) -> Result, AppError> { + pub fn get_all_universal_providers( + &self, + ) -> Result, AppError> { let conn = lock_conn!(self.conn); - + let mut stmt = conn .prepare("SELECT value FROM settings WHERE key = ?") .map_err(|e| AppError::Database(e.to_string()))?; - + let result: Option = stmt .query_row([UNIVERSAL_PROVIDERS_KEY], |row| row.get(0)) .ok(); - + match result { - Some(json) => { - serde_json::from_str(&json) - .map_err(|e| AppError::Database(format!("解析统一供应商数据失败: {e}"))) - } + Some(json) => serde_json::from_str(&json) + .map_err(|e| AppError::Database(format!("解析统一供应商数据失败: {e}"))), None => Ok(HashMap::new()), } } @@ -62,14 +62,13 @@ impl Database { ) -> Result<(), AppError> { let conn = lock_conn!(self.conn); let json = to_json_string(providers)?; - + conn.execute( "INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", [UNIVERSAL_PROVIDERS_KEY, &json], ) .map_err(|e| AppError::Database(e.to_string()))?; - + Ok(()) } } - diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7c5e2ed1..ceeef201 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,8 +25,8 @@ mod tray; mod usage_script; pub use app_config::{AppType, McpApps, McpServer, MultiAppConfig}; -pub use commands::open_provider_terminal; pub use codex_config::{get_codex_auth_path, get_codex_config_path, write_codex_live_atomic}; +pub use commands::open_provider_terminal; pub use commands::*; pub use config::{get_claude_mcp_path, get_claude_settings_path, read_json_file}; pub use database::Database;