mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-28 16:56:16 +08:00
feat: apply common config as runtime overlay instead of materialized merge
Common config snippets are now dynamically overlaid when writing live files, rather than being pre-merged into provider snapshots at edit time. This ensures that updating a snippet immediately takes effect for the current provider and automatically propagates to other providers on their next switch. Key changes: - Add write_live_with_common_config() overlay pipeline - Strip common config from live before backfilling provider snapshots - Normalize provider snapshots on save to keep them snippet-free - Add explicit commonConfigEnabled flag in ProviderMeta (Option<bool>) - Migrate legacy providers on snippet save (infer flag from subset check) - Add Codex TOML snippet validation in set_common_config_snippet - Stabilize onConfigChange callbacks with useCallback in ProviderForm
This commit is contained in:
@@ -237,13 +237,20 @@ export function ProviderForm({
|
||||
mode: "onSubmit",
|
||||
});
|
||||
|
||||
const handleSettingsConfigChange = useCallback(
|
||||
(config: string) => {
|
||||
form.setValue("settingsConfig", config);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const {
|
||||
apiKey,
|
||||
handleApiKeyChange,
|
||||
showApiKey: shouldShowApiKey,
|
||||
} = useApiKeyState({
|
||||
initialConfig: form.getValues("settingsConfig"),
|
||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||
onConfigChange: handleSettingsConfigChange,
|
||||
selectedPresetId,
|
||||
category,
|
||||
appType: appId,
|
||||
@@ -254,7 +261,7 @@ export function ProviderForm({
|
||||
category,
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
codexConfig: "",
|
||||
onSettingsConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||
onSettingsConfigChange: handleSettingsConfigChange,
|
||||
onCodexConfigChange: () => {},
|
||||
});
|
||||
|
||||
@@ -267,7 +274,7 @@ export function ProviderForm({
|
||||
handleModelChange,
|
||||
} = useModelState({
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||
onConfigChange: handleSettingsConfigChange,
|
||||
});
|
||||
|
||||
const [localApiFormat, setLocalApiFormat] = useState<ClaudeApiFormat>(() => {
|
||||
@@ -373,7 +380,7 @@ export function ProviderForm({
|
||||
selectedPresetId: appId === "claude" ? selectedPresetId : null,
|
||||
presetEntries: appId === "claude" ? presetEntries : [],
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||
onConfigChange: handleSettingsConfigChange,
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -386,8 +393,10 @@ export function ProviderForm({
|
||||
handleExtract: handleClaudeExtract,
|
||||
} = useCommonConfigSnippet({
|
||||
settingsConfig: form.getValues("settingsConfig"),
|
||||
onConfigChange: (config) => form.setValue("settingsConfig", config),
|
||||
onConfigChange: handleSettingsConfigChange,
|
||||
initialData: appId === "claude" ? initialData : undefined,
|
||||
initialEnabled:
|
||||
appId === "claude" ? initialData?.meta?.commonConfigEnabled : undefined,
|
||||
selectedPresetId: selectedPresetId ?? undefined,
|
||||
enabled: appId === "claude",
|
||||
});
|
||||
@@ -404,6 +413,8 @@ export function ProviderForm({
|
||||
codexConfig,
|
||||
onConfigChange: handleCodexConfigChange,
|
||||
initialData: appId === "codex" ? initialData : undefined,
|
||||
initialEnabled:
|
||||
appId === "codex" ? initialData?.meta?.commonConfigEnabled : undefined,
|
||||
selectedPresetId: selectedPresetId ?? undefined,
|
||||
});
|
||||
|
||||
@@ -487,6 +498,8 @@ export function ProviderForm({
|
||||
envStringToObj,
|
||||
envObjToString,
|
||||
initialData: appId === "gemini" ? initialData : undefined,
|
||||
initialEnabled:
|
||||
appId === "gemini" ? initialData?.meta?.commonConfigEnabled : undefined,
|
||||
selectedPresetId: selectedPresetId ?? undefined,
|
||||
});
|
||||
|
||||
@@ -809,6 +822,14 @@ export function ProviderForm({
|
||||
payload.meta ?? (initialData?.meta ? { ...initialData.meta } : undefined);
|
||||
payload.meta = {
|
||||
...(baseMeta ?? {}),
|
||||
commonConfigEnabled:
|
||||
appId === "claude"
|
||||
? useCommonConfig
|
||||
: appId === "codex"
|
||||
? useCodexCommonConfigFlag
|
||||
: appId === "gemini"
|
||||
? useGeminiCommonConfigFlag
|
||||
: undefined,
|
||||
endpointAutoSelect,
|
||||
testConfig: testConfig.enabled ? testConfig : undefined,
|
||||
proxyConfig: proxyConfig.enabled ? proxyConfig : undefined,
|
||||
|
||||
@@ -16,6 +16,7 @@ interface UseCodexCommonConfigProps {
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
initialEnabled?: boolean;
|
||||
selectedPresetId?: string;
|
||||
}
|
||||
|
||||
@@ -27,6 +28,7 @@ export function useCodexCommonConfig({
|
||||
codexConfig,
|
||||
onConfigChange,
|
||||
initialData,
|
||||
initialEnabled,
|
||||
selectedPresetId,
|
||||
}: UseCodexCommonConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -42,11 +44,14 @@ export function useCodexCommonConfig({
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化显式开关/预览
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
useEffect(() => {
|
||||
hasInitializedNewMode.current = false;
|
||||
}, [selectedPresetId]);
|
||||
hasInitializedEditMode.current = false;
|
||||
}, [selectedPresetId, initialEnabled]);
|
||||
|
||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
||||
useEffect(() => {
|
||||
@@ -107,10 +112,43 @@ export function useCodexCommonConfig({
|
||||
typeof initialData.settingsConfig.config === "string"
|
||||
? initialData.settingsConfig.config
|
||||
: "";
|
||||
const hasCommon = hasTomlCommonConfigSnippet(config, commonConfigSnippet);
|
||||
const inferredHasCommon = hasTomlCommonConfigSnippet(
|
||||
config,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
const hasCommon = initialEnabled ?? inferredHasCommon;
|
||||
setUseCommonConfig(hasCommon);
|
||||
|
||||
if (
|
||||
hasCommon &&
|
||||
!inferredHasCommon &&
|
||||
!hasInitializedEditMode.current
|
||||
) {
|
||||
hasInitializedEditMode.current = true;
|
||||
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
true,
|
||||
);
|
||||
if (!error) {
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
hasInitializedEditMode.current = true;
|
||||
}
|
||||
}
|
||||
}, [initialData, commonConfigSnippet, isLoading]);
|
||||
}, [
|
||||
codexConfig,
|
||||
commonConfigSnippet,
|
||||
initialData,
|
||||
initialEnabled,
|
||||
isLoading,
|
||||
onConfigChange,
|
||||
]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
useEffect(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ interface UseCommonConfigSnippetProps {
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
initialEnabled?: boolean;
|
||||
selectedPresetId?: string;
|
||||
/** When false, the hook skips all logic and returns disabled state. Default: true */
|
||||
enabled?: boolean;
|
||||
@@ -31,6 +32,7 @@ export function useCommonConfigSnippet({
|
||||
settingsConfig,
|
||||
onConfigChange,
|
||||
initialData,
|
||||
initialEnabled,
|
||||
selectedPresetId,
|
||||
enabled = true,
|
||||
}: UseCommonConfigSnippetProps) {
|
||||
@@ -47,12 +49,15 @@ export function useCommonConfigSnippet({
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化显式开关/预览
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
hasInitializedNewMode.current = false;
|
||||
}, [selectedPresetId, enabled]);
|
||||
hasInitializedEditMode.current = false;
|
||||
}, [selectedPresetId, enabled, initialEnabled]);
|
||||
|
||||
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
||||
useEffect(() => {
|
||||
@@ -115,13 +120,44 @@ export function useCommonConfigSnippet({
|
||||
if (!enabled) return;
|
||||
if (initialData && !isLoading) {
|
||||
const configString = JSON.stringify(initialData.settingsConfig, null, 2);
|
||||
const hasCommon = hasCommonConfigSnippet(
|
||||
const inferredHasCommon = hasCommonConfigSnippet(
|
||||
configString,
|
||||
commonConfigSnippet,
|
||||
);
|
||||
const hasCommon = initialEnabled ?? inferredHasCommon;
|
||||
setUseCommonConfig(hasCommon);
|
||||
|
||||
if (
|
||||
hasCommon &&
|
||||
!inferredHasCommon &&
|
||||
!hasInitializedEditMode.current
|
||||
) {
|
||||
hasInitializedEditMode.current = true;
|
||||
const { updatedConfig, error } = updateCommonConfigSnippet(
|
||||
settingsConfig,
|
||||
commonConfigSnippet,
|
||||
true,
|
||||
);
|
||||
if (!error) {
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onConfigChange(updatedConfig);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
}
|
||||
} else {
|
||||
hasInitializedEditMode.current = true;
|
||||
}
|
||||
}
|
||||
}, [enabled, initialData, commonConfigSnippet, isLoading]);
|
||||
}, [
|
||||
enabled,
|
||||
initialData,
|
||||
initialEnabled,
|
||||
commonConfigSnippet,
|
||||
isLoading,
|
||||
onConfigChange,
|
||||
settingsConfig,
|
||||
]);
|
||||
|
||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||
useEffect(() => {
|
||||
|
||||
@@ -19,6 +19,7 @@ interface UseGeminiCommonConfigProps {
|
||||
initialData?: {
|
||||
settingsConfig?: Record<string, unknown>;
|
||||
};
|
||||
initialEnabled?: boolean;
|
||||
selectedPresetId?: string;
|
||||
}
|
||||
|
||||
@@ -43,6 +44,7 @@ export function useGeminiCommonConfig({
|
||||
envStringToObj,
|
||||
envObjToString,
|
||||
initialData,
|
||||
initialEnabled,
|
||||
selectedPresetId,
|
||||
}: UseGeminiCommonConfigProps) {
|
||||
const { t } = useTranslation();
|
||||
@@ -58,11 +60,14 @@ export function useGeminiCommonConfig({
|
||||
const isUpdatingFromCommonConfig = useRef(false);
|
||||
// 用于跟踪新建模式是否已初始化默认勾选
|
||||
const hasInitializedNewMode = useRef(false);
|
||||
// 用于跟踪编辑模式是否已初始化显式开关/预览
|
||||
const hasInitializedEditMode = useRef(false);
|
||||
|
||||
// 当预设变化时,重置初始化标记,使新预设能够重新触发初始化逻辑
|
||||
useEffect(() => {
|
||||
hasInitializedNewMode.current = false;
|
||||
}, [selectedPresetId]);
|
||||
hasInitializedEditMode.current = false;
|
||||
}, [selectedPresetId, initialEnabled]);
|
||||
|
||||
const parseSnippetEnv = useCallback(
|
||||
(
|
||||
@@ -220,20 +225,46 @@ export function useGeminiCommonConfig({
|
||||
: {};
|
||||
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||
if (parsed.error) return;
|
||||
const hasCommon = hasEnvCommonConfigSnippet(
|
||||
const inferredHasCommon = hasEnvCommonConfigSnippet(
|
||||
env,
|
||||
parsed.env as Record<string, string>,
|
||||
);
|
||||
const hasCommon = initialEnabled ?? inferredHasCommon;
|
||||
setUseCommonConfig(hasCommon);
|
||||
|
||||
if (
|
||||
hasCommon &&
|
||||
!inferredHasCommon &&
|
||||
!hasInitializedEditMode.current
|
||||
) {
|
||||
hasInitializedEditMode.current = true;
|
||||
const currentEnv = envStringToObj(envValue);
|
||||
const merged = applySnippetToEnv(currentEnv, parsed.env);
|
||||
const nextEnvString = envObjToString(merged);
|
||||
|
||||
isUpdatingFromCommonConfig.current = true;
|
||||
onEnvChange(nextEnvString);
|
||||
setTimeout(() => {
|
||||
isUpdatingFromCommonConfig.current = false;
|
||||
}, 0);
|
||||
} else {
|
||||
hasInitializedEditMode.current = true;
|
||||
}
|
||||
} catch {
|
||||
// ignore parse error
|
||||
}
|
||||
}
|
||||
}, [
|
||||
applySnippetToEnv,
|
||||
commonConfigSnippet,
|
||||
envObjToString,
|
||||
envStringToObj,
|
||||
envValue,
|
||||
hasEnvCommonConfigSnippet,
|
||||
initialData,
|
||||
initialEnabled,
|
||||
isLoading,
|
||||
onEnvChange,
|
||||
parseSnippetEnv,
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user