diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 687b29c..dd4f0aa 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -10,6 +10,34 @@ use crate::config::{get_claude_settings_path, ConfigStatus}; use crate::provider::Provider; use crate::store::AppState; +fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), String> { + match app_type { + AppType::Claude => { + if !provider.settings_config.is_object() { + return Err("Claude 配置必须是 JSON 对象".to_string()); + } + } + AppType::Codex => { + let settings = provider + .settings_config + .as_object() + .ok_or_else(|| "Codex 配置必须是 JSON 对象".to_string())?; + let auth = settings + .get("auth") + .ok_or_else(|| "Codex 配置缺少 auth 字段".to_string())?; + if !auth.is_object() { + return Err("Codex auth 配置必须是 JSON 对象".to_string()); + } + if let Some(config_value) = settings.get("config") { + if !(config_value.is_string() || config_value.is_null()) { + return Err("Codex config 字段必须是字符串".to_string()); + } + } + } + } + Ok(()) +} + /// 获取所有供应商 #[tauri::command] pub async fn get_providers( @@ -74,6 +102,8 @@ pub async fn add_provider( .or_else(|| appType.as_deref().map(|s| s.into())) .unwrap_or(AppType::Claude); + validate_provider_settings(&app_type, &provider)?; + // 读取当前是否是激活供应商(短锁) let is_current = { let config = state @@ -139,6 +169,8 @@ pub async fn update_provider( .or_else(|| appType.as_deref().map(|s| s.into())) .unwrap_or(AppType::Claude); + validate_provider_settings(&app_type, &provider)?; + // 读取校验 & 是否当前(短锁) let (exists, is_current) = { let config = state diff --git a/src/components/JsonEditor.tsx b/src/components/JsonEditor.tsx index c0d6d6a..bdd1c16 100644 --- a/src/components/JsonEditor.tsx +++ b/src/components/JsonEditor.tsx @@ -1,9 +1,10 @@ -import React, { useRef, useEffect } from "react"; +import React, { useRef, useEffect, useMemo } from "react"; import { EditorView, basicSetup } from "codemirror"; import { json } from "@codemirror/lang-json"; import { oneDark } from "@codemirror/theme-one-dark"; import { EditorState } from "@codemirror/state"; import { placeholder } from "@codemirror/view"; +import { linter, Diagnostic } from "@codemirror/lint"; interface JsonEditorProps { value: string; @@ -11,6 +12,7 @@ interface JsonEditorProps { placeholder?: string; darkMode?: boolean; rows?: number; + showValidation?: boolean; } const JsonEditor: React.FC = ({ @@ -19,10 +21,50 @@ const JsonEditor: React.FC = ({ placeholder: placeholderText = "", darkMode = false, rows = 12, + showValidation = true, }) => { const editorRef = useRef(null); const viewRef = useRef(null); + // JSON linter 函数 + const jsonLinter = useMemo( + () => + linter((view) => { + const diagnostics: Diagnostic[] = []; + if (!showValidation) return diagnostics; + + const doc = view.state.doc.toString(); + if (!doc.trim()) return diagnostics; + + try { + const parsed = JSON.parse(doc); + // 检查是否是JSON对象 + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + // 格式正确 + } else { + diagnostics.push({ + from: 0, + to: doc.length, + severity: "error", + message: "配置必须是JSON对象,不能是数组或其他类型", + }); + } + } catch (e) { + // 简单处理JSON解析错误 + const message = e instanceof SyntaxError ? e.message : "JSON格式错误"; + diagnostics.push({ + from: 0, + to: doc.length, + severity: "error", + message, + }); + } + + return diagnostics; + }), + [showValidation] + ); + useEffect(() => { if (!editorRef.current) return; @@ -43,6 +85,7 @@ const JsonEditor: React.FC = ({ json(), placeholder(placeholderText || ""), sizingTheme, + jsonLinter, EditorView.updateListener.of((update) => { if (update.docChanged) { const newValue = update.state.doc.toString(); @@ -75,7 +118,7 @@ const JsonEditor: React.FC = ({ view.destroy(); viewRef.current = null; }; - }, [darkMode, rows]); // 依赖项中不包含 onChange 和 placeholder,避免不必要的重建 + }, [darkMode, rows, jsonLinter]); // 依赖项中不包含 onChange 和 placeholder,避免不必要的重建 // 当 value 从外部改变时更新编辑器内容 useEffect(() => { diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx index 67620cf..ed04869 100644 --- a/src/components/ProviderForm.tsx +++ b/src/components/ProviderForm.tsx @@ -9,6 +9,7 @@ import { setApiKeyInConfig, updateTomlCommonConfigSnippet, hasTomlCommonConfigSnippet, + validateJsonConfig, } from "../utils/providerConfigUtils"; import { providerPresets } from "../config/providerPresets"; import { codexProviderPresets } from "../config/codexProviderPresets"; @@ -77,6 +78,7 @@ const ProviderForm: React.FC = ({ const setCodexAuth = (value: string) => { setCodexAuthState(value); + setCodexAuthError(validateCodexAuth(value)); }; const setCodexConfig = (value: string) => { @@ -123,6 +125,7 @@ const ProviderForm: React.FC = ({ return DEFAULT_COMMON_CONFIG_SNIPPET; }); const [commonConfigError, setCommonConfigError] = useState(""); + const [settingsConfigError, setSettingsConfigError] = useState(""); // 用于跟踪是否正在通过通用配置更新 const isUpdatingFromCommonConfig = useRef(false); @@ -151,12 +154,40 @@ const ProviderForm: React.FC = ({ showPresets ? -1 : null, ); const [apiKey, setApiKey] = useState(""); + const [codexAuthError, setCodexAuthError] = useState(""); // Kimi 模型选择状态 const [kimiAnthropicModel, setKimiAnthropicModel] = useState(""); const [kimiAnthropicSmallFastModel, setKimiAnthropicSmallFastModel] = useState(""); + const validateSettingsConfig = (value: string): string => { + return validateJsonConfig(value, "配置内容"); + }; + + const validateCodexAuth = (value: string): string => { + if (!value.trim()) { + return ""; + } + try { + const parsed = JSON.parse(value); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return "auth.json 必须是 JSON 对象"; + } + return ""; + } catch { + return "auth.json 格式错误,请检查JSON语法"; + } + }; + + const updateSettingsConfigValue = (value: string) => { + setFormData((prev) => ({ + ...prev, + settingsConfig: value, + })); + setSettingsConfigError(validateSettingsConfig(value)); + }; + // 初始化自定义模式的默认配置 useEffect(() => { if ( @@ -175,11 +206,9 @@ const ProviderForm: React.FC = ({ // ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name" }, }; + const templateString = JSON.stringify(customTemplate, null, 2); - setFormData((prev) => ({ - ...prev, - settingsConfig: JSON.stringify(customTemplate, null, 2), - })); + updateSettingsConfigValue(templateString); setApiKey(""); } }, []); // 只在组件挂载时执行一次 @@ -194,6 +223,7 @@ const ProviderForm: React.FC = ({ commonConfigSnippet, ); setUseCommonConfig(hasCommon); + setSettingsConfigError(validateSettingsConfig(configString)); // 初始化模型配置(编辑模式) if ( @@ -279,6 +309,12 @@ const ProviderForm: React.FC = ({ let settingsConfig: Record; if (isCodex) { + const currentAuthError = validateCodexAuth(codexAuth); + setCodexAuthError(currentAuthError); + if (currentAuthError) { + setError(currentAuthError); + return; + } // Codex: 仅要求 auth.json 必填;config.toml 可为空 if (!codexAuth.trim()) { setError("请填写 auth.json 配置"); @@ -313,6 +349,14 @@ const ProviderForm: React.FC = ({ return; } } else { + const currentSettingsError = validateSettingsConfig( + formData.settingsConfig, + ); + setSettingsConfigError(currentSettingsError); + if (currentSettingsError) { + setError(currentSettingsError); + return; + } // Claude: 原有逻辑 if (!formData.settingsConfig.trim()) { setError("请填写配置内容"); @@ -353,10 +397,7 @@ const ProviderForm: React.FC = ({ setApiKey(parsedKey); // 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容 - setFormData((prev) => ({ - ...prev, - [name]: value, - })); + updateSettingsConfigValue(value); } else { setFormData({ ...formData, @@ -375,6 +416,9 @@ const ProviderForm: React.FC = ({ if (snippetError) { setCommonConfigError(snippetError); + if (snippetError.includes("配置 JSON 解析失败")) { + setSettingsConfigError("配置JSON格式错误,请检查语法"); + } setUseCommonConfig(false); return; } @@ -383,10 +427,7 @@ const ProviderForm: React.FC = ({ setUseCommonConfig(checked); // 标记正在通过通用配置更新 isUpdatingFromCommonConfig.current = true; - setFormData((prev) => ({ - ...prev, - settingsConfig: updatedConfig, - })); + updateSettingsConfigValue(updatedConfig); // 在下一个事件循环中重置标记 setTimeout(() => { isUpdatingFromCommonConfig.current = false; @@ -406,32 +447,34 @@ const ProviderForm: React.FC = ({ false, ); // 直接更新 formData,不通过 handleChange - setFormData((prev) => ({ - ...prev, - settingsConfig: updatedConfig, - })); + updateSettingsConfigValue(updatedConfig); setUseCommonConfig(false); } return; } // 验证JSON格式 - let isValidJson = false; - try { - JSON.parse(value); - isValidJson = true; + const validationError = validateJsonConfig(value, "通用配置片段"); + if (validationError) { + setCommonConfigError(validationError); + } else { setCommonConfigError(""); - } catch (err) { - setCommonConfigError("通用配置片段格式错误,需为合法 JSON"); } // 若当前启用通用配置且格式正确,需要替换为最新片段 - if (useCommonConfig && isValidJson) { + if (useCommonConfig && !validationError) { const removeResult = updateCommonConfigSnippet( formData.settingsConfig, previousSnippet, false, ); + if (removeResult.error) { + setCommonConfigError(removeResult.error); + if (removeResult.error.includes("配置 JSON 解析失败")) { + setSettingsConfigError("配置JSON格式错误,请检查语法"); + } + return; + } const addResult = updateCommonConfigSnippet( removeResult.updatedConfig, value, @@ -440,15 +483,15 @@ const ProviderForm: React.FC = ({ if (addResult.error) { setCommonConfigError(addResult.error); + if (addResult.error.includes("配置 JSON 解析失败")) { + setSettingsConfigError("配置JSON格式错误,请检查语法"); + } return; } // 标记正在通过通用配置更新,避免触发状态检查 isUpdatingFromCommonConfig.current = true; - setFormData((prev) => ({ - ...prev, - settingsConfig: addResult.updatedConfig, - })); + updateSettingsConfigValue(addResult.updatedConfig); // 在下一个事件循环中重置标记 setTimeout(() => { isUpdatingFromCommonConfig.current = false; @@ -456,7 +499,7 @@ const ProviderForm: React.FC = ({ } // 保存通用配置到 localStorage - if (isValidJson && typeof window !== "undefined") { + if (!validationError && typeof window !== "undefined") { try { window.localStorage.setItem(COMMON_CONFIG_STORAGE_KEY, value); } catch { @@ -473,6 +516,7 @@ const ProviderForm: React.FC = ({ websiteUrl: preset.websiteUrl, settingsConfig: configString, }); + setSettingsConfigError(validateSettingsConfig(configString)); setCategory( preset.category || (preset.isOfficial ? "official" : undefined), ); @@ -527,12 +571,14 @@ const ProviderForm: React.FC = ({ // ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name" }, }; + const templateString = JSON.stringify(customTemplate, null, 2); setFormData({ name: "", websiteUrl: "", - settingsConfig: JSON.stringify(customTemplate, null, 2), + settingsConfig: templateString, }); + setSettingsConfigError(validateSettingsConfig(templateString)); setApiKey(""); setBaseUrl("https://your-api-endpoint.com"); // 设置默认的基础 URL setUseCommonConfig(false); @@ -576,6 +622,7 @@ const ProviderForm: React.FC = ({ websiteUrl: "", settingsConfig: "", }); + setSettingsConfigError(validateSettingsConfig("")); setCodexAuth(""); setCodexConfig(""); setCodexApiKey(""); @@ -593,10 +640,7 @@ const ProviderForm: React.FC = ({ ); // 更新表单配置 - setFormData((prev) => ({ - ...prev, - settingsConfig: configString, - })); + updateSettingsConfigValue(configString); // 同步通用配置开关 const hasCommon = hasCommonConfigSnippet( @@ -617,10 +661,7 @@ const ProviderForm: React.FC = ({ } config.env.ANTHROPIC_BASE_URL = url.trim(); - setFormData((prev) => ({ - ...prev, - settingsConfig: JSON.stringify(config, null, 2), - })); + updateSettingsConfigValue(JSON.stringify(config, null, 2)); } catch { // ignore } @@ -861,10 +902,7 @@ const ProviderForm: React.FC = ({ delete currentConfig.env[field]; } - setFormData((prev) => ({ - ...prev, - settingsConfig: JSON.stringify(currentConfig, null, 2), - })); + updateSettingsConfigValue(JSON.stringify(currentConfig, null, 2)); } catch (err) { // 如果 JSON 解析失败,不做处理 } @@ -888,10 +926,7 @@ const ProviderForm: React.FC = ({ currentConfig.env[field] = value; const updatedConfigString = JSON.stringify(currentConfig, null, 2); - setFormData((prev) => ({ - ...prev, - settingsConfig: updatedConfigString, - })); + updateSettingsConfigValue(updatedConfigString); } catch (err) { console.error("更新 Kimi 模型配置失败:", err); } @@ -1144,6 +1179,7 @@ const ProviderForm: React.FC = ({ commonConfigSnippet={codexCommonConfigSnippet} onCommonConfigSnippetChange={handleCodexCommonConfigSnippetChange} commonConfigError={codexCommonConfigError} + authError={codexAuthError} /> ) : ( <> @@ -1215,6 +1251,7 @@ const ProviderForm: React.FC = ({ commonConfigSnippet={commonConfigSnippet} onCommonConfigSnippetChange={handleCommonConfigSnippetChange} commonConfigError={commonConfigError} + configError={settingsConfigError} /> )} diff --git a/src/components/ProviderForm/ClaudeConfigEditor.tsx b/src/components/ProviderForm/ClaudeConfigEditor.tsx index fb8bc27..2b75e6d 100644 --- a/src/components/ProviderForm/ClaudeConfigEditor.tsx +++ b/src/components/ProviderForm/ClaudeConfigEditor.tsx @@ -10,6 +10,7 @@ interface ClaudeConfigEditorProps { commonConfigSnippet: string; onCommonConfigSnippetChange: (value: string) => void; commonConfigError: string; + configError: string; } const ClaudeConfigEditor: React.FC = ({ @@ -20,6 +21,7 @@ const ClaudeConfigEditor: React.FC = ({ commonConfigSnippet, onCommonConfigSnippetChange, commonConfigError, + configError, }) => { const [isDarkMode, setIsDarkMode] = useState(false); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); @@ -117,6 +119,11 @@ const ClaudeConfigEditor: React.FC = ({ }`} rows={12} /> + {configError && ( +

