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:
Jason
2026-03-08 21:45:20 +08:00
parent fc6f2af4c6
commit 6d078e7f33
10 changed files with 982 additions and 34 deletions
@@ -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,
]);
+2
View File
@@ -126,6 +126,8 @@ export interface ProviderProxyConfig {
export interface ProviderMeta {
// 自定义端点:以 URL 为键,值为端点信息
custom_endpoints?: Record<string, CustomEndpoint>;
// 是否在切换/同步到 live 时应用通用配置片段
commonConfigEnabled?: boolean;
// 用量查询脚本配置
usage_script?: UsageScript;
// 请求地址管理:测速后自动选择最佳端点