diff --git a/src/components/ProviderForm/CodexConfigEditor.tsx b/src/components/ProviderForm/CodexConfigEditor.tsx index 17f6201..eae23be 100644 --- a/src/components/ProviderForm/CodexConfigEditor.tsx +++ b/src/components/ProviderForm/CodexConfigEditor.tsx @@ -33,15 +33,17 @@ interface CodexConfigEditorProps { authError: string; - isCustomMode?: boolean; // 新增:是否为自定义模式 + configError: string; // config.toml 错误提示 - onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调 + isCustomMode?: boolean; // 是否为自定义模式 - isTemplateModalOpen?: boolean; // 新增:模态框状态 + onWebsiteUrlChange?: (url: string) => void; // 更新网址回调 - setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态 + isTemplateModalOpen?: boolean; // 模态框状态 - onNameChange?: (name: string) => void; // 新增:更新供应商名称回调 + setIsTemplateModalOpen?: (open: boolean) => void; // 设置模态框状态 + + onNameChange?: (name: string) => void; // 更新供应商名称回调 } const CodexConfigEditor: React.FC = ({ @@ -67,6 +69,8 @@ const CodexConfigEditor: React.FC = ({ authError, + configError, + onWebsiteUrlChange, onNameChange, @@ -324,6 +328,10 @@ const CodexConfigEditor: React.FC = ({ data-enable-grammarly="false" /> + {configError && ( +

{configError}

+ )} +

{t("codexConfig.configTomlHint")}

diff --git a/src/components/providers/forms/ProviderForm.tsx b/src/components/providers/forms/ProviderForm.tsx index 89aa87d..c02d7a2 100644 --- a/src/components/providers/forms/ProviderForm.tsx +++ b/src/components/providers/forms/ProviderForm.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useCallback } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; @@ -32,6 +32,7 @@ import { useCommonConfigSnippet, useCodexCommonConfig, useSpeedTestEndpoints, + useCodexTomlValidation, } from "./hooks"; const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); @@ -149,10 +150,19 @@ export function ProviderForm({ setCodexAuth, handleCodexApiKeyChange, handleCodexBaseUrlChange, - handleCodexConfigChange, + handleCodexConfigChange: originalHandleCodexConfigChange, resetCodexConfig, } = useCodexConfigState({ initialData }); + // 使用 Codex TOML 校验 hook (仅 Codex 模式) + const { configError: codexConfigError, debouncedValidate } = useCodexTomlValidation(); + + // 包装 handleCodexConfigChange,添加实时校验 + const handleCodexConfigChange = useCallback((value: string) => { + originalHandleCodexConfigChange(value); + debouncedValidate(value); + }, [originalHandleCodexConfigChange, debouncedValidate]); + const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] = @@ -511,6 +521,7 @@ export function ProviderForm({ onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange} commonConfigError={codexCommonConfigError} authError={codexAuthError} + configError={codexConfigError} isCustomMode={selectedPresetId === "custom"} onWebsiteUrlChange={(url) => form.setValue("websiteUrl", url)} onNameChange={(name) => form.setValue("name", name)} diff --git a/src/components/providers/forms/hooks/index.ts b/src/components/providers/forms/hooks/index.ts index 09ea653..f1024ca 100644 --- a/src/components/providers/forms/hooks/index.ts +++ b/src/components/providers/forms/hooks/index.ts @@ -10,3 +10,4 @@ export { useTemplateValues } from "./useTemplateValues"; export { useCommonConfigSnippet } from "./useCommonConfigSnippet"; export { useCodexCommonConfig } from "./useCodexCommonConfig"; export { useSpeedTestEndpoints } from "./useSpeedTestEndpoints"; +export { useCodexTomlValidation } from "./useCodexTomlValidation"; diff --git a/src/components/providers/forms/hooks/useCodexTomlValidation.ts b/src/components/providers/forms/hooks/useCodexTomlValidation.ts new file mode 100644 index 0000000..49289d4 --- /dev/null +++ b/src/components/providers/forms/hooks/useCodexTomlValidation.ts @@ -0,0 +1,75 @@ +import { useState, useCallback, useEffect, useRef } from 'react'; +import TOML from 'smol-toml'; + +/** + * Codex config.toml 格式校验 Hook + * 使用 smol-toml 进行实时 TOML 语法校验(带 debounce) + */ +export function useCodexTomlValidation() { + const [configError, setConfigError] = useState(''); + const debounceTimerRef = useRef(null); + + /** + * 校验 TOML 格式 + * @param tomlText - 待校验的 TOML 文本 + * @returns 是否校验通过 + */ + const validateToml = useCallback((tomlText: string): boolean => { + // 空字符串视为合法(允许为空) + if (!tomlText.trim()) { + setConfigError(''); + return true; + } + + try { + TOML.parse(tomlText); + setConfigError(''); + return true; + } catch (error) { + const errorMessage = error instanceof Error + ? error.message + : 'TOML 格式错误'; + setConfigError(errorMessage); + return false; + } + }, []); + + /** + * 带 debounce 的校验函数(500ms 延迟) + * @param tomlText - 待校验的 TOML 文本 + */ + const debouncedValidate = useCallback((tomlText: string) => { + // 清除之前的定时器 + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // 设置新的定时器 + debounceTimerRef.current = setTimeout(() => { + validateToml(tomlText); + }, 500); + }, [validateToml]); + + /** + * 清空错误信息 + */ + const clearError = useCallback(() => { + setConfigError(''); + }, []); + + // 清理定时器 + useEffect(() => { + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + }; + }, []); + + return { + configError, + validateToml, + debouncedValidate, + clearError, + }; +}