fix: prevent common config modal infinite reopen loop and add draft editing

The auto-open useEffect in CodexConfigEditor and GeminiConfigEditor
created an inescapable loop: commonConfigError triggered modal open,
closing the modal didn't clear the error, so the effect immediately
reopened it — locking the entire UI.

- Remove auto-open useEffect from both Codex and Gemini config editors
- Convert common config modals to draft editing (edit locally, validate
  before save) instead of persisting on every keystroke
- Add TOML/JSON pre-validation via parseCommonConfigSnippet before any
  merge operation to prevent invalid content from being persisted
- Expose clearCommonConfigError so editors can clear stale errors on
  modal close
This commit is contained in:
Jason
2026-03-11 14:40:14 +08:00
parent 4ac7e28b10
commit 239c6fb2d7
9 changed files with 596 additions and 204 deletions
@@ -9,7 +9,7 @@ interface CodexCommonConfigModalProps {
isOpen: boolean;
onClose: () => void;
value: string;
onChange: (value: string) => void;
onSave: (value: string) => boolean;
error?: string;
onExtract?: () => void;
isExtracting?: boolean;
@@ -23,13 +23,14 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
isOpen,
onClose,
value,
onChange,
onSave,
error,
onExtract,
isExtracting,
}) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [draftValue, setDraftValue] = useState(value);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -46,11 +47,28 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
return () => observer.disconnect();
}, []);
useEffect(() => {
if (isOpen) {
setDraftValue(value);
}
}, [isOpen, value]);
const handleClose = () => {
setDraftValue(value);
onClose();
};
const handleSave = () => {
if (onSave(draftValue)) {
onClose();
}
};
return (
<FullScreenPanel
isOpen={isOpen}
title={t("codexConfig.editCommonConfigTitle")}
onClose={onClose}
onClose={handleClose}
footer={
<>
{onExtract && (
@@ -71,10 +89,10 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
<Button type="button" variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button type="button" onClick={onClose} className="gap-2">
<Button type="button" onClick={handleSave} className="gap-2">
<Save className="w-4 h-4" />
{t("common.save")}
</Button>
@@ -87,8 +105,8 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
</p>
<JsonEditor
value={value}
onChange={onChange}
value={draftValue}
onChange={setDraftValue}
placeholder={`# Common Codex config
# Add your common TOML configuration here`}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
@@ -19,7 +19,9 @@ interface CodexConfigEditorProps {
commonConfigSnippet: string;
onCommonConfigSnippetChange: (value: string) => void;
onCommonConfigSnippetChange: (value: string) => boolean;
onCommonConfigErrorClear: () => void;
commonConfigError: string;
@@ -42,6 +44,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
onCommonConfigToggle,
commonConfigSnippet,
onCommonConfigSnippetChange,
onCommonConfigErrorClear,
commonConfigError,
authError,
configError,
@@ -50,12 +53,10 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Auto-open common config modal if there's an error
useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) {
setIsCommonConfigModalOpen(true);
}
}, [commonConfigError, isCommonConfigModalOpen]);
const handleCloseCommonConfigModal = () => {
onCommonConfigErrorClear();
setIsCommonConfigModalOpen(false);
};
return (
<div className="space-y-6">
@@ -81,9 +82,9 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
{/* Common Config Modal */}
<CodexCommonConfigModal
isOpen={isCommonConfigModalOpen}
onClose={() => setIsCommonConfigModalOpen(false)}
onClose={handleCloseCommonConfigModal}
value={commonConfigSnippet}
onChange={onCommonConfigSnippetChange}
onSave={onCommonConfigSnippetChange}
error={commonConfigError}
onExtract={onExtract}
isExtracting={isExtracting}
@@ -9,7 +9,7 @@ interface GeminiCommonConfigModalProps {
isOpen: boolean;
onClose: () => void;
value: string;
onChange: (value: string) => void;
onSave: (value: string) => boolean;
error?: string;
onExtract?: () => void;
isExtracting?: boolean;
@@ -21,9 +21,10 @@ interface GeminiCommonConfigModalProps {
*/
export const GeminiCommonConfigModal: React.FC<
GeminiCommonConfigModalProps
> = ({ isOpen, onClose, value, onChange, error, onExtract, isExtracting }) => {
> = ({ isOpen, onClose, value, onSave, error, onExtract, isExtracting }) => {
const { t } = useTranslation();
const [isDarkMode, setIsDarkMode] = useState(false);
const [draftValue, setDraftValue] = useState(value);
useEffect(() => {
setIsDarkMode(document.documentElement.classList.contains("dark"));
@@ -40,13 +41,30 @@ export const GeminiCommonConfigModal: React.FC<
return () => observer.disconnect();
}, []);
useEffect(() => {
if (isOpen) {
setDraftValue(value);
}
}, [isOpen, value]);
const handleClose = () => {
setDraftValue(value);
onClose();
};
const handleSave = () => {
if (onSave(draftValue)) {
onClose();
}
};
return (
<FullScreenPanel
isOpen={isOpen}
title={t("geminiConfig.editCommonConfigTitle", {
defaultValue: "编辑 Gemini 通用配置片段",
})}
onClose={onClose}
onClose={handleClose}
footer={
<>
{onExtract && (
@@ -67,10 +85,10 @@ export const GeminiCommonConfigModal: React.FC<
})}
</Button>
)}
<Button type="button" variant="outline" onClick={onClose}>
<Button type="button" variant="outline" onClick={handleClose}>
{t("common.cancel")}
</Button>
<Button type="button" onClick={onClose} className="gap-2">
<Button type="button" onClick={handleSave} className="gap-2">
<Save className="w-4 h-4" />
{t("common.save")}
</Button>
@@ -86,8 +104,8 @@ export const GeminiCommonConfigModal: React.FC<
</p>
<JsonEditor
value={value}
onChange={onChange}
value={draftValue}
onChange={setDraftValue}
placeholder={`{
"GEMINI_MODEL": "gemini-3-pro-preview"
}`}
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { GeminiEnvSection, GeminiConfigSection } from "./GeminiConfigSections";
import { GeminiCommonConfigModal } from "./GeminiCommonConfigModal";
@@ -11,7 +11,8 @@ interface GeminiConfigEditorProps {
useCommonConfig: boolean;
onCommonConfigToggle: (checked: boolean) => void;
commonConfigSnippet: string;
onCommonConfigSnippetChange: (value: string) => void;
onCommonConfigSnippetChange: (value: string) => boolean;
onCommonConfigErrorClear: () => void;
commonConfigError: string;
envError: string;
configError: string;
@@ -29,6 +30,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
onCommonConfigToggle,
commonConfigSnippet,
onCommonConfigSnippetChange,
onCommonConfigErrorClear,
commonConfigError,
envError,
configError,
@@ -37,12 +39,10 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
}) => {
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
// Auto-open common config modal if there's an error
useEffect(() => {
if (commonConfigError && !isCommonConfigModalOpen) {
setIsCommonConfigModalOpen(true);
}
}, [commonConfigError, isCommonConfigModalOpen]);
const handleCloseCommonConfigModal = () => {
onCommonConfigErrorClear();
setIsCommonConfigModalOpen(false);
};
return (
<div className="space-y-6">
@@ -68,9 +68,9 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
{/* Common Config Modal */}
<GeminiCommonConfigModal
isOpen={isCommonConfigModalOpen}
onClose={() => setIsCommonConfigModalOpen(false)}
onClose={handleCloseCommonConfigModal}
value={commonConfigSnippet}
onChange={onCommonConfigSnippetChange}
onSave={onCommonConfigSnippetChange}
error={commonConfigError}
onExtract={onExtract}
isExtracting={isExtracting}
@@ -447,6 +447,7 @@ export function ProviderForm({
handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange,
isExtracting: isCodexExtracting,
handleExtract: handleCodexExtract,
clearCommonConfigError: clearCodexCommonConfigError,
} = useCodexCommonConfig({
codexConfig,
onConfigChange: handleCodexConfigChange,
@@ -530,6 +531,7 @@ export function ProviderForm({
handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange,
isExtracting: isGeminiExtracting,
handleExtract: handleGeminiExtract,
clearCommonConfigError: clearGeminiCommonConfigError,
} = useGeminiCommonConfig({
envValue: geminiEnv,
onEnvChange: handleGeminiEnvChange,
@@ -1467,6 +1469,7 @@ export function ProviderForm({
onCommonConfigToggle={handleCodexCommonConfigToggle}
commonConfigSnippet={codexCommonConfigSnippet}
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
onCommonConfigErrorClear={clearCodexCommonConfigError}
commonConfigError={codexCommonConfigError}
authError={codexAuthError}
configError={codexConfigError}
@@ -1488,6 +1491,7 @@ export function ProviderForm({
onCommonConfigSnippetChange={
handleGeminiCommonConfigSnippetChange
}
onCommonConfigErrorClear={clearGeminiCommonConfigError}
commonConfigError={geminiCommonConfigError}
envError={envError}
configError={geminiConfigError}
@@ -1,10 +1,12 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useTranslation } from "react-i18next";
import { parse as parseToml } from "smol-toml";
import {
updateTomlCommonConfigSnippet,
hasTomlCommonConfigSnippet,
} from "@/utils/providerConfigUtils";
import { configApi } from "@/lib/api";
import { normalizeTomlText } from "@/utils/textNormalization";
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
@@ -53,6 +55,30 @@ export function useCodexCommonConfig({
hasInitializedEditMode.current = false;
}, [selectedPresetId, initialEnabled]);
const parseCommonConfigSnippet = useCallback((snippetString: string) => {
const trimmed = snippetString.trim();
if (!trimmed) {
return {
hasContent: false,
};
}
try {
const parsed = parseToml(normalizeTomlText(snippetString)) as Record<
string,
unknown
>;
return {
hasContent: Object.keys(parsed).length > 0,
};
} catch (error) {
return {
hasContent: false,
error: error instanceof Error ? error.message : String(error),
};
}
}, []);
// 初始化:从 config.json 加载,支持从 localStorage 迁移
useEffect(() => {
let mounted = true;
@@ -107,36 +133,59 @@ export function useCodexCommonConfig({
// 初始化时检查通用配置片段(编辑模式)
useEffect(() => {
if (initialData?.settingsConfig && !isLoading) {
const config =
typeof initialData.settingsConfig.config === "string"
? initialData.settingsConfig.config
: "";
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;
}
if (
!initialData?.settingsConfig ||
isLoading ||
hasInitializedEditMode.current
) {
return;
}
hasInitializedEditMode.current = true;
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsedSnippet.error);
}
setUseCommonConfig(false);
return;
}
const config =
typeof initialData.settingsConfig.config === "string"
? initialData.settingsConfig.config
: "";
const inferredHasCommon = hasTomlCommonConfigSnippet(
config,
commonConfigSnippet,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
if (hasCommon && !inferredHasCommon) {
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (error) {
setCommonConfigError(error);
setUseCommonConfig(false);
return;
}
setCommonConfigError("");
setUseCommonConfig(true);
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
return;
}
setCommonConfigError("");
setUseCommonConfig(hasCommon);
}, [
codexConfig,
commonConfigSnippet,
@@ -144,49 +193,75 @@ export function useCodexCommonConfig({
initialEnabled,
isLoading,
onConfigChange,
parseCommonConfigSnippet,
]);
// 新建模式:如果通用配置片段存在且有效,默认启用
useEffect(() => {
// 仅新建模式、加载完成、尚未初始化过
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
// 检查 TOML 片段是否有实质内容(不只是注释和空行)
const lines = commonConfigSnippet.split("\n");
const hasContent = lines.some((line) => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith("#");
});
if (hasContent) {
setUseCommonConfig(true);
// 合并通用配置到当前配置
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (!error) {
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}
}
if (initialData || isLoading || hasInitializedNewMode.current) {
return;
}
hasInitializedNewMode.current = true;
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsedSnippet.error);
}
setUseCommonConfig(false);
return;
}
if (!parsedSnippet.hasContent) {
return;
}
const { updatedConfig, error } = updateTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
true,
);
if (error) {
setCommonConfigError(error);
setUseCommonConfig(false);
return;
}
setCommonConfigError("");
setUseCommonConfig(true);
isUpdatingFromCommonConfig.current = true;
onConfigChange(updatedConfig);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
}, [
initialData,
commonConfigSnippet,
isLoading,
codexConfig,
onConfigChange,
parseCommonConfigSnippet,
]);
// 处理通用配置开关
const handleCommonConfigToggle = useCallback(
(checked: boolean) => {
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
setCommonConfigError(parsedSnippet.error);
setUseCommonConfig(false);
return;
}
if (!parsedSnippet.hasContent) {
setCommonConfigError(
t("codexConfig.noCommonConfigToApply", {
defaultValue: "通用配置片段为空或没有可写入的内容",
}),
);
setUseCommonConfig(false);
return;
}
const { updatedConfig, error: snippetError } =
updateTomlCommonConfigSnippet(
codexConfig,
@@ -210,18 +285,39 @@ export function useCodexCommonConfig({
isUpdatingFromCommonConfig.current = false;
}, 0);
},
[codexConfig, commonConfigSnippet, onConfigChange],
[codexConfig, commonConfigSnippet, onConfigChange, parseCommonConfigSnippet, t],
);
// 处理通用配置片段变化
const handleCommonConfigSnippetChange = useCallback(
(value: string) => {
(value: string): boolean => {
const previousSnippet = commonConfigSnippet;
setCommonConfigSnippetState(value);
if (!value.trim()) {
setCommonConfigError("");
// 保存到 config.json(清空)
if (useCommonConfig) {
const previousParsed = parseCommonConfigSnippet(previousSnippet);
let updatedConfig = codexConfig;
if (!previousParsed.error && previousParsed.hasContent) {
const removeResult = updateTomlCommonConfigSnippet(
codexConfig,
previousSnippet,
false,
);
if (removeResult.error) {
setCommonConfigError(removeResult.error);
return false;
}
updatedConfig = removeResult.updatedConfig;
}
onConfigChange(updatedConfig);
setUseCommonConfig(false);
}
setCommonConfigSnippetState("");
configApi
.setCommonConfigSnippet("codex", "")
.catch((error: unknown) => {
@@ -230,51 +326,42 @@ export function useCodexCommonConfig({
t("codexConfig.saveFailed", { error: String(error) }),
);
});
return true;
}
if (useCommonConfig) {
const { updatedConfig } = updateTomlCommonConfigSnippet(
const parsedNextSnippet = parseCommonConfigSnippet(value);
if (parsedNextSnippet.error) {
setCommonConfigError(parsedNextSnippet.error);
return false;
}
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
let nextConfig = codexConfig;
const previousParsed = parseCommonConfigSnippet(previousSnippet);
if (!previousParsed.error && previousParsed.hasContent) {
const removeResult = updateTomlCommonConfigSnippet(
codexConfig,
previousSnippet,
false,
);
onConfigChange(updatedConfig);
setUseCommonConfig(false);
if (removeResult.error) {
setCommonConfigError(removeResult.error);
return false;
}
nextConfig = removeResult.updatedConfig;
}
return;
}
// TOML 格式校验较为复杂,暂时不做校验,直接清空错误
setCommonConfigError("");
// 保存到 config.json
configApi
.setCommonConfigSnippet("codex", value)
.catch((error: unknown) => {
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.saveFailed", { error: String(error) }),
);
});
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
const removeResult = updateTomlCommonConfigSnippet(
codexConfig,
previousSnippet,
false,
);
if (removeResult.error) {
setCommonConfigError(removeResult.error);
return;
}
const addResult = updateTomlCommonConfigSnippet(
removeResult.updatedConfig,
nextConfig,
value,
true,
);
if (addResult.error) {
setCommonConfigError(addResult.error);
return;
return false;
}
// 标记正在通过通用配置更新,避免触发状态检查
@@ -285,8 +372,28 @@ export function useCodexCommonConfig({
isUpdatingFromCommonConfig.current = false;
}, 0);
}
setCommonConfigError("");
setCommonConfigSnippetState(value);
configApi
.setCommonConfigSnippet("codex", value)
.catch((error: unknown) => {
console.error("保存 Codex 通用配置失败:", error);
setCommonConfigError(
t("codexConfig.saveFailed", { error: String(error) }),
);
});
return true;
},
[commonConfigSnippet, codexConfig, useCommonConfig, onConfigChange],
[
commonConfigSnippet,
codexConfig,
onConfigChange,
parseCommonConfigSnippet,
t,
useCommonConfig,
],
);
// 当配置变化时检查是否包含通用配置(但避免在通过通用配置更新时检查)
@@ -294,12 +401,17 @@ export function useCodexCommonConfig({
if (isUpdatingFromCommonConfig.current || isLoading) {
return;
}
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
if (parsedSnippet.error) {
setUseCommonConfig(false);
return;
}
const hasCommon = hasTomlCommonConfigSnippet(
codexConfig,
commonConfigSnippet,
);
setUseCommonConfig(hasCommon);
}, [codexConfig, commonConfigSnippet, isLoading]);
}, [codexConfig, commonConfigSnippet, isLoading, parseCommonConfigSnippet]);
// 从编辑器当前内容提取通用配置片段
const handleExtract = useCallback(async () => {
@@ -333,6 +445,10 @@ export function useCodexCommonConfig({
}
}, [codexConfig, t]);
const clearCommonConfigError = useCallback(() => {
setCommonConfigError("");
}, []);
return {
useCommonConfig,
commonConfigSnippet,
@@ -342,5 +458,6 @@ export function useCodexCommonConfig({
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
clearCommonConfigError,
};
}
@@ -216,43 +216,55 @@ export function useGeminiCommonConfig({
// 初始化时检查通用配置片段(编辑模式)
useEffect(() => {
if (initialData?.settingsConfig && !isLoading) {
try {
const env =
isPlainObject(initialData.settingsConfig.env) &&
Object.keys(initialData.settingsConfig.env).length > 0
? (initialData.settingsConfig.env as Record<string, string>)
: {};
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const inferredHasCommon = hasEnvCommonConfigSnippet(
env,
parsed.env as Record<string, string>,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
setUseCommonConfig(hasCommon);
if (
!initialData?.settingsConfig ||
isLoading ||
hasInitializedEditMode.current
) {
return;
}
if (
hasCommon &&
!inferredHasCommon &&
!hasInitializedEditMode.current
) {
hasInitializedEditMode.current = true;
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
hasInitializedEditMode.current = true;
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
} else {
hasInitializedEditMode.current = true;
try {
const env =
isPlainObject(initialData.settingsConfig.env) &&
Object.keys(initialData.settingsConfig.env).length > 0
? (initialData.settingsConfig.env as Record<string, string>)
: {};
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsed.error);
}
} catch {
// ignore parse error
setUseCommonConfig(false);
return;
}
const inferredHasCommon = hasEnvCommonConfigSnippet(
env,
parsed.env as Record<string, string>,
);
const hasCommon = initialEnabled ?? inferredHasCommon;
if (hasCommon && !inferredHasCommon) {
const currentEnv = envStringToObj(envValue);
const merged = applySnippetToEnv(currentEnv, parsed.env);
const nextEnvString = envObjToString(merged);
setCommonConfigError("");
setUseCommonConfig(true);
isUpdatingFromCommonConfig.current = true;
onEnvChange(nextEnvString);
setTimeout(() => {
isUpdatingFromCommonConfig.current = false;
}, 0);
return;
}
setCommonConfigError("");
setUseCommonConfig(hasCommon);
} catch {
// ignore parse error
}
}, [
applySnippetToEnv,
@@ -270,26 +282,34 @@ export function useGeminiCommonConfig({
// 新建模式:如果通用配置片段存在且有效,默认启用
useEffect(() => {
// 仅新建模式、加载完成、尚未初始化过
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
hasInitializedNewMode.current = true;
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) return;
const hasContent = Object.keys(parsed.env).length > 0;
if (!hasContent) return;
setUseCommonConfig(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);
if (initialData || isLoading || hasInitializedNewMode.current) {
return;
}
hasInitializedNewMode.current = true;
const parsed = parseSnippetEnv(commonConfigSnippet);
if (parsed.error) {
if (commonConfigSnippet.trim()) {
setCommonConfigError(parsed.error);
}
setUseCommonConfig(false);
return;
}
const hasContent = Object.keys(parsed.env).length > 0;
if (!hasContent) return;
setCommonConfigError("");
setUseCommonConfig(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);
}, [
initialData,
isLoading,
@@ -346,13 +366,29 @@ export function useGeminiCommonConfig({
// 处理通用配置片段变化
const handleCommonConfigSnippetChange = useCallback(
(value: string) => {
(value: string): boolean => {
const previousSnippet = commonConfigSnippet;
setCommonConfigSnippetState(value);
if (!value.trim()) {
setCommonConfigError("");
// 保存到 config.json(清空)
if (useCommonConfig) {
const parsedPrevious = parseSnippetEnv(previousSnippet);
if (
!parsedPrevious.error &&
Object.keys(parsedPrevious.env).length > 0
) {
const currentEnv = envStringToObj(envValue);
const updatedEnv = removeSnippetFromEnv(
currentEnv,
parsedPrevious.env,
);
onEnvChange(envObjToString(updatedEnv));
}
setUseCommonConfig(false);
}
setCommonConfigSnippetState("");
configApi
.setCommonConfigSnippet("gemini", "")
.catch((error: unknown) => {
@@ -361,36 +397,16 @@ export function useGeminiCommonConfig({
t("geminiConfig.saveFailed", { error: String(error) }),
);
});
if (useCommonConfig) {
const parsed = parseSnippetEnv(previousSnippet);
if (!parsed.error && Object.keys(parsed.env).length > 0) {
const currentEnv = envStringToObj(envValue);
const updatedEnv = removeSnippetFromEnv(currentEnv, parsed.env);
onEnvChange(envObjToString(updatedEnv));
}
setUseCommonConfig(false);
}
return;
return true;
}
// 校验 JSON 格式
const parsed = parseSnippetEnv(value);
if (parsed.error) {
setCommonConfigError(parsed.error);
return;
return false;
}
setCommonConfigError("");
configApi
.setCommonConfigSnippet("gemini", value)
.catch((error: unknown) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(
t("geminiConfig.saveFailed", { error: String(error) }),
);
});
// 若当前启用通用配置,需要替换为最新片段
if (useCommonConfig) {
const prevParsed = parseSnippetEnv(previousSnippet);
@@ -413,6 +429,19 @@ export function useGeminiCommonConfig({
isUpdatingFromCommonConfig.current = false;
}, 0);
}
setCommonConfigError("");
setCommonConfigSnippetState(value);
configApi
.setCommonConfigSnippet("gemini", value)
.catch((error: unknown) => {
console.error("保存 Gemini 通用配置失败:", error);
setCommonConfigError(
t("geminiConfig.saveFailed", { error: String(error) }),
);
});
return true;
},
[
applySnippetToEnv,
@@ -487,6 +516,10 @@ export function useGeminiCommonConfig({
}
}, [envStringToObj, envValue, parseSnippetEnv, t]);
const clearCommonConfigError = useCallback(() => {
setCommonConfigError("");
}, []);
return {
useCommonConfig,
commonConfigSnippet,
@@ -496,5 +529,6 @@ export function useGeminiCommonConfig({
handleCommonConfigToggle,
handleCommonConfigSnippetChange,
handleExtract,
clearCommonConfigError,
};
}