mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-24 23:10:39 +08:00
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user