mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
fix(claude): add clearing sync and save queue for common config
- Trigger sync when clearing common config to remove from all providers - Add saveSequenceRef and enqueueSave for ordered async saves - Add toast notification for sync failures - Prevent stale sync operations with sequence ID check
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
updateCommonConfigSnippet,
|
||||
hasCommonConfigSnippet,
|
||||
validateJsonConfig,
|
||||
hasCommonConfigSnippet,
|
||||
replaceCommonConfigSnippet,
|
||||
} from "@/utils/providerConfigUtils";
|
||||
import { configApi } from "@/lib/api";
|
||||
import type { ProviderMeta } from "@/types";
|
||||
|
||||
const LEGACY_STORAGE_KEY = "cc-switch:common-config-snippet";
|
||||
const DEFAULT_COMMON_CONFIG_SNIPPET = `{
|
||||
@@ -17,15 +20,18 @@ interface UseCommonConfigSnippetProps {
|
||||
onConfigChange: (config: string) => void;
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
meta?: ProviderMeta;
|
||||
};
|
||||
selectedPresetId?: string;
|
||||
/** When false, the hook skips all logic and returns disabled state. Default: true */
|
||||
enabled?: boolean;
|
||||
/** 当前正在编辑的供应商 ID(用于同步时跳过) */
|
||||
currentProviderId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理 Claude 通用配置片段
|
||||
* 从 config.json 读取和保存,支持从 localStorage 平滑迁移
|
||||
* 从数据库读取和保存,支持从 localStorage 平滑迁移
|
||||
*/
|
||||
export function useCommonConfigSnippet({
|
||||
settingsConfig,
|
||||
@@ -33,6 +39,7 @@ export function useCommonConfigSnippet({
|
||||
initialData,
|
||||
selectedPresetId,
|
||||
enabled = true,
|
||||
currentProviderId,
|
||||
}: UseCommonConfigSnippetProps) {
|
||||
const { t } = useTranslation();
|
||||
const [useCommonConfig, setUseCommonConfig] = useState(false);
|
||||
@@ -43,18 +50,54 @@ export function useCommonConfigSnippet({
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isExtracting, setIsExtracting] = useState(false);
|
||||
|
||||
const getSnippetApplyError = useCallback(
|
||||
(snippet: string) => {
|
||||
if (!snippet.trim()) {
|
||||
return t("claudeConfig.noCommonConfigToApply");
|
||||
}
|
||||
const validationError = validateJsonConfig(snippet, "通用配置片段");
|
||||
if (validationError) {
|
||||
return validationError;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(snippet) as Record<string, unknown>;
|
||||
if (Object.keys(parsed).length === 0) {
|
||||
return t("claudeConfig.noCommonConfigToApply");
|
||||
}
|
||||
} catch {
|
||||
return t("claudeConfig.noCommonConfigToApply");
|
||||
}
|
||||
return "";
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
// 用于跟踪是否正在通过通用配置更新
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪用户是否手动切换,避免自动检测覆盖用户意图
|
||||
const hasUserToggledCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化(避免反复覆盖用户切换)
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
// 用于避免异步保存乱序导致的过期同步
|
||||
const saveSequenceRef = useRef(0);
|
||||
const saveQueueRef = useRef<Promise<void>>(Promise.resolve());
|
||||
const enqueueSave = useCallback((saveFn: () => Promise<void>) => {
|
||||
const next = saveQueueRef.current.then(saveFn);
|
||||
saveQueueRef.current = next.catch(() => {});
|
||||
return next;
|
||||
}, []);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
hasInitializedNewMode.current = false;
|
||||
hasInitializedEditMode.current = false;
|
||||
hasUserToggledCommonConfig.current = false;
|
||||
}, [selectedPresetId, enabled]);
|
||||
|
||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
||||
// 初始化:从数据库加载,支持从 localStorage 迁移
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setIsLoading(false);
|
||||
@@ -72,7 +115,7 @@ export function useCommonConfigSnippet({
|
||||
setCommonConfigSnippetState(snippet);
|
||||
}
|
||||
} else {
|
||||
// 如果 config.json 中没有,尝试从 localStorage 迁移
|
||||
// 如果数据库中没有,尝试从 localStorage 迁移
|
||||
if (typeof window !== "undefined") {
|
||||
try {
|
||||
const legacySnippet =
|
||||
@@ -86,7 +129,7 @@ export function useCommonConfigSnippet({
|
||||
// 清理 localStorage
|
||||
window.localStorage.removeItem(LEGACY_STORAGE_KEY);
|
||||
console.log(
|
||||
"[迁移] Claude 通用配置已从 localStorage 迁移到 config.json",
|
||||
"[迁移] Claude 通用配置已从 localStorage 迁移到数据库",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -110,18 +153,52 @@ export function useCommonConfigSnippet({
|
||||
};
|
||||
}, [enabled]);
|
||||
|
||||
// 初始化时检查通用配置片段(编辑模式)
|
||||
// 初始化时从 meta 读取启用状态(编辑模式)
|
||||
// 优先使用 meta,若 meta 未定义则回退到内容检测
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (initialData && !isLoading) {
|
||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||
const hasCommon = hasCommonConfigSnippet(
|
||||
configString,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
if (initialData && !isLoading && !hasInitializedEditMode.current) {
|
||||
hasInitializedEditMode.current = true;
|
||||
|
||||
// 使用 meta 中记录的按 app 启用状态
|
||||
const metaByApp = initialData.meta?.commonConfigEnabledByApp;
|
||||
const resolvedMetaEnabled =
|
||||
metaByApp?.claude ?? initialData.meta?.commonConfigEnabled;
|
||||
|
||||
if (resolvedMetaEnabled !== undefined) {
|
||||
if (!resolvedMetaEnabled) {
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
setCommonConfigError("");
|
||||
setUseCommonConfig(true);
|
||||
return;
|
||||
} else {
|
||||
// meta 未定义,回退到内容检测
|
||||
const settingsConfigStr =
|
||||
typeof initialData.settingsConfig === "string"
|
||||
? initialData.settingsConfig
|
||||
: JSON.stringify(initialData.settingsConfig ?? {});
|
||||
const detected = hasCommonConfigSnippet(
|
||||
settingsConfigStr,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(detected);
|
||||
}
|
||||
}
|
||||
}, [enabled, initialData, commonConfigSnippet, isLoading]);
|
||||
}, [
|
||||
enabled,
|
||||
initialData,
|
||||
isLoading,
|
||||
commonConfigSnippet,
|
||||
getSnippetApplyError,
|
||||
]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
useEffect(() => {
|
||||
@@ -166,6 +243,15 @@ export function useCommonConfigSnippet({
|
||||
// 处理通用配置开关
|
||||
const handleCommonConfigToggle = useCallback(
|
||||
(checked: boolean) => {
|
||||
hasUserToggledCommonConfig.current = true;
|
||||
if (checked) {
|
||||
const snippetError = getSnippetApplyError(commonConfigSnippet);
|
||||
if (snippetError) {
|
||||
setCommonConfigError(snippetError);
|
||||
setUseCommonConfig(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { updatedConfig, error: snippetError } = updateCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
commonConfigSnippet,
|
||||
@@ -188,7 +274,7 @@ export function useCommonConfigSnippet({
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
},
|
||||
[settingsConfig, commonConfigSnippet, onConfigChange],
|
||||
[settingsConfig, commonConfigSnippet, onConfigChange, getSnippetApplyError],
|
||||
);
|
||||
|
||||
// 处理通用配置片段变化
|
||||
@@ -198,14 +284,34 @@ export function useCommonConfigSnippet({
|
||||
setCommonConfigSnippetState(value);
|
||||
|
||||
if (!value.trim()) {
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到 config.json(清空)
|
||||
configApi.setCommonConfigSnippet("claude", "").catch((error) => {
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
// 保存到数据库(清空)
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("claude", ""))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 清空时也需要同步:移除所有供应商的通用配置片段
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"claude",
|
||||
previousSnippet,
|
||||
"", // newSnippet 为空表示移除
|
||||
replaceCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
|
||||
if (useCommonConfig) {
|
||||
const { updatedConfig } = updateCommonConfigSnippet(
|
||||
@@ -223,15 +329,36 @@ export function useCommonConfigSnippet({
|
||||
const validationError = validateJsonConfig(value, "通用配置片段");
|
||||
if (validationError) {
|
||||
setCommonConfigError(validationError);
|
||||
return;
|
||||
} else {
|
||||
const saveId = ++saveSequenceRef.current;
|
||||
setCommonConfigError("");
|
||||
// 保存到 config.json
|
||||
configApi.setCommonConfigSnippet("claude", value).catch((error) => {
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
// 保存到数据库
|
||||
enqueueSave(() => configApi.setCommonConfigSnippet("claude", value))
|
||||
.then(() => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
// 保存成功后,同步更新所有启用了通用配置的供应商
|
||||
configApi.syncCommonConfigToProviders(
|
||||
"claude",
|
||||
previousSnippet,
|
||||
value,
|
||||
replaceCommonConfigSnippet,
|
||||
currentProviderId,
|
||||
(result) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
if (result.error) {
|
||||
toast.error(t("providerForm.commonConfigSyncFailed"));
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (saveSequenceRef.current !== saveId) return;
|
||||
console.error("保存通用配置失败:", error);
|
||||
setCommonConfigError(
|
||||
t("claudeConfig.saveFailed", { error: String(error) }),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 若当前启用通用配置且格式正确,需要替换为最新片段
|
||||
@@ -265,21 +392,36 @@ export function useCommonConfigSnippet({
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
[commonConfigSnippet, settingsConfig, useCommonConfig, onConfigChange],
|
||||
[
|
||||
commonConfigSnippet,
|
||||
settingsConfig,
|
||||
useCommonConfig,
|
||||
onConfigChange,
|
||||
currentProviderId,
|
||||
enqueueSave,
|
||||
t,
|
||||
],
|
||||
);
|
||||
|
||||
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
|
||||
// 当配置变化时检查是否包含通用配置(避免通过通用配置更新时反复覆盖)
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||
return;
|
||||
}
|
||||
const metaByApp = initialData?.meta?.commonConfigEnabledByApp;
|
||||
const hasExplicitMeta =
|
||||
metaByApp?.claude !== undefined ||
|
||||
initialData?.meta?.commonConfigEnabled !== undefined;
|
||||
if (hasExplicitMeta || hasUserToggledCommonConfig.current) {
|
||||
return;
|
||||
}
|
||||
const hasCommon = hasCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
setUseCommonConfig(hasCommon);
|
||||
}, [enabled, settingsConfig, commonConfigSnippet, isLoading]);
|
||||
}, [enabled, settingsConfig, commonConfigSnippet, isLoading, initialData]);
|
||||
|
||||
// 从编辑器当前内容提取通用配置片段
|
||||
const handleExtract = useCallback(async () => {
|
||||
|
||||
Reference in New Issue
Block a user