From bb48f4f6af9b17ec35107eb978912245efd7a19f Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Oct 2025 13:02:38 +0800 Subject: [PATCH] refactor: consolidate provider form components This commit completes Stage 2.5-2.6 of the refactoring plan by: - Consolidating 8 provider form files (1941+ lines) into a single unified ProviderForm component (353 lines), reducing code by ~82% - Implementing modern form management with react-hook-form and zod - Adding preset provider categorization with grouped select UI - Supporting dual-mode operation for both Claude and Codex configs - Removing redundant subcomponents: - ApiKeyInput.tsx (72 lines) - ClaudeConfigEditor.tsx (205 lines) - CodexConfigEditor.tsx (667 lines) - EndpointSpeedTest.tsx (636 lines) - KimiModelSelector.tsx (195 lines) - PresetSelector.tsx (119 lines) Key improvements: - Type-safe form values with ProviderFormValues extension - Automatic template value application for presets - Better internationalization coverage - Cleaner separation of concerns - Enhanced UX with categorized preset groups Updates AddProviderDialog and EditProviderDialog to pass appType prop and handle preset category metadata. --- docs/REFACTORING_CHECKLIST.md | 7 +- src/App.tsx | 1 + src/components/ProviderForm.tsx | 1941 ----------------- src/components/ProviderForm/ApiKeyInput.tsx | 72 - .../ProviderForm/ClaudeConfigEditor.tsx | 205 -- .../ProviderForm/CodexConfigEditor.tsx | 667 ------ .../ProviderForm/EndpointSpeedTest.tsx | 636 ------ .../ProviderForm/KimiModelSelector.tsx | 195 -- .../ProviderForm/PresetSelector.tsx | 119 - .../providers/AddProviderDialog.tsx | 3 +- .../providers/EditProviderDialog.tsx | 7 + .../providers/forms/ProviderForm.tsx | 220 +- 12 files changed, 222 insertions(+), 3851 deletions(-) delete mode 100644 src/components/ProviderForm.tsx delete mode 100644 src/components/ProviderForm/ApiKeyInput.tsx delete mode 100644 src/components/ProviderForm/ClaudeConfigEditor.tsx delete mode 100644 src/components/ProviderForm/CodexConfigEditor.tsx delete mode 100644 src/components/ProviderForm/EndpointSpeedTest.tsx delete mode 100644 src/components/ProviderForm/KimiModelSelector.tsx delete mode 100644 src/components/ProviderForm/PresetSelector.tsx diff --git a/docs/REFACTORING_CHECKLIST.md b/docs/REFACTORING_CHECKLIST.md index 91c379b..657a8e1 100644 --- a/docs/REFACTORING_CHECKLIST.md +++ b/docs/REFACTORING_CHECKLIST.md @@ -205,9 +205,10 @@ pnpm add class-variance-authority clsx tailwind-merge tailwindcss-animate ### 2.6 清理旧组件 -- [ ] 删除 `src/components/AddProviderModal.tsx` -- [ ] 删除 `src/components/EditProviderModal.tsx` -- [ ] 更新所有引用这些组件的地方 +- [x] 删除 `src/components/AddProviderModal.tsx` +- [x] 删除 `src/components/EditProviderModal.tsx` +- [x] 更新所有引用这些组件的地方 +- [x] 删除 `src/components/ProviderForm.tsx` 及 `src/components/ProviderForm/` **完成时间**: ___________ **遇到的问题**: ___________ diff --git a/src/App.tsx b/src/App.tsx index fab27db..f1c038f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -293,6 +293,7 @@ function App() { } }} onSubmit={handleEditProvider} + appType={activeApp} /> {usageProvider && ( diff --git a/src/components/ProviderForm.tsx b/src/components/ProviderForm.tsx deleted file mode 100644 index f2b1df5..0000000 --- a/src/components/ProviderForm.tsx +++ /dev/null @@ -1,1941 +0,0 @@ -import React, { useState, useEffect, useRef, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { Provider, ProviderCategory, CustomEndpoint } from "../types"; -import type { AppType } from "@/lib/api"; -import { - updateCommonConfigSnippet, - hasCommonConfigSnippet, - getApiKeyFromConfig, - hasApiKeyField, - setApiKeyInConfig, - updateTomlCommonConfigSnippet, - hasTomlCommonConfigSnippet, - validateJsonConfig, - applyTemplateValues, - extractCodexBaseUrl, - setCodexBaseUrl as setCodexBaseUrlInConfig, -} from "../utils/providerConfigUtils"; -import { providerPresets } from "../config/providerPresets"; -import type { TemplateValueConfig } from "../config/providerPresets"; -import { - codexProviderPresets, - generateThirdPartyAuth, - generateThirdPartyConfig, -} from "../config/codexProviderPresets"; -import PresetSelector from "./ProviderForm/PresetSelector"; -import ApiKeyInput from "./ProviderForm/ApiKeyInput"; -import ClaudeConfigEditor from "./ProviderForm/ClaudeConfigEditor"; -import CodexConfigEditor from "./ProviderForm/CodexConfigEditor"; -import KimiModelSelector from "./ProviderForm/KimiModelSelector"; -import { X, AlertCircle, Save, Zap } from "lucide-react"; -import { isLinux } from "../lib/platform"; -import EndpointSpeedTest, { - EndpointCandidate, -} from "./ProviderForm/EndpointSpeedTest"; -// 分类仅用于控制少量交互(如官方禁用 API Key),不显示介绍组件 - -type TemplateValueMap = Record; - -type TemplatePath = Array; - -const collectTemplatePaths = ( - source: unknown, - templateKeys: string[], - currentPath: TemplatePath = [], - acc: TemplatePath[] = [], -): TemplatePath[] => { - if (typeof source === "string") { - const hasPlaceholder = templateKeys.some((key) => - source.includes(`\${${key}}`), - ); - if (hasPlaceholder) { - acc.push([...currentPath]); - } - return acc; - } - - if (Array.isArray(source)) { - source.forEach((item, index) => - collectTemplatePaths(item, templateKeys, [...currentPath, index], acc), - ); - return acc; - } - - if (source && typeof source === "object") { - Object.entries(source).forEach(([key, value]) => - collectTemplatePaths(value, templateKeys, [...currentPath, key], acc), - ); - } - - return acc; -}; - -const getValueAtPath = (source: any, path: TemplatePath) => { - return path.reduce((acc, key) => { - if (acc === undefined || acc === null) { - return undefined; - } - return acc[key as keyof typeof acc]; - }, source); -}; - -const setValueAtPath = ( - target: any, - path: TemplatePath, - value: unknown, -): any => { - if (path.length === 0) { - return value; - } - - let current = target; - - for (let i = 0; i < path.length - 1; i++) { - const key = path[i]; - const nextKey = path[i + 1]; - const isNextIndex = typeof nextKey === "number"; - - if (current[key as keyof typeof current] === undefined) { - current[key as keyof typeof current] = isNextIndex ? [] : {}; - } else { - const currentValue = current[key as keyof typeof current]; - if (isNextIndex && !Array.isArray(currentValue)) { - current[key as keyof typeof current] = []; - } else if ( - !isNextIndex && - (typeof currentValue !== "object" || currentValue === null) - ) { - current[key as keyof typeof current] = {}; - } - } - - current = current[key as keyof typeof current]; - } - - const finalKey = path[path.length - 1]; - current[finalKey as keyof typeof current] = value; - return target; -}; - -const applyTemplateValuesToConfigString = ( - presetConfig: any, - currentConfigString: string, - values: TemplateValueMap, -) => { - const replacedConfig = applyTemplateValues(presetConfig, values); - const templateKeys = Object.keys(values); - if (templateKeys.length === 0) { - return JSON.stringify(replacedConfig, null, 2); - } - - const placeholderPaths = collectTemplatePaths(presetConfig, templateKeys); - - try { - const parsedConfig = currentConfigString.trim() - ? JSON.parse(currentConfigString) - : {}; - let targetConfig: any; - if (Array.isArray(parsedConfig)) { - targetConfig = [...parsedConfig]; - } else if (parsedConfig && typeof parsedConfig === "object") { - targetConfig = JSON.parse(JSON.stringify(parsedConfig)); - } else { - targetConfig = {}; - } - - if (placeholderPaths.length === 0) { - return JSON.stringify(targetConfig, null, 2); - } - - let mutatedConfig = targetConfig; - - for (const path of placeholderPaths) { - const nextValue = getValueAtPath(replacedConfig, path); - if (path.length === 0) { - mutatedConfig = nextValue; - } else { - setValueAtPath(mutatedConfig, path, nextValue); - } - } - - return JSON.stringify(mutatedConfig, null, 2); - } catch { - return JSON.stringify(replacedConfig, null, 2); - } -}; - -const COMMON_CONFIG_STORAGE_KEY = "cc-switch:common-config-snippet"; -const CODEX_COMMON_CONFIG_STORAGE_KEY = "cc-switch:codex-common-config-snippet"; -const DEFAULT_COMMON_CONFIG_SNIPPET = `{ - "includeCoAuthoredBy": false -}`; -const DEFAULT_CODEX_COMMON_CONFIG_SNIPPET = `# Common Codex config -# Add your common TOML configuration here`; - -interface ProviderFormProps { - appType?: AppType; - title: string; - submitText: string; - initialData?: Provider; - showPresets?: boolean; - onSubmit: (data: Omit) => void; - onClose: () => void; -} - -const ProviderForm: React.FC = ({ - appType = "claude", - title, - submitText, - initialData, - showPresets = false, - onSubmit, - onClose, -}) => { - const { t } = useTranslation(); - // 对于 Codex,需要分离 auth 和 config - const isCodex = appType === "codex"; - - const [formData, setFormData] = useState({ - name: initialData?.name || "", - websiteUrl: initialData?.websiteUrl || "", - settingsConfig: initialData - ? JSON.stringify(initialData.settingsConfig, null, 2) - : "", - }); - const [category, setCategory] = useState( - initialData?.category, - ); - - // Claude 模型配置状态 - const [claudeModel, setClaudeModel] = useState(""); - const [claudeSmallFastModel, setClaudeSmallFastModel] = useState(""); - const [baseUrl, setBaseUrl] = useState(""); // 新增:基础 URL 状态 - // 模板变量状态 - const [templateValues, setTemplateValues] = useState< - Record - >({}); - - // Codex 特有的状态 - const [codexAuth, setCodexAuthState] = useState(""); - const [codexConfig, setCodexConfigState] = useState(""); - const [codexApiKey, setCodexApiKey] = useState(""); - const [codexBaseUrl, setCodexBaseUrl] = useState(""); - const [isCodexTemplateModalOpen, setIsCodexTemplateModalOpen] = - useState(false); - // 新建供应商:收集端点测速弹窗中的“自定义端点”,提交时一次性落盘到 meta.custom_endpoints - const [draftCustomEndpoints, setDraftCustomEndpoints] = useState( - [], - ); - // 端点测速弹窗状态 - const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); - const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = - useState(false); - // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 - const [selectedCodexPreset, setSelectedCodexPreset] = useState( - showPresets && isCodex ? -1 : null, - ); - - const setCodexAuth = (value: string) => { - setCodexAuthState(value); - setCodexAuthError(validateCodexAuth(value)); - }; - - const setCodexConfig = (value: string | ((prev: string) => string)) => { - setCodexConfigState((prev) => - typeof value === "function" - ? (value as (input: string) => string)(prev) - : value, - ); - }; - - const setCodexCommonConfigSnippet = (value: string) => { - setCodexCommonConfigSnippetState(value); - }; - - // 初始化 Codex 配置 - useEffect(() => { - if (isCodex && initialData) { - const config = initialData.settingsConfig; - if (typeof config === "object" && config !== null) { - setCodexAuth(JSON.stringify(config.auth || {}, null, 2)); - setCodexConfig(config.config || ""); - const initialBaseUrl = extractCodexBaseUrl(config.config); - if (initialBaseUrl) { - setCodexBaseUrl(initialBaseUrl); - } - try { - const auth = config.auth || {}; - if (auth && typeof auth.OPENAI_API_KEY === "string") { - setCodexApiKey(auth.OPENAI_API_KEY); - } - } catch { - // ignore - } - } - } - }, [isCodex, initialData]); - - const [error, setError] = useState(""); - const [useCommonConfig, setUseCommonConfig] = useState(false); - const [commonConfigSnippet, setCommonConfigSnippet] = useState(() => { - if (typeof window === "undefined") { - return DEFAULT_COMMON_CONFIG_SNIPPET; - } - try { - const stored = window.localStorage.getItem(COMMON_CONFIG_STORAGE_KEY); - if (stored && stored.trim()) { - return stored; - } - } catch { - // ignore localStorage 读取失败 - } - return DEFAULT_COMMON_CONFIG_SNIPPET; - }); - const [commonConfigError, setCommonConfigError] = useState(""); - const [settingsConfigError, setSettingsConfigError] = useState(""); - // 用于跟踪是否正在通过通用配置更新 - const isUpdatingFromCommonConfig = useRef(false); - - // Codex 通用配置状态 - const [useCodexCommonConfig, setUseCodexCommonConfig] = useState(false); - const [codexCommonConfigSnippet, setCodexCommonConfigSnippetState] = - useState(() => { - if (typeof window === "undefined") { - return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET.trim(); - } - try { - const stored = window.localStorage.getItem( - CODEX_COMMON_CONFIG_STORAGE_KEY, - ); - if (stored && stored.trim()) { - return stored.trim(); - } - } catch { - // ignore localStorage 读取失败 - } - return DEFAULT_CODEX_COMMON_CONFIG_SNIPPET.trim(); - }); - const [codexCommonConfigError, setCodexCommonConfigError] = useState(""); - const isUpdatingFromCodexCommonConfig = useRef(false); - const isUpdatingBaseUrlRef = useRef(false); - const isUpdatingCodexBaseUrlRef = useRef(false); - - // -1 表示自定义,null 表示未选择,>= 0 表示预设索引 - const [selectedPreset, setSelectedPreset] = useState( - 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 => { - const err = validateJsonConfig(value, "配置内容"); - return err ? t("providerForm.configJsonError") : ""; - }; - - const validateCodexAuth = (value: string): string => { - if (!value.trim()) return ""; - try { - const parsed = JSON.parse(value); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return t("providerForm.authJsonRequired"); - } - return ""; - } catch { - return t("providerForm.authJsonError"); - } - }; - - const updateSettingsConfigValue = (value: string) => { - setFormData((prev) => ({ - ...prev, - settingsConfig: value, - })); - setSettingsConfigError(validateSettingsConfig(value)); - }; - - // 初始化自定义模式的默认配置 - useEffect(() => { - if ( - showPresets && - selectedPreset === -1 && - !initialData && - formData.settingsConfig === "" - ) { - // 设置自定义模板 - const customTemplate = { - env: { - ANTHROPIC_BASE_URL: "https://your-api-endpoint.com", - ANTHROPIC_AUTH_TOKEN: "", - // 可选配置 - // ANTHROPIC_MODEL: "your-model-name", - // ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name" - }, - }; - const templateString = JSON.stringify(customTemplate, null, 2); - - updateSettingsConfigValue(templateString); - setApiKey(""); - } - }, []); // 只在组件挂载时执行一次 - - // 初始化时检查通用配置片段 - useEffect(() => { - if (initialData) { - if (!isCodex) { - const configString = JSON.stringify( - initialData.settingsConfig, - null, - 2, - ); - const hasCommon = hasCommonConfigSnippet( - configString, - commonConfigSnippet, - ); - setUseCommonConfig(hasCommon); - setSettingsConfigError(validateSettingsConfig(configString)); - - // 初始化模型配置(编辑模式) - if ( - initialData.settingsConfig && - typeof initialData.settingsConfig === "object" - ) { - const config = initialData.settingsConfig as { - env?: Record; - }; - if (config.env) { - setClaudeModel(config.env.ANTHROPIC_MODEL || ""); - setClaudeSmallFastModel( - config.env.ANTHROPIC_SMALL_FAST_MODEL || "", - ); - setBaseUrl(config.env.ANTHROPIC_BASE_URL || ""); // 初始化基础 URL - - // 初始化 Kimi 模型选择 - setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || ""); - setKimiAnthropicSmallFastModel( - config.env.ANTHROPIC_SMALL_FAST_MODEL || "", - ); - } - } - } else { - // Codex 初始化时检查 TOML 通用配置 - const hasCommon = hasTomlCommonConfigSnippet( - codexConfig, - codexCommonConfigSnippet, - ); - setUseCodexCommonConfig(hasCommon); - } - } - }, [ - initialData, - commonConfigSnippet, - codexCommonConfigSnippet, - isCodex, - codexConfig, - ]); - - // 当选择预设变化时,同步类别 - useEffect(() => { - if (!showPresets) return; - if (!isCodex) { - if (selectedPreset !== null && selectedPreset >= 0) { - const preset = providerPresets[selectedPreset]; - setCategory( - preset?.category || (preset?.isOfficial ? "official" : undefined), - ); - } else if (selectedPreset === -1) { - setCategory("custom"); - } - } else { - if (selectedCodexPreset !== null && selectedCodexPreset >= 0) { - const preset = codexProviderPresets[selectedCodexPreset]; - setCategory( - preset?.category || (preset?.isOfficial ? "official" : undefined), - ); - } else if (selectedCodexPreset === -1) { - setCategory("custom"); - } - } - }, [showPresets, isCodex, selectedPreset, selectedCodexPreset]); - - // 与 JSON 配置保持基础 URL 同步(Claude 第三方/自定义) - useEffect(() => { - if (isCodex) return; - const currentCategory = category ?? initialData?.category; - if (currentCategory !== "third_party" && currentCategory !== "custom") { - return; - } - if (isUpdatingBaseUrlRef.current) { - return; - } - try { - const config = JSON.parse(formData.settingsConfig || "{}"); - const envUrl: unknown = config?.env?.ANTHROPIC_BASE_URL; - if (typeof envUrl === "string" && envUrl && envUrl !== baseUrl) { - setBaseUrl(envUrl.trim()); - } - } catch { - // ignore JSON parse errors - } - }, [isCodex, category, initialData, formData.settingsConfig, baseUrl]); - - // 与 TOML 配置保持基础 URL 同步(Codex 第三方/自定义) - useEffect(() => { - if (!isCodex) return; - const currentCategory = category ?? initialData?.category; - if (currentCategory !== "third_party" && currentCategory !== "custom") { - return; - } - if (isUpdatingCodexBaseUrlRef.current) { - return; - } - const extracted = extractCodexBaseUrl(codexConfig) || ""; - if (extracted !== codexBaseUrl) { - setCodexBaseUrl(extracted); - } - }, [isCodex, category, initialData, codexConfig, codexBaseUrl]); - - // 同步本地存储的通用配置片段 - useEffect(() => { - if (typeof window === "undefined") return; - try { - if (commonConfigSnippet.trim()) { - window.localStorage.setItem( - COMMON_CONFIG_STORAGE_KEY, - commonConfigSnippet, - ); - } else { - window.localStorage.removeItem(COMMON_CONFIG_STORAGE_KEY); - } - } catch { - // ignore - } - }, [commonConfigSnippet]); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - - if (!formData.name) { - setError(t("providerForm.fillSupplierName")); - return; - } - - 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(t("providerForm.fillAuthJson")); - return; - } - - try { - const authJson = JSON.parse(codexAuth); - - // 非官方预设强制要求 OPENAI_API_KEY - if (selectedCodexPreset !== null) { - const preset = codexProviderPresets[selectedCodexPreset]; - const isOfficial = Boolean(preset?.isOfficial); - if (!isOfficial) { - const key = - typeof authJson.OPENAI_API_KEY === "string" - ? authJson.OPENAI_API_KEY.trim() - : ""; - if (!key) { - setError(t("providerForm.fillApiKey")); - return; - } - } - } - - settingsConfig = { - auth: authJson, - config: codexConfig ?? "", - }; - } catch (err) { - setError(t("providerForm.authJsonError")); - return; - } - } else { - const currentSettingsError = validateSettingsConfig( - formData.settingsConfig, - ); - setSettingsConfigError(currentSettingsError); - if (currentSettingsError) { - setError(t("providerForm.configJsonError")); - return; - } - - if (selectedTemplatePreset && templateValueEntries.length > 0) { - for (const [key, config] of templateValueEntries) { - const entry = templateValues[key]; - const resolvedValue = ( - entry?.editorValue ?? - entry?.defaultValue ?? - config.defaultValue ?? - "" - ).trim(); - if (!resolvedValue) { - setError(t("providerForm.fillParameter", { label: config.label })); - return; - } - } - } - // Claude: 原有逻辑 - if (!formData.settingsConfig.trim()) { - setError(t("providerForm.fillConfigContent")); - return; - } - - try { - settingsConfig = JSON.parse(formData.settingsConfig); - } catch (err) { - setError(t("providerForm.configJsonError")); - return; - } - } - - // 构造基础提交数据 - const basePayload: Omit = { - name: formData.name, - websiteUrl: formData.websiteUrl, - settingsConfig, - // 仅在用户选择了预设或手动选择“自定义”时持久化分类 - ...(category ? { category } : {}), - }; - - // 若为"新建供应商",将端点候选一并随提交落盘到 meta.custom_endpoints: - // - 用户在弹窗中新增的自定义端点(draftCustomEndpoints,已去重) - // - 预设中的 endpointCandidates(若存在) - // - 当前选中的基础 URL(baseUrl/codexBaseUrl) - if (!initialData) { - const urlSet = new Set(); - const push = (raw?: string) => { - const url = (raw || "").trim().replace(/\/+$/, ""); - if (url) urlSet.add(url); - }; - - // 自定义端点(仅来自用户新增) - for (const u of draftCustomEndpoints) push(u); - - // 预设端点候选 - if (!isCodex) { - if ( - selectedPreset !== null && - selectedPreset >= 0 && - selectedPreset < providerPresets.length - ) { - const preset = providerPresets[selectedPreset] as any; - if (Array.isArray(preset?.endpointCandidates)) { - for (const u of preset.endpointCandidates as string[]) push(u); - } - } - // 当前 Claude 基础地址 - push(baseUrl); - } else { - if ( - selectedCodexPreset !== null && - selectedCodexPreset >= 0 && - selectedCodexPreset < codexProviderPresets.length - ) { - const preset = codexProviderPresets[selectedCodexPreset] as any; - if (Array.isArray(preset?.endpointCandidates)) { - for (const u of preset.endpointCandidates as string[]) push(u); - } - } - // 当前 Codex 基础地址 - push(codexBaseUrl); - } - - const urls = Array.from(urlSet.values()); - if (urls.length > 0) { - const now = Date.now(); - const customMap: Record = {}; - for (const url of urls) { - if (!customMap[url]) { - customMap[url] = { url, addedAt: now, lastUsed: undefined }; - } - } - onSubmit({ ...basePayload, meta: { custom_endpoints: customMap } }); - return; - } - } - - onSubmit(basePayload); - }; - - const handleChange = ( - e: React.ChangeEvent, - ) => { - const { name, value } = e.target; - - if (name === "settingsConfig") { - // 只有在不是通过通用配置更新时,才检查并同步选择框状态 - if (!isUpdatingFromCommonConfig.current) { - const hasCommon = hasCommonConfigSnippet(value, commonConfigSnippet); - setUseCommonConfig(hasCommon); - } - - // 同步 API Key 输入框显示与值 - const parsedKey = getApiKeyFromConfig(value); - setApiKey(parsedKey); - - // 不再从 JSON 自动提取或覆盖官网地址,只更新配置内容 - updateSettingsConfigValue(value); - } else { - setFormData((prev) => ({ - ...prev, - [name]: value, - })); - } - }; - - // 处理通用配置开关 - const handleCommonConfigToggle = (checked: boolean) => { - const { updatedConfig, error: snippetError } = updateCommonConfigSnippet( - formData.settingsConfig, - commonConfigSnippet, - checked, - ); - - if (snippetError) { - setCommonConfigError(snippetError); - if (snippetError.includes("配置 JSON 解析失败")) { - setSettingsConfigError(t("providerForm.configJsonError")); - } - setUseCommonConfig(false); - return; - } - - setCommonConfigError(""); - setUseCommonConfig(checked); - // 标记正在通过通用配置更新 - isUpdatingFromCommonConfig.current = true; - updateSettingsConfigValue(updatedConfig); - // 在下一个事件循环中重置标记 - setTimeout(() => { - isUpdatingFromCommonConfig.current = false; - }, 0); - }; - - const handleCommonConfigSnippetChange = (value: string) => { - const previousSnippet = commonConfigSnippet; - setCommonConfigSnippet(value); - - if (!value.trim()) { - setCommonConfigError(""); - if (useCommonConfig) { - const { updatedConfig } = updateCommonConfigSnippet( - formData.settingsConfig, - previousSnippet, - false, - ); - // 直接更新 formData,不通过 handleChange - updateSettingsConfigValue(updatedConfig); - setUseCommonConfig(false); - } - return; - } - - // 验证JSON格式 - const validationError = validateJsonConfig(value, "通用配置片段"); - if (validationError) { - setCommonConfigError(validationError); - } else { - setCommonConfigError(""); - } - - // 若当前启用通用配置且格式正确,需要替换为最新片段 - if (useCommonConfig && !validationError) { - const removeResult = updateCommonConfigSnippet( - formData.settingsConfig, - previousSnippet, - false, - ); - if (removeResult.error) { - setCommonConfigError(removeResult.error); - if (removeResult.error.includes("配置 JSON 解析失败")) { - setSettingsConfigError(t("providerForm.configJsonError")); - } - return; - } - const addResult = updateCommonConfigSnippet( - removeResult.updatedConfig, - value, - true, - ); - - if (addResult.error) { - setCommonConfigError(addResult.error); - if (addResult.error.includes("配置 JSON 解析失败")) { - setSettingsConfigError(t("providerForm.configJsonError")); - } - return; - } - - // 标记正在通过通用配置更新,避免触发状态检查 - isUpdatingFromCommonConfig.current = true; - updateSettingsConfigValue(addResult.updatedConfig); - // 在下一个事件循环中重置标记 - setTimeout(() => { - isUpdatingFromCommonConfig.current = false; - }, 0); - } - - // 保存通用配置到 localStorage - if (!validationError && typeof window !== "undefined") { - try { - window.localStorage.setItem(COMMON_CONFIG_STORAGE_KEY, value); - } catch { - // ignore localStorage 写入失败 - } - } - }; - - const applyPreset = (preset: (typeof providerPresets)[0], index: number) => { - let appliedSettingsConfig = preset.settingsConfig; - let initialTemplateValues: TemplateValueMap = {}; - - if (preset.templateValues) { - initialTemplateValues = Object.fromEntries( - Object.entries(preset.templateValues).map(([key, config]) => [ - key, - { - ...config, - editorValue: config.editorValue - ? config.editorValue - : (config.defaultValue ?? ""), - }, - ]), - ); - appliedSettingsConfig = applyTemplateValues( - preset.settingsConfig, - initialTemplateValues, - ); - } - - setTemplateValues(initialTemplateValues); - - const configString = JSON.stringify(appliedSettingsConfig, null, 2); - - setFormData({ - name: preset.name, - websiteUrl: preset.websiteUrl, - settingsConfig: configString, - }); - setSettingsConfigError(validateSettingsConfig(configString)); - setCategory( - preset.category || (preset.isOfficial ? "official" : undefined), - ); - - // 设置选中的预设 - setSelectedPreset(index); - - // 清空 API Key 输入框,让用户重新输入 - setApiKey(""); - - // 同步通用配置状态 - const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet); - setUseCommonConfig(hasCommon); - setCommonConfigError(""); - - // 如果预设包含模型配置,初始化模型输入框 - if (appliedSettingsConfig && typeof appliedSettingsConfig === "object") { - const config = appliedSettingsConfig as { env?: Record }; - if (config.env) { - setClaudeModel(config.env.ANTHROPIC_MODEL || ""); - setClaudeSmallFastModel(config.env.ANTHROPIC_SMALL_FAST_MODEL || ""); - const presetBaseUrl = - typeof config.env.ANTHROPIC_BASE_URL === "string" - ? config.env.ANTHROPIC_BASE_URL - : ""; - setBaseUrl(presetBaseUrl); - - // 如果是 Kimi 预设,同步 Kimi 模型选择 - if (preset.name?.includes("Kimi")) { - setKimiAnthropicModel(config.env.ANTHROPIC_MODEL || ""); - setKimiAnthropicSmallFastModel( - config.env.ANTHROPIC_SMALL_FAST_MODEL || "", - ); - } - } else { - setClaudeModel(""); - setClaudeSmallFastModel(""); - setBaseUrl(""); - } - } - }; - - // 处理点击自定义按钮 - const handleCustomClick = () => { - setSelectedPreset(-1); - setTemplateValues({}); - - // 设置自定义模板 - const customTemplate = { - env: { - ANTHROPIC_BASE_URL: "https://your-api-endpoint.com", - ANTHROPIC_AUTH_TOKEN: "", - // 可选配置 - // ANTHROPIC_MODEL: "your-model-name", - // ANTHROPIC_SMALL_FAST_MODEL: "your-fast-model-name" - }, - }; - const templateString = JSON.stringify(customTemplate, null, 2); - - setFormData({ - name: "", - websiteUrl: "", - settingsConfig: templateString, - }); - setSettingsConfigError(validateSettingsConfig(templateString)); - setApiKey(""); - setBaseUrl("https://your-api-endpoint.com"); // 设置默认的基础 URL - setUseCommonConfig(false); - setCommonConfigError(""); - setClaudeModel(""); - setClaudeSmallFastModel(""); - setKimiAnthropicModel(""); - setKimiAnthropicSmallFastModel(""); - setCategory("custom"); - }; - - // Codex: 应用预设 - const applyCodexPreset = ( - preset: (typeof codexProviderPresets)[0], - index: number, - ) => { - const authString = JSON.stringify(preset.auth || {}, null, 2); - setCodexAuth(authString); - setCodexConfig(preset.config || ""); - const presetBaseUrl = extractCodexBaseUrl(preset.config); - if (presetBaseUrl) { - setCodexBaseUrl(presetBaseUrl); - } - - setFormData((prev) => ({ - ...prev, - name: preset.name, - websiteUrl: preset.websiteUrl, - })); - - setSelectedCodexPreset(index); - setCategory( - preset.category || (preset.isOfficial ? "official" : undefined), - ); - - // 清空 API Key,让用户重新输入 - setCodexApiKey(""); - }; - - // Codex: 处理点击自定义按钮 - const handleCodexCustomClick = () => { - setSelectedCodexPreset(-1); - - // 设置自定义模板 - const customAuth = generateThirdPartyAuth(""); - const customConfig = generateThirdPartyConfig( - "custom", - "https://your-api-endpoint.com/v1", - "gpt-5-codex", - ); - - setFormData({ - name: "", - websiteUrl: "", - settingsConfig: "", - }); - setSettingsConfigError(validateSettingsConfig("")); - setCodexAuth(JSON.stringify(customAuth, null, 2)); - setCodexConfig(customConfig); - setCodexApiKey(""); - setCodexBaseUrl("https://your-api-endpoint.com/v1"); - setCategory("custom"); - }; - - // 处理 API Key 输入并自动更新配置 - const handleApiKeyChange = (key: string) => { - setApiKey(key); - - const configString = setApiKeyInConfig( - formData.settingsConfig, - key.trim(), - { createIfMissing: selectedPreset !== null && selectedPreset !== -1 }, - ); - - // 更新表单配置 - updateSettingsConfigValue(configString); - - // 同步通用配置开关 - const hasCommon = hasCommonConfigSnippet(configString, commonConfigSnippet); - setUseCommonConfig(hasCommon); - }; - - // 处理基础 URL 变化 - const handleBaseUrlChange = (url: string) => { - const sanitized = url.trim().replace(/\/+$/, ""); - setBaseUrl(sanitized); - isUpdatingBaseUrlRef.current = true; - - try { - const config = JSON.parse(formData.settingsConfig || "{}"); - if (!config.env) { - config.env = {}; - } - config.env.ANTHROPIC_BASE_URL = sanitized; - - updateSettingsConfigValue(JSON.stringify(config, null, 2)); - } catch { - // ignore - } finally { - setTimeout(() => { - isUpdatingBaseUrlRef.current = false; - }, 0); - } - }; - - const handleCodexBaseUrlChange = (url: string) => { - const sanitized = url.trim().replace(/\/+$/, ""); - setCodexBaseUrl(sanitized); - - if (!sanitized) { - return; - } - - isUpdatingCodexBaseUrlRef.current = true; - setCodexConfig((prev) => setCodexBaseUrlInConfig(prev, sanitized)); - setTimeout(() => { - isUpdatingCodexBaseUrlRef.current = false; - }, 0); - }; - - // Codex: 处理 API Key 输入并写回 auth.json - const handleCodexApiKeyChange = (key: string) => { - setCodexApiKey(key); - try { - const auth = JSON.parse(codexAuth || "{}"); - auth.OPENAI_API_KEY = key.trim(); - setCodexAuth(JSON.stringify(auth, null, 2)); - } catch { - // ignore - } - }; - - // Codex: 处理通用配置开关 - const handleCodexCommonConfigToggle = (checked: boolean) => { - const snippet = codexCommonConfigSnippet.trim(); - const { updatedConfig, error: snippetError } = - updateTomlCommonConfigSnippet(codexConfig, snippet, checked); - - if (snippetError) { - setCodexCommonConfigError(snippetError); - setUseCodexCommonConfig(false); - return; - } - - setCodexCommonConfigError(""); - setUseCodexCommonConfig(checked); - // 标记正在通过通用配置更新 - isUpdatingFromCodexCommonConfig.current = true; - setCodexConfig(updatedConfig); - // 在下一个事件循环中重置标记 - setTimeout(() => { - isUpdatingFromCodexCommonConfig.current = false; - }, 0); - }; - - // Codex: 处理通用配置片段变化 - const handleCodexCommonConfigSnippetChange = (value: string) => { - const previousSnippet = codexCommonConfigSnippet.trim(); - const sanitizedValue = value.trim(); - setCodexCommonConfigSnippet(value); - - if (!sanitizedValue) { - setCodexCommonConfigError(""); - if (useCodexCommonConfig) { - const { updatedConfig } = updateTomlCommonConfigSnippet( - codexConfig, - previousSnippet, - false, - ); - setCodexConfig(updatedConfig); - setUseCodexCommonConfig(false); - } - return; - } - - // TOML 不需要验证 JSON 格式,直接更新 - if (useCodexCommonConfig) { - const removeResult = updateTomlCommonConfigSnippet( - codexConfig, - previousSnippet, - false, - ); - const addResult = updateTomlCommonConfigSnippet( - removeResult.updatedConfig, - sanitizedValue, - true, - ); - - if (addResult.error) { - setCodexCommonConfigError(addResult.error); - return; - } - - // 标记正在通过通用配置更新 - isUpdatingFromCodexCommonConfig.current = true; - setCodexConfig(addResult.updatedConfig); - // 在下一个事件循环中重置标记 - setTimeout(() => { - isUpdatingFromCodexCommonConfig.current = false; - }, 0); - } - - // 保存 Codex 通用配置到 localStorage - if (typeof window !== "undefined") { - try { - window.localStorage.setItem( - CODEX_COMMON_CONFIG_STORAGE_KEY, - sanitizedValue, - ); - } catch { - // ignore localStorage 写入失败 - } - } - }; - - // Codex: 处理 config 变化 - const handleCodexConfigChange = (value: string) => { - if (!isUpdatingFromCodexCommonConfig.current) { - const hasCommon = hasTomlCommonConfigSnippet( - value, - codexCommonConfigSnippet, - ); - setUseCodexCommonConfig(hasCommon); - } - setCodexConfig(value); - if (!isUpdatingCodexBaseUrlRef.current) { - const extracted = extractCodexBaseUrl(value) || ""; - if (extracted !== codexBaseUrl) { - setCodexBaseUrl(extracted); - } - } - }; - - // 根据当前配置决定是否展示 API Key 输入框 - // 自定义模式(-1)也需要显示 API Key 输入框 - const showApiKey = - selectedPreset !== null || - (!showPresets && hasApiKeyField(formData.settingsConfig)); - - const normalizedCategory = category ?? initialData?.category; - const shouldShowSpeedTest = - normalizedCategory === "third_party" || normalizedCategory === "custom"; - - const selectedTemplatePreset = - !isCodex && - selectedPreset !== null && - selectedPreset >= 0 && - selectedPreset < providerPresets.length - ? providerPresets[selectedPreset] - : null; - - const templateValueEntries: Array<[string, TemplateValueConfig]> = - selectedTemplatePreset?.templateValues - ? (Object.entries(selectedTemplatePreset.templateValues) as Array< - [string, TemplateValueConfig] - >) - : []; - - // 判断当前选中的预设是否是官方 - const isOfficialPreset = - (selectedPreset !== null && - selectedPreset >= 0 && - (providerPresets[selectedPreset]?.isOfficial === true || - providerPresets[selectedPreset]?.category === "official")) || - category === "official"; - - // 判断当前选中的预设是否是 Kimi - const isKimiPreset = - selectedPreset !== null && - selectedPreset >= 0 && - providerPresets[selectedPreset]?.name?.includes("Kimi"); - - // 判断当前编辑的是否是 Kimi 提供商(通过名称或配置判断) - const isEditingKimi = - initialData && - (formData.name.includes("Kimi") || - formData.name.includes("kimi") || - (formData.settingsConfig.includes("api.moonshot.cn") && - formData.settingsConfig.includes("ANTHROPIC_MODEL"))); - - // 综合判断是否应该显示 Kimi 模型选择器 - const shouldShowKimiSelector = isKimiPreset || isEditingKimi; - - const claudeSpeedTestEndpoints = useMemo(() => { - if (isCodex) return []; - const map = new Map(); - const add = (url?: string) => { - if (!url) return; - const sanitized = url.trim().replace(/\/+$/, ""); - if (!sanitized || map.has(sanitized)) return; - map.set(sanitized, { url: sanitized }); - }; - - if (baseUrl) { - add(baseUrl); - } - - if (initialData && typeof initialData.settingsConfig === "object") { - const envUrl = (initialData.settingsConfig as any)?.env - ?.ANTHROPIC_BASE_URL; - if (typeof envUrl === "string") { - add(envUrl); - } - } - - if ( - selectedPreset !== null && - selectedPreset >= 0 && - selectedPreset < providerPresets.length - ) { - const preset = providerPresets[selectedPreset]; - const presetEnv = (preset.settingsConfig as any)?.env?.ANTHROPIC_BASE_URL; - if (typeof presetEnv === "string") { - add(presetEnv); - } - // 合并预设内置的请求地址候选 - if (Array.isArray((preset as any).endpointCandidates)) { - ((preset as any).endpointCandidates as string[]).forEach((u) => add(u)); - } - } - - return Array.from(map.values()); - }, [isCodex, baseUrl, initialData, selectedPreset]); - - const codexSpeedTestEndpoints = useMemo(() => { - if (!isCodex) return []; - const map = new Map(); - const add = (url?: string) => { - if (!url) return; - const sanitized = url.trim().replace(/\/+$/, ""); - if (!sanitized || map.has(sanitized)) return; - map.set(sanitized, { url: sanitized }); - }; - - if (codexBaseUrl) { - add(codexBaseUrl); - } - - const initialCodexConfig = - initialData && typeof initialData.settingsConfig?.config === "string" - ? (initialData.settingsConfig as any).config - : ""; - const existing = extractCodexBaseUrl(initialCodexConfig); - if (existing) { - add(existing); - } - - if ( - selectedCodexPreset !== null && - selectedCodexPreset >= 0 && - selectedCodexPreset < codexProviderPresets.length - ) { - const preset = codexProviderPresets[selectedCodexPreset]; - const presetBase = extractCodexBaseUrl(preset?.config || ""); - if (presetBase) { - add(presetBase); - } - // 合并预设内置的请求地址候选 - if (Array.isArray((preset as any)?.endpointCandidates)) { - ((preset as any).endpointCandidates as string[]).forEach((u) => add(u)); - } - } - - return Array.from(map.values()); - }, [isCodex, codexBaseUrl, initialData, selectedCodexPreset]); - - // 判断是否显示"获取 API Key"链接(国产官方、聚合站和第三方显示) - const shouldShowApiKeyLink = - !isCodex && - !isOfficialPreset && - (category === "cn_official" || - category === "aggregator" || - category === "third_party" || - (selectedPreset !== null && - selectedPreset >= 0 && - (providerPresets[selectedPreset]?.category === "cn_official" || - providerPresets[selectedPreset]?.category === "aggregator" || - providerPresets[selectedPreset]?.category === "third_party"))); - - // 获取当前供应商的网址 - const getCurrentWebsiteUrl = () => { - if (selectedPreset !== null && selectedPreset >= 0) { - const preset = providerPresets[selectedPreset]; - if (!preset) return ""; - // 仅第三方供应商使用专用 apiKeyUrl,其余使用官网地址 - return preset.category === "third_party" - ? preset.apiKeyUrl || preset.websiteUrl || "" - : preset.websiteUrl || ""; - } - return formData.websiteUrl || ""; - }; - - // 获取 Codex 当前供应商的网址 - const getCurrentCodexWebsiteUrl = () => { - if (selectedCodexPreset !== null && selectedCodexPreset >= 0) { - const preset = codexProviderPresets[selectedCodexPreset]; - if (!preset) return ""; - // 仅第三方供应商使用专用 apiKeyUrl,其余使用官网地址 - return preset.category === "third_party" - ? preset.apiKeyUrl || preset.websiteUrl || "" - : preset.websiteUrl || ""; - } - return formData.websiteUrl || ""; - }; - - // Codex: 控制显示 API Key 与官方标记 - const getCodexAuthApiKey = (authString: string): string => { - try { - const auth = JSON.parse(authString || "{}"); - return typeof auth.OPENAI_API_KEY === "string" ? auth.OPENAI_API_KEY : ""; - } catch { - return ""; - } - }; - - // 自定义模式(-1)不显示独立的 API Key 输入框 - const showCodexApiKey = - (selectedCodexPreset !== null && selectedCodexPreset !== -1) || - (!showPresets && getCodexAuthApiKey(codexAuth) !== ""); - - // 不再渲染分类介绍组件,避免造成干扰 - - const isCodexOfficialPreset = - (selectedCodexPreset !== null && - selectedCodexPreset >= 0 && - (codexProviderPresets[selectedCodexPreset]?.isOfficial === true || - codexProviderPresets[selectedCodexPreset]?.category === "official")) || - category === "official"; - - // 判断是否显示 Codex 的"获取 API Key"链接(国产官方、聚合站和第三方显示) - const shouldShowCodexApiKeyLink = - isCodex && - !isCodexOfficialPreset && - (category === "cn_official" || - category === "aggregator" || - category === "third_party" || - (selectedCodexPreset !== null && - selectedCodexPreset >= 0 && - (codexProviderPresets[selectedCodexPreset]?.category === - "cn_official" || - codexProviderPresets[selectedCodexPreset]?.category === - "aggregator" || - codexProviderPresets[selectedCodexPreset]?.category === - "third_party"))); - - // 处理模型输入变化,自动更新 JSON 配置 - const handleModelChange = ( - field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", - value: string, - ) => { - if (field === "ANTHROPIC_MODEL") { - setClaudeModel(value); - } else { - setClaudeSmallFastModel(value); - } - - // 更新 JSON 配置 - try { - const currentConfig = formData.settingsConfig - ? JSON.parse(formData.settingsConfig) - : { env: {} }; - if (!currentConfig.env) currentConfig.env = {}; - - if (value.trim()) { - currentConfig.env[field] = value.trim(); - } else { - delete currentConfig.env[field]; - } - - updateSettingsConfigValue(JSON.stringify(currentConfig, null, 2)); - } catch (err) { - // 如果 JSON 解析失败,不做处理 - } - }; - - // Kimi 模型选择处理函数 - const handleKimiModelChange = ( - field: "ANTHROPIC_MODEL" | "ANTHROPIC_SMALL_FAST_MODEL", - value: string, - ) => { - if (field === "ANTHROPIC_MODEL") { - setKimiAnthropicModel(value); - } else { - setKimiAnthropicSmallFastModel(value); - } - - // 更新配置 JSON - try { - const currentConfig = JSON.parse(formData.settingsConfig || "{}"); - if (!currentConfig.env) currentConfig.env = {}; - currentConfig.env[field] = value; - - const updatedConfigString = JSON.stringify(currentConfig, null, 2); - updateSettingsConfigValue(updatedConfigString); - } catch (err) { - console.error("更新 Kimi 模型配置失败:", err); - } - }; - - // 初始时从配置中同步 API Key(编辑模式) - useEffect(() => { - if (!initialData) return; - const parsedKey = getApiKeyFromConfig( - JSON.stringify(initialData.settingsConfig), - ); - if (parsedKey) setApiKey(parsedKey); - }, [initialData]); - - // 支持按下 ESC 关闭弹窗 - useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - // 若有子弹窗(端点测速/模板向导)处于打开状态,则交由子弹窗自身处理,避免级联关闭 - if ( - isEndpointModalOpen || - isCodexEndpointModalOpen || - isCodexTemplateModalOpen - ) { - return; - } - e.preventDefault(); - onClose(); - } - }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [ - onClose, - isEndpointModalOpen, - isCodexEndpointModalOpen, - isCodexTemplateModalOpen, - ]); - - return ( -
{ - if (e.target === e.currentTarget) onClose(); - }} - > - {/* Backdrop */} -
- - {/* Modal */} -
- {/* Header */} -
-

