feat(config): migrate app_config_dir to Tauri Store for independent management (#109)

This commit is contained in:
ZyphrZero
2025-10-15 09:15:53 +08:00
committed by GitHub
parent 3e4df2c96a
commit 3b6048b1e8
14 changed files with 456 additions and 10 deletions

View File

@@ -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",

10
pnpm-lock.yaml generated
View File

@@ -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

17
src-tauri/Cargo.lock generated
View File

@@ -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"

View File

@@ -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
View 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(())
}

View File

@@ -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)
}

View File

@@ -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")

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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);
} }
// 如果修改了 appConfigDir,需要提示用户重启应用程序
if (appConfigDirChanged) {
setShowRestartDialog(true);
} else {
onClose(); 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>
); );
} }

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 对象,兼容现有代码

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

@@ -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>;