- 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:
113
src-tauri/src/app_config.rs
Normal file
113
src-tauri/src/app_config.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
159
src-tauri/src/codex_config.rs
Normal file
159
src-tauri/src/codex_config.rs
Normal 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)
|
||||
}
|
||||
@@ -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<String>,
|
||||
) -> Result<HashMap<String, Provider>, 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<String, String> {
|
||||
let manager = state
|
||||
.provider_manager
|
||||
pub async fn get_current_provider(
|
||||
state: State<'_, AppState>,
|
||||
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()
|
||||
.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<bool, String> {
|
||||
let mut manager = state
|
||||
.provider_manager
|
||||
pub async fn add_provider(
|
||||
state: State<'_, AppState>,
|
||||
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()
|
||||
.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<String>,
|
||||
provider: Provider,
|
||||
) -> Result<bool, String> {
|
||||
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<bool, String> {
|
||||
let mut manager = state
|
||||
.provider_manager
|
||||
pub async fn delete_provider(
|
||||
state: State<'_, AppState>,
|
||||
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()
|
||||
.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<b
|
||||
|
||||
/// 切换供应商
|
||||
#[tauri::command]
|
||||
pub async fn switch_provider(state: State<'_, AppState>, id: String) -> Result<bool, String> {
|
||||
let mut manager = state
|
||||
.provider_manager
|
||||
pub async fn switch_provider(
|
||||
state: State<'_, AppState>,
|
||||
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()
|
||||
.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<b
|
||||
|
||||
/// 导入当前配置为默认供应商
|
||||
#[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 供应商,则直接返回,避免重复导入
|
||||
{
|
||||
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<bool, S
|
||||
);
|
||||
|
||||
// 添加到管理器
|
||||
let mut manager = state
|
||||
.provider_manager
|
||||
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_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<bool, S
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
drop(manager); // 释放锁
|
||||
drop(config); // 释放锁
|
||||
state.save()?;
|
||||
|
||||
Ok(true)
|
||||
@@ -152,6 +382,27 @@ pub async fn get_claude_config_status() -> Result<ConfigStatus, String> {
|
||||
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 配置文件路径
|
||||
#[tauri::command]
|
||||
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]
|
||||
pub async fn open_config_folder(app: tauri::AppHandle) -> Result<bool, String> {
|
||||
let config_dir = crate::config::get_claude_config_dir();
|
||||
pub async fn open_config_folder(app: tauri::AppHandle, app_type: Option<AppType>) -> Result<bool, String> {
|
||||
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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<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> {
|
||||
// 保存供应商配置到独立文件
|
||||
@@ -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<String, Provider> {
|
||||
&self.providers
|
||||
|
||||
@@ -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<ProviderManager>,
|
||||
pub config: Mutex<MultiAppConfig>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 保留按需扩展:若未来需要热加载,可在此实现
|
||||
}
|
||||
|
||||
42
src/App.css
42
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 {
|
||||
|
||||
43
src/App.tsx
43
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<AppType>("claude");
|
||||
const [providers, setProviders] = useState<Record<string, Provider>>({});
|
||||
const [currentProviderId, setCurrentProviderId] = useState<string>("");
|
||||
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 (
|
||||
<div className="app">
|
||||
<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">
|
||||
<button className="add-btn" onClick={() => setIsAddModalOpen(true)}>
|
||||
添加供应商
|
||||
@@ -228,6 +245,7 @@ function App() {
|
||||
|
||||
{isAddModalOpen && (
|
||||
<AddProviderModal
|
||||
appType={activeApp}
|
||||
onAdd={handleAddProvider}
|
||||
onClose={() => setIsAddModalOpen(false)}
|
||||
/>
|
||||
@@ -235,6 +253,7 @@ function App() {
|
||||
|
||||
{editingProviderId && providers[editingProviderId] && (
|
||||
<EditProviderModal
|
||||
appType={activeApp}
|
||||
provider={providers[editingProviderId]}
|
||||
onSave={handleEditProvider}
|
||||
onClose={() => setEditingProviderId(null)}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import React from "react";
|
||||
import { Provider } from "../types";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import ProviderForm from "./ProviderForm";
|
||||
|
||||
interface AddProviderModalProps {
|
||||
appType: AppType;
|
||||
onAdd: (provider: Omit<Provider, "id">) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const AddProviderModal: React.FC<AddProviderModalProps> = ({
|
||||
appType,
|
||||
onAdd,
|
||||
onClose,
|
||||
}) => {
|
||||
return (
|
||||
<ProviderForm
|
||||
appType={appType}
|
||||
title="添加新供应商"
|
||||
submitText="添加"
|
||||
showPresets={true}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React from "react";
|
||||
import { Provider } from "../types";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import ProviderForm from "./ProviderForm";
|
||||
|
||||
interface EditProviderModalProps {
|
||||
appType: AppType;
|
||||
provider: Provider;
|
||||
onSave: (provider: Provider) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||
appType,
|
||||
provider,
|
||||
onSave,
|
||||
onClose,
|
||||
@@ -22,6 +25,7 @@ const EditProviderModal: React.FC<EditProviderModalProps> = ({
|
||||
|
||||
return (
|
||||
<ProviderForm
|
||||
appType={appType}
|
||||
title="编辑供应商"
|
||||
submitText="保存"
|
||||
initialData={provider}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Provider } from "../types";
|
||||
import { AppType } from "../lib/tauri-api";
|
||||
import {
|
||||
updateCoAuthoredSetting,
|
||||
checkCoAuthoredSetting,
|
||||
@@ -12,6 +13,7 @@ import { providerPresets } from "../config/providerPresets";
|
||||
import "./AddProviderModal.css";
|
||||
|
||||
interface ProviderFormProps {
|
||||
appType?: AppType;
|
||||
title: string;
|
||||
submitText: string;
|
||||
initialData?: Provider;
|
||||
@@ -21,6 +23,7 @@ interface ProviderFormProps {
|
||||
}
|
||||
|
||||
const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
appType = "claude",
|
||||
title,
|
||||
submitText,
|
||||
initialData,
|
||||
@@ -28,6 +31,9 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
onSubmit,
|
||||
onClose,
|
||||
}) => {
|
||||
// 对于 Codex,需要分离 auth 和 config
|
||||
const isCodex = appType === "codex";
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: initialData?.name || "",
|
||||
websiteUrl: initialData?.websiteUrl || "",
|
||||
@@ -35,6 +41,21 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
? 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 [disableCoAuthored, setDisableCoAuthored] = useState(false);
|
||||
const [selectedPreset, setSelectedPreset] = useState<number | null>(null);
|
||||
@@ -58,18 +79,38 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.settingsConfig.trim()) {
|
||||
setError("请填写配置内容");
|
||||
return;
|
||||
}
|
||||
|
||||
let settingsConfig: Record<string, any>;
|
||||
|
||||
try {
|
||||
settingsConfig = JSON.parse(formData.settingsConfig);
|
||||
} catch (err) {
|
||||
setError("配置JSON格式错误,请检查语法");
|
||||
return;
|
||||
if (isCodex) {
|
||||
// Codex: 验证两个文件
|
||||
if (!codexAuth.trim() || !codexConfig.trim()) {
|
||||
setError("请填写 auth.json 和 config.toml 配置");
|
||||
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({
|
||||
@@ -226,7 +267,7 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
<div className="modal-body">
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
{showPresets && (
|
||||
{showPresets && !isCodex && (
|
||||
<div className="presets">
|
||||
<label>一键导入(只需要填写 key)</label>
|
||||
<div className="preset-buttons">
|
||||
@@ -262,33 +303,35 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
||||
>
|
||||
<label htmlFor="apiKey">API Key *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
placeholder={
|
||||
isOfficialPreset
|
||||
? "官方登录无需填写 API Key,直接保存即可"
|
||||
: "只需要填这里,下方配置会自动填充"
|
||||
}
|
||||
disabled={isOfficialPreset}
|
||||
autoComplete="off"
|
||||
style={
|
||||
isOfficialPreset
|
||||
? {
|
||||
backgroundColor: "#f5f5f5",
|
||||
cursor: "not-allowed",
|
||||
color: "#999",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{!isCodex && (
|
||||
<div
|
||||
className={`form-group api-key-group ${!showApiKey ? "hidden" : ""}`}
|
||||
>
|
||||
<label htmlFor="apiKey">API Key *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="apiKey"
|
||||
value={apiKey}
|
||||
onChange={(e) => handleApiKeyChange(e.target.value)}
|
||||
placeholder={
|
||||
isOfficialPreset
|
||||
? "官方登录无需填写 API Key,直接保存即可"
|
||||
: "只需要填这里,下方配置会自动填充"
|
||||
}
|
||||
disabled={isOfficialPreset}
|
||||
autoComplete="off"
|
||||
style={
|
||||
isOfficialPreset
|
||||
? {
|
||||
backgroundColor: "#f5f5f5",
|
||||
cursor: "not-allowed",
|
||||
color: "#999",
|
||||
}
|
||||
: {}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="websiteUrl">官网地址</label>
|
||||
@@ -303,39 +346,80 @@ const ProviderForm: React.FC<ProviderFormProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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 或 Codex 的配置部分 */}
|
||||
{isCodex ? (
|
||||
// Codex: 双编辑器
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label htmlFor="codexAuth">auth.json (JSON) *</label>
|
||||
<textarea
|
||||
id="codexAuth"
|
||||
value={codexAuth}
|
||||
onChange={(e) => setCodexAuth(e.target.value)}
|
||||
placeholder={`{
|
||||
"api_key": "your-codex-api-key"
|
||||
}`}
|
||||
rows={6}
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
required
|
||||
/>
|
||||
禁止 Claude Code 签名
|
||||
</label>
|
||||
</div>
|
||||
<textarea
|
||||
id="settingsConfig"
|
||||
name="settingsConfig"
|
||||
value={formData.settingsConfig}
|
||||
onChange={handleChange}
|
||||
placeholder={`{
|
||||
<small className="field-hint">Codex auth.json 配置内容</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="codexConfig">config.toml (TOML) *</label>
|
||||
<textarea
|
||||
id="codexConfig"
|
||||
value={codexConfig}
|
||||
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": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here"
|
||||
}
|
||||
}`}
|
||||
rows={12}
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
required
|
||||
/>
|
||||
<small className="field-hint">
|
||||
完整的 Claude Code settings.json 配置内容
|
||||
</small>
|
||||
</div>
|
||||
rows={12}
|
||||
style={{ fontFamily: "monospace", fontSize: "14px" }}
|
||||
required
|
||||
/>
|
||||
<small className="field-hint">
|
||||
完整的 Claude Code settings.json 配置内容
|
||||
</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="modal-footer">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { Provider } from "../types";
|
||||
|
||||
// 应用类型
|
||||
export type AppType = "claude" | "codex";
|
||||
|
||||
// 定义配置状态类型
|
||||
interface ConfigStatus {
|
||||
exists: boolean;
|
||||
@@ -17,9 +20,9 @@ interface ImportResult {
|
||||
// Tauri API 封装,提供统一的全局 API 接口
|
||||
export const tauriAPI = {
|
||||
// 获取所有供应商
|
||||
getProviders: async (): Promise<Record<string, Provider>> => {
|
||||
getProviders: async (app?: AppType): Promise<Record<string, Provider>> => {
|
||||
try {
|
||||
return await invoke("get_providers");
|
||||
return await invoke("get_providers", { app });
|
||||
} catch (error) {
|
||||
console.error("获取供应商列表失败:", error);
|
||||
return {};
|
||||
@@ -27,9 +30,9 @@ export const tauriAPI = {
|
||||
},
|
||||
|
||||
// 获取当前供应商ID
|
||||
getCurrentProvider: async (): Promise<string> => {
|
||||
getCurrentProvider: async (app?: AppType): Promise<string> => {
|
||||
try {
|
||||
return await invoke("get_current_provider");
|
||||
return await invoke("get_current_provider", { app });
|
||||
} catch (error) {
|
||||
console.error("获取当前供应商失败:", error);
|
||||
return "";
|
||||
@@ -37,9 +40,9 @@ export const tauriAPI = {
|
||||
},
|
||||
|
||||
// 添加供应商
|
||||
addProvider: async (provider: Provider): Promise<boolean> => {
|
||||
addProvider: async (provider: Provider, app?: AppType): Promise<boolean> => {
|
||||
try {
|
||||
return await invoke("add_provider", { provider });
|
||||
return await invoke("add_provider", { provider, app });
|
||||
} catch (error) {
|
||||
console.error("添加供应商失败:", 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 {
|
||||
return await invoke("update_provider", { provider });
|
||||
return await invoke("update_provider", { provider, app });
|
||||
} catch (error) {
|
||||
console.error("更新供应商失败:", 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 {
|
||||
return await invoke("delete_provider", { id });
|
||||
return await invoke("delete_provider", { id, app });
|
||||
} catch (error) {
|
||||
console.error("删除供应商失败:", 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 {
|
||||
return await invoke("switch_provider", { id: providerId });
|
||||
return await invoke("switch_provider", { id: providerId, app });
|
||||
} catch (error) {
|
||||
console.error("切换供应商失败:", error);
|
||||
return false;
|
||||
@@ -77,9 +86,11 @@ export const tauriAPI = {
|
||||
},
|
||||
|
||||
// 导入当前配置为默认供应商
|
||||
importCurrentConfigAsDefault: async (): Promise<ImportResult> => {
|
||||
importCurrentConfigAsDefault: async (
|
||||
app?: AppType,
|
||||
): Promise<ImportResult> => {
|
||||
try {
|
||||
const success = await invoke<boolean>("import_default_config");
|
||||
const success = await invoke<boolean>("import_default_config", { app });
|
||||
return {
|
||||
success,
|
||||
message: success ? "成功导入默认配置" : "导入失败",
|
||||
@@ -117,10 +128,24 @@ export const tauriAPI = {
|
||||
}
|
||||
},
|
||||
|
||||
// 打开配置文件夹
|
||||
openConfigFolder: async (): Promise<void> => {
|
||||
// 获取应用配置状态(通用)
|
||||
getConfigStatus: async (app?: AppType): Promise<ConfigStatus> => {
|
||||
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) {
|
||||
console.error("打开配置文件夹失败:", error);
|
||||
}
|
||||
|
||||
18
src/vite-env.d.ts
vendored
18
src/vite-env.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import { Provider } from "./types";
|
||||
import { AppType } from "./lib/tauri-api";
|
||||
|
||||
interface ImportResult {
|
||||
success: boolean;
|
||||
@@ -16,17 +17,18 @@ interface ConfigStatus {
|
||||
declare global {
|
||||
interface Window {
|
||||
api: {
|
||||
getProviders: () => Promise<Record<string, Provider>>;
|
||||
getCurrentProvider: () => Promise<string>;
|
||||
addProvider: (provider: Provider) => Promise<boolean>;
|
||||
deleteProvider: (id: string) => Promise<boolean>;
|
||||
updateProvider: (provider: Provider) => Promise<boolean>;
|
||||
switchProvider: (providerId: string) => Promise<boolean>;
|
||||
importCurrentConfigAsDefault: () => Promise<ImportResult>;
|
||||
getProviders: (app?: AppType) => Promise<Record<string, Provider>>;
|
||||
getCurrentProvider: (app?: AppType) => Promise<string>;
|
||||
addProvider: (provider: Provider, app?: AppType) => Promise<boolean>;
|
||||
deleteProvider: (id: string, app?: AppType) => Promise<boolean>;
|
||||
updateProvider: (provider: Provider, app?: AppType) => Promise<boolean>;
|
||||
switchProvider: (providerId: string, app?: AppType) => Promise<boolean>;
|
||||
importCurrentConfigAsDefault: (app?: AppType) => Promise<ImportResult>;
|
||||
getClaudeCodeConfigPath: () => Promise<string>;
|
||||
getClaudeConfigStatus: () => Promise<ConfigStatus>;
|
||||
getConfigStatus: (app?: AppType) => Promise<ConfigStatus>;
|
||||
selectConfigFile: () => Promise<string | null>;
|
||||
openConfigFolder: () => Promise<void>;
|
||||
openConfigFolder: (app?: AppType) => Promise<void>;
|
||||
openExternal: (url: string) => Promise<void>;
|
||||
};
|
||||
platform: {
|
||||
|
||||
Reference in New Issue
Block a user