From 21fd7cc9fd6214857942fcb4602ea4db6a48c020 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 13 Nov 2025 22:45:58 +0800 Subject: [PATCH] feat: migrate Claude common config snippet from localStorage to config.json Migrate the Claude common config snippet storage from browser localStorage to the persistent config.json file for better cross-device sync and backup support. **Backend Changes:** - Add `claude_common_config_snippet` field to `MultiAppConfig` struct - Add `get_claude_common_config_snippet` and `set_claude_common_config_snippet` Tauri commands - Include JSON validation in the setter command **Frontend Changes:** - Create new `lib/api/config.ts` API module - Refactor `useCommonConfigSnippet` hook to use config.json instead of localStorage - Add automatic one-time migration from localStorage to config.json - Add loading state during initialization **Benefits:** - Cross-device synchronization via backup/restore - More reliable persistence than browser storage - Centralized configuration management - Seamless migration for existing users --- src-tauri/src/app_config.rs | 4 + src-tauri/src/commands/config.rs | 36 ++++++ src-tauri/src/lib.rs | 2 + .../forms/hooks/useCommonConfigSnippet.ts | 109 ++++++++++++------ src/lib/api/config.ts | 21 ++++ src/lib/api/index.ts | 1 + 6 files changed, 135 insertions(+), 38 deletions(-) create mode 100644 src/lib/api/config.ts diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index 131fe57..0bea690 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -95,6 +95,9 @@ pub struct MultiAppConfig { /// Prompt 配置(按客户端分治) #[serde(default)] pub prompts: PromptRoot, + /// Claude 通用配置片段(JSON 字符串,用于跨供应商共享配置) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub claude_common_config_snippet: Option, } fn default_version() -> u32 { @@ -113,6 +116,7 @@ impl Default for MultiAppConfig { apps, mcp: McpRoot::default(), prompts: PromptRoot::default(), + claude_common_config_snippet: None, } } } diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index b6b721d..df7e5ef 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -135,3 +135,39 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result { Ok(true) } + +/// 获取 Claude 通用配置片段 +#[tauri::command] +pub async fn get_claude_common_config_snippet( + state: tauri::State<'_, crate::store::AppState>, +) -> Result, String> { + let guard = state.config.read().map_err(|e| format!("读取配置锁失败: {e}"))?; + Ok(guard.claude_common_config_snippet.clone()) +} + +/// 设置 Claude 通用配置片段 +#[tauri::command] +pub async fn set_claude_common_config_snippet( + snippet: String, + state: tauri::State<'_, crate::store::AppState>, +) -> Result<(), String> { + let mut guard = state + .config + .write() + .map_err(|e| format!("写入配置锁失败: {e}"))?; + + // 验证是否为有效的 JSON(如果不为空) + if !snippet.trim().is_empty() { + serde_json::from_str::(&snippet) + .map_err(|e| format!("无效的 JSON 格式: {e}"))?; + } + + guard.claude_common_config_snippet = if snippet.trim().is_empty() { + None + } else { + Some(snippet) + }; + + guard.save().map_err(|e| e.to_string())?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 18ba090..511e8fe 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -511,6 +511,8 @@ pub fn run() { commands::get_init_error, commands::get_app_config_path, commands::open_app_config_folder, + commands::get_claude_common_config_snippet, + commands::set_claude_common_config_snippet, commands::read_live_provider_settings, commands::get_settings, commands::save_settings, diff --git a/src/components/providers/forms/hooks/useCommonConfigSnippet.ts b/src/components/providers/forms/hooks/useCommonConfigSnippet.ts index 4ecdd92..fe9e06c 100644 --- a/src/components/providers/forms/hooks/useCommonConfigSnippet.ts +++ b/src/components/providers/forms/hooks/useCommonConfigSnippet.ts @@ -4,8 +4,9 @@ import { hasCommonConfigSnippet, validateJsonConfig, } from "@/utils/providerConfigUtils"; +import { configApi } from "@/lib/api"; -const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet"; +const LEGACY_STORAGE_KEY = "cc-switch:common-config-snippet"; const DEFAULT_COMMON_CONFIG_SNIPPET = `{ "includeCoAuthoredBy": false }`; @@ -20,6 +21,7 @@ interface UseCommonConfigSnippetProps { /** * 管理 Claude 通用配置片段 + * 从 config.json 读取和保存,支持从 localStorage 平滑迁移 */ export function useCommonConfigSnippet({ settingsConfig, @@ -27,30 +29,67 @@ export function useCommonConfigSnippet({ initialData, }: UseCommonConfigSnippetProps) { const [useCommonConfig, setUseCommonConfig] = useState(false); - const [commonConfigSnippet, setCommonConfigSnippetState] = useState( - () => { - if (typeof window === "undefined") { - return DEFAULT_COMMON_CONFIG_SNIPPET; - } - try { - const stored = window.localStorage.getItem(COMMON_CONFIG_STORAGE_KEY); - if (stored && stored.trim()) { - return stored; - } - } catch { - // ignore localStorage 读取失败 - } - return DEFAULT_COMMON_CONFIG_SNIPPET; - }, - ); + const [commonConfigSnippet, setCommonConfigSnippetState] = + useState(DEFAULT_COMMON_CONFIG_SNIPPET); const [commonConfigError, setCommonConfigError] = useState(""); + const [isLoading, setIsLoading] = useState(true); // 用于跟踪是否正在通过通用配置更新 const isUpdatingFromCommonConfig = useRef(false); + // 初始化:从 config.json 加载,支持从 localStorage 迁移 + useEffect(() => { + let mounted = true; + + const loadSnippet = async () => { + try { + // 尝试从 config.json 加载 + const snippet = await configApi.getClaudeCommonConfigSnippet(); + + if (snippet && snippet.trim()) { + if (mounted) { + setCommonConfigSnippetState(snippet); + } + } else { + // 如果 config.json 中没有,尝试从 localStorage 迁移 + if (typeof window !== "undefined") { + try { + const legacySnippet = + window.localStorage.getItem(LEGACY_STORAGE_KEY); + if (legacySnippet && legacySnippet.trim()) { + // 迁移到 config.json + await configApi.setClaudeCommonConfigSnippet(legacySnippet); + if (mounted) { + setCommonConfigSnippetState(legacySnippet); + } + // 清理 localStorage + window.localStorage.removeItem(LEGACY_STORAGE_KEY); + console.log("[迁移] 通用配置已从 localStorage 迁移到 config.json"); + } + } catch (e) { + console.warn("[迁移] 从 localStorage 迁移失败:", e); + } + } + } + } catch (error) { + console.error("加载通用配置失败:", error); + } finally { + if (mounted) { + setIsLoading(false); + } + } + }; + + loadSnippet(); + + return () => { + mounted = false; + }; + }, []); + // 初始化时检查通用配置片段(编辑模式) useEffect(() => { - if (initialData) { + if (initialData && !isLoading) { const configString = JSON.stringify(initialData.settingsConfig, null, 2); const hasCommon = hasCommonConfigSnippet( configString, @@ -58,24 +97,7 @@ export function useCommonConfigSnippet({ ); setUseCommonConfig(hasCommon); } - }, [initialData, commonConfigSnippet]); - - // 同步本地存储的通用配置片段 - useEffect(() => { - if (typeof window === "undefined") return; - try { - if (commonConfigSnippet.trim()) { - window.localStorage.setItem( - COMMON_CONFIG_STORAGE_KEY, - commonConfigSnippet, - ); - } else { - window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY); - } - } catch { - // ignore - } - }, [commonConfigSnippet]); + }, [initialData, commonConfigSnippet, isLoading]); // 处理通用配置开关 const handleCommonConfigToggle = useCallback( @@ -113,6 +135,11 @@ export function useCommonConfigSnippet({ if (!value.trim()) { setCommonConfigError(""); + // 保存到 config.json(清空) + configApi.setClaudeCommonConfigSnippet("").catch((error) => { + console.error("保存通用配置失败:", error); + }); + if (useCommonConfig) { const { updatedConfig } = updateCommonConfigSnippet( settingsConfig, @@ -131,6 +158,11 @@ export function useCommonConfigSnippet({ setCommonConfigError(validationError); } else { setCommonConfigError(""); + // 保存到 config.json + configApi.setClaudeCommonConfigSnippet(value).catch((error) => { + console.error("保存通用配置失败:", error); + setCommonConfigError(`保存失败: ${error}`); + }); } // 若当前启用通用配置且格式正确,需要替换为最新片段 @@ -169,7 +201,7 @@ export function useCommonConfigSnippet({ // 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查) useEffect(() => { - if (isUpdatingFromCommonConfig.current) { + if (isUpdatingFromCommonConfig.current || isLoading) { return; } const hasCommon = hasCommonConfigSnippet( @@ -177,12 +209,13 @@ export function useCommonConfigSnippet({ commonConfigSnippet, ); setUseCommonConfig(hasCommon); - }, [settingsConfig, commonConfigSnippet]); + }, [settingsConfig, commonConfigSnippet, isLoading]); return { useCommonConfig, commonConfigSnippet, commonConfigError, + isLoading, handleCommonConfigToggle, handleCommonConfigSnippetChange, }; diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts new file mode 100644 index 0000000..81061aa --- /dev/null +++ b/src/lib/api/config.ts @@ -0,0 +1,21 @@ +// 配置相关 API +import { invoke } from "@tauri-apps/api/core"; + +/** + * 获取 Claude 通用配置片段 + * @returns 通用配置片段(JSON 字符串),如果不存在则返回 null + */ +export async function getClaudeCommonConfigSnippet(): Promise { + return invoke("get_claude_common_config_snippet"); +} + +/** + * 设置 Claude 通用配置片段 + * @param snippet - 通用配置片段(JSON 字符串) + * @throws 如果 JSON 格式无效 + */ +export async function setClaudeCommonConfigSnippet( + snippet: string, +): Promise { + return invoke("set_claude_common_config_snippet", { snippet }); +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 618cd9d..9053449 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -5,5 +5,6 @@ export { mcpApi } from "./mcp"; export { promptsApi } from "./prompts"; export { usageApi } from "./usage"; export { vscodeApi } from "./vscode"; +export * as configApi from "./config"; export type { ProviderSwitchEvent } from "./providers"; export type { Prompt } from "./prompts";