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
This commit is contained in:
@@ -95,6 +95,9 @@ pub struct MultiAppConfig {
|
|||||||
/// Prompt 配置(按客户端分治)
|
/// Prompt 配置(按客户端分治)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub prompts: PromptRoot,
|
pub prompts: PromptRoot,
|
||||||
|
/// Claude 通用配置片段(JSON 字符串,用于跨供应商共享配置)
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub claude_common_config_snippet: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_version() -> u32 {
|
fn default_version() -> u32 {
|
||||||
@@ -113,6 +116,7 @@ impl Default for MultiAppConfig {
|
|||||||
apps,
|
apps,
|
||||||
mcp: McpRoot::default(),
|
mcp: McpRoot::default(),
|
||||||
prompts: PromptRoot::default(),
|
prompts: PromptRoot::default(),
|
||||||
|
claude_common_config_snippet: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,3 +135,39 @@ pub async fn open_app_config_folder(handle: AppHandle) -> Result<bool, String> {
|
|||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 获取 Claude 通用配置片段
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_claude_common_config_snippet(
|
||||||
|
state: tauri::State<'_, crate::store::AppState>,
|
||||||
|
) -> Result<Option<String>, 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::<serde_json::Value>(&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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -511,6 +511,8 @@ pub fn run() {
|
|||||||
commands::get_init_error,
|
commands::get_init_error,
|
||||||
commands::get_app_config_path,
|
commands::get_app_config_path,
|
||||||
commands::open_app_config_folder,
|
commands::open_app_config_folder,
|
||||||
|
commands::get_claude_common_config_snippet,
|
||||||
|
commands::set_claude_common_config_snippet,
|
||||||
commands::read_live_provider_settings,
|
commands::read_live_provider_settings,
|
||||||
commands::get_settings,
|
commands::get_settings,
|
||||||
commands::save_settings,
|
commands::save_settings,
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import {
|
|||||||
hasCommonConfigSnippet,
|
hasCommonConfigSnippet,
|
||||||
validateJsonConfig,
|
validateJsonConfig,
|
||||||
} from "@/utils/providerConfigUtils";
|
} 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 = `{
|
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
||||||
"includeCoAuthoredBy": false
|
"includeCoAuthoredBy": false
|
||||||
}`;
|
}`;
|
||||||
@@ -20,6 +21,7 @@ interface UseCommonConfigSnippetProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 管理 Claude 通用配置片段
|
* 管理 Claude 通用配置片段
|
||||||
|
* 从 config.json 读取和保存,支持从 localStorage 平滑迁移
|
||||||
*/
|
*/
|
||||||
export function useCommonConfigSnippet({
|
export function useCommonConfigSnippet({
|
||||||
settingsConfig,
|
settingsConfig,
|
||||||
@@ -27,30 +29,67 @@ export function useCommonConfigSnippet({
|
|||||||
initialData,
|
initialData,
|
||||||
}: UseCommonConfigSnippetProps) {
|
}: UseCommonConfigSnippetProps) {
|
||||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
||||||
const [commonConfigSnippet, setCommonConfigSnippetState] = useState<string>(
|
const [commonConfigSnippet, setCommonConfigSnippetState] =
|
||||||
() => {
|
useState<string>(DEFAULT_COMMON_CONFIG_SNIPPET);
|
||||||
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 [commonConfigError, setCommonConfigError] = useState("");
|
const [commonConfigError, setCommonConfigError] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
// 用于跟踪是否正在通过通用配置更新
|
// 用于跟踪是否正在通过通用配置更新
|
||||||
const isUpdatingFromCommonConfig = useRef(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (initialData) {
|
if (initialData && !isLoading) {
|
||||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||||
const hasCommon = hasCommonConfigSnippet(
|
const hasCommon = hasCommonConfigSnippet(
|
||||||
configString,
|
configString,
|
||||||
@@ -58,24 +97,7 @@ export function useCommonConfigSnippet({
|
|||||||
);
|
);
|
||||||
setUseCommonConfig(hasCommon);
|
setUseCommonConfig(hasCommon);
|
||||||
}
|
}
|
||||||
}, [initialData, commonConfigSnippet]);
|
}, [initialData, commonConfigSnippet, isLoading]);
|
||||||
|
|
||||||
// 同步本地存储的通用配置片段
|
|
||||||
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]);
|
|
||||||
|
|
||||||
// 处理通用配置开关
|
// 处理通用配置开关
|
||||||
const handleCommonConfigToggle = useCallback(
|
const handleCommonConfigToggle = useCallback(
|
||||||
@@ -113,6 +135,11 @@ export function useCommonConfigSnippet({
|
|||||||
|
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
setCommonConfigError("");
|
setCommonConfigError("");
|
||||||
|
// 保存到 config.json(清空)
|
||||||
|
configApi.setClaudeCommonConfigSnippet("").catch((error) => {
|
||||||
|
console.error("保存通用配置失败:", error);
|
||||||
|
});
|
||||||
|
|
||||||
if (useCommonConfig) {
|
if (useCommonConfig) {
|
||||||
const { updatedConfig } = updateCommonConfigSnippet(
|
const { updatedConfig } = updateCommonConfigSnippet(
|
||||||
settingsConfig,
|
settingsConfig,
|
||||||
@@ -131,6 +158,11 @@ export function useCommonConfigSnippet({
|
|||||||
setCommonConfigError(validationError);
|
setCommonConfigError(validationError);
|
||||||
} else {
|
} else {
|
||||||
setCommonConfigError("");
|
setCommonConfigError("");
|
||||||
|
// 保存到 config.json
|
||||||
|
configApi.setClaudeCommonConfigSnippet(value).catch((error) => {
|
||||||
|
console.error("保存通用配置失败:", error);
|
||||||
|
setCommonConfigError(`保存失败: ${error}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
||||||
@@ -169,7 +201,7 @@ export function useCommonConfigSnippet({
|
|||||||
|
|
||||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isUpdatingFromCommonConfig.current) {
|
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hasCommon = hasCommonConfigSnippet(
|
const hasCommon = hasCommonConfigSnippet(
|
||||||
@@ -177,12 +209,13 @@ export function useCommonConfigSnippet({
|
|||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
);
|
);
|
||||||
setUseCommonConfig(hasCommon);
|
setUseCommonConfig(hasCommon);
|
||||||
}, [settingsConfig, commonConfigSnippet]);
|
}, [settingsConfig, commonConfigSnippet, isLoading]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useCommonConfig,
|
useCommonConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
commonConfigError,
|
commonConfigError,
|
||||||
|
isLoading,
|
||||||
handleCommonConfigToggle,
|
handleCommonConfigToggle,
|
||||||
handleCommonConfigSnippetChange,
|
handleCommonConfigSnippetChange,
|
||||||
};
|
};
|
||||||
|
|||||||
21
src/lib/api/config.ts
Normal file
21
src/lib/api/config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 配置相关 API
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Claude 通用配置片段
|
||||||
|
* @returns 通用配置片段(JSON 字符串),如果不存在则返回 null
|
||||||
|
*/
|
||||||
|
export async function getClaudeCommonConfigSnippet(): Promise<string | null> {
|
||||||
|
return invoke<string | null>("get_claude_common_config_snippet");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Claude 通用配置片段
|
||||||
|
* @param snippet - 通用配置片段(JSON 字符串)
|
||||||
|
* @throws 如果 JSON 格式无效
|
||||||
|
*/
|
||||||
|
export async function setClaudeCommonConfigSnippet(
|
||||||
|
snippet: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return invoke("set_claude_common_config_snippet", { snippet });
|
||||||
|
}
|
||||||
@@ -5,5 +5,6 @@ export { mcpApi } from "./mcp";
|
|||||||
export { promptsApi } from "./prompts";
|
export { promptsApi } from "./prompts";
|
||||||
export { usageApi } from "./usage";
|
export { usageApi } from "./usage";
|
||||||
export { vscodeApi } from "./vscode";
|
export { vscodeApi } from "./vscode";
|
||||||
|
export * as configApi from "./config";
|
||||||
export type { ProviderSwitchEvent } from "./providers";
|
export type { ProviderSwitchEvent } from "./providers";
|
||||||
export type { Prompt } from "./prompts";
|
export type { Prompt } from "./prompts";
|
||||||
|
|||||||
Reference in New Issue
Block a user