- {title} -

- -
- -
-
- {error && ( -
- -

- {error} -

-
- )} - - {showPresets && !isCodex && ( - - applyPreset(providerPresets[index], index) - } - onCustomClick={handleCustomClick} - /> - )} - - {showPresets && isCodex && ( - - applyCodexPreset(codexProviderPresets[index], index) - } - onCustomClick={handleCodexCustomClick} - renderCustomDescription={() => ( - <> - {t("providerForm.manualConfig")} - - - )} - /> - )} - -
- - -
- -
- - -
- - {!isCodex && showApiKey && ( -
- - {shouldShowApiKeyLink && getCurrentWebsiteUrl() && ( - - )} -
- )} - - {!isCodex && - selectedTemplatePreset && - templateValueEntries.length > 0 && ( -
-

- {t("providerForm.parameterConfig", { - name: selectedTemplatePreset.name.trim(), - })} -

-
- {templateValueEntries.map(([key, config]) => ( -
- - { - const newValue = e.target.value; - setTemplateValues((prev) => { - const prevEntry = prev[key]; - const nextEntry: TemplateValueConfig = { - ...config, - ...(prevEntry ?? {}), - editorValue: newValue, - }; - const nextValues: TemplateValueMap = { - ...prev, - [key]: nextEntry, - }; - - if (selectedTemplatePreset) { - try { - const configString = - applyTemplateValuesToConfigString( - selectedTemplatePreset.settingsConfig, - formData.settingsConfig, - nextValues, - ); - setFormData((prevForm) => ({ - ...prevForm, - settingsConfig: configString, - })); - setSettingsConfigError( - validateSettingsConfig(configString), - ); - } catch (err) { - console.error("更新模板值失败:", err); - } - } - - return nextValues; - }); - }} - aria-label={config.label} - autoComplete="off" - className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" - /> -
- ))} -
-
- )} - - {!isCodex && shouldShowSpeedTest && ( -
-
- - -
- handleBaseUrlChange(e.target.value)} - placeholder={t("providerForm.apiEndpointPlaceholder")} - autoComplete="off" - className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" - /> -
-

- {t("providerForm.apiHint")} -

-
-
- )} - - {/* 端点测速弹窗 - Claude */} - {!isCodex && shouldShowSpeedTest && isEndpointModalOpen && ( - setIsEndpointModalOpen(false)} - onCustomEndpointsChange={setDraftCustomEndpoints} - /> - )} - - {!isCodex && shouldShowKimiSelector && ( - - )} - - {isCodex && showCodexApiKey && ( -
- = 0 && - !isCodexOfficialPreset - } - /> - {shouldShowCodexApiKeyLink && getCurrentCodexWebsiteUrl() && ( - - )} -
- )} - - {isCodex && shouldShowSpeedTest && ( -
-
- - -
- handleCodexBaseUrlChange(e.target.value)} - placeholder={t("providerForm.codexApiEndpointPlaceholder")} - autoComplete="off" - className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" - /> -
-

- {t("providerForm.codexApiHint")} -

-
-
- )} - - {/* 端点测速弹窗 - Codex */} - {isCodex && shouldShowSpeedTest && isCodexEndpointModalOpen && ( - setIsCodexEndpointModalOpen(false)} - onCustomEndpointsChange={setDraftCustomEndpoints} - /> - )} - - {/* Claude 或 Codex 的配置部分 */} - {isCodex ? ( - { - try { - const auth = JSON.parse(codexAuth || "{}"); - const key = - typeof auth.OPENAI_API_KEY === "string" - ? auth.OPENAI_API_KEY - : ""; - setCodexApiKey(key); - } catch { - // ignore - } - }} - useCommonConfig={useCodexCommonConfig} - onCommonConfigToggle={handleCodexCommonConfigToggle} - commonConfigSnippet={codexCommonConfigSnippet} - onCommonConfigSnippetChange={ - handleCodexCommonConfigSnippetChange - } - commonConfigError={codexCommonConfigError} - authError={codexAuthError} - isCustomMode={selectedCodexPreset === -1} - onWebsiteUrlChange={(url) => { - setFormData((prev) => ({ - ...prev, - websiteUrl: url, - })); - }} - onNameChange={(name) => { - setFormData((prev) => ({ - ...prev, - name, - })); - }} - isTemplateModalOpen={isCodexTemplateModalOpen} - setIsTemplateModalOpen={setIsCodexTemplateModalOpen} - /> - ) : ( - <> - {/* 可选的模型配置输入框 - 仅在非官方且非 Kimi 时显示 */} - {!isOfficialPreset && !shouldShowKimiSelector && ( -
-
-
- - - handleModelChange("ANTHROPIC_MODEL", e.target.value) - } - placeholder={t("providerForm.mainModelPlaceholder")} - autoComplete="off" - className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" - /> -
- -
- - - handleModelChange( - "ANTHROPIC_SMALL_FAST_MODEL", - e.target.value, - ) - } - placeholder={t("providerForm.fastModelPlaceholder")} - autoComplete="off" - className="w-full px-3 py-2 border border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400 transition-colors" - /> -
-
- -
-

- {t("providerForm.modelHint")} -

-
-
- )} - - - handleChange({ - target: { name: "settingsConfig", value }, - } as React.ChangeEvent) - } - useCommonConfig={useCommonConfig} - onCommonConfigToggle={handleCommonConfigToggle} - commonConfigSnippet={commonConfigSnippet} - onCommonConfigSnippetChange={handleCommonConfigSnippetChange} - commonConfigError={commonConfigError} - configError={settingsConfigError} - /> - - )} -
- - {/* Footer */} -
- - -
-
-
-
- ); -}; - -export default ProviderForm; diff --git a/src/components/ProviderForm/ApiKeyInput.tsx b/src/components/ProviderForm/ApiKeyInput.tsx deleted file mode 100644 index d78fd0d..0000000 --- a/src/components/ProviderForm/ApiKeyInput.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React, { useState } from "react"; -import { Eye, EyeOff } from "lucide-react"; -import { useTranslation } from "react-i18next"; - -interface ApiKeyInputProps { - value: string; - onChange: (value: string) => void; - placeholder?: string; - disabled?: boolean; - required?: boolean; - label?: string; - id?: string; -} - -const ApiKeyInput: React.FC = ({ - value, - onChange, - placeholder, - disabled = false, - required = false, - label = "API Key", - id = "apiKey", -}) => { - const { t } = useTranslation(); - const [showKey, setShowKey] = useState(false); - - const toggleShowKey = () => { - setShowKey(!showKey); - }; - - const inputClass = `w-full px-3 py-2 pr-10 border rounded-lg text-sm transition-colors ${ - disabled - ? "bg-gray-100 dark:bg-gray-800 border-gray-200 dark:border-gray-700 text-gray-400 dark:text-gray-500 cursor-not-allowed" - : "border-gray-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:focus:ring-blue-400/20 focus:border-blue-500 dark:focus:border-blue-400" - }`; - - return ( -
- -
- onChange(e.target.value)} - placeholder={placeholder ?? t("apiKeyInput.placeholder")} - disabled={disabled} - required={required} - autoComplete="off" - className={inputClass} - /> - {!disabled && value && ( - - )} -
-
- ); -}; - -export default ApiKeyInput; diff --git a/src/components/ProviderForm/ClaudeConfigEditor.tsx b/src/components/ProviderForm/ClaudeConfigEditor.tsx deleted file mode 100644 index 1def4e6..0000000 --- a/src/components/ProviderForm/ClaudeConfigEditor.tsx +++ /dev/null @@ -1,205 +0,0 @@ -import React, { useEffect, useState } from "react"; -import JsonEditor from "../JsonEditor"; -import { X, Save } from "lucide-react"; -import { isLinux } from "../../lib/platform"; -import { useTranslation } from "react-i18next"; - -interface ClaudeConfigEditorProps { - value: string; - onChange: (value: string) => void; - useCommonConfig: boolean; - onCommonConfigToggle: (checked: boolean) => void; - commonConfigSnippet: string; - onCommonConfigSnippetChange: (value: string) => void; - commonConfigError: string; - configError: string; -} - -const ClaudeConfigEditor: React.FC = ({ - value, - onChange, - useCommonConfig, - onCommonConfigToggle, - commonConfigSnippet, - onCommonConfigSnippetChange, - commonConfigError, - configError, -}) => { - const { t } = useTranslation(); - const [isDarkMode, setIsDarkMode] = useState(false); - const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); - - useEffect(() => { - // 检测暗色模式 - const checkDarkMode = () => { - setIsDarkMode(document.documentElement.classList.contains("dark")); - }; - - checkDarkMode(); - - // 监听暗色模式变化 - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === "class") { - checkDarkMode(); - } - }); - }); - - observer.observe(document.documentElement, { - attributes: true, - attributeFilter: ["class"], - }); - - return () => observer.disconnect(); - }, []); - - useEffect(() => { - if (commonConfigError && !isCommonConfigModalOpen) { - setIsCommonConfigModalOpen(true); - } - }, [commonConfigError, isCommonConfigModalOpen]); - - // 支持按下 ESC 关闭弹窗 - useEffect(() => { - if (!isCommonConfigModalOpen) return; - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - closeModal(); - } - }; - window.addEventListener("keydown", onKeyDown); - return () => window.removeEventListener("keydown", onKeyDown); - }, [isCommonConfigModalOpen]); - - const closeModal = () => { - setIsCommonConfigModalOpen(false); - }; - return ( -
-
- - -
-
- -
- {commonConfigError && !isCommonConfigModalOpen && ( -

- {commonConfigError} -

- )} - - {configError && ( -

{configError}

- )} -

- {t("claudeConfig.fullSettingsHint")} -

- {isCommonConfigModalOpen && ( -
{ - if (e.target === e.currentTarget) closeModal(); - }} - > - {/* Backdrop - 统一背景样式 */} -
- - {/* Modal - 统一窗口样式 */} -
- {/* Header - 统一标题栏样式 */} -
-

- {t("claudeConfig.editCommonConfigTitle")} -

- -
- - {/* Content - 统一内容区域样式 */} -
-

- {t("claudeConfig.commonConfigHint")} -

- - {commonConfigError && ( -

- {commonConfigError} -

- )} -
- - {/* Footer - 统一底部按钮样式 */} -
- - -
-
-
- )} -
- ); -}; - -export default ClaudeConfigEditor; diff --git a/src/components/ProviderForm/CodexConfigEditor.tsx b/src/components/ProviderForm/CodexConfigEditor.tsx deleted file mode 100644 index 17f6201..0000000 --- a/src/components/ProviderForm/CodexConfigEditor.tsx +++ /dev/null @@ -1,667 +0,0 @@ -import React, { useState, useEffect, useRef } from "react"; - -import { X, Save } from "lucide-react"; - -import { isLinux } from "../../lib/platform"; -import { useTranslation } from "react-i18next"; - -import { - generateThirdPartyAuth, - generateThirdPartyConfig, -} from "../../config/codexProviderPresets"; - -interface CodexConfigEditorProps { - authValue: string; - - configValue: string; - - onAuthChange: (value: string) => void; - - onConfigChange: (value: string) => void; - - onAuthBlur?: () => void; - - useCommonConfig: boolean; - - onCommonConfigToggle: (checked: boolean) => void; - - commonConfigSnippet: string; - - onCommonConfigSnippetChange: (value: string) => void; - - commonConfigError: string; - - authError: string; - - isCustomMode?: boolean; // 新增:是否为自定义模式 - - onWebsiteUrlChange?: (url: string) => void; // 新增:更新网址回调 - - isTemplateModalOpen?: boolean; // 新增:模态框状态 - - setIsTemplateModalOpen?: (open: boolean) => void; // 新增:设置模态框状态 - - onNameChange?: (name: string) => void; // 新增:更新供应商名称回调 -} - -const CodexConfigEditor: React.FC = ({ - authValue, - - configValue, - - onAuthChange, - - onConfigChange, - - onAuthBlur, - - useCommonConfig, - - onCommonConfigToggle, - - commonConfigSnippet, - - onCommonConfigSnippetChange, - - commonConfigError, - - authError, - - onWebsiteUrlChange, - - onNameChange, - - isTemplateModalOpen: externalTemplateModalOpen, - - setIsTemplateModalOpen: externalSetTemplateModalOpen, -}) => { - const { t } = useTranslation(); - const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); - - // 使用内部状态或外部状态 - - const [internalTemplateModalOpen, setInternalTemplateModalOpen] = - useState(false); - - const isTemplateModalOpen = - externalTemplateModalOpen ?? internalTemplateModalOpen; - - const setIsTemplateModalOpen = - externalSetTemplateModalOpen ?? setInternalTemplateModalOpen; - - const [templateApiKey, setTemplateApiKey] = useState(""); - - const [templateProviderName, setTemplateProviderName] = useState(""); - - const [templateBaseUrl, setTemplateBaseUrl] = useState(""); - - const [templateWebsiteUrl, setTemplateWebsiteUrl] = useState(""); - - const [templateModelName, setTemplateModelName] = useState("gpt-5-codex"); - const apiKeyInputRef = useRef(null); - - const baseUrlInputRef = useRef(null); - - const modelNameInputRef = useRef(null); - const displayNameInputRef = useRef(null); - - // 移除自动填充逻辑,因为现在在点击自定义按钮时就已经填充 - - const [templateDisplayName, setTemplateDisplayName] = useState(""); - - useEffect(() => { - if (commonConfigError && !isCommonConfigModalOpen) { - setIsCommonConfigModalOpen(true); - } - }, [commonConfigError, isCommonConfigModalOpen]); - - // 支持按下 ESC 关闭弹窗 - - useEffect(() => { - if (!isCommonConfigModalOpen) return; - - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - - closeModal(); - } - }; - - window.addEventListener("keydown", onKeyDown); - - return () => window.removeEventListener("keydown", onKeyDown); - }, [isCommonConfigModalOpen]); - - const closeModal = () => { - setIsCommonConfigModalOpen(false); - }; - - const closeTemplateModal = () => { - setIsTemplateModalOpen(false); - }; - - const applyTemplate = () => { - const requiredInputs = [ - displayNameInputRef.current, - apiKeyInputRef.current, - baseUrlInputRef.current, - modelNameInputRef.current, - ]; - - for (const input of requiredInputs) { - if (input && !input.checkValidity()) { - input.reportValidity(); - input.focus(); - return; - } - } - - const trimmedKey = templateApiKey.trim(); - - const trimmedBaseUrl = templateBaseUrl.trim(); - - const trimmedModel = templateModelName.trim(); - - const auth = generateThirdPartyAuth(trimmedKey); - - const config = generateThirdPartyConfig( - templateProviderName || "custom", - - trimmedBaseUrl, - - trimmedModel, - ); - - onAuthChange(JSON.stringify(auth, null, 2)); - - onConfigChange(config); - - if (onWebsiteUrlChange) { - const trimmedWebsite = templateWebsiteUrl.trim(); - - if (trimmedWebsite) { - onWebsiteUrlChange(trimmedWebsite); - } - } - - if (onNameChange) { - const trimmedName = templateDisplayName.trim(); - if (trimmedName) { - onNameChange(trimmedName); - } - } - - setTemplateApiKey(""); - - setTemplateProviderName(""); - - setTemplateBaseUrl(""); - - setTemplateWebsiteUrl(""); - - setTemplateModelName("gpt-5-codex"); - - setTemplateDisplayName(""); - - closeTemplateModal(); - }; - - const handleTemplateInputKeyDown = ( - e: React.KeyboardEvent, - ) => { - if (e.key === "Enter") { - e.preventDefault(); - - e.stopPropagation(); - - applyTemplate(); - } - }; - - const handleAuthChange = (value: string) => { - onAuthChange(value); - }; - - const handleConfigChange = (value: string) => { - onConfigChange(value); - }; - - const handleCommonConfigSnippetChange = (value: string) => { - onCommonConfigSnippetChange(value); - }; - - return ( -
-
- - -