diff --git a/src-tauri/src/codex_config.rs b/src-tauri/src/codex_config.rs index f525156..9a62742 100644 --- a/src-tauri/src/codex_config.rs +++ b/src-tauri/src/codex_config.rs @@ -4,9 +4,9 @@ use std::path::PathBuf; use crate::config::{ atomic_write, delete_file, sanitize_provider_name, write_json_file, write_text_file, }; +use serde_json::Value; use std::fs; use std::path::Path; -use serde_json::Value; /// 获取 Codex 配置目录路径 pub fn get_codex_config_dir() -> PathBuf { @@ -77,7 +77,8 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R None => String::new(), }; if !cfg_text.trim().is_empty() { - toml::from_str::(&cfg_text).map_err(|e| format!("config.toml 格式错误: {}", e))?; + toml::from_str::(&cfg_text) + .map_err(|e| format!("config.toml 格式错误: {}", e))?; } // 第一步:写 auth.json @@ -121,7 +122,9 @@ pub fn validate_config_toml(text: &str) -> Result<(), String> { if text.trim().is_empty() { return Ok(()); } - toml::from_str::(text).map(|_| ()).map_err(|e| format!("config.toml 语法错误: {}", e)) + toml::from_str::(text) + .map(|_| ()) + .map_err(|e| format!("config.toml 语法错误: {}", e)) } /// 读取并校验 `~/.codex/config.toml`,返回文本(可能为空) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index bfd706a..2c47493 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -6,7 +6,7 @@ use tauri_plugin_opener::OpenerExt; use crate::app_config::AppType; use crate::codex_config; -use crate::config::{ConfigStatus, get_claude_settings_path}; +use crate::config::{get_claude_settings_path, ConfigStatus}; use crate::provider::Provider; use crate::store::AppState; @@ -116,7 +116,9 @@ pub async fn add_provider( let manager = config .get_manager_mut(&app_type) .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; - manager.providers.insert(provider.id.clone(), provider.clone()); + manager + .providers + .insert(provider.id.clone(), provider.clone()); } state.save()?; @@ -146,7 +148,10 @@ pub async fn update_provider( let manager = config .get_manager(&app_type) .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; - (manager.providers.contains_key(&provider.id), manager.current == provider.id) + ( + manager.providers.contains_key(&provider.id), + manager.current == provider.id, + ) }; if !exists { return Err(format!("供应商不存在: {}", provider.id)); @@ -182,7 +187,9 @@ pub async fn update_provider( let manager = config .get_manager_mut(&app_type) .ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?; - manager.providers.insert(provider.id.clone(), provider.clone()); + manager + .providers + .insert(provider.id.clone(), provider.clone()); } state.save()?; @@ -390,7 +397,8 @@ pub async fn import_default_config( if !auth_path.exists() { return Err("Codex 配置文件不存在".to_string()); } - let auth: serde_json::Value = crate::config::read_json_file::(&auth_path)?; + let auth: serde_json::Value = + crate::config::read_json_file::(&auth_path)?; let config_str = match crate::codex_config::read_and_validate_codex_config_text() { Ok(s) => s, Err(e) => return Err(e), @@ -488,7 +496,7 @@ pub async fn open_config_folder( .or_else(|| app.as_deref().map(|s| s.into())) .or_else(|| appType.as_deref().map(|s| s.into())) .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(), @@ -500,7 +508,8 @@ pub async fn open_config_folder( } // 使用 opener 插件打开文件夹 - handle.opener() + handle + .opener() .open_path(config_dir.to_string_lossy().to_string(), None::) .map_err(|e| format!("打开文件夹失败: {}", e))?; @@ -524,3 +533,68 @@ pub async fn open_external(app: tauri::AppHandle, url: String) -> Result Result { + use crate::config::get_app_config_path; + + let config_path = get_app_config_path(); + Ok(config_path.to_string_lossy().to_string()) +} + +/// 打开应用配置文件夹 +#[tauri::command] +pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result { + use crate::config::get_app_config_dir; + + let config_dir = get_app_config_dir(); + + // 确保目录存在 + if !config_dir.exists() { + std::fs::create_dir_all(&config_dir).map_err(|e| format!("创建目录失败: {}", e))?; + } + + // 使用 opener 插件打开文件夹 + handle + .opener() + .open_path(config_dir.to_string_lossy().to_string(), None::) + .map_err(|e| format!("打开文件夹失败: {}", e))?; + + Ok(true) +} + +/// 获取设置 +#[tauri::command] +pub async fn get_settings(_state: State<'_, AppState>) -> Result { + // 暂时返回默认设置 + Ok(serde_json::json!({ + "showInDock": true + })) +} + +/// 保存设置 +#[tauri::command] +pub async fn save_settings( + _state: State<'_, AppState>, + settings: serde_json::Value, +) -> Result { + // TODO: 实现设置保存逻辑 + log::info!("保存设置: {:?}", settings); + Ok(true) +} + +/// 检查更新 +#[tauri::command] +pub async fn check_for_updates(handle: tauri::AppHandle) -> Result { + // 打开 GitHub releases 页面 + handle + .opener() + .open_url( + "https://github.com/yungookim/cc-switch/releases", + None::, + ) + .map_err(|e| format!("打开更新页面失败: {}", e))?; + + Ok(true) +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 5c18ce5..10b39c4 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -88,7 +88,6 @@ pub fn archive_file(ts: u64, category: &str, src: &Path) -> Result String { name.chars() diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index eb1c37f..c776796 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -326,6 +326,11 @@ pub fn run() { commands::get_claude_code_config_path, commands::open_config_folder, commands::open_external, + commands::get_app_config_path, + commands::open_app_config_folder, + commands::get_settings, + commands::save_settings, + commands::check_for_updates, update_tray_menu, ]) .run(tauri::generate_context!()) diff --git a/src-tauri/src/migration.rs b/src-tauri/src/migration.rs index c440383..e3e6083 100644 --- a/src-tauri/src/migration.rs +++ b/src-tauri/src/migration.rs @@ -47,7 +47,10 @@ fn extract_claude_api_key(value: &Value) -> Option { fn extract_codex_api_key(value: &Value) -> Option { value .get("auth") - .and_then(|auth| auth.get("OPENAI_API_KEY").or_else(|| auth.get("openai_api_key"))) + .and_then(|auth| { + auth.get("OPENAI_API_KEY") + .or_else(|| auth.get("openai_api_key")) + }) .and_then(|v| v.as_str()) .map(|s| s.to_string()) } @@ -77,7 +80,9 @@ fn scan_claude_copies() -> Vec<(String, PathBuf, Value)> { if !fname.starts_with("settings-") || !fname.ends_with(".json") { continue; } - let name = fname.trim_start_matches("settings-").trim_end_matches(".json"); + let name = fname + .trim_start_matches("settings-") + .trim_end_matches(".json"); if let Ok(val) = crate::config::read_json_file::(&p) { items.push((name.to_string(), p, val)); } @@ -104,7 +109,9 @@ fn scan_codex_copies() -> Vec<(String, Option, Option, Value)> let entry = by_name.entry(name.to_string()).or_default(); entry.0 = Some(p); } else if fname.starts_with("config-") && fname.ends_with(".toml") { - let name = fname.trim_start_matches("config-").trim_end_matches(".toml"); + let name = fname + .trim_start_matches("config-") + .trim_end_matches(".toml"); let entry = by_name.entry(name.to_string()).or_default(); entry.1 = Some(p); } @@ -183,17 +190,14 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result Result Result { log::warn!("读取 Codex live auth.json 失败: {}", e); @@ -277,17 +277,14 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result Result Result { let _ = delete_file(&ap); } + Ok(Some(_)) => { + let _ = delete_file(&ap); + } _ => {} } } if let Some(cp) = cp { match archive_file(ts, "codex", &cp) { - Ok(Some(_)) => { let _ = delete_file(&cp); } + Ok(Some(_)) => { + let _ = delete_file(&cp); + } _ => {} } } @@ -404,7 +399,11 @@ pub fn dedupe_config(config: &mut MultiAppConfig) -> usize { let mut keep: Map = Map::new(); // key -> id 保留 let mut remove: Vec = Vec::new(); for (id, p) in mgr.providers.iter() { - let k = format!("{}|{}", norm_name(&p.name), extract_key(&p.settings_config).unwrap_or_default()); + let k = format!( + "{}|{}", + norm_name(&p.name), + extract_key(&p.settings_config).unwrap_or_default() + ); if let Some(exist_id) = keep.get(&k) { // 若当前是正在使用的,则用当前替换之前的,反之丢弃当前 if *id == mgr.current { diff --git a/src/App.tsx b/src/App.tsx index 376aab7..6d5a90c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -330,9 +330,7 @@ function App() { )} {isSettingsOpen && ( - setIsSettingsOpen(false)} - /> + setIsSettingsOpen(false)} /> )} ); diff --git a/src/components/ProviderList.tsx b/src/components/ProviderList.tsx index c98036a..1eac355 100644 --- a/src/components/ProviderList.tsx +++ b/src/components/ProviderList.tsx @@ -100,7 +100,10 @@ const ProviderList: React.FC = ({ {provider.websiteUrl} ) : ( - + {apiUrl} )} diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 159f2ad..6627fee 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -1,10 +1,7 @@ import { useState, useEffect } from "react"; import { X, Info, RefreshCw, FolderOpen } from "lucide-react"; import "../lib/tauri-api"; - -interface Settings { - showInDock: boolean; -} +import type { Settings } from "../types"; interface SettingsModalProps { onClose: () => void; @@ -36,9 +33,9 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const loadConfigPath = async () => { try { - const status = await window.api.getConfigStatus("claude"); - if (status?.path) { - setConfigPath(status.path.replace("/claude_code_config.json", "")); + const path = await window.api.getAppConfigPath(); + if (path) { + setConfigPath(path); } } catch (error) { console.error("获取配置路径失败:", error); @@ -67,7 +64,7 @@ export default function SettingsModal({ onClose }: SettingsModalProps) { const handleOpenConfigFolder = async () => { try { - await window.api.openConfigFolder("claude"); + await window.api.openAppConfigFolder(); } catch (error) { console.error("打开配置文件夹失败:", error); } diff --git a/src/lib/tauri-api.ts b/src/lib/tauri-api.ts index 41ebe24..d49275a 100644 --- a/src/lib/tauri-api.ts +++ b/src/lib/tauri-api.ts @@ -1,6 +1,6 @@ import { invoke } from "@tauri-apps/api/core"; import { listen, UnlistenFn } from "@tauri-apps/api/event"; -import { Provider } from "../types"; +import { Provider, Settings } from "../types"; // 应用类型 export type AppType = "claude" | "codex"; @@ -196,7 +196,7 @@ export const tauriAPI = { }, // 获取设置 - getSettings: async (): Promise => { + getSettings: async (): Promise => { try { return await invoke("get_settings"); } catch (error) { @@ -206,7 +206,7 @@ export const tauriAPI = { }, // 保存设置 - saveSettings: async (settings: any): Promise => { + saveSettings: async (settings: Settings): Promise => { try { return await invoke("save_settings", { settings }); } catch (error) { @@ -223,6 +223,25 @@ export const tauriAPI = { console.error("检查更新失败:", error); } }, + + // 获取应用配置文件路径 + getAppConfigPath: async (): Promise => { + try { + return await invoke("get_app_config_path"); + } catch (error) { + console.error("获取应用配置路径失败:", error); + return ""; + } + }, + + // 打开应用配置文件夹 + openAppConfigFolder: async (): Promise => { + try { + await invoke("open_app_config_folder"); + } catch (error) { + console.error("打开应用配置文件夹失败:", error); + } + }, }; // 创建全局 API 对象,兼容现有代码 diff --git a/src/types.ts b/src/types.ts index 5d448ee..8ec7dd5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -9,3 +9,8 @@ export interface AppConfig { providers: Record; current: string; } + +// 应用设置类型(用于 SettingsModal 与 Tauri API) +export interface Settings { + showInDock: boolean; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index eb320f3..2e05abd 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1,6 +1,6 @@ /// -import { Provider } from "./types"; +import { Provider, Settings } from "./types"; import { AppType } from "./lib/tauri-api"; import type { UnlistenFn } from "@tauri-apps/api/event"; @@ -35,9 +35,11 @@ declare global { onProviderSwitched: ( callback: (data: { appType: string; providerId: string }) => void, ) => Promise; - getSettings: () => Promise; - saveSettings: (settings: any) => Promise; + getSettings: () => Promise; + saveSettings: (settings: Settings) => Promise; checkForUpdates: () => Promise; + getAppConfigPath: () => Promise; + openAppConfigFolder: () => Promise; }; platform: { isMac: boolean;