refactor(types): introduce Settings and apply in API
- style(prettier): format src files - style(rustfmt): format Rust sources - refactor(tauri-api): type-safe getSettings/saveSettings - refactor(d.ts): declare window.api with Settings [skip ci]
This commit is contained in:
@@ -4,9 +4,9 @@ use std::path::PathBuf;
|
||||
use crate::config::{
|
||||
atomic_write, delete_file, sanitize_provider_name, write_json_file, write_text_file,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use serde_json::Value;
|
||||
|
||||
/// 获取 Codex 配置目录路径
|
||||
pub fn get_codex_config_dir() -> PathBuf {
|
||||
@@ -77,7 +77,8 @@ pub fn write_codex_live_atomic(auth: &Value, config_text_opt: Option<&str>) -> R
|
||||
None => String::new(),
|
||||
};
|
||||
if !cfg_text.trim().is_empty() {
|
||||
toml::from_str::<toml::Table>(&cfg_text).map_err(|e| format!("config.toml 格式错误: {}", e))?;
|
||||
toml::from_str::<toml::Table>(&cfg_text)
|
||||
.map_err(|e| format!("config.toml 格式错误: {}", e))?;
|
||||
}
|
||||
|
||||
// 第一步:写 auth.json
|
||||
@@ -121,7 +122,9 @@ pub fn validate_config_toml(text: &str) -> Result<(), String> {
|
||||
if text.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
toml::from_str::<toml::Table>(text).map(|_| ()).map_err(|e| format!("config.toml 语法错误: {}", e))
|
||||
toml::from_str::<toml::Table>(text)
|
||||
.map(|_| ())
|
||||
.map_err(|e| format!("config.toml 语法错误: {}", e))
|
||||
}
|
||||
|
||||
/// 读取并校验 `~/.codex/config.toml`,返回文本(可能为空)
|
||||
|
||||
@@ -6,7 +6,7 @@ use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
use crate::app_config::AppType;
|
||||
use crate::codex_config;
|
||||
use crate::config::{ConfigStatus, get_claude_settings_path};
|
||||
use crate::config::{get_claude_settings_path, ConfigStatus};
|
||||
use crate::provider::Provider;
|
||||
use crate::store::AppState;
|
||||
|
||||
@@ -116,7 +116,9 @@ pub async fn add_provider(
|
||||
let manager = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
manager.providers.insert(provider.id.clone(), provider.clone());
|
||||
manager
|
||||
.providers
|
||||
.insert(provider.id.clone(), provider.clone());
|
||||
}
|
||||
state.save()?;
|
||||
|
||||
@@ -146,7 +148,10 @@ pub async fn update_provider(
|
||||
let manager = config
|
||||
.get_manager(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
(manager.providers.contains_key(&provider.id), manager.current == provider.id)
|
||||
(
|
||||
manager.providers.contains_key(&provider.id),
|
||||
manager.current == provider.id,
|
||||
)
|
||||
};
|
||||
if !exists {
|
||||
return Err(format!("供应商不存在: {}", provider.id));
|
||||
@@ -182,7 +187,9 @@ pub async fn update_provider(
|
||||
let manager = config
|
||||
.get_manager_mut(&app_type)
|
||||
.ok_or_else(|| format!("应用类型不存在: {:?}", app_type))?;
|
||||
manager.providers.insert(provider.id.clone(), provider.clone());
|
||||
manager
|
||||
.providers
|
||||
.insert(provider.id.clone(), provider.clone());
|
||||
}
|
||||
state.save()?;
|
||||
|
||||
@@ -390,7 +397,8 @@ pub async fn import_default_config(
|
||||
if !auth_path.exists() {
|
||||
return Err("Codex 配置文件不存在".to_string());
|
||||
}
|
||||
let auth: serde_json::Value = crate::config::read_json_file::<serde_json::Value>(&auth_path)?;
|
||||
let auth: serde_json::Value =
|
||||
crate::config::read_json_file::<serde_json::Value>(&auth_path)?;
|
||||
let config_str = match crate::codex_config::read_and_validate_codex_config_text() {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Err(e),
|
||||
@@ -488,7 +496,7 @@ pub async fn open_config_folder(
|
||||
.or_else(|| app.as_deref().map(|s| s.into()))
|
||||
.or_else(|| appType.as_deref().map(|s| s.into()))
|
||||
.unwrap_or(AppType::Claude);
|
||||
|
||||
|
||||
let config_dir = match app_type {
|
||||
AppType::Claude => crate::config::get_claude_config_dir(),
|
||||
AppType::Codex => crate::codex_config::get_codex_config_dir(),
|
||||
@@ -500,7 +508,8 @@ pub async fn open_config_folder(
|
||||
}
|
||||
|
||||
// 使用 opener 插件打开文件夹
|
||||
handle.opener()
|
||||
handle
|
||||
.opener()
|
||||
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
|
||||
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||
|
||||
@@ -524,3 +533,68 @@ pub async fn open_external(app: tauri::AppHandle, url: String) -> Result<bool, S
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 获取应用配置文件路径
|
||||
#[tauri::command]
|
||||
pub async fn get_app_config_path() -> Result<String, String> {
|
||||
use crate::config::get_app_config_path;
|
||||
|
||||
let config_path = get_app_config_path();
|
||||
Ok(config_path.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// 打开应用配置文件夹
|
||||
#[tauri::command]
|
||||
pub async fn open_app_config_folder(handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
use crate::config::get_app_config_dir;
|
||||
|
||||
let config_dir = get_app_config_dir();
|
||||
|
||||
// 确保目录存在
|
||||
if !config_dir.exists() {
|
||||
std::fs::create_dir_all(&config_dir).map_err(|e| format!("创建目录失败: {}", e))?;
|
||||
}
|
||||
|
||||
// 使用 opener 插件打开文件夹
|
||||
handle
|
||||
.opener()
|
||||
.open_path(config_dir.to_string_lossy().to_string(), None::<String>)
|
||||
.map_err(|e| format!("打开文件夹失败: {}", e))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 获取设置
|
||||
#[tauri::command]
|
||||
pub async fn get_settings(_state: State<'_, AppState>) -> Result<serde_json::Value, String> {
|
||||
// 暂时返回默认设置
|
||||
Ok(serde_json::json!({
|
||||
"showInDock": true
|
||||
}))
|
||||
}
|
||||
|
||||
/// 保存设置
|
||||
#[tauri::command]
|
||||
pub async fn save_settings(
|
||||
_state: State<'_, AppState>,
|
||||
settings: serde_json::Value,
|
||||
) -> Result<bool, String> {
|
||||
// TODO: 实现设置保存逻辑
|
||||
log::info!("保存设置: {:?}", settings);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 检查更新
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String> {
|
||||
// 打开 GitHub releases 页面
|
||||
handle
|
||||
.opener()
|
||||
.open_url(
|
||||
"https://github.com/yungookim/cc-switch/releases",
|
||||
None::<String>,
|
||||
)
|
||||
.map_err(|e| format!("打开更新页面失败: {}", e))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ pub fn archive_file(ts: u64, category: &str, src: &Path) -> Result<Option<PathBu
|
||||
Ok(Some(dest))
|
||||
}
|
||||
|
||||
|
||||
/// 清理供应商名称,确保文件名安全
|
||||
pub fn sanitize_provider_name(name: &str) -> String {
|
||||
name.chars()
|
||||
|
||||
@@ -326,6 +326,11 @@ pub fn run() {
|
||||
commands::get_claude_code_config_path,
|
||||
commands::open_config_folder,
|
||||
commands::open_external,
|
||||
commands::get_app_config_path,
|
||||
commands::open_app_config_folder,
|
||||
commands::get_settings,
|
||||
commands::save_settings,
|
||||
commands::check_for_updates,
|
||||
update_tray_menu,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
@@ -47,7 +47,10 @@ fn extract_claude_api_key(value: &Value) -> Option<String> {
|
||||
fn extract_codex_api_key(value: &Value) -> Option<String> {
|
||||
value
|
||||
.get("auth")
|
||||
.and_then(|auth| auth.get("OPENAI_API_KEY").or_else(|| auth.get("openai_api_key")))
|
||||
.and_then(|auth| {
|
||||
auth.get("OPENAI_API_KEY")
|
||||
.or_else(|| auth.get("openai_api_key"))
|
||||
})
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
@@ -77,7 +80,9 @@ fn scan_claude_copies() -> Vec<(String, PathBuf, Value)> {
|
||||
if !fname.starts_with("settings-") || !fname.ends_with(".json") {
|
||||
continue;
|
||||
}
|
||||
let name = fname.trim_start_matches("settings-").trim_end_matches(".json");
|
||||
let name = fname
|
||||
.trim_start_matches("settings-")
|
||||
.trim_end_matches(".json");
|
||||
if let Ok(val) = crate::config::read_json_file::<Value>(&p) {
|
||||
items.push((name.to_string(), p, val));
|
||||
}
|
||||
@@ -104,7 +109,9 @@ fn scan_codex_copies() -> Vec<(String, Option<PathBuf>, Option<PathBuf>, Value)>
|
||||
let entry = by_name.entry(name.to_string()).or_default();
|
||||
entry.0 = Some(p);
|
||||
} else if fname.starts_with("config-") && fname.ends_with(".toml") {
|
||||
let name = fname.trim_start_matches("config-").trim_end_matches(".toml");
|
||||
let name = fname
|
||||
.trim_start_matches("config-")
|
||||
.trim_end_matches(".toml");
|
||||
let entry = by_name.entry(name.to_string()).or_default();
|
||||
entry.1 = Some(p);
|
||||
}
|
||||
@@ -183,17 +190,14 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
||||
|
||||
if let Some((name, value)) = &live_claude {
|
||||
let cand_key = extract_claude_api_key(value);
|
||||
let exist_id = manager
|
||||
.providers
|
||||
.iter()
|
||||
.find_map(|(id, p)| {
|
||||
let pk = extract_claude_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||
let pk = extract_claude_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(exist_id) = exist_id {
|
||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||
log::info!("合并到已存在 Claude 供应商 '{}' (by name+key)", name);
|
||||
@@ -203,43 +207,36 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
||||
} else {
|
||||
let id = next_unique_id(&ids, name);
|
||||
ids.insert(id.clone());
|
||||
let provider = crate::provider::Provider::with_id(
|
||||
id.clone(),
|
||||
name.clone(),
|
||||
value.clone(),
|
||||
None,
|
||||
);
|
||||
let provider =
|
||||
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||
manager.providers.insert(provider.id.clone(), provider);
|
||||
live_claude_id = Some(id);
|
||||
}
|
||||
}
|
||||
for (name, path, value) in claude_items.iter() {
|
||||
let cand_key = extract_claude_api_key(value);
|
||||
let exist_id = manager
|
||||
.providers
|
||||
.iter()
|
||||
.find_map(|(id, p)| {
|
||||
let pk = extract_claude_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||
let pk = extract_claude_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(exist_id) = exist_id {
|
||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||
log::info!("覆盖 Claude 供应商 '{}' 来自 {} (by name+key)", name, path.display());
|
||||
log::info!(
|
||||
"覆盖 Claude 供应商 '{}' 来自 {} (by name+key)",
|
||||
name,
|
||||
path.display()
|
||||
);
|
||||
prov.settings_config = value.clone();
|
||||
}
|
||||
} else {
|
||||
let id = next_unique_id(&ids, name);
|
||||
ids.insert(id.clone());
|
||||
let provider = crate::provider::Provider::with_id(
|
||||
id.clone(),
|
||||
name.clone(),
|
||||
value.clone(),
|
||||
None,
|
||||
);
|
||||
let provider =
|
||||
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||
manager.providers.insert(provider.id.clone(), provider);
|
||||
}
|
||||
}
|
||||
@@ -257,7 +254,10 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
Some(("default".to_string(), serde_json::json!({"auth": auth, "config": cfg})))
|
||||
Some((
|
||||
"default".to_string(),
|
||||
serde_json::json!({"auth": auth, "config": cfg}),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("读取 Codex live auth.json 失败: {}", e);
|
||||
@@ -277,17 +277,14 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
||||
|
||||
if let Some((name, value)) = &live_codex {
|
||||
let cand_key = extract_codex_api_key(value);
|
||||
let exist_id = manager
|
||||
.providers
|
||||
.iter()
|
||||
.find_map(|(id, p)| {
|
||||
let pk = extract_codex_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||
let pk = extract_codex_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(exist_id) = exist_id {
|
||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||
log::info!("合并到已存在 Codex 供应商 '{}' (by name+key)", name);
|
||||
@@ -297,43 +294,37 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
||||
} else {
|
||||
let id = next_unique_id(&ids, name);
|
||||
ids.insert(id.clone());
|
||||
let provider = crate::provider::Provider::with_id(
|
||||
id.clone(),
|
||||
name.clone(),
|
||||
value.clone(),
|
||||
None,
|
||||
);
|
||||
let provider =
|
||||
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||
manager.providers.insert(provider.id.clone(), provider);
|
||||
live_codex_id = Some(id);
|
||||
}
|
||||
}
|
||||
for (name, authp, cfgp, value) in codex_items.iter() {
|
||||
let cand_key = extract_codex_api_key(value);
|
||||
let exist_id = manager
|
||||
.providers
|
||||
.iter()
|
||||
.find_map(|(id, p)| {
|
||||
let pk = extract_codex_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let exist_id = manager.providers.iter().find_map(|(id, p)| {
|
||||
let pk = extract_codex_api_key(&p.settings_config);
|
||||
if norm_name(&p.name) == norm_name(name) && pk == cand_key {
|
||||
Some(id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(exist_id) = exist_id {
|
||||
if let Some(prov) = manager.providers.get_mut(&exist_id) {
|
||||
log::info!("覆盖 Codex 供应商 '{}' 来自 {:?}/{:?} (by name+key)", name, authp, cfgp);
|
||||
log::info!(
|
||||
"覆盖 Codex 供应商 '{}' 来自 {:?}/{:?} (by name+key)",
|
||||
name,
|
||||
authp,
|
||||
cfgp
|
||||
);
|
||||
prov.settings_config = value.clone();
|
||||
}
|
||||
} else {
|
||||
let id = next_unique_id(&ids, name);
|
||||
ids.insert(id.clone());
|
||||
let provider = crate::provider::Provider::with_id(
|
||||
id.clone(),
|
||||
name.clone(),
|
||||
value.clone(),
|
||||
None,
|
||||
);
|
||||
let provider =
|
||||
crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None);
|
||||
manager.providers.insert(provider.id.clone(), provider);
|
||||
}
|
||||
}
|
||||
@@ -370,13 +361,17 @@ pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result<bool, S
|
||||
for (_, ap, cp, _) in codex_items.into_iter() {
|
||||
if let Some(ap) = ap {
|
||||
match archive_file(ts, "codex", &ap) {
|
||||
Ok(Some(_)) => { let _ = delete_file(&ap); }
|
||||
Ok(Some(_)) => {
|
||||
let _ = delete_file(&ap);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(cp) = cp {
|
||||
match archive_file(ts, "codex", &cp) {
|
||||
Ok(Some(_)) => { let _ = delete_file(&cp); }
|
||||
Ok(Some(_)) => {
|
||||
let _ = delete_file(&cp);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -404,7 +399,11 @@ pub fn dedupe_config(config: &mut MultiAppConfig) -> usize {
|
||||
let mut keep: Map<String, String> = Map::new(); // key -> id 保留
|
||||
let mut remove: Vec<String> = Vec::new();
|
||||
for (id, p) in mgr.providers.iter() {
|
||||
let k = format!("{}|{}", norm_name(&p.name), extract_key(&p.settings_config).unwrap_or_default());
|
||||
let k = format!(
|
||||
"{}|{}",
|
||||
norm_name(&p.name),
|
||||
extract_key(&p.settings_config).unwrap_or_default()
|
||||
);
|
||||
if let Some(exist_id) = keep.get(&k) {
|
||||
// 若当前是正在使用的,则用当前替换之前的,反之丢弃当前
|
||||
if *id == mgr.current {
|
||||
|
||||
Reference in New Issue
Block a user