From c10ace7a840fdb259f2cb06b1aa8d9f5cdb8d9b6 Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 30 Aug 2025 21:54:11 +0800 Subject: [PATCH] =?UTF-8?q?-=20feat(codex):=20=E5=BC=95=E5=85=A5=20Codex?= =?UTF-8?q?=20=E5=BA=94=E7=94=A8=E4=B8=8E=E4=BE=9B=E5=BA=94=E5=95=86?= =?UTF-8?q?=E5=88=87=E6=8D=A2=EF=BC=88=E7=AE=A1=E7=90=86=20auth.json/confi?= =?UTF-8?q?g.toml=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=87=E4=BB=BD=E4=B8=8E?= =?UTF-8?q?=E6=81=A2=E5=A4=8D=EF=BC=89=20-=20feat(core):=20=E5=A4=9A?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E9=85=8D=E7=BD=AE=20v2=EF=BC=88claude/codex?= =?UTF-8?q?=EF=BC=89=E4=B8=8E=20ProviderManager=EF=BC=9B=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20v1=E2=86=92v2=20=E8=87=AA=E5=8A=A8=E8=BF=81=E7=A7=BB=20-=20f?= =?UTF-8?q?eat(ui):=20=E6=96=B0=E5=A2=9E=20Codex=20=E9=A1=B5=E7=AD=BE?= =?UTF-8?q?=E4=B8=8E=E5=8F=8C=E7=BC=96=E8=BE=91=E5=99=A8=E8=A1=A8=E5=8D=95?= =?UTF-8?q?=EF=BC=9B=E7=BB=9F=E4=B8=80=20window.api=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=20app=20=E5=8F=82=E6=95=B0=20-=20feat(tauri):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20get=5Fconfig=5Fstatus/open=5Fconfig=5Ffolder/open?= =?UTF-8?q?=5Fexternal=20=E5=91=BD=E4=BB=A4=E5=B9=B6=E9=80=82=E9=85=8D=20C?= =?UTF-8?q?odex=20-=20fix(codex):=20=E4=B8=BB=E9=85=8D=E7=BD=AE=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E6=97=B6=E4=B8=8D=E6=89=A7=E8=A1=8C=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=AF=BC=E5=85=A5=EF=BC=88=E5=AF=B9=E9=BD=90=20Claude=20?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=EF=BC=89=20-=20chore:=20=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E5=B1=95=E7=A4=BA=E4=B8=8E=E9=87=8D=E5=90=AF?= =?UTF-8?q?=E6=8F=90=E7=A4=BA=E7=AD=89=E7=BB=86=E8=8A=82=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/app_config.rs | 113 +++++++++ src-tauri/src/codex_config.rs | 159 +++++++++++++ src-tauri/src/commands.rs | 330 ++++++++++++++++++++++++--- src-tauri/src/lib.rs | 50 ++-- src-tauri/src/provider.rs | 113 +-------- src-tauri/src/store.rs | 21 +- src/App.css | 42 ++++ src/App.tsx | 43 +++- src/components/AddProviderModal.tsx | 4 + src/components/EditProviderModal.tsx | 4 + src/components/ProviderForm.tsx | 214 +++++++++++------ src/lib/tauri-api.ts | 59 +++-- src/vite-env.d.ts | 18 +- 13 files changed, 891 insertions(+), 279 deletions(-) create mode 100644 src-tauri/src/app_config.rs create mode 100644 src-tauri/src/codex_config.rs diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs new file mode 100644 index 0000000..5bdadd3 --- /dev/null +++ b/src-tauri/src/app_config.rs @@ -0,0 +1,113 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +use crate::config::{get_app_config_path, write_json_file}; +use crate::provider::ProviderManager; + +/// 应用类型 +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AppType { + Claude, + Codex, +} + +impl AppType { + pub fn as_str(&self) -> &str { + match self { + AppType::Claude => "claude", + AppType::Codex => "codex", + } + } +} + +impl From<&str> for AppType { + fn from(s: &str) -> Self { + match s.to_lowercase().as_str() { + "codex" => AppType::Codex, + _ => AppType::Claude, // 默认为 Claude + } + } +} + +/// 多应用配置结构(向后兼容) +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MultiAppConfig { + #[serde(default = "default_version")] + pub version: u32, + #[serde(flatten)] + pub apps: HashMap, +} + +fn default_version() -> u32 { + 2 +} + +impl Default for MultiAppConfig { + fn default() -> Self { + let mut apps = HashMap::new(); + apps.insert("claude".to_string(), ProviderManager::default()); + apps.insert("codex".to_string(), ProviderManager::default()); + + Self { version: 2, apps } + } +} + +impl MultiAppConfig { + /// 从文件加载配置(处理v1到v2的迁移) + pub fn load() -> Result { + let config_path = get_app_config_path(); + + if !config_path.exists() { + log::info!("配置文件不存在,创建新的多应用配置"); + return Ok(Self::default()); + } + + // 尝试读取文件 + let content = std::fs::read_to_string(&config_path) + .map_err(|e| format!("读取配置文件失败: {}", e))?; + + // 检查是否是旧版本格式(v1) + if let Ok(v1_config) = serde_json::from_str::(&content) { + log::info!("检测到v1配置,自动迁移到v2"); + + // 迁移到新格式 + let mut apps = HashMap::new(); + apps.insert("claude".to_string(), v1_config); + apps.insert("codex".to_string(), ProviderManager::default()); + + let config = Self { version: 2, apps }; + + // 保存迁移后的配置 + config.save()?; + return Ok(config); + } + + // 尝试读取v2格式 + serde_json::from_str::(&content).map_err(|e| format!("解析配置文件失败: {}", e)) + } + + /// 保存配置到文件 + pub fn save(&self) -> Result<(), String> { + let config_path = get_app_config_path(); + write_json_file(&config_path, self) + } + + /// 获取指定应用的管理器 + pub fn get_manager(&self, app: &AppType) -> Option<&ProviderManager> { + self.apps.get(app.as_str()) + } + + /// 获取指定应用的管理器(可变引用) + pub fn get_manager_mut(&mut self, app: &AppType) -> Option<&mut ProviderManager> { + self.apps.get_mut(app.as_str()) + } + + /// 确保应用存在 + pub fn ensure_app(&mut self, app: &AppType) { + if !self.apps.contains_key(app.as_str()) { + self.apps + .insert(app.as_str().to_string(), ProviderManager::default()); + } + } +} diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs new file mode 100644 index 0000000..a3b42b4 --- /dev/null +++ b/src-tauri/src/codex_config.rs @@ -0,0 +1,159 @@ +use serde_json::Value; +use std::fs; +use std::path::PathBuf; + +use crate::config::{ + copy_file, delete_file, read_json_file, sanitize_provider_name, write_json_file, +}; + +/// 获取 Codex 配置目录路径 +pub fn get_codex_config_dir() -> PathBuf { + dirs::home_dir().expect("无法获取用户主目录").join(".codex") +} + +/// 获取 Codex auth.json 路径 +pub fn get_codex_auth_path() -> PathBuf { + get_codex_config_dir().join("auth.json") +} + +/// 获取 Codex config.toml 路径 +pub fn get_codex_config_path() -> PathBuf { + get_codex_config_dir().join("config.toml") +} + +/// 获取 Codex 供应商配置文件路径 +pub fn get_codex_provider_paths( + provider_id: &str, + provider_name: Option<&str>, +) -> (PathBuf, PathBuf) { + let base_name = provider_name + .map(|name| sanitize_provider_name(name)) + .unwrap_or_else(|| sanitize_provider_name(provider_id)); + + let auth_path = get_codex_config_dir().join(format!("auth-{}.json", base_name)); + let config_path = get_codex_config_dir().join(format!("config-{}.toml", base_name)); + + (auth_path, config_path) +} + +/// 备份 Codex 当前配置 +pub fn backup_codex_config(provider_id: &str, provider_name: &str) -> Result<(), String> { + let auth_path = get_codex_auth_path(); + let config_path = get_codex_config_path(); + let (backup_auth_path, backup_config_path) = + get_codex_provider_paths(provider_id, Some(provider_name)); + + // 备份 auth.json + if auth_path.exists() { + copy_file(&auth_path, &backup_auth_path)?; + log::info!("已备份 Codex auth.json: {}", backup_auth_path.display()); + } + + // 备份 config.toml + if config_path.exists() { + copy_file(&config_path, &backup_config_path)?; + log::info!("已备份 Codex config.toml: {}", backup_config_path.display()); + } + + Ok(()) +} + +/// 保存 Codex 供应商配置副本 +pub fn save_codex_provider_config( + provider_id: &str, + provider_name: &str, + settings_config: &Value, +) -> Result<(), String> { + let (auth_path, config_path) = get_codex_provider_paths(provider_id, Some(provider_name)); + + // 保存 auth.json + if let Some(auth) = settings_config.get("auth") { + write_json_file(&auth_path, auth)?; + } + + // 保存 config.toml + if let Some(config) = settings_config.get("config") { + if let Some(config_str) = config.as_str() { + fs::write(&config_path, config_str) + .map_err(|e| format!("写入供应商 config.toml 失败: {}", e))?; + } + } + + Ok(()) +} + +/// 删除 Codex 供应商配置文件 +pub fn delete_codex_provider_config(provider_id: &str, provider_name: &str) -> Result<(), String> { + let (auth_path, config_path) = get_codex_provider_paths(provider_id, Some(provider_name)); + + delete_file(&auth_path).ok(); + delete_file(&config_path).ok(); + + Ok(()) +} + +/// 从 Codex 供应商配置副本恢复到主配置 +pub fn restore_codex_provider_config(provider_id: &str, provider_name: &str) -> Result<(), String> { + let (provider_auth_path, provider_config_path) = + get_codex_provider_paths(provider_id, Some(provider_name)); + let auth_path = get_codex_auth_path(); + let config_path = get_codex_config_path(); + + // 确保目录存在 + if let Some(parent) = auth_path.parent() { + fs::create_dir_all(parent).map_err(|e| format!("创建 Codex 目录失败: {}", e))?; + } + + // 复制 auth.json + if provider_auth_path.exists() { + copy_file(&provider_auth_path, &auth_path)?; + log::info!("已恢复 Codex auth.json"); + } else { + return Err(format!( + "供应商 auth.json 不存在: {}", + provider_auth_path.display() + )); + } + + // 复制 config.toml + if provider_config_path.exists() { + copy_file(&provider_config_path, &config_path)?; + log::info!("已恢复 Codex config.toml"); + } else { + return Err(format!( + "供应商 config.toml 不存在: {}", + provider_config_path.display() + )); + } + + Ok(()) +} + +/// 导入当前 Codex 配置为默认供应商 +pub fn import_current_codex_config() -> Result { + let auth_path = get_codex_auth_path(); + let config_path = get_codex_config_path(); + + // 参考 Claude Code 行为:主配置缺失时不导入 + if !auth_path.exists() || !config_path.exists() { + return Err("Codex 配置文件不存在".to_string()); + } + + // 读取 auth.json + let auth = read_json_file::(&auth_path)?; + + // 读取 config.toml + let config_str = fs::read_to_string(&config_path) + .map_err(|e| format!("读取 config.toml 失败: {}", e))?; + + // 组合成完整配置 + let settings_config = serde_json::json!({ + "auth": auth, + "config": config_str + }); + + // 保存为默认供应商副本 + save_codex_provider_config("default", "default", &settings_config)?; + + Ok(settings_config) +} diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 4a7b88d..36c04bd 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use tauri::State; use tauri_plugin_opener::OpenerExt; +use crate::app_config::AppType; +use crate::codex_config; use crate::config::{ConfigStatus, get_claude_settings_path, import_current_config_as_default}; use crate::provider::Provider; use crate::store::AppState; @@ -10,38 +12,82 @@ use crate::store::AppState; #[tauri::command] pub async fn get_providers( state: State<'_, AppState>, + app: Option, ) -> Result, String> { - let manager = state - .provider_manager + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + + let config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; + let manager = config + .get_manager(&app_type) + .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; + Ok(manager.get_all_providers().clone()) } /// 获取当前供应商ID #[tauri::command] -pub async fn get_current_provider(state: State<'_, AppState>) -> Result { - let manager = state - .provider_manager +pub async fn get_current_provider( + state: State<'_, AppState>, + app: Option, +) -> Result { + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + + let config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; + let manager = config + .get_manager(&app_type) + .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; + Ok(manager.current.clone()) } /// 添加供应商 #[tauri::command] -pub async fn add_provider(state: State<'_, AppState>, provider: Provider) -> Result { - let mut manager = state - .provider_manager +pub async fn add_provider( + state: State<'_, AppState>, + app: Option, + provider: Provider, +) -> Result { + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + + let mut config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - manager.add_provider(provider)?; + let manager = config + .get_manager_mut(&app_type) + .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; + + // 根据应用类型保存配置文件 + match app_type { + AppType::Codex => { + // Codex: 保存两个文件 + codex_config::save_codex_provider_config( + &provider.id, + &provider.name, + &provider.settings_config, + )?; + } + AppType::Claude => { + // Claude: 使用原有逻辑 + use crate::config::{get_provider_config_path, write_json_file}; + let config_path = get_provider_config_path(&provider.id, Some(&provider.name)); + write_json_file(&config_path, &provider.settings_config)?; + } + } + + manager.providers.insert(provider.id.clone(), provider); // 保存配置 - drop(manager); // 释放锁 + drop(config); // 释放锁 state.save()?; Ok(true) @@ -51,17 +97,64 @@ pub async fn add_provider(state: State<'_, AppState>, provider: Provider) -> Res #[tauri::command] pub async fn update_provider( state: State<'_, AppState>, + app: Option, provider: Provider, ) -> Result { - let mut manager = state - .provider_manager + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + + let mut config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - manager.update_provider(provider)?; + let manager = config + .get_manager_mut(&app_type) + .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; + + // 检查供应商是否存在 + if !manager.providers.contains_key(&provider.id) { + return Err(format!("供应商不存在: {}", provider.id)); + } + + // 如果名称改变了,需要处理配置文件 + if let Some(old_provider) = manager.providers.get(&provider.id) { + if old_provider.name != provider.name { + // 删除旧配置文件 + match app_type { + AppType::Codex => { + codex_config::delete_codex_provider_config(&provider.id, &old_provider.name) + .ok(); + } + AppType::Claude => { + use crate::config::{delete_file, get_provider_config_path}; + let old_config_path = + get_provider_config_path(&provider.id, Some(&old_provider.name)); + delete_file(&old_config_path).ok(); + } + } + } + } + + // 保存新配置文件 + match app_type { + AppType::Codex => { + codex_config::save_codex_provider_config( + &provider.id, + &provider.name, + &provider.settings_config, + )?; + } + AppType::Claude => { + use crate::config::{get_provider_config_path, write_json_file}; + let config_path = get_provider_config_path(&provider.id, Some(&provider.name)); + write_json_file(&config_path, &provider.settings_config)?; + } + } + + manager.providers.insert(provider.id.clone(), provider); // 保存配置 - drop(manager); // 释放锁 + drop(config); // 释放锁 state.save()?; Ok(true) @@ -69,16 +162,51 @@ pub async fn update_provider( /// 删除供应商 #[tauri::command] -pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result { - let mut manager = state - .provider_manager +pub async fn delete_provider( + state: State<'_, AppState>, + app: Option, + id: String, +) -> Result { + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + + let mut config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - manager.delete_provider(&id)?; + let manager = config + .get_manager_mut(&app_type) + .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; + + // 检查是否为当前供应商 + if manager.current == id { + return Err("不能删除当前正在使用的供应商".to_string()); + } + + // 获取供应商信息 + let provider = manager + .providers + .get(&id) + .ok_or_else(|| format!("供应商不存在: {}", id))? + .clone(); + + // 删除配置文件 + match app_type { + AppType::Codex => { + codex_config::delete_codex_provider_config(&id, &provider.name)?; + } + AppType::Claude => { + use crate::config::{delete_file, get_provider_config_path}; + let config_path = get_provider_config_path(&id, Some(&provider.name)); + delete_file(&config_path)?; + } + } + + // 从管理器删除 + manager.providers.remove(&id); // 保存配置 - drop(manager); // 释放锁 + drop(config); // 释放锁 state.save()?; Ok(true) @@ -86,16 +214,87 @@ pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result, id: String) -> Result { - let mut manager = state - .provider_manager +pub async fn switch_provider( + state: State<'_, AppState>, + app: Option, + id: String, +) -> Result { + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + + let mut config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - manager.switch_provider(&id)?; + let manager = config + .get_manager_mut(&app_type) + .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; + + // 检查供应商是否存在 + let provider = manager + .providers + .get(&id) + .ok_or_else(|| format!("供应商不存在: {}", id))? + .clone(); + + // 根据应用类型执行切换 + match app_type { + AppType::Codex => { + // 备份当前配置(如果存在) + if !manager.current.is_empty() { + if let Some(current_provider) = manager.providers.get(&manager.current) { + codex_config::backup_codex_config(&manager.current, ¤t_provider.name)?; + log::info!("已备份当前 Codex 供应商配置: {}", current_provider.name); + } + } + + // 恢复目标供应商配置 + codex_config::restore_codex_provider_config(&id, &provider.name)?; + } + AppType::Claude => { + // 使用原有的 Claude 切换逻辑 + use crate::config::{ + backup_config, copy_file, get_claude_settings_path, get_provider_config_path, + }; + + let settings_path = get_claude_settings_path(); + let provider_config_path = get_provider_config_path(&id, Some(&provider.name)); + + // 检查供应商配置文件是否存在 + if !provider_config_path.exists() { + return Err(format!( + "供应商配置文件不存在: {}", + provider_config_path.display() + )); + } + + // 如果当前有配置,先备份到当前供应商 + if settings_path.exists() && !manager.current.is_empty() { + if let Some(current_provider) = manager.providers.get(&manager.current) { + let current_provider_path = + get_provider_config_path(&manager.current, Some(¤t_provider.name)); + backup_config(&settings_path, ¤t_provider_path)?; + log::info!("已备份当前供应商配置: {}", current_provider.name); + } + } + + // 确保主配置父目录存在 + if let Some(parent) = settings_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; + } + + // 复制新供应商配置到主配置 + copy_file(&provider_config_path, &settings_path)?; + } + } + + // 更新当前供应商 + manager.current = id; + + log::info!("成功切换到供应商: {}", provider.name); // 保存配置 - drop(manager); // 释放锁 + drop(config); // 释放锁 state.save()?; Ok(true) @@ -103,20 +302,31 @@ pub async fn switch_provider(state: State<'_, AppState>, id: String) -> Result) -> Result { +pub async fn import_default_config( + state: State<'_, AppState>, + app: Option, +) -> Result { + let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude); + // 若已存在 default 供应商,则直接返回,避免重复导入 { - let manager = state - .provider_manager + let config = state + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - if manager.get_all_providers().contains_key("default") { - return Ok(true); + + if let Some(manager) = config.get_manager(&app_type) { + if manager.get_all_providers().contains_key("default") { + return Ok(true); + } } } - // 导入配置 - let settings_config = import_current_config_as_default()?; + // 根据应用类型导入配置 + let settings_config = match app_type { + AppType::Codex => codex_config::import_current_codex_config()?, + AppType::Claude => import_current_config_as_default()?, + }; // 创建默认供应商 let provider = Provider::with_id( @@ -127,12 +337,32 @@ pub async fn import_default_config(state: State<'_, AppState>) -> Result { + codex_config::save_codex_provider_config( + &provider.id, + &provider.name, + &provider.settings_config, + )?; + } + AppType::Claude => { + use crate::config::{get_provider_config_path, write_json_file}; + let config_path = get_provider_config_path(&provider.id, Some(&provider.name)); + write_json_file(&config_path, &provider.settings_config)?; + } + } + + manager.providers.insert(provider.id.clone(), provider); // 如果没有当前供应商,设置为 default if manager.current.is_empty() { @@ -140,7 +370,7 @@ pub async fn import_default_config(state: State<'_, AppState>) -> Result Result { Ok(crate::config::get_claude_config_status()) } +/// 获取应用配置状态(通用) +#[tauri::command] +pub async fn get_config_status(app_type: Option) -> Result { + let app = app_type.unwrap_or(AppType::Claude); + + match app { + AppType::Claude => Ok(crate::config::get_claude_config_status()), + AppType::Codex => { + use crate::codex_config::{get_codex_auth_path, get_codex_config_path}; + let auth_path = get_codex_auth_path(); + let config_path = get_codex_config_path(); + + // Codex 需要两个文件都存在才算配置存在 + let exists = auth_path.exists() && config_path.exists(); + let path = format!("~/.codex/"); + + Ok(ConfigStatus { exists, path }) + } + } +} + /// 获取 Claude Code 配置文件路径 #[tauri::command] pub async fn get_claude_code_config_path() -> Result { @@ -160,8 +411,13 @@ pub async fn get_claude_code_config_path() -> Result { /// 打开配置文件夹 #[tauri::command] -pub async fn open_config_folder(app: tauri::AppHandle) -> Result { - let config_dir = crate::config::get_claude_config_dir(); +pub async fn open_config_folder(app: tauri::AppHandle, app_type: Option) -> Result { + let app_type = app_type.unwrap_or(AppType::Claude); + + let config_dir = match app_type { + AppType::Claude => crate::config::get_claude_config_dir(), + AppType::Codex => crate::codex_config::get_codex_config_dir(), + }; // 确保目录存在 if !config_dir.exists() { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b4402ab..82b7886 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,5 @@ +mod app_config; +mod codex_config; mod commands; mod config; mod provider; @@ -55,34 +57,51 @@ pub fn run() { // 如果没有供应商且存在 Claude Code 配置,自动导入 { - let manager = app_state.provider_manager.lock().unwrap(); - if manager.providers.is_empty() { - drop(manager); // 释放锁 + let mut config = app_state.config.lock().unwrap(); + // 检查 Claude 供应商 + let need_import = if let Some(claude_manager) = + config.get_manager(&app_config::AppType::Claude) + { + claude_manager.providers.is_empty() + } else { + // 确保 Claude 应用存在 + config.ensure_app(&app_config::AppType::Claude); + true + }; + + if need_import { let settings_path = config::get_claude_settings_path(); if settings_path.exists() { log::info!("检测到 Claude Code 配置,自动导入为默认供应商"); if let Ok(settings_config) = config::import_current_config_as_default() { - let mut manager = app_state.provider_manager.lock().unwrap(); - let provider = provider::Provider::with_id( - "default".to_string(), - "default".to_string(), - settings_config, - None, - ); + if let Some(manager) = + config.get_manager_mut(&app_config::AppType::Claude) + { + let provider = provider::Provider::with_id( + "default".to_string(), + "default".to_string(), + settings_config, + None, + ); - if manager.add_provider(provider).is_ok() { - manager.current = "default".to_string(); - drop(manager); - let _ = app_state.save(); - log::info!("成功导入默认供应商"); + if manager.add_provider(provider).is_ok() { + manager.current = "default".to_string(); + log::info!("成功导入默认供应商"); + } } } } } + + // 确保 Codex 应用存在 + config.ensure_app(&app_config::AppType::Codex); } + // 保存配置 + let _ = app_state.save(); + // 将同一个实例注入到全局状态,避免重复创建导致的不一致 app.manage(app_state); Ok(()) @@ -96,6 +115,7 @@ pub fn run() { commands::switch_provider, commands::import_default_config, commands::get_claude_config_status, + commands::get_config_status, commands::get_claude_code_config_path, commands::open_config_folder, commands::open_external, diff --git a/src-tauri/src/provider.rs b/src-tauri/src/provider.rs index 0287b37..a972339 100644 --- a/src-tauri/src/provider.rs +++ b/src-tauri/src/provider.rs @@ -1,12 +1,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -use std::path::Path; -use crate::config::{ - backup_config, copy_file, delete_file, get_claude_settings_path, get_provider_config_path, - read_json_file, write_json_file, -}; +use crate::config::{get_provider_config_path, write_json_file}; /// 供应商结构体 #[derive(Debug, Clone, Serialize, Deserialize)] @@ -54,21 +50,6 @@ impl Default for ProviderManager { } impl ProviderManager { - /// 加载供应商列表 - pub fn load_from_file(path: &Path) -> Result { - if !path.exists() { - log::info!("配置文件不存在,创建新的供应商管理器"); - return Ok(Self::default()); - } - - read_json_file(path) - } - - /// 保存供应商列表 - pub fn save_to_file(&self, path: &Path) -> Result<(), String> { - write_json_file(path, self) - } - /// 添加供应商 pub fn add_provider(&mut self, provider: Provider) -> Result<(), String> { // 保存供应商配置到独立文件 @@ -80,98 +61,6 @@ impl ProviderManager { Ok(()) } - /// 更新供应商 - pub fn update_provider(&mut self, provider: Provider) -> Result<(), String> { - // 检查供应商是否存在 - if !self.providers.contains_key(&provider.id) { - return Err(format!("供应商不存在: {}", provider.id)); - } - - // 如果名称改变了,需要处理配置文件 - if let Some(old_provider) = self.providers.get(&provider.id) { - if old_provider.name != provider.name { - // 删除旧配置文件 - let old_config_path = - get_provider_config_path(&provider.id, Some(&old_provider.name)); - delete_file(&old_config_path).ok(); // 忽略删除错误 - } - } - - // 保存新配置文件 - let config_path = get_provider_config_path(&provider.id, Some(&provider.name)); - write_json_file(&config_path, &provider.settings_config)?; - - // 更新管理器 - self.providers.insert(provider.id.clone(), provider); - Ok(()) - } - - /// 删除供应商 - pub fn delete_provider(&mut self, provider_id: &str) -> Result<(), String> { - // 检查是否为当前供应商 - if self.current == provider_id { - return Err("不能删除当前正在使用的供应商".to_string()); - } - - // 获取供应商信息 - let provider = self - .providers - .get(provider_id) - .ok_or_else(|| format!("供应商不存在: {}", provider_id))?; - - // 删除配置文件 - let config_path = get_provider_config_path(provider_id, Some(&provider.name)); - delete_file(&config_path)?; - - // 从管理器删除 - self.providers.remove(provider_id); - Ok(()) - } - - /// 切换供应商 - pub fn switch_provider(&mut self, provider_id: &str) -> Result<(), String> { - // 检查供应商是否存在 - let provider = self - .providers - .get(provider_id) - .ok_or_else(|| format!("供应商不存在: {}", provider_id))?; - - let settings_path = get_claude_settings_path(); - let provider_config_path = get_provider_config_path(provider_id, Some(&provider.name)); - - // 检查供应商配置文件是否存在 - if !provider_config_path.exists() { - return Err(format!( - "供应商配置文件不存在: {}", - provider_config_path.display() - )); - } - - // 如果当前有配置,先备份到当前供应商 - if settings_path.exists() && !self.current.is_empty() { - if let Some(current_provider) = self.providers.get(&self.current) { - let current_provider_path = - get_provider_config_path(&self.current, Some(¤t_provider.name)); - backup_config(&settings_path, ¤t_provider_path)?; - log::info!("已备份当前供应商配置: {}", current_provider.name); - } - } - - // 确保主配置父目录存在 - if let Some(parent) = settings_path.parent() { - std::fs::create_dir_all(parent).map_err(|e| format!("创建目录失败: {}", e))?; - } - - // 复制新供应商配置到主配置 - copy_file(&provider_config_path, &settings_path)?; - - // 更新当前供应商 - self.current = provider_id.to_string(); - - log::info!("成功切换到供应商: {}", provider.name); - Ok(()) - } - /// 获取所有供应商 pub fn get_all_providers(&self) -> &HashMap { &self.providers diff --git a/src-tauri/src/store.rs b/src-tauri/src/store.rs index 77382b8..194c33b 100644 --- a/src-tauri/src/store.rs +++ b/src-tauri/src/store.rs @@ -1,36 +1,31 @@ -use crate::config::get_app_config_path; -use crate::provider::ProviderManager; +use crate::app_config::MultiAppConfig; use std::sync::Mutex; /// 全局应用状态 pub struct AppState { - pub provider_manager: Mutex, + pub config: Mutex, } impl AppState { /// 创建新的应用状态 pub fn new() -> Self { - let config_path = get_app_config_path(); - let provider_manager = ProviderManager::load_from_file(&config_path).unwrap_or_else(|e| { + let config = MultiAppConfig::load().unwrap_or_else(|e| { log::warn!("加载配置失败: {}, 使用默认配置", e); - ProviderManager::default() + MultiAppConfig::default() }); Self { - provider_manager: Mutex::new(provider_manager), + config: Mutex::new(config), } } /// 保存配置到文件 pub fn save(&self) -> Result<(), String> { - let config_path = get_app_config_path(); - let manager = self - .provider_manager + let config = self + .config .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - manager.save_to_file(&config_path) + config.save() } - - // 保留按需扩展:若未来需要热加载,可在此实现 } diff --git a/src/App.css b/src/App.css index 8c79ef0..fcc5df8 100644 --- a/src/App.css +++ b/src/App.css @@ -14,11 +14,53 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); user-select: none; min-height: 3rem; + position: relative; +} + +.app-tabs { + position: absolute; + left: 2rem; + top: 0; + display: flex; + height: 100%; +} + +.app-tab { + background: transparent; + color: rgba(255, 255, 255, 0.7); + border: none; + padding: 0 1.5rem; + cursor: pointer; + font-size: 0.95rem; + font-weight: 500; + transition: all 0.2s; + position: relative; +} + +.app-tab:hover { + color: white; + background: rgba(255, 255, 255, 0.1); +} + +.app-tab.active { + color: white; + background: rgba(255, 255, 255, 0.15); +} + +.app-tab.active::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 3px; + background: white; } .app-header h1 { font-size: 1.5rem; font-weight: 500; + margin: 0 auto; } .header-actions { diff --git a/src/App.tsx b/src/App.tsx index 3f8975c..c05c13b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { Provider } from "./types"; +import { AppType } from "./lib/tauri-api"; import ProviderList from "./components/ProviderList"; import AddProviderModal from "./components/AddProviderModal"; import EditProviderModal from "./components/EditProviderModal"; @@ -7,6 +8,7 @@ import { ConfirmDialog } from "./components/ConfirmDialog"; import "./App.css"; function App() { + const [activeApp, setActiveApp] = useState("claude"); const [providers, setProviders] = useState>({}); const [currentProviderId, setCurrentProviderId] = useState(""); const [isAddModalOpen, setIsAddModalOpen] = useState(false); @@ -60,7 +62,7 @@ function App() { useEffect(() => { loadProviders(); loadConfigStatus(); - }, []); + }, [activeApp]); // 当切换应用时重新加载 // 清理定时器 useEffect(() => { @@ -72,8 +74,8 @@ function App() { }, []); const loadProviders = async () => { - const loadedProviders = await window.api.getProviders(); - const currentId = await window.api.getCurrentProvider(); + const loadedProviders = await window.api.getProviders(activeApp); + const currentId = await window.api.getCurrentProvider(activeApp); setProviders(loadedProviders); setCurrentProviderId(currentId); @@ -84,7 +86,7 @@ function App() { }; const loadConfigStatus = async () => { - const status = await window.api.getClaudeConfigStatus(); + const status = await window.api.getConfigStatus(activeApp); setConfigStatus({ exists: Boolean(status?.exists), path: String(status?.path || ""), @@ -101,14 +103,14 @@ function App() { ...provider, id: generateId(), }; - await window.api.addProvider(newProvider); + await window.api.addProvider(newProvider, activeApp); await loadProviders(); setIsAddModalOpen(false); }; const handleEditProvider = async (provider: Provider) => { try { - await window.api.updateProvider(provider); + await window.api.updateProvider(provider, activeApp); await loadProviders(); setEditingProviderId(null); // 显示编辑成功提示 @@ -127,7 +129,7 @@ function App() { title: "删除供应商", message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`, onConfirm: async () => { - await window.api.deleteProvider(id); + await window.api.deleteProvider(id, activeApp); await loadProviders(); setConfirmDialog(null); showNotification("供应商删除成功", "success"); @@ -136,12 +138,13 @@ function App() { }; const handleSwitchProvider = async (id: string) => { - const success = await window.api.switchProvider(id); + const success = await window.api.switchProvider(id, activeApp); if (success) { setCurrentProviderId(id); // 显示重启提示 + const appName = activeApp === "claude" ? "Claude Code" : "Codex"; showNotification( - "切换成功!请重启 Claude Code 终端以生效", + `切换成功!请重启 ${appName} 终端以生效`, "success", 2000, ); @@ -153,7 +156,7 @@ function App() { // 自动导入现有配置为"default"供应商 const handleAutoImportDefault = async () => { try { - const result = await window.api.importCurrentConfigAsDefault(); + const result = await window.api.importCurrentConfigAsDefault(activeApp); if (result.success) { await loadProviders(); @@ -171,13 +174,27 @@ function App() { }; const handleOpenConfigFolder = async () => { - await window.api.openConfigFolder(); + await window.api.openConfigFolder(activeApp); }; return (
-

Claude Code 供应商切换器

+
+ + +
+

{activeApp === "claude" ? "Claude Code" : "Codex"} 供应商切换器