mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-04 19:45:34 +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;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onSave: (value: string) => boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
onExtract?: () => void;
|
onExtract?: () => void;
|
||||||
isExtracting?: boolean;
|
isExtracting?: boolean;
|
||||||
@@ -23,13 +23,14 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onSave,
|
||||||
error,
|
error,
|
||||||
onExtract,
|
onExtract,
|
||||||
isExtracting,
|
isExtracting,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
const [draftValue, setDraftValue] = useState(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
@@ -46,11 +47,28 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setDraftValue(value);
|
||||||
|
}
|
||||||
|
}, [isOpen, value]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setDraftValue(value);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (onSave(draftValue)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullScreenPanel
|
<FullScreenPanel
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={t("codexConfig.editCommonConfigTitle")}
|
title={t("codexConfig.editCommonConfigTitle")}
|
||||||
onClose={onClose}
|
onClose={handleClose}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
{onExtract && (
|
{onExtract && (
|
||||||
@@ -71,10 +89,10 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="button" variant="outline" onClick={onClose}>
|
<Button type="button" variant="outline" onClick={handleClose}>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" onClick={onClose} className="gap-2">
|
<Button type="button" onClick={handleSave} className="gap-2">
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -87,8 +105,8 @@ export const CodexCommonConfigModal: React.FC<CodexCommonConfigModalProps> = ({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={value}
|
value={draftValue}
|
||||||
onChange={onChange}
|
onChange={setDraftValue}
|
||||||
placeholder={`# Common Codex config
|
placeholder={`# Common Codex config
|
||||||
|
|
||||||
# Add your common TOML configuration here`}
|
# 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 { CodexAuthSection, CodexConfigSection } from "./CodexConfigSections";
|
||||||
import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
|
import { CodexCommonConfigModal } from "./CodexCommonConfigModal";
|
||||||
|
|
||||||
@@ -19,7 +19,9 @@ interface CodexConfigEditorProps {
|
|||||||
|
|
||||||
commonConfigSnippet: string;
|
commonConfigSnippet: string;
|
||||||
|
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
onCommonConfigSnippetChange: (value: string) => boolean;
|
||||||
|
|
||||||
|
onCommonConfigErrorClear: () => void;
|
||||||
|
|
||||||
commonConfigError: string;
|
commonConfigError: string;
|
||||||
|
|
||||||
@@ -42,6 +44,7 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
onCommonConfigToggle,
|
onCommonConfigToggle,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
onCommonConfigSnippetChange,
|
onCommonConfigSnippetChange,
|
||||||
|
onCommonConfigErrorClear,
|
||||||
commonConfigError,
|
commonConfigError,
|
||||||
authError,
|
authError,
|
||||||
configError,
|
configError,
|
||||||
@@ -50,12 +53,10 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||||
|
|
||||||
// Auto-open common config modal if there's an error
|
const handleCloseCommonConfigModal = () => {
|
||||||
useEffect(() => {
|
onCommonConfigErrorClear();
|
||||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
setIsCommonConfigModalOpen(false);
|
||||||
setIsCommonConfigModalOpen(true);
|
};
|
||||||
}
|
|
||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -81,9 +82,9 @@ const CodexConfigEditor: React.FC<CodexConfigEditorProps> = ({
|
|||||||
{/* Common Config Modal */}
|
{/* Common Config Modal */}
|
||||||
<CodexCommonConfigModal
|
<CodexCommonConfigModal
|
||||||
isOpen={isCommonConfigModalOpen}
|
isOpen={isCommonConfigModalOpen}
|
||||||
onClose={() => setIsCommonConfigModalOpen(false)}
|
onClose={handleCloseCommonConfigModal}
|
||||||
value={commonConfigSnippet}
|
value={commonConfigSnippet}
|
||||||
onChange={onCommonConfigSnippetChange}
|
onSave={onCommonConfigSnippetChange}
|
||||||
error={commonConfigError}
|
error={commonConfigError}
|
||||||
onExtract={onExtract}
|
onExtract={onExtract}
|
||||||
isExtracting={isExtracting}
|
isExtracting={isExtracting}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ interface GeminiCommonConfigModalProps {
|
|||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onSave: (value: string) => boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
onExtract?: () => void;
|
onExtract?: () => void;
|
||||||
isExtracting?: boolean;
|
isExtracting?: boolean;
|
||||||
@@ -21,9 +21,10 @@ interface GeminiCommonConfigModalProps {
|
|||||||
*/
|
*/
|
||||||
export const GeminiCommonConfigModal: React.FC<
|
export const GeminiCommonConfigModal: React.FC<
|
||||||
GeminiCommonConfigModalProps
|
GeminiCommonConfigModalProps
|
||||||
> = ({ isOpen, onClose, value, onChange, error, onExtract, isExtracting }) => {
|
> = ({ isOpen, onClose, value, onSave, error, onExtract, isExtracting }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [isDarkMode, setIsDarkMode] = useState(false);
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
const [draftValue, setDraftValue] = useState(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
setIsDarkMode(document.documentElement.classList.contains("dark"));
|
||||||
@@ -40,13 +41,30 @@ export const GeminiCommonConfigModal: React.FC<
|
|||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setDraftValue(value);
|
||||||
|
}
|
||||||
|
}, [isOpen, value]);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setDraftValue(value);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
if (onSave(draftValue)) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullScreenPanel
|
<FullScreenPanel
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
title={t("geminiConfig.editCommonConfigTitle", {
|
title={t("geminiConfig.editCommonConfigTitle", {
|
||||||
defaultValue: "编辑 Gemini 通用配置片段",
|
defaultValue: "编辑 Gemini 通用配置片段",
|
||||||
})}
|
})}
|
||||||
onClose={onClose}
|
onClose={handleClose}
|
||||||
footer={
|
footer={
|
||||||
<>
|
<>
|
||||||
{onExtract && (
|
{onExtract && (
|
||||||
@@ -67,10 +85,10 @@ export const GeminiCommonConfigModal: React.FC<
|
|||||||
})}
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button type="button" variant="outline" onClick={onClose}>
|
<Button type="button" variant="outline" onClick={handleClose}>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" onClick={onClose} className="gap-2">
|
<Button type="button" onClick={handleSave} className="gap-2">
|
||||||
<Save className="w-4 h-4" />
|
<Save className="w-4 h-4" />
|
||||||
{t("common.save")}
|
{t("common.save")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -86,8 +104,8 @@ export const GeminiCommonConfigModal: React.FC<
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<JsonEditor
|
<JsonEditor
|
||||||
value={value}
|
value={draftValue}
|
||||||
onChange={onChange}
|
onChange={setDraftValue}
|
||||||
placeholder={`{
|
placeholder={`{
|
||||||
"GEMINI_MODEL": "gemini-3-pro-preview"
|
"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 { GeminiEnvSection, GeminiConfigSection } from "./GeminiConfigSections";
|
||||||
import { GeminiCommonConfigModal } from "./GeminiCommonConfigModal";
|
import { GeminiCommonConfigModal } from "./GeminiCommonConfigModal";
|
||||||
|
|
||||||
@@ -11,7 +11,8 @@ interface GeminiConfigEditorProps {
|
|||||||
useCommonConfig: boolean;
|
useCommonConfig: boolean;
|
||||||
onCommonConfigToggle: (checked: boolean) => void;
|
onCommonConfigToggle: (checked: boolean) => void;
|
||||||
commonConfigSnippet: string;
|
commonConfigSnippet: string;
|
||||||
onCommonConfigSnippetChange: (value: string) => void;
|
onCommonConfigSnippetChange: (value: string) => boolean;
|
||||||
|
onCommonConfigErrorClear: () => void;
|
||||||
commonConfigError: string;
|
commonConfigError: string;
|
||||||
envError: string;
|
envError: string;
|
||||||
configError: string;
|
configError: string;
|
||||||
@@ -29,6 +30,7 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
|||||||
onCommonConfigToggle,
|
onCommonConfigToggle,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
onCommonConfigSnippetChange,
|
onCommonConfigSnippetChange,
|
||||||
|
onCommonConfigErrorClear,
|
||||||
commonConfigError,
|
commonConfigError,
|
||||||
envError,
|
envError,
|
||||||
configError,
|
configError,
|
||||||
@@ -37,12 +39,10 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false);
|
||||||
|
|
||||||
// Auto-open common config modal if there's an error
|
const handleCloseCommonConfigModal = () => {
|
||||||
useEffect(() => {
|
onCommonConfigErrorClear();
|
||||||
if (commonConfigError && !isCommonConfigModalOpen) {
|
setIsCommonConfigModalOpen(false);
|
||||||
setIsCommonConfigModalOpen(true);
|
};
|
||||||
}
|
|
||||||
}, [commonConfigError, isCommonConfigModalOpen]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -68,9 +68,9 @@ const GeminiConfigEditor: React.FC<GeminiConfigEditorProps> = ({
|
|||||||
{/* Common Config Modal */}
|
{/* Common Config Modal */}
|
||||||
<GeminiCommonConfigModal
|
<GeminiCommonConfigModal
|
||||||
isOpen={isCommonConfigModalOpen}
|
isOpen={isCommonConfigModalOpen}
|
||||||
onClose={() => setIsCommonConfigModalOpen(false)}
|
onClose={handleCloseCommonConfigModal}
|
||||||
value={commonConfigSnippet}
|
value={commonConfigSnippet}
|
||||||
onChange={onCommonConfigSnippetChange}
|
onSave={onCommonConfigSnippetChange}
|
||||||
error={commonConfigError}
|
error={commonConfigError}
|
||||||
onExtract={onExtract}
|
onExtract={onExtract}
|
||||||
isExtracting={isExtracting}
|
isExtracting={isExtracting}
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ export function ProviderForm({
|
|||||||
handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange,
|
handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange,
|
||||||
isExtracting: isCodexExtracting,
|
isExtracting: isCodexExtracting,
|
||||||
handleExtract: handleCodexExtract,
|
handleExtract: handleCodexExtract,
|
||||||
|
clearCommonConfigError: clearCodexCommonConfigError,
|
||||||
} = useCodexCommonConfig({
|
} = useCodexCommonConfig({
|
||||||
codexConfig,
|
codexConfig,
|
||||||
onConfigChange: handleCodexConfigChange,
|
onConfigChange: handleCodexConfigChange,
|
||||||
@@ -530,6 +531,7 @@ export function ProviderForm({
|
|||||||
handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange,
|
handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange,
|
||||||
isExtracting: isGeminiExtracting,
|
isExtracting: isGeminiExtracting,
|
||||||
handleExtract: handleGeminiExtract,
|
handleExtract: handleGeminiExtract,
|
||||||
|
clearCommonConfigError: clearGeminiCommonConfigError,
|
||||||
} = useGeminiCommonConfig({
|
} = useGeminiCommonConfig({
|
||||||
envValue: geminiEnv,
|
envValue: geminiEnv,
|
||||||
onEnvChange: handleGeminiEnvChange,
|
onEnvChange: handleGeminiEnvChange,
|
||||||
@@ -1467,6 +1469,7 @@ export function ProviderForm({
|
|||||||
onCommonConfigToggle={handleCodexCommonConfigToggle}
|
onCommonConfigToggle={handleCodexCommonConfigToggle}
|
||||||
commonConfigSnippet={codexCommonConfigSnippet}
|
commonConfigSnippet={codexCommonConfigSnippet}
|
||||||
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
|
onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange}
|
||||||
|
onCommonConfigErrorClear={clearCodexCommonConfigError}
|
||||||
commonConfigError={codexCommonConfigError}
|
commonConfigError={codexCommonConfigError}
|
||||||
authError={codexAuthError}
|
authError={codexAuthError}
|
||||||
configError={codexConfigError}
|
configError={codexConfigError}
|
||||||
@@ -1488,6 +1491,7 @@ export function ProviderForm({
|
|||||||
onCommonConfigSnippetChange={
|
onCommonConfigSnippetChange={
|
||||||
handleGeminiCommonConfigSnippetChange
|
handleGeminiCommonConfigSnippetChange
|
||||||
}
|
}
|
||||||
|
onCommonConfigErrorClear={clearGeminiCommonConfigError}
|
||||||
commonConfigError={geminiCommonConfigError}
|
commonConfigError={geminiCommonConfigError}
|
||||||
envError={envError}
|
envError={envError}
|
||||||
configError={geminiConfigError}
|
configError={geminiConfigError}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { useState, useEffect, useCallback, useRef } from "react";
|
import { useState, useEffect, useCallback, useRef } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { parse as parseToml } from "smol-toml";
|
||||||
import {
|
import {
|
||||||
updateTomlCommonConfigSnippet,
|
updateTomlCommonConfigSnippet,
|
||||||
hasTomlCommonConfigSnippet,
|
hasTomlCommonConfigSnippet,
|
||||||
} from "@/utils/providerConfigUtils";
|
} from "@/utils/providerConfigUtils";
|
||||||
import { configApi } from "@/lib/api";
|
import { configApi } from "@/lib/api";
|
||||||
|
import { normalizeTomlText } from "@/utils/textNormalization";
|
||||||
|
|
||||||
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
const LEGACY_STORAGE_KEY = "cc-switch:codex-common-config-snippet";
|
||||||
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
|
const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config
|
||||||
@@ -53,6 +55,30 @@ export function useCodexCommonConfig({
|
|||||||
hasInitializedEditMode.current = false;
|
hasInitializedEditMode.current = false;
|
||||||
}, [selectedPresetId, initialEnabled]);
|
}, [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 迁移
|
// 初始化:从 config.json 加载,支持从 localStorage 迁移
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let mounted = true;
|
let mounted = true;
|
||||||
@@ -107,36 +133,59 @@ export function useCodexCommonConfig({
|
|||||||
|
|
||||||
// 初始化时检查通用配置片段(编辑模式)
|
// 初始化时检查通用配置片段(编辑模式)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData?.settingsConfig && !isLoading) {
|
if (
|
||||||
const config =
|
!initialData?.settingsConfig ||
|
||||||
typeof initialData.settingsConfig.config === "string"
|
isLoading ||
|
||||||
? initialData.settingsConfig.config
|
hasInitializedEditMode.current
|
||||||
: "";
|
) {
|
||||||
const inferredHasCommon = hasTomlCommonConfigSnippet(
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
codexConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
@@ -144,49 +193,75 @@ export function useCodexCommonConfig({
|
|||||||
initialEnabled,
|
initialEnabled,
|
||||||
isLoading,
|
isLoading,
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
|
parseCommonConfigSnippet,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 仅新建模式、加载完成、尚未初始化过
|
if (initialData || isLoading || hasInitializedNewMode.current) {
|
||||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
return;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
initialData,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
isLoading,
|
isLoading,
|
||||||
codexConfig,
|
codexConfig,
|
||||||
onConfigChange,
|
onConfigChange,
|
||||||
|
parseCommonConfigSnippet,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// 处理通用配置开关
|
// 处理通用配置开关
|
||||||
const handleCommonConfigToggle = useCallback(
|
const handleCommonConfigToggle = useCallback(
|
||||||
(checked: boolean) => {
|
(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 } =
|
const { updatedConfig, error: snippetError } =
|
||||||
updateTomlCommonConfigSnippet(
|
updateTomlCommonConfigSnippet(
|
||||||
codexConfig,
|
codexConfig,
|
||||||
@@ -210,18 +285,39 @@ export function useCodexCommonConfig({
|
|||||||
isUpdatingFromCommonConfig.current = false;
|
isUpdatingFromCommonConfig.current = false;
|
||||||
}, 0);
|
}, 0);
|
||||||
},
|
},
|
||||||
[codexConfig, commonConfigSnippet, onConfigChange],
|
[codexConfig, commonConfigSnippet, onConfigChange, parseCommonConfigSnippet, t],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 处理通用配置片段变化
|
// 处理通用配置片段变化
|
||||||
const handleCommonConfigSnippetChange = useCallback(
|
const handleCommonConfigSnippetChange = useCallback(
|
||||||
(value: string) => {
|
(value: string): boolean => {
|
||||||
const previousSnippet = commonConfigSnippet;
|
const previousSnippet = commonConfigSnippet;
|
||||||
setCommonConfigSnippetState(value);
|
|
||||||
|
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
setCommonConfigError("");
|
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
|
configApi
|
||||||
.setCommonConfigSnippet("codex", "")
|
.setCommonConfigSnippet("codex", "")
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
@@ -230,51 +326,42 @@ export function useCodexCommonConfig({
|
|||||||
t("codexConfig.saveFailed", { error: String(error) }),
|
t("codexConfig.saveFailed", { error: String(error) }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (useCommonConfig) {
|
const parsedNextSnippet = parseCommonConfigSnippet(value);
|
||||||
const { updatedConfig } = updateTomlCommonConfigSnippet(
|
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,
|
codexConfig,
|
||||||
previousSnippet,
|
previousSnippet,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
onConfigChange(updatedConfig);
|
if (removeResult.error) {
|
||||||
setUseCommonConfig(false);
|
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(
|
const addResult = updateTomlCommonConfigSnippet(
|
||||||
removeResult.updatedConfig,
|
nextConfig,
|
||||||
value,
|
value,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (addResult.error) {
|
if (addResult.error) {
|
||||||
setCommonConfigError(addResult.error);
|
setCommonConfigError(addResult.error);
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 标记正在通过通用配置更新,避免触发状态检查
|
// 标记正在通过通用配置更新,避免触发状态检查
|
||||||
@@ -285,8 +372,28 @@ export function useCodexCommonConfig({
|
|||||||
isUpdatingFromCommonConfig.current = false;
|
isUpdatingFromCommonConfig.current = false;
|
||||||
}, 0);
|
}, 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) {
|
if (isUpdatingFromCommonConfig.current || isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const parsedSnippet = parseCommonConfigSnippet(commonConfigSnippet);
|
||||||
|
if (parsedSnippet.error) {
|
||||||
|
setUseCommonConfig(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const hasCommon = hasTomlCommonConfigSnippet(
|
const hasCommon = hasTomlCommonConfigSnippet(
|
||||||
codexConfig,
|
codexConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
);
|
);
|
||||||
setUseCommonConfig(hasCommon);
|
setUseCommonConfig(hasCommon);
|
||||||
}, [codexConfig, commonConfigSnippet, isLoading]);
|
}, [codexConfig, commonConfigSnippet, isLoading, parseCommonConfigSnippet]);
|
||||||
|
|
||||||
// 从编辑器当前内容提取通用配置片段
|
// 从编辑器当前内容提取通用配置片段
|
||||||
const handleExtract = useCallback(async () => {
|
const handleExtract = useCallback(async () => {
|
||||||
@@ -333,6 +445,10 @@ export function useCodexCommonConfig({
|
|||||||
}
|
}
|
||||||
}, [codexConfig, t]);
|
}, [codexConfig, t]);
|
||||||
|
|
||||||
|
const clearCommonConfigError = useCallback(() => {
|
||||||
|
setCommonConfigError("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useCommonConfig,
|
useCommonConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
@@ -342,5 +458,6 @@ export function useCodexCommonConfig({
|
|||||||
handleCommonConfigToggle,
|
handleCommonConfigToggle,
|
||||||
handleCommonConfigSnippetChange,
|
handleCommonConfigSnippetChange,
|
||||||
handleExtract,
|
handleExtract,
|
||||||
|
clearCommonConfigError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,43 +216,55 @@ export function useGeminiCommonConfig({
|
|||||||
|
|
||||||
// 初始化时检查通用配置片段(编辑模式)
|
// 初始化时检查通用配置片段(编辑模式)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialData?.settingsConfig && !isLoading) {
|
if (
|
||||||
try {
|
!initialData?.settingsConfig ||
|
||||||
const env =
|
isLoading ||
|
||||||
isPlainObject(initialData.settingsConfig.env) &&
|
hasInitializedEditMode.current
|
||||||
Object.keys(initialData.settingsConfig.env).length > 0
|
) {
|
||||||
? (initialData.settingsConfig.env as Record<string, string>)
|
return;
|
||||||
: {};
|
}
|
||||||
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 (
|
hasInitializedEditMode.current = true;
|
||||||
hasCommon &&
|
|
||||||
!inferredHasCommon &&
|
|
||||||
!hasInitializedEditMode.current
|
|
||||||
) {
|
|
||||||
hasInitializedEditMode.current = true;
|
|
||||||
const currentEnv = envStringToObj(envValue);
|
|
||||||
const merged = applySnippetToEnv(currentEnv, parsed.env);
|
|
||||||
const nextEnvString = envObjToString(merged);
|
|
||||||
|
|
||||||
isUpdatingFromCommonConfig.current = true;
|
try {
|
||||||
onEnvChange(nextEnvString);
|
const env =
|
||||||
setTimeout(() => {
|
isPlainObject(initialData.settingsConfig.env) &&
|
||||||
isUpdatingFromCommonConfig.current = false;
|
Object.keys(initialData.settingsConfig.env).length > 0
|
||||||
}, 0);
|
? (initialData.settingsConfig.env as Record<string, string>)
|
||||||
} else {
|
: {};
|
||||||
hasInitializedEditMode.current = true;
|
const parsed = parseSnippetEnv(commonConfigSnippet);
|
||||||
|
if (parsed.error) {
|
||||||
|
if (commonConfigSnippet.trim()) {
|
||||||
|
setCommonConfigError(parsed.error);
|
||||||
}
|
}
|
||||||
} catch {
|
setUseCommonConfig(false);
|
||||||
// ignore parse error
|
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,
|
applySnippetToEnv,
|
||||||
@@ -270,26 +282,34 @@ export function useGeminiCommonConfig({
|
|||||||
|
|
||||||
// 新建模式:如果通用配置片段存在且有效,默认启用
|
// 新建模式:如果通用配置片段存在且有效,默认启用
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 仅新建模式、加载完成、尚未初始化过
|
if (initialData || isLoading || hasInitializedNewMode.current) {
|
||||||
if (!initialData && !isLoading && !hasInitializedNewMode.current) {
|
return;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
initialData,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -346,13 +366,29 @@ export function useGeminiCommonConfig({
|
|||||||
|
|
||||||
// 处理通用配置片段变化
|
// 处理通用配置片段变化
|
||||||
const handleCommonConfigSnippetChange = useCallback(
|
const handleCommonConfigSnippetChange = useCallback(
|
||||||
(value: string) => {
|
(value: string): boolean => {
|
||||||
const previousSnippet = commonConfigSnippet;
|
const previousSnippet = commonConfigSnippet;
|
||||||
setCommonConfigSnippetState(value);
|
|
||||||
|
|
||||||
if (!value.trim()) {
|
if (!value.trim()) {
|
||||||
setCommonConfigError("");
|
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
|
configApi
|
||||||
.setCommonConfigSnippet("gemini", "")
|
.setCommonConfigSnippet("gemini", "")
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
@@ -361,36 +397,16 @@ export function useGeminiCommonConfig({
|
|||||||
t("geminiConfig.saveFailed", { error: String(error) }),
|
t("geminiConfig.saveFailed", { error: String(error) }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 校验 JSON 格式
|
// 校验 JSON 格式
|
||||||
const parsed = parseSnippetEnv(value);
|
const parsed = parseSnippetEnv(value);
|
||||||
if (parsed.error) {
|
if (parsed.error) {
|
||||||
setCommonConfigError(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) {
|
if (useCommonConfig) {
|
||||||
const prevParsed = parseSnippetEnv(previousSnippet);
|
const prevParsed = parseSnippetEnv(previousSnippet);
|
||||||
@@ -413,6 +429,19 @@ export function useGeminiCommonConfig({
|
|||||||
isUpdatingFromCommonConfig.current = false;
|
isUpdatingFromCommonConfig.current = false;
|
||||||
}, 0);
|
}, 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,
|
applySnippetToEnv,
|
||||||
@@ -487,6 +516,10 @@ export function useGeminiCommonConfig({
|
|||||||
}
|
}
|
||||||
}, [envStringToObj, envValue, parseSnippetEnv, t]);
|
}, [envStringToObj, envValue, parseSnippetEnv, t]);
|
||||||
|
|
||||||
|
const clearCommonConfigError = useCallback(() => {
|
||||||
|
setCommonConfigError("");
|
||||||
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
useCommonConfig,
|
useCommonConfig,
|
||||||
commonConfigSnippet,
|
commonConfigSnippet,
|
||||||
@@ -496,5 +529,6 @@ export function useGeminiCommonConfig({
|
|||||||
handleCommonConfigToggle,
|
handleCommonConfigToggle,
|
||||||
handleCommonConfigSnippetChange,
|
handleCommonConfigSnippetChange,
|
||||||
handleExtract,
|
handleExtract,
|
||||||
|
clearCommonConfigError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import CodexConfigEditor from "@/components/providers/forms/CodexConfigEditor";
|
||||||
|
import GeminiConfigEditor from "@/components/providers/forms/GeminiConfigEditor";
|
||||||
|
|
||||||
|
vi.mock("@/components/common/FullScreenPanel", () => ({
|
||||||
|
FullScreenPanel: ({
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
children,
|
||||||
|
footer,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
onClose: () => void;
|
||||||
|
children: ReactNode;
|
||||||
|
footer?: ReactNode;
|
||||||
|
}) =>
|
||||||
|
isOpen ? (
|
||||||
|
<div data-testid="common-config-panel">
|
||||||
|
<button type="button" onClick={onClose}>
|
||||||
|
panel-close
|
||||||
|
</button>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<div>{children}</div>
|
||||||
|
<div>{footer}</div>
|
||||||
|
</div>
|
||||||
|
) : null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/JsonEditor", () => ({
|
||||||
|
default: ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) => (
|
||||||
|
<textarea
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => onChange(event.target.value)}
|
||||||
|
aria-label="mock-editor"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("Common config modals", () => {
|
||||||
|
it("keeps the Codex common config modal closed after user closes it with an error present", async () => {
|
||||||
|
render(
|
||||||
|
<CodexConfigEditor
|
||||||
|
authValue="{}"
|
||||||
|
configValue=""
|
||||||
|
onAuthChange={() => {}}
|
||||||
|
onConfigChange={() => {}}
|
||||||
|
useCommonConfig={false}
|
||||||
|
onCommonConfigToggle={() => {}}
|
||||||
|
commonConfigSnippet={`base_url = "https://example.com"`}
|
||||||
|
onCommonConfigSnippetChange={() => false}
|
||||||
|
onCommonConfigErrorClear={() => {}}
|
||||||
|
commonConfigError="Invalid TOML"
|
||||||
|
authError=""
|
||||||
|
configError=""
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("common-config-panel")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", { name: /codexConfig.editCommonConfig|编辑通用配置/ }),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("common-config-panel")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.cancel" }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("common-config-panel"),
|
||||||
|
).not.toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the Gemini common config modal closed after user closes it with an error present", async () => {
|
||||||
|
render(
|
||||||
|
<GeminiConfigEditor
|
||||||
|
envValue="{}"
|
||||||
|
configValue="{}"
|
||||||
|
onEnvChange={() => {}}
|
||||||
|
onConfigChange={() => {}}
|
||||||
|
useCommonConfig={false}
|
||||||
|
onCommonConfigToggle={() => {}}
|
||||||
|
commonConfigSnippet={`{"GEMINI_MODEL":"gemini-2.5-pro"}`}
|
||||||
|
onCommonConfigSnippetChange={() => false}
|
||||||
|
onCommonConfigErrorClear={() => {}}
|
||||||
|
commonConfigError="Invalid JSON"
|
||||||
|
envError=""
|
||||||
|
configError=""
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByTestId("common-config-panel")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
screen.getByRole("button", {
|
||||||
|
name: /geminiConfig.editCommonConfig|编辑通用配置/,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("common-config-panel")).toBeInTheDocument();
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "common.cancel" }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("common-config-panel"),
|
||||||
|
).not.toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { useCodexCommonConfig } from "@/components/providers/forms/hooks/useCodexCommonConfig";
|
||||||
|
import { useGeminiCommonConfig } from "@/components/providers/forms/hooks/useGeminiCommonConfig";
|
||||||
|
|
||||||
|
const getCommonConfigSnippetMock = vi.fn();
|
||||||
|
const setCommonConfigSnippetMock = vi.fn();
|
||||||
|
const extractCommonConfigSnippetMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("@/lib/api", () => ({
|
||||||
|
configApi: {
|
||||||
|
getCommonConfigSnippet: (...args: unknown[]) =>
|
||||||
|
getCommonConfigSnippetMock(...args),
|
||||||
|
setCommonConfigSnippet: (...args: unknown[]) =>
|
||||||
|
setCommonConfigSnippetMock(...args),
|
||||||
|
extractCommonConfigSnippet: (...args: unknown[]) =>
|
||||||
|
extractCommonConfigSnippetMock(...args),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("common config snippet saving", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getCommonConfigSnippetMock.mockResolvedValue("");
|
||||||
|
setCommonConfigSnippetMock.mockResolvedValue(undefined);
|
||||||
|
extractCommonConfigSnippetMock.mockResolvedValue("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not persist an invalid Codex common config snippet", async () => {
|
||||||
|
const onConfigChange = vi.fn();
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useCodexCommonConfig({
|
||||||
|
codexConfig: "model = \"gpt-5\"",
|
||||||
|
onConfigChange,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||||
|
|
||||||
|
let saved = false;
|
||||||
|
act(() => {
|
||||||
|
saved = result.current.handleCommonConfigSnippetChange(
|
||||||
|
"base_url = https://bad.example/v1",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saved).toBe(false);
|
||||||
|
expect(setCommonConfigSnippetMock).not.toHaveBeenCalled();
|
||||||
|
expect(onConfigChange).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.commonConfigError).toContain("invalid value");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not persist an invalid Gemini common config snippet", async () => {
|
||||||
|
const onEnvChange = vi.fn();
|
||||||
|
const { result } = renderHook(() =>
|
||||||
|
useGeminiCommonConfig({
|
||||||
|
envValue: "",
|
||||||
|
onEnvChange,
|
||||||
|
envStringToObj: () => ({}),
|
||||||
|
envObjToString: () => "",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(result.current.isLoading).toBe(false));
|
||||||
|
|
||||||
|
let saved = false;
|
||||||
|
act(() => {
|
||||||
|
saved = result.current.handleCommonConfigSnippetChange(
|
||||||
|
JSON.stringify({ GEMINI_MODEL: 123 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(saved).toBe(false);
|
||||||
|
expect(setCommonConfigSnippetMock).not.toHaveBeenCalled();
|
||||||
|
expect(onEnvChange).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.commonConfigError).toBe(
|
||||||
|
"geminiConfig.commonConfigInvalidValues",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user