fix(config): resolve common config sync issues across frontend and backend

- Fix backfill pollution: extract custom config from live when common config enabled
- Fix proxy backup: merge common config before backup to preserve full config
- Unify null override semantics: null no longer overrides in both frontend and backend
- Add missing i18n keys for common config UI
- Hide format button in readonly JsonEditor
- Add mapGeminiWarningToI18n for user-friendly warning messages
This commit is contained in:
YoVinchen
2026-01-27 12:28:46 +08:00
parent 733605ae5c
commit 9e25ecf475
11 changed files with 200 additions and 26 deletions
+1 -1
View File
@@ -280,7 +280,7 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
style={{ width: "100%", height: isFullHeight ? undefined : "auto" }}
className={isFullHeight ? "flex-1 min-h-0" : ""}
/>
{language === "json" && (
{language === "json" && !readOnly && (
<button
type="button"
onClick={handleFormat}
@@ -12,7 +12,10 @@ import {
import { providersApi, vscodeApi, configApi, type AppId } from "@/lib/api";
import { extractDifference, isPlainObject } from "@/utils/configMerge";
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
import { parseGeminiCommonConfigSnippet } from "@/utils/providerConfigUtils";
import {
parseGeminiCommonConfigSnippet,
mapGeminiWarningToI18n,
} from "@/utils/providerConfigUtils";
interface EditProviderDialogProps {
open: boolean;
@@ -138,7 +141,7 @@ export function EditProviderDialog({
} else {
// Show warning toast if keys were filtered
if (parseResult.warning) {
toast.warning(parseResult.warning);
toast.warning(mapGeminiWarningToI18n(parseResult.warning, t));
}
if (
@@ -38,7 +38,10 @@ import { applyTemplateValues } from "@/utils/providerConfigUtils";
import { mergeProviderMeta } from "@/utils/providerMetaUtils";
import { extractDifference, isPlainObject } from "@/utils/configMerge";
import { extractTomlDifference } from "@/utils/tomlConfigMerge";
import { parseGeminiCommonConfigSnippet } from "@/utils/providerConfigUtils";
import {
parseGeminiCommonConfigSnippet,
mapGeminiWarningToI18n,
} from "@/utils/providerConfigUtils";
import { getCodexCustomTemplate } from "@/config/codexTemplates";
import CodexConfigEditor from "./CodexConfigEditor";
import { CommonConfigEditor } from "./CommonConfigEditor";
@@ -979,7 +982,7 @@ export function ProviderForm({
);
// Show warning toast if keys were filtered
if (warning) {
toast.warning(warning);
toast.warning(mapGeminiWarningToI18n(warning, t));
}
if (isPlainObject(envObj) && isPlainObject(commonEnvObj)) {
const { customConfig } = extractDifference(envObj, commonEnvObj);
+10 -3
View File
@@ -36,7 +36,8 @@
"search": "Search",
"reset": "Reset",
"actions": "Actions",
"deleting": "Deleting..."
"deleting": "Deleting...",
"readonly": "Read-only"
},
"apiKeyInput": {
"placeholder": "Enter API Key",
@@ -58,7 +59,12 @@
"extractFromCurrent": "Extract from Editor",
"extractNoCommonConfig": "No common config available to extract from editor",
"extractFailed": "Extract failed: {{error}}",
"saveFailed": "Save failed: {{error}}"
"saveFailed": "Save failed: {{error}}",
"hidePreview": "Hide merged preview",
"showPreview": "Show merged preview",
"customConfig": "Custom config (overrides common)",
"mergedPreview": "Merged preview (read-only)",
"mergedPreviewHint": "Common config + Custom config = Final config"
},
"header": {
"viewOnGithub": "View on GitHub",
@@ -553,7 +559,8 @@
"commonConfigInvalidValues": "Common config snippet values must be strings",
"noCommonConfigToApply": "Common config snippet is empty or has no applicable entries",
"configMergeFailed": "Config merge failed: {{error}}",
"configReplaceFailed": "Config replace failed: {{error}}"
"configReplaceFailed": "Config replace failed: {{error}}",
"forbiddenKeysWarning": "The following keys were filtered (not allowed in common config): {{keys}}"
},
"opencode": {
"npmPackage": "API Format",
+10 -3
View File
@@ -36,7 +36,8 @@
"search": "検索",
"reset": "リセット",
"actions": "操作",
"deleting": "削除中..."
"deleting": "削除中...",
"readonly": "読み取り専用"
},
"apiKeyInput": {
"placeholder": "API Key を入力",
@@ -58,7 +59,12 @@
"extractFromCurrent": "編集内容から抽出",
"extractNoCommonConfig": "編集内容から抽出できる共通設定がありません",
"extractFailed": "抽出に失敗しました: {{error}}",
"saveFailed": "保存に失敗しました: {{error}}"
"saveFailed": "保存に失敗しました: {{error}}",
"hidePreview": "マージプレビューを非表示",
"showPreview": "マージプレビューを表示",
"customConfig": "カスタム設定(共通設定を上書き)",
"mergedPreview": "マージプレビュー(読み取り専用)",
"mergedPreviewHint": "共通設定 + カスタム設定 = 最終設定"
},
"header": {
"viewOnGithub": "GitHub で見る",
@@ -553,7 +559,8 @@
"commonConfigInvalidValues": "共通設定スニペットの値は文字列である必要があります",
"noCommonConfigToApply": "共通設定スニペットが空、または適用できる項目がありません",
"configMergeFailed": "設定のマージに失敗しました: {{error}}",
"configReplaceFailed": "設定の置換に失敗しました: {{error}}"
"configReplaceFailed": "設定の置換に失敗しました: {{error}}",
"forbiddenKeysWarning": "以下のキーはフィルタされました(共通設定では許可されていません):{{keys}}"
},
"opencode": {
"npmPackage": "API フォーマット",
+10 -3
View File
@@ -36,7 +36,8 @@
"search": "查询",
"reset": "重置",
"actions": "操作",
"deleting": "删除中..."
"deleting": "删除中...",
"readonly": "只读"
},
"apiKeyInput": {
"placeholder": "请输入API Key",
@@ -58,7 +59,12 @@
"extractFromCurrent": "从编辑内容提取",
"extractNoCommonConfig": "当前编辑内容没有可提取的通用配置",
"extractFailed": "提取失败: {{error}}",
"saveFailed": "保存失败: {{error}}"
"saveFailed": "保存失败: {{error}}",
"hidePreview": "隐藏合并预览",
"showPreview": "显示合并预览",
"customConfig": "自定义配置(覆盖通用配置)",
"mergedPreview": "合并预览(只读)",
"mergedPreviewHint": "通用配置 + 自定义配置 = 最终配置"
},
"header": {
"viewOnGithub": "在 GitHub 上查看",
@@ -553,7 +559,8 @@
"commonConfigInvalidValues": "通用配置片段的值必须是字符串",
"noCommonConfigToApply": "通用配置片段为空或没有可写入的内容",
"configMergeFailed": "配置合并失败: {{error}}",
"configReplaceFailed": "配置替换失败: {{error}}"
"configReplaceFailed": "配置替换失败: {{error}}",
"forbiddenKeysWarning": "以下密钥已被自动过滤(禁止在通用配置中设置):{{keys}}"
},
"opencode": {
"npmPackage": "接口格式",
+3 -3
View File
@@ -73,7 +73,7 @@ export const deepEqual = (a: unknown, b: unknown): boolean => {
* - 嵌套对象:递归合并
* - 数组:source 完全替换 target(不做元素级合并)
* - 原始值:source 覆盖 target
* - undefined:不覆盖
* - undefined/null:不覆盖(与后端 config_merge.rs 保持一致)
*/
export const deepMerge = <T extends Record<string, unknown>>(
target: T,
@@ -85,8 +85,8 @@ export const deepMerge = <T extends Record<string, unknown>>(
const sourceValue = source[key];
const targetValue = result[key];
// undefined 不覆盖
if (sourceValue === undefined) {
// undefined 和 null 都不覆盖(与后端保持一致)
if (sourceValue === undefined || sourceValue === null) {
continue;
}
+23
View File
@@ -1201,3 +1201,26 @@ export function parseGeminiCommonConfigSnippet(
warning: warnings.length > 0 ? warnings.join("; ") : undefined,
};
}
/**
* Map Gemini common config warning to i18n-friendly message.
*
* @param warning - The raw warning string from parseGeminiCommonConfigSnippet
* @param t - The i18n translation function
* @returns Translated warning message
*/
export function mapGeminiWarningToI18n(
warning: string,
t: (key: string, options?: { keys?: string; defaultValue?: string }) => string,
): string {
if (warning.startsWith(GEMINI_CONFIG_ERROR_CODES.FORBIDDEN_KEYS)) {
// Extract key list: "GEMINI_CONFIG_FORBIDDEN_KEYS: KEY1, KEY2" -> "KEY1, KEY2"
const keys = warning.replace(
`${GEMINI_CONFIG_ERROR_CODES.FORBIDDEN_KEYS}: `,
"",
);
return t("geminiConfig.forbiddenKeysWarning", { keys });
}
// Other warnings: return as-is
return warning;
}