diff --git a/src/components/mcp/McpFormModal.tsx b/src/components/mcp/McpFormModal.tsx index eb80282..81871f5 100644 --- a/src/components/mcp/McpFormModal.tsx +++ b/src/components/mcp/McpFormModal.tsx @@ -32,6 +32,7 @@ import { extractIdFromToml, mcpServerToToml, } from "@/utils/tomlUtils"; +import { normalizeTomlText } from "@/utils/textNormalization"; import { useMcpValidation } from "./useMcpValidation"; interface McpFormModalProps { @@ -228,19 +229,21 @@ const McpFormModal: React.FC = ({ }; const handleConfigChange = (value: string) => { - setFormConfig(value); + // 若为 TOML 模式,先做引号归一化,避免中文输入法导致的格式错误 + const nextValue = useToml ? normalizeTomlText(value) : value; + setFormConfig(nextValue); if (useToml) { // TOML validation (use hook's complete validation) - const err = validateTomlConfig(value); + const err = validateTomlConfig(nextValue); if (err) { setConfigError(err); return; } // Try to extract ID (if user hasn't filled it yet) - if (value.trim() && !formId.trim()) { - const extractedId = extractIdFromToml(value); + if (nextValue.trim() && !formId.trim()) { + const extractedId = extractIdFromToml(nextValue); if (extractedId) { setFormId(extractedId); } diff --git a/src/components/providers/forms/hooks/useCodexConfigState.ts b/src/components/providers/forms/hooks/useCodexConfigState.ts index 056ddbb..5f2823e 100644 --- a/src/components/providers/forms/hooks/useCodexConfigState.ts +++ b/src/components/providers/forms/hooks/useCodexConfigState.ts @@ -3,6 +3,7 @@ import { extractCodexBaseUrl, setCodexBaseUrl as setCodexBaseUrlInConfig, } from "@/utils/providerConfigUtils"; +import { normalizeTomlText } from "@/utils/textNormalization"; interface UseCodexConfigStateProps { initialData?: { @@ -159,10 +160,12 @@ export function useCodexConfigState({ initialData }: UseCodexConfigStateProps) { // 处理 config 变化(同步 Base URL) const handleCodexConfigChange = useCallback( (value: string) => { - setCodexConfig(value); + // 归一化中文/全角/弯引号,避免 TOML 解析报错 + const normalized = normalizeTomlText(value); + setCodexConfig(normalized); if (!isUpdatingCodexBaseUrlRef.current) { - const extracted = extractCodexBaseUrl(value) || ""; + const extracted = extractCodexBaseUrl(normalized) || ""; if (extracted !== codexBaseUrl) { setCodexBaseUrl(extracted); } diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx index cb10801..e7041b1 100644 --- a/src/components/ui/textarea.tsx +++ b/src/components/ui/textarea.tsx @@ -11,6 +11,10 @@ const Textarea = React.forwardRef( "flex min-h-[80px] w-full rounded-md border border-border-default bg-background px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 disabled:cursor-not-allowed disabled:opacity-50", className, )} + autoComplete="off" + autoCorrect="off" + autoCapitalize="none" + spellCheck={false} ref={ref} {...props} /> diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index db22342..bf6d24b 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -1,6 +1,7 @@ // 供应商配置处理工具函数 import type { TemplateValueConfig } from "../config/claudeProviderPresets"; +import { normalizeQuotes } from "@/utils/textNormalization"; const isPlainObject = (value: unknown): value is Record => { return Object.prototype.toString.call(value) === "[object Object]"; @@ -357,7 +358,9 @@ export const extractCodexBaseUrl = ( configText: string | undefined | null, ): string | undefined => { try { - const text = typeof configText === "string" ? configText : ""; + const raw = typeof configText === "string" ? configText : ""; + // 归一化中文/全角引号,避免正则提取失败 + const text = normalizeQuotes(raw); if (!text) return undefined; const m = text.match(/base_url\s*=\s*(['"])([^'\"]+)\1/); return m && m[2] ? m[2] : undefined; @@ -390,16 +393,20 @@ export const setCodexBaseUrl = ( if (!trimmed) { return configText; } + // 归一化原文本中的引号(既能匹配,也能输出稳定格式) + const normalizedText = normalizeQuotes(configText); const normalizedUrl = trimmed.replace(/\s+/g, "").replace(/\/+$/, ""); const replacementLine = `base_url = "${normalizedUrl}"`; const pattern = /base_url\s*=\s*(["'])([^"']+)\1/; - if (pattern.test(configText)) { - return configText.replace(pattern, replacementLine); + if (pattern.test(normalizedText)) { + return normalizedText.replace(pattern, replacementLine); } const prefix = - configText && !configText.endsWith("\n") ? `${configText}\n` : configText; + normalizedText && !normalizedText.endsWith("\n") + ? `${normalizedText}\n` + : normalizedText; return `${prefix}${replacementLine}\n`; }; diff --git a/src/utils/textNormalization.ts b/src/utils/textNormalization.ts new file mode 100644 index 0000000..218338f --- /dev/null +++ b/src/utils/textNormalization.ts @@ -0,0 +1,20 @@ +/** + * 将常见的中文/全角/弯引号统一为 ASCII 引号,以避免 TOML 解析失败。 + * - 双引号:” “ „ ‟ " → " + * - 单引号:’ ‘ ' → ' + * 保守起见,不替换书名号/角引号(《》、「」等),避免误伤内容语义。 + */ +export const normalizeQuotes = (text: string): string => { + if (!text) return text; + return text + // 双引号族 → " + .replace(/[“”„‟"]/g, '"') + // 单引号族 → ' + .replace(/[‘’']/g, "'"); +}; + +/** + * 专用于 TOML 文本的归一化;目前等同于 normalizeQuotes,后续可扩展(如空白、行尾等)。 + */ +export const normalizeTomlText = (text: string): string => normalizeQuotes(text); + diff --git a/src/utils/tomlUtils.ts b/src/utils/tomlUtils.ts index 47a711b..90e87b5 100644 --- a/src/utils/tomlUtils.ts +++ b/src/utils/tomlUtils.ts @@ -1,4 +1,5 @@ import { parse as parseToml, stringify as stringifyToml } from "smol-toml"; +import { normalizeTomlText } from "@/utils/textNormalization"; import { McpServerSpec } from "../types"; /** @@ -9,7 +10,8 @@ import { McpServerSpec } from "../types"; export const validateToml = (text: string): string => { if (!text.trim()) return ""; try { - const parsed = parseToml(text); + const normalized = normalizeTomlText(text); + const parsed = parseToml(normalized); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { return "mustBeObject"; } @@ -52,7 +54,7 @@ export const tomlToMcpServer = (tomlText: string): McpServerSpec => { throw new Error("TOML 内容不能为空"); } - const parsed = parseToml(tomlText); + const parsed = parseToml(normalizeTomlText(tomlText)); // 情况 1: 直接是服务器配置(包含 type/command/url 等字段) if ( @@ -185,7 +187,7 @@ function normalizeServerConfig(config: any): McpServerSpec { */ export const extractIdFromToml = (tomlText: string): string => { try { - const parsed = parseToml(tomlText); + const parsed = parseToml(normalizeTomlText(tomlText)); // 尝试从 [mcp.servers.] 或 [mcp_servers.] 中提取 ID if (parsed.mcp && typeof parsed.mcp === "object") {