- feat(codex): 引入 Codex 应用与供应商切换(管理 auth.json/config.toml,支持备份与恢复)

- feat(core): 多应用配置 v2(claude/codex)与 ProviderManager;支持 v1→v2 自动迁移
- feat(ui): 新增 Codex 页签与双编辑器表单;统一 window.api 支持 app 参数
- feat(tauri): 新增 get_config_status/open_config_folder/open_external 命令并适配 Codex
- fix(codex): 主配置缺失时不执行默认导入(对齐 Claude 行为)
- chore: 配置目录展示与重启提示等细节优化
This commit is contained in:
Jason
2025-08-30 21:54:11 +08:00
parent 0e803b53d8
commit c10ace7a84
13 changed files with 891 additions and 279 deletions

113
src-tauri/src/app_config.rs Normal file
View File

@@ -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<String, ProviderManager>,
}
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<Self, String> {
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::<ProviderManager>(&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::<Self>(&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());
}
}
}

View File

@@ -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<Value, String> {
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::<Value>(&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)
}

View File

@@ -2,6 +2,8 @@ use std::collections::HashMap;
use tauri::State; use tauri::State;
use tauri_plugin_opener::OpenerExt; 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::config::{ConfigStatus, get_claude_settings_path, import_current_config_as_default};
use crate::provider::Provider; use crate::provider::Provider;
use crate::store::AppState; use crate::store::AppState;
@@ -10,38 +12,82 @@ use crate::store::AppState;
#[tauri::command] #[tauri::command]
pub async fn get_providers( pub async fn get_providers(
state: State<'_, AppState>, state: State<'_, AppState>,
app: Option<String>,
) -> Result<HashMap<String, Provider>, String> { ) -> Result<HashMap<String, Provider>, String> {
let manager = state let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
.provider_manager
let config = state
.config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .map_err(|e| format!("获取锁失败: {}", e))?;
let manager = config
.get_manager(&app_type)
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
Ok(manager.get_all_providers().clone()) Ok(manager.get_all_providers().clone())
} }
/// 获取当前供应商ID /// 获取当前供应商ID
#[tauri::command] #[tauri::command]
pub async fn get_current_provider(state: State<'_, AppState>) -> Result<String, String> { pub async fn get_current_provider(
let manager = state state: State<'_, AppState>,
.provider_manager app: Option<String>,
) -> Result<String, String> {
let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
let config = state
.config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .map_err(|e| format!("获取锁失败: {}", e))?;
let manager = config
.get_manager(&app_type)
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
Ok(manager.current.clone()) Ok(manager.current.clone())
} }
/// 添加供应商 /// 添加供应商
#[tauri::command] #[tauri::command]
pub async fn add_provider(state: State<'_, AppState>, provider: Provider) -> Result<bool, String> { pub async fn add_provider(
let mut manager = state state: State<'_, AppState>,
.provider_manager app: Option<String>,
provider: Provider,
) -> Result<bool, String> {
let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
let mut config = state
.config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .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()?; state.save()?;
Ok(true) Ok(true)
@@ -51,17 +97,64 @@ pub async fn add_provider(state: State<'_, AppState>, provider: Provider) -> Res
#[tauri::command] #[tauri::command]
pub async fn update_provider( pub async fn update_provider(
state: State<'_, AppState>, state: State<'_, AppState>,
app: Option<String>,
provider: Provider, provider: Provider,
) -> Result<bool, String> { ) -> Result<bool, String> {
let mut manager = state let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
.provider_manager
let mut config = state
.config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .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()?; state.save()?;
Ok(true) Ok(true)
@@ -69,16 +162,51 @@ pub async fn update_provider(
/// 删除供应商 /// 删除供应商
#[tauri::command] #[tauri::command]
pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> { pub async fn delete_provider(
let mut manager = state state: State<'_, AppState>,
.provider_manager app: Option<String>,
id: String,
) -> Result<bool, String> {
let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
let mut config = state
.config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .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()?; state.save()?;
Ok(true) Ok(true)
@@ -86,16 +214,87 @@ pub async fn delete_provider(state: State<'_, AppState>, id: String) -> Result<b
/// 切换供应商 /// 切换供应商
#[tauri::command] #[tauri::command]
pub async fn switch_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> { pub async fn switch_provider(
let mut manager = state state: State<'_, AppState>,
.provider_manager app: Option<String>,
id: String,
) -> Result<bool, String> {
let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
let mut config = state
.config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .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, &current_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(&current_provider.name));
backup_config(&settings_path, &current_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()?; state.save()?;
Ok(true) Ok(true)
@@ -103,20 +302,31 @@ pub async fn switch_provider(state: State<'_, AppState>, id: String) -> Result<b
/// 导入当前配置为默认供应商 /// 导入当前配置为默认供应商
#[tauri::command] #[tauri::command]
pub async fn import_default_config(state: State<'_, AppState>) -> Result<bool, String> { pub async fn import_default_config(
state: State<'_, AppState>,
app: Option<String>,
) -> Result<bool, String> {
let app_type = app.as_deref().map(|s| s.into()).unwrap_or(AppType::Claude);
// 若已存在 default 供应商,则直接返回,避免重复导入 // 若已存在 default 供应商,则直接返回,避免重复导入
{ {
let manager = state let config = state
.provider_manager .config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .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( let provider = Provider::with_id(
@@ -127,12 +337,32 @@ pub async fn import_default_config(state: State<'_, AppState>) -> Result<bool, S
); );
// 添加到管理器 // 添加到管理器
let mut manager = state let mut config = state
.provider_manager .config
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .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_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 // 如果没有当前供应商,设置为 default
if manager.current.is_empty() { if manager.current.is_empty() {
@@ -140,7 +370,7 @@ pub async fn import_default_config(state: State<'_, AppState>) -> Result<bool, S
} }
// 保存配置 // 保存配置
drop(manager); // 释放锁 drop(config); // 释放锁
state.save()?; state.save()?;
Ok(true) Ok(true)
@@ -152,6 +382,27 @@ pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
Ok(crate::config::get_claude_config_status()) Ok(crate::config::get_claude_config_status())
} }
/// 获取应用配置状态(通用)
#[tauri::command]
pub async fn get_config_status(app_type: Option<AppType>) -> Result<ConfigStatus, String> {
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 配置文件路径 /// 获取 Claude Code 配置文件路径
#[tauri::command] #[tauri::command]
pub async fn get_claude_code_config_path() -> Result<String, String> { pub async fn get_claude_code_config_path() -> Result<String, String> {
@@ -160,8 +411,13 @@ pub async fn get_claude_code_config_path() -> Result<String, String> {
/// 打开配置文件夹 /// 打开配置文件夹
#[tauri::command] #[tauri::command]
pub async fn open_config_folder(app: tauri::AppHandle) -> Result<bool, String> { pub async fn open_config_folder(app: tauri::AppHandle, app_type: Option<AppType>) -> Result<bool, String> {
let config_dir = crate::config::get_claude_config_dir(); 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() { if !config_dir.exists() {

View File

@@ -1,3 +1,5 @@
mod app_config;
mod codex_config;
mod commands; mod commands;
mod config; mod config;
mod provider; mod provider;
@@ -55,34 +57,51 @@ pub fn run() {
// 如果没有供应商且存在 Claude Code 配置,自动导入 // 如果没有供应商且存在 Claude Code 配置,自动导入
{ {
let manager = app_state.provider_manager.lock().unwrap(); let mut config = app_state.config.lock().unwrap();
if manager.providers.is_empty() {
drop(manager); // 释放锁
// 检查 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(); let settings_path = config::get_claude_settings_path();
if settings_path.exists() { if settings_path.exists() {
log::info!("检测到 Claude Code 配置,自动导入为默认供应商"); log::info!("检测到 Claude Code 配置,自动导入为默认供应商");
if let Ok(settings_config) = config::import_current_config_as_default() { if let Ok(settings_config) = config::import_current_config_as_default() {
let mut manager = app_state.provider_manager.lock().unwrap(); if let Some(manager) =
let provider = provider::Provider::with_id( config.get_manager_mut(&app_config::AppType::Claude)
"default".to_string(), {
"default".to_string(), let provider = provider::Provider::with_id(
settings_config, "default".to_string(),
None, "default".to_string(),
); settings_config,
None,
);
if manager.add_provider(provider).is_ok() { if manager.add_provider(provider).is_ok() {
manager.current = "default".to_string(); manager.current = "default".to_string();
drop(manager); log::info!("成功导入默认供应商");
let _ = app_state.save(); }
log::info!("成功导入默认供应商");
} }
} }
} }
} }
// 确保 Codex 应用存在
config.ensure_app(&app_config::AppType::Codex);
} }
// 保存配置
let _ = app_state.save();
// 将同一个实例注入到全局状态,避免重复创建导致的不一致 // 将同一个实例注入到全局状态,避免重复创建导致的不一致
app.manage(app_state); app.manage(app_state);
Ok(()) Ok(())
@@ -96,6 +115,7 @@ pub fn run() {
commands::switch_provider, commands::switch_provider,
commands::import_default_config, commands::import_default_config,
commands::get_claude_config_status, commands::get_claude_config_status,
commands::get_config_status,
commands::get_claude_code_config_path, commands::get_claude_code_config_path,
commands::open_config_folder, commands::open_config_folder,
commands::open_external, commands::open_external,

View File

@@ -1,12 +1,8 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path;
use crate::config::{ use crate::config::{get_provider_config_path, write_json_file};
backup_config, copy_file, delete_file, get_claude_settings_path, get_provider_config_path,
read_json_file, write_json_file,
};
/// 供应商结构体 /// 供应商结构体
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -54,21 +50,6 @@ impl Default for ProviderManager {
} }
impl ProviderManager { impl ProviderManager {
/// 加载供应商列表
pub fn load_from_file(path: &Path) -> Result<Self, String> {
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> { pub fn add_provider(&mut self, provider: Provider) -> Result<(), String> {
// 保存供应商配置到独立文件 // 保存供应商配置到独立文件
@@ -80,98 +61,6 @@ impl ProviderManager {
Ok(()) 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(&current_provider.name));
backup_config(&settings_path, &current_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<String, Provider> { pub fn get_all_providers(&self) -> &HashMap<String, Provider> {
&self.providers &self.providers

View File

@@ -1,36 +1,31 @@
use crate::config::get_app_config_path; use crate::app_config::MultiAppConfig;
use crate::provider::ProviderManager;
use std::sync::Mutex; use std::sync::Mutex;
/// 全局应用状态 /// 全局应用状态
pub struct AppState { pub struct AppState {
pub provider_manager: Mutex<ProviderManager>, pub config: Mutex<MultiAppConfig>,
} }
impl AppState { impl AppState {
/// 创建新的应用状态 /// 创建新的应用状态
pub fn new() -> Self { pub fn new() -> Self {
let config_path = get_app_config_path(); let config = MultiAppConfig::load().unwrap_or_else(|e| {
let provider_manager = ProviderManager::load_from_file(&config_path).unwrap_or_else(|e| {
log::warn!("加载配置失败: {}, 使用默认配置", e); log::warn!("加载配置失败: {}, 使用默认配置", e);
ProviderManager::default() MultiAppConfig::default()
}); });
Self { Self {
provider_manager: Mutex::new(provider_manager), config: Mutex::new(config),
} }
} }
/// 保存配置到文件 /// 保存配置到文件
pub fn save(&self) -> Result<(), String> { pub fn save(&self) -> Result<(), String> {
let config_path = get_app_config_path(); let config = self
let manager = self .config
.provider_manager
.lock() .lock()
.map_err(|e| format!("获取锁失败: {}", e))?; .map_err(|e| format!("获取锁失败: {}", e))?;
manager.save_to_file(&config_path) config.save()
} }
// 保留按需扩展:若未来需要热加载,可在此实现
} }

View File

@@ -14,11 +14,53 @@
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
user-select: none; user-select: none;
min-height: 3rem; 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 { .app-header h1 {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 500; font-weight: 500;
margin: 0 auto;
} }
.header-actions { .header-actions {

View File

@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef } from "react";
import { Provider } from "./types"; import { Provider } from "./types";
import { AppType } from "./lib/tauri-api";
import ProviderList from "./components/ProviderList"; import ProviderList from "./components/ProviderList";
import AddProviderModal from "./components/AddProviderModal"; import AddProviderModal from "./components/AddProviderModal";
import EditProviderModal from "./components/EditProviderModal"; import EditProviderModal from "./components/EditProviderModal";
@@ -7,6 +8,7 @@ import { ConfirmDialog } from "./components/ConfirmDialog";
import "./App.css"; import "./App.css";
function App() { function App() {
const [activeApp, setActiveApp] = useState<AppType>("claude");
const [providers, setProviders] = useState<Record<string, Provider>>({}); const [providers, setProviders] = useState<Record<string, Provider>>({});
const [currentProviderId, setCurrentProviderId] = useState<string>(""); const [currentProviderId, setCurrentProviderId] = useState<string>("");
const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false);
@@ -60,7 +62,7 @@ function App() {
useEffect(() => { useEffect(() => {
loadProviders(); loadProviders();
loadConfigStatus(); loadConfigStatus();
}, []); }, [activeApp]); // 当切换应用时重新加载
// 清理定时器 // 清理定时器
useEffect(() => { useEffect(() => {
@@ -72,8 +74,8 @@ function App() {
}, []); }, []);
const loadProviders = async () => { const loadProviders = async () => {
const loadedProviders = await window.api.getProviders(); const loadedProviders = await window.api.getProviders(activeApp);
const currentId = await window.api.getCurrentProvider(); const currentId = await window.api.getCurrentProvider(activeApp);
setProviders(loadedProviders); setProviders(loadedProviders);
setCurrentProviderId(currentId); setCurrentProviderId(currentId);
@@ -84,7 +86,7 @@ function App() {
}; };
const loadConfigStatus = async () => { const loadConfigStatus = async () => {
const status = await window.api.getClaudeConfigStatus(); const status = await window.api.getConfigStatus(activeApp);
setConfigStatus({ setConfigStatus({
exists: Boolean(status?.exists), exists: Boolean(status?.exists),
path: String(status?.path || ""), path: String(status?.path || ""),
@@ -101,14 +103,14 @@ function App() {
...provider, ...provider,
id: generateId(), id: generateId(),
}; };
await window.api.addProvider(newProvider); await window.api.addProvider(newProvider, activeApp);
await loadProviders(); await loadProviders();
setIsAddModalOpen(false); setIsAddModalOpen(false);
}; };
const handleEditProvider = async (provider: Provider) => { const handleEditProvider = async (provider: Provider) => {
try { try {
await window.api.updateProvider(provider); await window.api.updateProvider(provider, activeApp);
await loadProviders(); await loadProviders();
setEditingProviderId(null); setEditingProviderId(null);
// 显示编辑成功提示 // 显示编辑成功提示
@@ -127,7 +129,7 @@ function App() {
title: "删除供应商", title: "删除供应商",
message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`, message: `确定要删除供应商 "${provider?.name}" 吗?此操作无法撤销。`,
onConfirm: async () => { onConfirm: async () => {
await window.api.deleteProvider(id); await window.api.deleteProvider(id, activeApp);
await loadProviders(); await loadProviders();
setConfirmDialog(null); setConfirmDialog(null);
showNotification("供应商删除成功", "success"); showNotification("供应商删除成功", "success");
@@ -136,12 +138,13 @@ function App() {
}; };
const handleSwitchProvider = async (id: string) => { const handleSwitchProvider = async (id: string) => {
const success = await window.api.switchProvider(id); const success = await window.api.switchProvider(id, activeApp);
if (success) { if (success) {
setCurrentProviderId(id); setCurrentProviderId(id);
// 显示重启提示 // 显示重启提示
const appName = activeApp === "claude" ? "Claude Code" : "Codex";
showNotification( showNotification(
"切换成功!请重启 Claude Code 终端以生效", `切换成功!请重启 ${appName} 终端以生效`,
"success", "success",
2000, 2000,
); );
@@ -153,7 +156,7 @@ function App() {
// 自动导入现有配置为"default"供应商 // 自动导入现有配置为"default"供应商
const handleAutoImportDefault = async () => { const handleAutoImportDefault = async () => {
try { try {
const result = await window.api.importCurrentConfigAsDefault(); const result = await window.api.importCurrentConfigAsDefault(activeApp);
if (result.success) { if (result.success) {
await loadProviders(); await loadProviders();
@@ -171,13 +174,27 @@ function App() {
}; };
const handleOpenConfigFolder = async () => { const handleOpenConfigFolder = async () => {
await window.api.openConfigFolder(); await window.api.openConfigFolder(activeApp);
}; };
return ( return (
<div className="app"> <div className="app">
<header className="app-header"> <header className="app-header">
<h1>Claude Code </h1> <div className="app-tabs">
<button
className={`app-tab ${activeApp === "claude" ? "active" : ""}`}
onClick={() => setActiveApp("claude")}
>
Claude Code
</button>
<button
className={`app-tab ${activeApp === "codex" ? "active" : ""}`}
onClick={() => setActiveApp("codex")}
>
Codex
</button>
</div>
<h1>{activeApp === "claude" ? "Claude Code" : "Codex"} </h1>
<div className="header-actions"> <div className="header-actions">
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}> <button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
@@ -228,6 +245,7 @@ function App() {
{isAddModalOpen && ( {isAddModalOpen && (
<AddProviderModal <AddProviderModal
appType={activeApp}
onAdd={handleAddProvider} onAdd={handleAddProvider}
onClose={() => setIsAddModalOpen(false)} onClose={() => setIsAddModalOpen(false)}
/> />
@@ -235,6 +253,7 @@ function App() {
{editingProviderId && providers[editingProviderId] && ( {editingProviderId && providers[editingProviderId] && (
<EditProviderModal <EditProviderModal
appType={activeApp}
provider={providers[editingProviderId]} provider={providers[editingProviderId]}
onSave={handleEditProvider} onSave={handleEditProvider}
onClose={() => setEditingProviderId(null)} onClose={() => setEditingProviderId(null)}

View File

@@ -1,18 +1,22 @@
import React from "react"; import React from "react";
import { Provider } from "../types"; import { Provider } from "../types";
import { AppType } from "../lib/tauri-api";
import ProviderForm from "./ProviderForm"; import ProviderForm from "./ProviderForm";
interface AddProviderModalProps { interface AddProviderModalProps {
appType: AppType;
onAdd: (provider: Omit<Provider, "id">) => void; onAdd: (provider: Omit<Provider, "id">) => void;
onClose: () => void; onClose: () => void;
} }
const AddProviderModal: React.FC<AddProviderModalProps> = ({ const AddProviderModal: React.FC<AddProviderModalProps> = ({
appType,
onAdd, onAdd,
onClose, onClose,
}) => { }) => {
return ( return (
<ProviderForm <ProviderForm
appType={appType}
title="添加新供应商" title="添加新供应商"
submitText="添加" submitText="添加"
showPresets={true} showPresets={true}

View File

@@ -1,14 +1,17 @@
import React from "react"; import React from "react";
import { Provider } from "../types"; import { Provider } from "../types";
import { AppType } from "../lib/tauri-api";
import ProviderForm from "./ProviderForm"; import ProviderForm from "./ProviderForm";
interface EditProviderModalProps { interface EditProviderModalProps {
appType: AppType;
provider: Provider; provider: Provider;
onSave: (provider: Provider) => void; onSave: (provider: Provider) => void;
onClose: () => void; onClose: () => void;
} }
const EditProviderModal: React.FC<EditProviderModalProps> = ({ const EditProviderModal: React.FC<EditProviderModalProps> = ({
appType,
provider, provider,
onSave, onSave,
onClose, onClose,
@@ -22,6 +25,7 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
return ( return (
<ProviderForm <ProviderForm
appType={appType}
title="编辑供应商" title="编辑供应商"
submitText="保存" submitText="保存"
initialData={provider} initialData={provider}

View File

@@ -1,5 +1,6 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Provider } from "../types"; import { Provider } from "../types";
import { AppType } from "../lib/tauri-api";
import { import {
updateCoAuthoredSetting, updateCoAuthoredSetting,
checkCoAuthoredSetting, checkCoAuthoredSetting,
@@ -12,6 +13,7 @@ import { providerPresets } from "../config/providerPresets";
import "./AddProviderModal.css"; import "./AddProviderModal.css";
interface ProviderFormProps { interface ProviderFormProps {
appType?: AppType;
title: string; title: string;
submitText: string; submitText: string;
initialData?: Provider; initialData?: Provider;
@@ -21,6 +23,7 @@ interface ProviderFormProps {
} }
const ProviderForm: React.FC<ProviderFormProps> = ({ const ProviderForm: React.FC<ProviderFormProps> = ({
appType = "claude",
title, title,
submitText, submitText,
initialData, initialData,
@@ -28,6 +31,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
onSubmit, onSubmit,
onClose, onClose,
}) => { }) => {
// 对于 Codex需要分离 auth 和 config
const isCodex = appType === "codex";
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: initialData?.name || "", name: initialData?.name || "",
websiteUrl: initialData?.websiteUrl || "", websiteUrl: initialData?.websiteUrl || "",
@@ -35,6 +41,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
? JSON.stringify(initialData.settingsConfig, null, 2) ? JSON.stringify(initialData.settingsConfig, null, 2)
: "", : "",
}); });
// Codex 特有的状态
const [codexAuth, setCodexAuth] = useState("");
const [codexConfig, setCodexConfig] = useState("");
// 初始化 Codex 配置
useEffect(() => {
if (isCodex && initialData) {
const config = initialData.settingsConfig;
if (typeof config === "object" && config !== null) {
setCodexAuth(JSON.stringify(config.auth || {}, null, 2));
setCodexConfig(config.config || "");
}
}
}, [isCodex, initialData]);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [disableCoAuthored, setDisableCoAuthored] = useState(false); const [disableCoAuthored, setDisableCoAuthored] = useState(false);
const [selectedPreset, setSelectedPreset] = useState<number | null>(null); const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
@@ -58,18 +79,38 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
return; return;
} }
if (!formData.settingsConfig.trim()) {
setError("请填写配置内容");
return;
}
let settingsConfig: Record<string, any>; let settingsConfig: Record<string, any>;
try { if (isCodex) {
settingsConfig = JSON.parse(formData.settingsConfig); // Codex: 验证两个文件
} catch (err) { if (!codexAuth.trim() || !codexConfig.trim()) {
setError("配置JSON格式错误请检查语法"); setError("请填写 auth.json 和 config.toml 配置");
return; return;
}
try {
const authJson = JSON.parse(codexAuth);
settingsConfig = {
auth: authJson,
config: codexConfig,
};
} catch (err) {
setError("auth.json 格式错误请检查JSON语法");
return;
}
} else {
// Claude: 原有逻辑
if (!formData.settingsConfig.trim()) {
setError("请填写配置内容");
return;
}
try {
settingsConfig = JSON.parse(formData.settingsConfig);
} catch (err) {
setError("配置JSON格式错误请检查语法");
return;
}
} }
onSubmit({ onSubmit({
@@ -226,7 +267,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
<div className="modal-body"> <div className="modal-body">
{error && <div className="error-message">{error}</div>} {error && <div className="error-message">{error}</div>}
{showPresets && ( {showPresets && !isCodex && (
<div className="presets"> <div className="presets">
<label> key</label> <label> key</label>
<div className="preset-buttons"> <div className="preset-buttons">
@@ -262,33 +303,35 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
/> />
</div> </div>
<div {!isCodex && (
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`} <div
> className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
<label htmlFor="apiKey">API Key *</label> >
<input <label htmlFor="apiKey">API Key *</label>
type="text" <input
id="apiKey" type="text"
value={apiKey} id="apiKey"
onChange={(e) => handleApiKeyChange(e.target.value)} value={apiKey}
placeholder={ onChange={(e) => handleApiKeyChange(e.target.value)}
isOfficialPreset placeholder={
? "官方登录无需填写 API Key直接保存即可" isOfficialPreset
: "只需要填这里,下方配置会自动填充" ? "官方登录无需填写 API Key直接保存即可"
} : "只需要填这里,下方配置会自动填充"
disabled={isOfficialPreset} }
autoComplete="off" disabled={isOfficialPreset}
style={ autoComplete="off"
isOfficialPreset style={
? { isOfficialPreset
backgroundColor: "#f5f5f5", ? {
cursor: "not-allowed", backgroundColor: "#f5f5f5",
color: "#999", cursor: "not-allowed",
} color: "#999",
: {} }
} : {}
/> }
</div> />
</div>
)}
<div className="form-group"> <div className="form-group">
<label htmlFor="websiteUrl"></label> <label htmlFor="websiteUrl"></label>
@@ -303,39 +346,80 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
/> />
</div> </div>
<div className="form-group"> {/* Claude 或 Codex 的配置部分 */}
<div className="label-with-checkbox"> {isCodex ? (
<label htmlFor="settingsConfig"> // Codex: 双编辑器
Claude Code (JSON) * <>
</label> <div className="form-group">
<label className="checkbox-label"> <label htmlFor="codexAuth">auth.json (JSON) *</label>
<input <textarea
type="checkbox" id="codexAuth"
checked={disableCoAuthored} value={codexAuth}
onChange={(e) => handleCoAuthoredToggle(e.target.checked)} onChange={(e) => setCodexAuth(e.target.value)}
placeholder={`{
"api_key": "your-codex-api-key"
}`}
rows={6}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/> />
Claude Code <small className="field-hint">Codex auth.json </small>
</label> </div>
</div>
<textarea <div className="form-group">
id="settingsConfig" <label htmlFor="codexConfig">config.toml (TOML) *</label>
name="settingsConfig" <textarea
value={formData.settingsConfig} id="codexConfig"
onChange={handleChange} value={codexConfig}
placeholder={`{ onChange={(e) => setCodexConfig(e.target.value)}
placeholder={`# Codex configuration
model = "codex-model"
temperature = 0.7`}
rows={8}
style={{ fontFamily: "monospace", fontSize: "14px" }}
required
/>
<small className="field-hint">
Codex config.toml
</small>
</div>
</>
) : (
// Claude: 原有的单编辑器
<div className="form-group">
<div className="label-with-checkbox">
<label htmlFor="settingsConfig">
Claude Code (JSON) *
</label>
<label className="checkbox-label">
<input
type="checkbox"
checked={disableCoAuthored}
onChange={(e) => handleCoAuthoredToggle(e.target.checked)}
/>
Claude Code
</label>
</div>
<textarea
id="settingsConfig"
name="settingsConfig"
value={formData.settingsConfig}
onChange={handleChange}
placeholder={`{
"env": { "env": {
"ANTHROPIC_BASE_URL": "https://api.anthropic.com", "ANTHROPIC_BASE_URL": "https://api.anthropic.com",
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here" "ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
} }
}`} }`}
rows={12} rows={12}
style={{ fontFamily: "monospace", fontSize: "14px" }} style={{ fontFamily: "monospace", fontSize: "14px" }}
required required
/> />
<small className="field-hint"> <small className="field-hint">
Claude Code settings.json Claude Code settings.json
</small> </small>
</div> </div>
)}
</div> </div>
<div className="modal-footer"> <div className="modal-footer">

View File

@@ -1,6 +1,9 @@
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import { Provider } from "../types"; import { Provider } from "../types";
// 应用类型
export type AppType = "claude" | "codex";
// 定义配置状态类型 // 定义配置状态类型
interface ConfigStatus { interface ConfigStatus {
exists: boolean; exists: boolean;
@@ -17,9 +20,9 @@ interface ImportResult {
// Tauri API 封装,提供统一的全局 API 接口 // Tauri API 封装,提供统一的全局 API 接口
export const tauriAPI = { export const tauriAPI = {
// 获取所有供应商 // 获取所有供应商
getProviders: async (): Promise<Record<string, Provider>> => { getProviders: async (app?: AppType): Promise<Record<string, Provider>> => {
try { try {
return await invoke("get_providers"); return await invoke("get_providers", { app });
} catch (error) { } catch (error) {
console.error("获取供应商列表失败:", error); console.error("获取供应商列表失败:", error);
return {}; return {};
@@ -27,9 +30,9 @@ export const tauriAPI = {
}, },
// 获取当前供应商ID // 获取当前供应商ID
getCurrentProvider: async (): Promise<string> => { getCurrentProvider: async (app?: AppType): Promise<string> => {
try { try {
return await invoke("get_current_provider"); return await invoke("get_current_provider", { app });
} catch (error) { } catch (error) {
console.error("获取当前供应商失败:", error); console.error("获取当前供应商失败:", error);
return ""; return "";
@@ -37,9 +40,9 @@ export const tauriAPI = {
}, },
// 添加供应商 // 添加供应商
addProvider: async (provider: Provider): Promise<boolean> => { addProvider: async (provider: Provider, app?: AppType): Promise<boolean> => {
try { try {
return await invoke("add_provider", { provider }); return await invoke("add_provider", { provider, app });
} catch (error) { } catch (error) {
console.error("添加供应商失败:", error); console.error("添加供应商失败:", error);
throw error; throw error;
@@ -47,9 +50,12 @@ export const tauriAPI = {
}, },
// 更新供应商 // 更新供应商
updateProvider: async (provider: Provider): Promise<boolean> => { updateProvider: async (
provider: Provider,
app?: AppType,
): Promise<boolean> => {
try { try {
return await invoke("update_provider", { provider }); return await invoke("update_provider", { provider, app });
} catch (error) { } catch (error) {
console.error("更新供应商失败:", error); console.error("更新供应商失败:", error);
throw error; throw error;
@@ -57,9 +63,9 @@ export const tauriAPI = {
}, },
// 删除供应商 // 删除供应商
deleteProvider: async (id: string): Promise<boolean> => { deleteProvider: async (id: string, app?: AppType): Promise<boolean> => {
try { try {
return await invoke("delete_provider", { id }); return await invoke("delete_provider", { id, app });
} catch (error) { } catch (error) {
console.error("删除供应商失败:", error); console.error("删除供应商失败:", error);
throw error; throw error;
@@ -67,9 +73,12 @@ export const tauriAPI = {
}, },
// 切换供应商 // 切换供应商
switchProvider: async (providerId: string): Promise<boolean> => { switchProvider: async (
providerId: string,
app?: AppType,
): Promise<boolean> => {
try { try {
return await invoke("switch_provider", { id: providerId }); return await invoke("switch_provider", { id: providerId, app });
} catch (error) { } catch (error) {
console.error("切换供应商失败:", error); console.error("切换供应商失败:", error);
return false; return false;
@@ -77,9 +86,11 @@ export const tauriAPI = {
}, },
// 导入当前配置为默认供应商 // 导入当前配置为默认供应商
importCurrentConfigAsDefault: async (): Promise<ImportResult> => { importCurrentConfigAsDefault: async (
app?: AppType,
): Promise<ImportResult> => {
try { try {
const success = await invoke<boolean>("import_default_config"); const success = await invoke<boolean>("import_default_config", { app });
return { return {
success, success,
message: success ? "成功导入默认配置" : "导入失败", message: success ? "成功导入默认配置" : "导入失败",
@@ -117,10 +128,24 @@ export const tauriAPI = {
} }
}, },
// 打开配置文件夹 // 获取应用配置状态(通用)
openConfigFolder: async (): Promise<void> => { getConfigStatus: async (app?: AppType): Promise<ConfigStatus> => {
try { try {
await invoke("open_config_folder"); return await invoke("get_config_status", { appType: app });
} catch (error) {
console.error("获取配置状态失败:", error);
return {
exists: false,
path: "",
error: String(error),
};
}
},
// 打开配置文件夹
openConfigFolder: async (app?: AppType): Promise<void> => {
try {
await invoke("open_config_folder", { appType: app });
} catch (error) { } catch (error) {
console.error("打开配置文件夹失败:", error); console.error("打开配置文件夹失败:", error);
} }

18
src/vite-env.d.ts vendored
View File

@@ -1,6 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import { Provider } from "./types"; import { Provider } from "./types";
import { AppType } from "./lib/tauri-api";
interface ImportResult { interface ImportResult {
success: boolean; success: boolean;
@@ -16,17 +17,18 @@ interface ConfigStatus {
declare global { declare global {
interface Window { interface Window {
api: { api: {
getProviders: () => Promise<Record<string, Provider>>; getProviders: (app?: AppType) => Promise<Record<string, Provider>>;
getCurrentProvider: () => Promise<string>; getCurrentProvider: (app?: AppType) => Promise<string>;
addProvider: (provider: Provider) => Promise<boolean>; addProvider: (provider: Provider, app?: AppType) => Promise<boolean>;
deleteProvider: (id: string) => Promise<boolean>; deleteProvider: (id: string, app?: AppType) => Promise<boolean>;
updateProvider: (provider: Provider) => Promise<boolean>; updateProvider: (provider: Provider, app?: AppType) => Promise<boolean>;
switchProvider: (providerId: string) => Promise<boolean>; switchProvider: (providerId: string, app?: AppType) => Promise<boolean>;
importCurrentConfigAsDefault: () => Promise<ImportResult>; importCurrentConfigAsDefault: (app?: AppType) => Promise<ImportResult>;
getClaudeCodeConfigPath: () => Promise<string>; getClaudeCodeConfigPath: () => Promise<string>;
getClaudeConfigStatus: () => Promise<ConfigStatus>; getClaudeConfigStatus: () => Promise<ConfigStatus>;
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
selectConfigFile: () => Promise<string | null>; selectConfigFile: () => Promise<string | null>;
openConfigFolder: () => Promise<void>; openConfigFolder: (app?: AppType) => Promise<void>;
openExternal: (url: string) => Promise<void>; openExternal: (url: string) => Promise<void>;
}; };
platform: { platform: {