feat(config): migrate app_config_dir to Tauri Store for independent management (#109)
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
"@tauri-apps/api": "^2.8.0",
|
"@tauri-apps/api": "^2.8.0",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.0",
|
"@tauri-apps/plugin-dialog": "^2.4.0",
|
||||||
"@tauri-apps/plugin-process": "^2.0.0",
|
"@tauri-apps/plugin-process": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-store": "^2.0.0",
|
||||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"i18next": "^25.5.2",
|
"i18next": "^25.5.2",
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-process':
|
'@tauri-apps/plugin-process':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
|
'@tauri-apps/plugin-store':
|
||||||
|
specifier: ^2.0.0
|
||||||
|
version: 2.4.0
|
||||||
'@tauri-apps/plugin-updater':
|
'@tauri-apps/plugin-updater':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.9.0
|
version: 2.9.0
|
||||||
@@ -689,6 +692,9 @@ packages:
|
|||||||
'@tauri-apps/plugin-process@2.3.0':
|
'@tauri-apps/plugin-process@2.3.0':
|
||||||
resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==}
|
resolution: {integrity: sha512-0DNj6u+9csODiV4seSxxRbnLpeGYdojlcctCuLOCgpH9X3+ckVZIEj6H7tRQ7zqWr7kSTEWnrxtAdBb0FbtrmQ==}
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.0':
|
||||||
|
resolution: {integrity: sha512-PjBnlnH6jyI71MGhrPaxUUCsOzc7WO1mbc4gRhME0m2oxLgCqbksw6JyeKQimuzv4ysdpNO3YbmaY2haf82a3A==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.9.0':
|
'@tauri-apps/plugin-updater@2.9.0':
|
||||||
resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
|
resolution: {integrity: sha512-j++sgY8XpeDvzImTrzWA08OqqGqgkNyxczLD7FjNJJx/uXxMZFz5nDcfkyoI/rCjYuj2101Tci/r/HFmOmoxCg==}
|
||||||
|
|
||||||
@@ -1561,6 +1567,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.8.0
|
'@tauri-apps/api': 2.8.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-store@2.4.0':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.8.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-updater@2.9.0':
|
'@tauri-apps/plugin-updater@2.9.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.8.0
|
'@tauri-apps/api': 2.8.0
|
||||||
@@ -1894,4 +1904,4 @@ snapshots:
|
|||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
yallist@5.0.0: {}
|
yallist@5.0.0: {}
|
||||||
17
src-tauri/Cargo.lock
generated
17
src-tauri/Cargo.lock
generated
@@ -583,6 +583,7 @@ dependencies = [
|
|||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
|
"tauri-plugin-store",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toml 0.8.2",
|
"toml 0.8.2",
|
||||||
@@ -4542,6 +4543,22 @@ dependencies = [
|
|||||||
"zbus",
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-store"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d85dd80d60a76ee2c2fdce09e9ef30877b239c2a6bb76e6d7d03708aa5f13a19"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-updater"
|
name = "tauri-plugin-updater"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ tauri-plugin-opener = "2"
|
|||||||
tauri-plugin-process = "2"
|
tauri-plugin-process = "2"
|
||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-store = "2"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "json"] }
|
||||||
|
|||||||
137
src-tauri/src/app_store.rs
Normal file
137
src-tauri/src/app_store.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
use serde_json::Value;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{OnceLock, RwLock};
|
||||||
|
use tauri_plugin_store::StoreExt;
|
||||||
|
|
||||||
|
/// Store 中的键名
|
||||||
|
const STORE_KEY_APP_CONFIG_DIR: &str = "app_config_dir_override";
|
||||||
|
|
||||||
|
/// 全局缓存的 AppHandle (在应用启动时设置)
|
||||||
|
static APP_HANDLE: OnceLock<RwLock<Option<tauri::AppHandle>>> = OnceLock::new();
|
||||||
|
|
||||||
|
/// 设置全局 AppHandle
|
||||||
|
pub fn set_app_handle(handle: tauri::AppHandle) {
|
||||||
|
let store = APP_HANDLE.get_or_init(|| RwLock::new(None));
|
||||||
|
if let Ok(mut guard) = store.write() {
|
||||||
|
*guard = Some(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取全局 AppHandle
|
||||||
|
fn get_app_handle() -> Option<tauri::AppHandle> {
|
||||||
|
let store = APP_HANDLE.get()?;
|
||||||
|
let guard = store.read().ok()?;
|
||||||
|
guard.as_ref().cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 Tauri Store 读取 app_config_dir 覆盖配置 (无需 AppHandle 版本)
|
||||||
|
pub fn get_app_config_dir_override() -> Option<PathBuf> {
|
||||||
|
let app = get_app_handle()?;
|
||||||
|
get_app_config_dir_from_store(&app)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从 Tauri Store 读取 app_config_dir 覆盖配置(公开函数)
|
||||||
|
pub fn get_app_config_dir_from_store(app: &tauri::AppHandle) -> Option<PathBuf> {
|
||||||
|
let store = app.store_builder("app_paths.json").build();
|
||||||
|
|
||||||
|
if let Err(e) = &store {
|
||||||
|
log::warn!("无法创建 Store: {}", e);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let store = store.unwrap();
|
||||||
|
|
||||||
|
match store.get(STORE_KEY_APP_CONFIG_DIR) {
|
||||||
|
Some(Value::String(path_str)) => {
|
||||||
|
let path_str = path_str.trim();
|
||||||
|
if path_str.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = resolve_path(path_str);
|
||||||
|
|
||||||
|
// 验证路径是否存在
|
||||||
|
if !path.exists() {
|
||||||
|
log::warn!(
|
||||||
|
"Store 中配置的 app_config_dir 不存在: {:?}\n\
|
||||||
|
将使用默认路径。",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("使用 Store 中的 app_config_dir: {:?}", path);
|
||||||
|
Some(path)
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
log::warn!("Store 中的 {} 类型不正确,应为字符串", STORE_KEY_APP_CONFIG_DIR);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 写入 app_config_dir 到 Tauri Store
|
||||||
|
pub fn set_app_config_dir_to_store(
|
||||||
|
app: &tauri::AppHandle,
|
||||||
|
path: Option<&str>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let store = app
|
||||||
|
.store_builder("app_paths.json")
|
||||||
|
.build()
|
||||||
|
.map_err(|e| format!("创建 Store 失败: {}", e))?;
|
||||||
|
|
||||||
|
match path {
|
||||||
|
Some(p) => {
|
||||||
|
let trimmed = p.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
store.set(STORE_KEY_APP_CONFIG_DIR, Value::String(trimmed.to_string()));
|
||||||
|
log::info!("已将 app_config_dir 写入 Store: {}", trimmed);
|
||||||
|
} else {
|
||||||
|
// 空字符串 = 删除配置
|
||||||
|
store.delete(STORE_KEY_APP_CONFIG_DIR);
|
||||||
|
log::info!("已从 Store 中删除 app_config_dir 配置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// None = 删除配置
|
||||||
|
store.delete(STORE_KEY_APP_CONFIG_DIR);
|
||||||
|
log::info!("已从 Store 中删除 app_config_dir 配置");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
store.save().map_err(|e| format!("保存 Store 失败: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析路径,支持 ~ 开头的相对路径
|
||||||
|
fn resolve_path(raw: &str) -> PathBuf {
|
||||||
|
if raw == "~" {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
return home;
|
||||||
|
}
|
||||||
|
} else if let Some(stripped) = raw.strip_prefix("~/") {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
return home.join(stripped);
|
||||||
|
}
|
||||||
|
} else if let Some(stripped) = raw.strip_prefix("~\\") {
|
||||||
|
if let Some(home) = dirs::home_dir() {
|
||||||
|
return home.join(stripped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBuf::from(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 从旧的 settings.json 迁移 app_config_dir 到 Store
|
||||||
|
pub fn migrate_app_config_dir_from_settings(app: &tauri::AppHandle) -> Result<(), String> {
|
||||||
|
// app_config_dir 已从 settings.json 移除,此函数保留但不再执行迁移
|
||||||
|
// 如果用户在旧版本设置过 app_config_dir,需要在 Store 中手动配置
|
||||||
|
log::info!("app_config_dir 迁移功能已移除,请在设置中重新配置");
|
||||||
|
|
||||||
|
// 确保 Store 初始化正常
|
||||||
|
let _ = get_app_config_dir_from_store(app);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1222,6 +1222,13 @@ pub async fn save_settings(settings: crate::settings::AppSettings) -> Result<boo
|
|||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 重启应用程序(当 app_config_dir 变更后使用)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn restart_app(app: tauri::AppHandle) -> Result<bool, String> {
|
||||||
|
// 使用 tauri-plugin-process 重启应用
|
||||||
|
app.restart();
|
||||||
|
}
|
||||||
|
|
||||||
/// 检查更新
|
/// 检查更新
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String> {
|
pub async fn check_for_updates(handle: tauri::AppHandle) -> Result<bool, String> {
|
||||||
@@ -1469,3 +1476,20 @@ pub async fn update_endpoint_last_used(
|
|||||||
state.save()?;
|
state.save()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取 app_config_dir 覆盖配置 (从 Store)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_app_config_dir_override(app: tauri::AppHandle) -> Result<Option<String>, String> {
|
||||||
|
Ok(crate::app_store::get_app_config_dir_from_store(&app)
|
||||||
|
.map(|p| p.to_string_lossy().to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 设置 app_config_dir 覆盖配置 (到 Store)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn set_app_config_dir_override(
|
||||||
|
app: tauri::AppHandle,
|
||||||
|
path: Option<String>,
|
||||||
|
) -> Result<bool, String> {
|
||||||
|
crate::app_store::set_app_config_dir_to_store(&app, path.as_deref())?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
@@ -33,6 +33,10 @@ pub fn get_claude_settings_path() -> PathBuf {
|
|||||||
|
|
||||||
/// 获取应用配置目录路径 (~/.cc-switch)
|
/// 获取应用配置目录路径 (~/.cc-switch)
|
||||||
pub fn get_app_config_dir() -> PathBuf {
|
pub fn get_app_config_dir() -> PathBuf {
|
||||||
|
if let Some(custom) = crate::app_store::get_app_config_dir_override() {
|
||||||
|
return custom;
|
||||||
|
}
|
||||||
|
|
||||||
dirs::home_dir()
|
dirs::home_dir()
|
||||||
.expect("无法获取用户主目录")
|
.expect("无法获取用户主目录")
|
||||||
.join(".cc-switch")
|
.join(".cc-switch")
|
||||||
@@ -245,4 +249,4 @@ pub fn get_claude_config_status() -> ConfigStatus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//(移除未使用的备份/导入函数,避免 dead_code 告警)
|
//(移除未使用的备份/导入函数,避免 dead_code 告警)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
mod app_config;
|
mod app_config;
|
||||||
|
mod app_store;
|
||||||
mod claude_mcp;
|
mod claude_mcp;
|
||||||
mod claude_plugin;
|
mod claude_plugin;
|
||||||
mod codex_config;
|
mod codex_config;
|
||||||
@@ -305,7 +306,10 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(tauri_plugin_store::Builder::new().build())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
|
// 设置全局 AppHandle 以供 Store 使用
|
||||||
|
app_store::set_app_handle(app.handle().clone());
|
||||||
// 注册 Updater 插件(桌面端)
|
// 注册 Updater 插件(桌面端)
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
{
|
{
|
||||||
@@ -359,6 +363,11 @@ pub fn run() {
|
|||||||
// 初始化应用状态(仅创建一次,并在本函数末尾注入 manage)
|
// 初始化应用状态(仅创建一次,并在本函数末尾注入 manage)
|
||||||
let app_state = AppState::new();
|
let app_state = AppState::new();
|
||||||
|
|
||||||
|
// 迁移旧的 app_config_dir 配置到 Store
|
||||||
|
if let Err(e) = app_store::migrate_app_config_dir_from_settings(&app.handle()) {
|
||||||
|
log::warn!("迁移 app_config_dir 失败: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
// 首次启动迁移:扫描副本文件,合并到 config.json,并归档副本;旧 config.json 先归档
|
// 首次启动迁移:扫描副本文件,合并到 config.json,并归档副本;旧 config.json 先归档
|
||||||
{
|
{
|
||||||
let mut config_guard = app_state.config.lock().unwrap();
|
let mut config_guard = app_state.config.lock().unwrap();
|
||||||
@@ -418,6 +427,7 @@ pub fn run() {
|
|||||||
commands::read_live_provider_settings,
|
commands::read_live_provider_settings,
|
||||||
commands::get_settings,
|
commands::get_settings,
|
||||||
commands::save_settings,
|
commands::save_settings,
|
||||||
|
commands::restart_app,
|
||||||
commands::check_for_updates,
|
commands::check_for_updates,
|
||||||
commands::is_portable_mode,
|
commands::is_portable_mode,
|
||||||
commands::get_claude_plugin_status,
|
commands::get_claude_plugin_status,
|
||||||
@@ -447,6 +457,9 @@ pub fn run() {
|
|||||||
commands::add_custom_endpoint,
|
commands::add_custom_endpoint,
|
||||||
commands::remove_custom_endpoint,
|
commands::remove_custom_endpoint,
|
||||||
commands::update_endpoint_last_used,
|
commands::update_endpoint_last_used,
|
||||||
|
// app_config_dir override via Store
|
||||||
|
commands::get_app_config_dir_override,
|
||||||
|
commands::set_app_config_dir_override,
|
||||||
// theirs: config import/export and dialogs
|
// theirs: config import/export and dialogs
|
||||||
import_export::export_config_to_file,
|
import_export::export_config_to_file,
|
||||||
import_export::import_config_from_file,
|
import_export::import_config_from_file,
|
||||||
@@ -480,4 +493,4 @@ pub fn run() {
|
|||||||
let _ = (app_handle, event);
|
let _ = (app_handle, event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,12 @@ impl Default for AppSettings {
|
|||||||
|
|
||||||
impl AppSettings {
|
impl AppSettings {
|
||||||
fn settings_path() -> PathBuf {
|
fn settings_path() -> PathBuf {
|
||||||
crate::config::get_app_config_dir().join("settings.json")
|
// settings.json 必须使用固定路径,不能被 app_config_dir 覆盖
|
||||||
|
// 否则会造成循环依赖:读取 settings 需要知道路径,但路径在 settings 中
|
||||||
|
dirs::home_dir()
|
||||||
|
.expect("无法获取用户主目录")
|
||||||
|
.join(".cc-switch")
|
||||||
|
.join("settings.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_paths(&mut self) {
|
fn normalize_paths(&mut self) {
|
||||||
@@ -178,4 +183,4 @@ pub fn get_codex_override_dir() -> Option<PathBuf> {
|
|||||||
.codex_config_dir
|
.codex_config_dir
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|p| resolve_override_path(p))
|
.map(|p| resolve_override_path(p))
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,10 @@ export default function SettingsModal({
|
|||||||
codexConfigDir: undefined,
|
codexConfigDir: undefined,
|
||||||
language: persistedLanguage,
|
language: persistedLanguage,
|
||||||
});
|
});
|
||||||
|
// appConfigDir 现在从 Store 独立管理
|
||||||
|
const [appConfigDir, setAppConfigDir] = useState<string | undefined>(
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">(
|
const [initialLanguage, setInitialLanguage] = useState<"zh" | "en">(
|
||||||
persistedLanguage,
|
persistedLanguage,
|
||||||
);
|
);
|
||||||
@@ -69,9 +73,14 @@ export default function SettingsModal({
|
|||||||
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const [showUpToDate, setShowUpToDate] = useState(false);
|
const [showUpToDate, setShowUpToDate] = useState(false);
|
||||||
|
const [resolvedAppConfigDir, setResolvedAppConfigDir] = useState<string>("");
|
||||||
const [resolvedClaudeDir, setResolvedClaudeDir] = useState<string>("");
|
const [resolvedClaudeDir, setResolvedClaudeDir] = useState<string>("");
|
||||||
const [resolvedCodexDir, setResolvedCodexDir] = useState<string>("");
|
const [resolvedCodexDir, setResolvedCodexDir] = useState<string>("");
|
||||||
const [isPortable, setIsPortable] = useState(false);
|
const [isPortable, setIsPortable] = useState(false);
|
||||||
|
const [initialAppConfigDir, setInitialAppConfigDir] = useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined);
|
||||||
|
const [showRestartDialog, setShowRestartDialog] = useState(false);
|
||||||
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
const { hasUpdate, updateInfo, updateHandle, checkUpdate, resetDismiss } =
|
||||||
useUpdate();
|
useUpdate();
|
||||||
|
|
||||||
@@ -86,6 +95,7 @@ export default function SettingsModal({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
loadAppConfigDirFromStore(); // 从 Store 加载 appConfigDir
|
||||||
loadConfigPath();
|
loadConfigPath();
|
||||||
loadVersion();
|
loadVersion();
|
||||||
loadResolvedDirs();
|
loadResolvedDirs();
|
||||||
@@ -103,6 +113,24 @@ export default function SettingsModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 从 Tauri Store 加载 appConfigDir
|
||||||
|
const loadAppConfigDirFromStore = async () => {
|
||||||
|
try {
|
||||||
|
const storeValue = await (window as any).api.getAppConfigDirOverride();
|
||||||
|
if (storeValue) {
|
||||||
|
setAppConfigDir(storeValue);
|
||||||
|
setInitialAppConfigDir(storeValue);
|
||||||
|
setResolvedAppConfigDir(storeValue);
|
||||||
|
} else {
|
||||||
|
// 使用默认值
|
||||||
|
const defaultDir = await computeDefaultAppConfigDir();
|
||||||
|
setResolvedAppConfigDir(defaultDir);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("从 Store 加载 appConfigDir 失败:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
try {
|
try {
|
||||||
const loadedSettings = await window.api.getSettings();
|
const loadedSettings = await window.api.getSettings();
|
||||||
@@ -195,7 +223,17 @@ export default function SettingsModal({
|
|||||||
: undefined,
|
: undefined,
|
||||||
language: selectedLanguage,
|
language: selectedLanguage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 保存 settings.json (不包含 appConfigDir)
|
||||||
await window.api.saveSettings(payload);
|
await window.api.saveSettings(payload);
|
||||||
|
|
||||||
|
// 单独保存 appConfigDir 到 Store
|
||||||
|
const normalizedAppConfigDir =
|
||||||
|
appConfigDir && appConfigDir.trim() !== ""
|
||||||
|
? appConfigDir.trim()
|
||||||
|
: null;
|
||||||
|
await (window as any).api.setAppConfigDirOverride(normalizedAppConfigDir);
|
||||||
|
|
||||||
// 立即生效:根据开关无条件写入/移除 ~/.claude/config.json
|
// 立即生效:根据开关无条件写入/移除 ~/.claude/config.json
|
||||||
try {
|
try {
|
||||||
if (payload.enableClaudePluginIntegration) {
|
if (payload.enableClaudePluginIntegration) {
|
||||||
@@ -206,7 +244,14 @@ export default function SettingsModal({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("[Settings] Apply Claude plugin config on save failed", e);
|
console.warn("[Settings] Apply Claude plugin config on save failed", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检测 appConfigDir 是否真正发生变化
|
||||||
|
const appConfigDirChanged =
|
||||||
|
(normalizedAppConfigDir || undefined) !==
|
||||||
|
(initialAppConfigDir || undefined);
|
||||||
|
|
||||||
setSettings(payload);
|
setSettings(payload);
|
||||||
|
setInitialAppConfigDir(normalizedAppConfigDir ?? undefined);
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem("language", selectedLanguage);
|
window.localStorage.setItem("language", selectedLanguage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -216,12 +261,47 @@ export default function SettingsModal({
|
|||||||
if (i18n.language !== selectedLanguage) {
|
if (i18n.language !== selectedLanguage) {
|
||||||
void i18n.changeLanguage(selectedLanguage);
|
void i18n.changeLanguage(selectedLanguage);
|
||||||
}
|
}
|
||||||
onClose();
|
|
||||||
|
// 如果修改了 appConfigDir,需要提示用户重启应用程序
|
||||||
|
if (appConfigDirChanged) {
|
||||||
|
setShowRestartDialog(true);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(t("console.saveSettingsFailed"), error);
|
console.error(t("console.saveSettingsFailed"), error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRestartNow = async () => {
|
||||||
|
// 开发模式下不真正重启,只提示
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
onNotify?.(
|
||||||
|
t("settings.devModeRestartHint"),
|
||||||
|
"success",
|
||||||
|
5000,
|
||||||
|
);
|
||||||
|
setShowRestartDialog(false);
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产模式下真正重启应用
|
||||||
|
try {
|
||||||
|
await window.api.restartApp();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Settings] Restart app failed", e);
|
||||||
|
// 如果重启失败,仍然关闭设置窗口
|
||||||
|
setShowRestartDialog(false);
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRestartLater = () => {
|
||||||
|
setShowRestartDialog(false);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleLanguageChange = (lang: "zh" | "en") => {
|
const handleLanguageChange = (lang: "zh" | "en") => {
|
||||||
setSettings((prev) => ({ ...prev, language: lang }));
|
setSettings((prev) => ({ ...prev, language: lang }));
|
||||||
if (i18n.language !== lang) {
|
if (i18n.language !== lang) {
|
||||||
@@ -298,6 +378,28 @@ export default function SettingsModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBrowseAppConfigDir = async () => {
|
||||||
|
try {
|
||||||
|
const currentResolved = appConfigDir ?? resolvedAppConfigDir;
|
||||||
|
const selected = await window.api.selectConfigDirectory(currentResolved);
|
||||||
|
|
||||||
|
if (!selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = selected.trim();
|
||||||
|
|
||||||
|
if (sanitized === "") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAppConfigDir(sanitized);
|
||||||
|
setResolvedAppConfigDir(sanitized);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(t("console.selectConfigDirFailed"), error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleBrowseConfigDir = async (app: AppType) => {
|
const handleBrowseConfigDir = async (app: AppType) => {
|
||||||
try {
|
try {
|
||||||
const currentResolved =
|
const currentResolved =
|
||||||
@@ -340,6 +442,24 @@ export default function SettingsModal({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const computeDefaultAppConfigDir = async () => {
|
||||||
|
try {
|
||||||
|
const home = await homeDir();
|
||||||
|
return await join(home, ".cc-switch");
|
||||||
|
} catch (error) {
|
||||||
|
console.error(t("console.getDefaultConfigDirFailed"), error);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetAppConfigDir = async () => {
|
||||||
|
setAppConfigDir(undefined);
|
||||||
|
const defaultDir = await computeDefaultAppConfigDir();
|
||||||
|
if (defaultDir) {
|
||||||
|
setResolvedAppConfigDir(defaultDir);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleResetConfigDir = async (app: AppType) => {
|
const handleResetConfigDir = async (app: AppType) => {
|
||||||
setSettings((prev) => ({
|
setSettings((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -605,6 +725,40 @@ export default function SettingsModal({
|
|||||||
{t("settings.configDirectoryDescription")}
|
{t("settings.configDirectoryDescription")}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||||
|
{t("settings.appConfigDir")}
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-gray-400 dark:text-gray-500 mb-1">
|
||||||
|
{t("settings.appConfigDirDescription")}
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={appConfigDir ?? resolvedAppConfigDir ?? ""}
|
||||||
|
onChange={(e) => setAppConfigDir(e.target.value)}
|
||||||
|
placeholder={t("settings.browsePlaceholderApp")}
|
||||||
|
className="flex-1 px-3 py-2 text-xs font-mono bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBrowseAppConfigDir}
|
||||||
|
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||||
|
title={t("settings.browseDirectory")}
|
||||||
|
>
|
||||||
|
<FolderSearch size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleResetAppConfigDir}
|
||||||
|
className="px-2 py-2 text-xs text-gray-500 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||||
|
title={t("settings.resetDefault")}
|
||||||
|
>
|
||||||
|
<Undo2 size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
<label className="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
|
||||||
{t("settings.claudeConfigDir")}
|
{t("settings.claudeConfigDir")}
|
||||||
@@ -854,6 +1008,39 @@ export default function SettingsModal({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Restart Confirmation Dialog */}
|
||||||
|
{showRestartDialog && (
|
||||||
|
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||||
|
<div
|
||||||
|
className={`absolute inset-0 bg-black/50 dark:bg-black/70${
|
||||||
|
isLinux() ? "" : " backdrop-blur-sm"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
<div className="relative bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-[400px] p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">
|
||||||
|
{t("settings.restartRequired")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||||
|
{t("settings.restartRequiredMessage")}
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
onClick={handleRestartLater}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{t("settings.restartLater")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleRestartNow}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{t("settings.restartNow")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -109,6 +109,9 @@
|
|||||||
"openFolder": "Open Folder",
|
"openFolder": "Open Folder",
|
||||||
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
"configDirectoryOverride": "Configuration Directory Override (Advanced)",
|
||||||
"configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory in WSL to keep provider data consistent with the main environment.",
|
"configDirectoryDescription": "When using Claude Code or Codex in environments like WSL, you can manually specify the configuration directory in WSL to keep provider data consistent with the main environment.",
|
||||||
|
"appConfigDir": "CC-Switch Configuration Directory",
|
||||||
|
"appConfigDirDescription": "Customize the storage location for CC-Switch configuration files (config.json, etc.)",
|
||||||
|
"browsePlaceholderApp": "e.g., C:\\Users\\Administrator\\.cc-switch",
|
||||||
"claudeConfigDir": "Claude Code Configuration Directory",
|
"claudeConfigDir": "Claude Code Configuration Directory",
|
||||||
"codexConfigDir": "Codex Configuration Directory",
|
"codexConfigDir": "Codex Configuration Directory",
|
||||||
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
|
"browsePlaceholderClaude": "e.g., /home/<your-username>/.claude",
|
||||||
@@ -123,7 +126,12 @@
|
|||||||
"releaseNotes": "Release Notes",
|
"releaseNotes": "Release Notes",
|
||||||
"viewReleaseNotes": "View release notes for this version",
|
"viewReleaseNotes": "View release notes for this version",
|
||||||
"viewCurrentReleaseNotes": "View current version release notes",
|
"viewCurrentReleaseNotes": "View current version release notes",
|
||||||
"exportFailedError": "Export config failed:"
|
"exportFailedError": "Export config failed:",
|
||||||
|
"restartRequired": "Restart Required",
|
||||||
|
"restartRequiredMessage": "Modifying the CC-Switch configuration directory requires restarting the application to take effect. Restart now?",
|
||||||
|
"restartNow": "Restart Now",
|
||||||
|
"restartLater": "Restart Later",
|
||||||
|
"devModeRestartHint": "Dev Mode: Configuration saved. Please manually restart the application for changes to take effect"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"claude": "Claude Code",
|
"claude": "Claude Code",
|
||||||
|
|||||||
@@ -109,6 +109,9 @@
|
|||||||
"openFolder": "打开文件夹",
|
"openFolder": "打开文件夹",
|
||||||
"configDirectoryOverride": "配置目录覆盖(高级)",
|
"configDirectoryOverride": "配置目录覆盖(高级)",
|
||||||
"configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定 WSL 里的配置目录,供应商数据与主环境保持一致。",
|
"configDirectoryDescription": "在 WSL 等环境使用 Claude Code 或 Codex 的时候,可手动指定 WSL 里的配置目录,供应商数据与主环境保持一致。",
|
||||||
|
"appConfigDir": "CC-Switch 配置目录",
|
||||||
|
"appConfigDirDescription": "自定义 CC-Switch 的配置存储位置(config.json 等文件)",
|
||||||
|
"browsePlaceholderApp": "例如:C:\\Users\\Administrator\\.cc-switch",
|
||||||
"claudeConfigDir": "Claude Code 配置目录",
|
"claudeConfigDir": "Claude Code 配置目录",
|
||||||
"codexConfigDir": "Codex 配置目录",
|
"codexConfigDir": "Codex 配置目录",
|
||||||
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
"browsePlaceholderClaude": "例如:/home/<你的用户名>/.claude",
|
||||||
@@ -123,7 +126,12 @@
|
|||||||
"releaseNotes": "更新日志",
|
"releaseNotes": "更新日志",
|
||||||
"viewReleaseNotes": "查看该版本更新日志",
|
"viewReleaseNotes": "查看该版本更新日志",
|
||||||
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
"viewCurrentReleaseNotes": "查看当前版本更新日志",
|
||||||
"exportFailedError": "导出配置失败:"
|
"exportFailedError": "导出配置失败:",
|
||||||
|
"restartRequired": "需要重启应用",
|
||||||
|
"restartRequiredMessage": "修改 CC-Switch 配置目录后需要重启应用才能生效,是否立即重启?",
|
||||||
|
"restartNow": "立即重启",
|
||||||
|
"restartLater": "稍后重启",
|
||||||
|
"devModeRestartHint": "开发模式:配置已保存,请手动重启应用以使新配置生效"
|
||||||
},
|
},
|
||||||
"apps": {
|
"apps": {
|
||||||
"claude": "Claude Code",
|
"claude": "Claude Code",
|
||||||
|
|||||||
@@ -209,6 +209,16 @@ export const tauriAPI = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 重启应用程序
|
||||||
|
restartApp: async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke("restart_app");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("重启应用失败:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// 检查更新
|
// 检查更新
|
||||||
checkForUpdates: async (): Promise<void> => {
|
checkForUpdates: async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
@@ -653,6 +663,26 @@ export const tauriAPI = {
|
|||||||
});
|
});
|
||||||
return unlisten;
|
return unlisten;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 获取 app_config_dir 覆盖配置(从 Store)
|
||||||
|
getAppConfigDirOverride: async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
return await invoke<string | null>("get_app_config_dir_override");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取 app_config_dir 覆盖配置失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置 app_config_dir 覆盖配置(到 Store)
|
||||||
|
setAppConfigDirOverride: async (path: string | null): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await invoke<boolean>("set_app_config_dir_override", { path });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("设置 app_config_dir 覆盖配置失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 创建全局 API 对象,兼容现有代码
|
// 创建全局 API 对象,兼容现有代码
|
||||||
@@ -662,4 +692,4 @@ if (typeof window !== "undefined") {
|
|||||||
(window as any).api = tauriAPI;
|
(window as any).api = tauriAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default tauriAPI;
|
export default tauriAPI;
|
||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@@ -58,6 +58,7 @@ declare global {
|
|||||||
) => Promise<UnlistenFn>;
|
) => Promise<UnlistenFn>;
|
||||||
getSettings: () => Promise<Settings>;
|
getSettings: () => Promise<Settings>;
|
||||||
saveSettings: (settings: Settings) => Promise<boolean>;
|
saveSettings: (settings: Settings) => Promise<boolean>;
|
||||||
|
restartApp: () => Promise<boolean>;
|
||||||
checkForUpdates: () => Promise<void>;
|
checkForUpdates: () => Promise<void>;
|
||||||
isPortable: () => Promise<boolean>;
|
isPortable: () => Promise<boolean>;
|
||||||
getAppConfigPath: () => Promise<string>;
|
getAppConfigPath: () => Promise<string>;
|
||||||
|
|||||||
Reference in New Issue
Block a user