+ {configError} +

+ )}

完整的 Claude Code settings.json 配置内容

diff --git a/src/components/ProviderForm/CodexConfigEditor.tsx b/src/components/ProviderForm/CodexConfigEditor.tsx index 0209b35..ebd6eee 100644 --- a/src/components/ProviderForm/CodexConfigEditor.tsx +++ b/src/components/ProviderForm/CodexConfigEditor.tsx @@ -12,6 +12,7 @@ interface CodexConfigEditorProps { commonConfigSnippet: string; onCommonConfigSnippetChange: (value: string) => void; commonConfigError: string; + authError: string; } const CodexConfigEditor: React.FC = ({ @@ -25,6 +26,7 @@ const CodexConfigEditor: React.FC = ({ commonConfigSnippet, onCommonConfigSnippetChange, commonConfigError, + authError, }) => { const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); @@ -94,6 +96,11 @@ const CodexConfigEditor: React.FC = ({ data-gramm_editor="false" data-enable-grammarly="false" /> + {authError && ( +

+ {authError} +

+ )}

Codex auth.json 配置内容

diff --git a/src/utils/providerConfigUtils.ts b/src/utils/providerConfigUtils.ts index c090589..aa5e284 100644 --- a/src/utils/providerConfigUtils.ts +++ b/src/utils/providerConfigUtils.ts @@ -77,6 +77,22 @@ export interface UpdateCommonConfigResult { error?: string; } +// 验证JSON配置格式 +export const validateJsonConfig = (value: string, fieldName: string = "配置"): string => { + if (!value.trim()) { + return ""; + } + try { + const parsed = JSON.parse(value); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return `${fieldName}必须是 JSON 对象`; + } + return ""; + } catch { + return `${fieldName}JSON格式错误,请检查语法`; + } +}; + // 将通用配置片段写入/移除 settingsConfig export const updateCommonConfigSnippet = ( jsonString: string, @@ -99,22 +115,16 @@ export const updateCommonConfigSnippet = ( }; } - let snippet: Record; - try { - const parsed = JSON.parse(snippetString); - if (!isPlainObject(parsed)) { - return { - updatedConfig: JSON.stringify(config, null, 2), - error: "通用配置片段必须是 JSON 对象", - }; - } - snippet = parsed; - } catch (err) { + // 使用统一的验证函数 + const snippetError = validateJsonConfig(snippetString, "通用配置片段"); + if (snippetError) { return { updatedConfig: JSON.stringify(config, null, 2), - error: "通用配置片段格式错误,需为合法 JSON", + error: snippetError, }; } + + const snippet = JSON.parse(snippetString) as Record; if (enabled) { const merged = deepMerge(deepClone(config), snippet);