import { useEffect, useMemo, useState, useCallback } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; import { Button } from "@/components/ui/button"; import { Form } from "@/components/ui/form"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppType } from "@/lib/api"; import type { ProviderCategory, CustomEndpoint } from "@/types"; import { providerPresets, type ProviderPreset } from "@/config/providerPresets"; import { codexProviderPresets, type CodexProviderPreset, } from "@/config/codexProviderPresets"; import { applyTemplateValues } from "@/utils/providerConfigUtils"; import CodexConfigEditor from "./CodexConfigEditor"; import { CommonConfigEditor } from "./CommonConfigEditor"; import { ProviderPresetSelector } from "./ProviderPresetSelector"; import { BasicFormFields } from "./BasicFormFields"; import { ClaudeFormFields } from "./ClaudeFormFields"; import { CodexFormFields } from "./CodexFormFields"; import { useProviderCategory, useApiKeyState, useBaseUrlState, useModelState, useCodexConfigState, useApiKeyLink, useCustomEndpoints, useKimiModelSelector, useTemplateValues, useCommonConfigSnippet, useCodexCommonConfig, useSpeedTestEndpoints, useCodexTomlValidation, } from "./hooks"; const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {}, config: {} }, null, 2); const CODEX_DEFAULT_CONFIG = JSON.stringify({ auth: {}, config: "" }, null, 2); type PresetEntry = { id: string; preset: ProviderPreset | CodexProviderPreset; }; interface ProviderFormProps { appType: AppType; submitLabel: string; onSubmit: (values: ProviderFormValues) => void; onCancel: () => void; initialData?: { name?: string; websiteUrl?: string; settingsConfig?: Record; }; } export function ProviderForm({ appType, submitLabel, onSubmit, onCancel, initialData, }: ProviderFormProps) { const { t } = useTranslation(); const isEditMode = Boolean(initialData); const [selectedPresetId, setSelectedPresetId] = useState( initialData ? null : "custom", ); const [activePreset, setActivePreset] = useState<{ id: string; category?: ProviderCategory; } | null>(null); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); // 新建供应商:收集端点测速弹窗中的"自定义端点",提交时一次性落盘到 meta.custom_endpoints const [draftCustomEndpoints, setDraftCustomEndpoints] = useState( [], ); // 使用 category hook const { category } = useProviderCategory({ appType, selectedPresetId, isEditMode, }); useEffect(() => { setSelectedPresetId(initialData ? null : "custom"); setActivePreset(null); }, [appType, initialData]); const defaultValues: ProviderFormData = useMemo( () => ({ name: initialData?.name ?? "", websiteUrl: initialData?.websiteUrl ?? "", settingsConfig: initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig, null, 2) : appType === "codex" ? CODEX_DEFAULT_CONFIG : CLAUDE_DEFAULT_CONFIG, }), [initialData, appType], ); const form = useForm({ resolver: zodResolver(providerSchema), defaultValues, mode: "onSubmit", }); // 使用 API Key hook const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey, } = useApiKeyState({ initialConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), selectedPresetId, }); // 使用 Base URL hook (仅 Claude 模式) const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ appType, category, settingsConfig: form.watch("settingsConfig"), codexConfig: "", onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), onCodexConfigChange: () => { // Codex 使用 useCodexConfigState 管理 Base URL }, }); // 使用 Model hook const { claudeModel, claudeSmallFastModel, handleModelChange } = useModelState({ settingsConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), }); // 使用 Codex 配置 hook (仅 Codex 模式) const { codexAuth, codexConfig, codexApiKey, codexBaseUrl, codexAuthError, setCodexAuth, handleCodexApiKeyChange, handleCodexBaseUrlChange, 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] = useState(false); useEffect(() => { form.reset(defaultValues); }, [defaultValues, form]); const presetCategoryLabels: Record = useMemo( () => ({ official: t("providerPreset.categoryOfficial", { defaultValue: "官方", }), cn_official: t("providerPreset.categoryCnOfficial", { defaultValue: "国内官方", }), aggregator: t("providerPreset.categoryAggregator", { defaultValue: "聚合服务", }), third_party: t("providerPreset.categoryThirdParty", { defaultValue: "第三方", }), }), [t], ); const presetEntries = useMemo(() => { if (appType === "codex") { return codexProviderPresets.map((preset, index) => ({ id: `codex-${index}`, preset, })); } return providerPresets.map((preset, index) => ({ id: `claude-${index}`, preset, })); }, [appType]); // 使用 Kimi 模型选择器 hook const { shouldShow: shouldShowKimiSelector, kimiAnthropicModel, kimiAnthropicSmallFastModel, handleKimiModelChange, } = useKimiModelSelector({ initialData, settingsConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), selectedPresetId, presetName: selectedPresetId && selectedPresetId !== "custom" ? presetEntries.find((item) => item.id === selectedPresetId)?.preset .name || "" : "", }); // 使用模板变量 hook (仅 Claude 模式) const { templateValues, templateValueEntries, selectedPreset: templatePreset, handleTemplateValueChange, validateTemplateValues, } = useTemplateValues({ selectedPresetId: appType === "claude" ? selectedPresetId : null, presetEntries: appType === "claude" ? presetEntries : [], settingsConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), }); // 使用通用配置片段 hook (仅 Claude 模式) const { useCommonConfig, commonConfigSnippet, commonConfigError, handleCommonConfigToggle, handleCommonConfigSnippetChange, } = useCommonConfigSnippet({ settingsConfig: form.watch("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), initialData: appType === "claude" ? initialData : undefined, }); // 使用 Codex 通用配置片段 hook (仅 Codex 模式) const { useCommonConfig: useCodexCommonConfigFlag, commonConfigSnippet: codexCommonConfigSnippet, commonConfigError: codexCommonConfigError, handleCommonConfigToggle: handleCodexCommonConfigToggle, handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange, } = useCodexCommonConfig({ codexConfig, onConfigChange: handleCodexConfigChange, initialData: appType === "codex" ? initialData : undefined, }); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const handleSubmit = (values: ProviderFormData) => { // 验证模板变量(仅 Claude 模式) if (appType === "claude" && templateValueEntries.length > 0) { const validation = validateTemplateValues(); if (!validation.isValid && validation.missingField) { form.setError("settingsConfig", { type: "manual", message: t("providerForm.fillParameter", { label: validation.missingField.label, defaultValue: `请填写 ${validation.missingField.label}`, }), }); return; } } let settingsConfig: string; // Codex: 组合 auth 和 config if (appType === "codex") { try { const authJson = JSON.parse(codexAuth); const configObj = { auth: authJson, config: codexConfig ?? "", }; settingsConfig = JSON.stringify(configObj); } catch (err) { // 如果解析失败,使用表单中的配置 settingsConfig = values.settingsConfig.trim(); } } else { // Claude: 使用表单配置 settingsConfig = values.settingsConfig.trim(); } const payload: ProviderFormValues = { ...values, name: values.name.trim(), websiteUrl: values.websiteUrl?.trim() ?? "", settingsConfig, }; if (activePreset) { payload.presetId = activePreset.id; if (activePreset.category) { payload.presetCategory = activePreset.category; } } // 新建供应商时:添加自定义端点 if (!initialData && customEndpointsMap) { payload.meta = { custom_endpoints: customEndpointsMap }; } onSubmit(payload); }; const groupedPresets = useMemo(() => { return presetEntries.reduce>((acc, entry) => { const category = entry.preset.category ?? "others"; if (!acc[category]) { acc[category] = []; } acc[category].push(entry); return acc; }, {}); }, [presetEntries]); const categoryKeys = useMemo(() => { return Object.keys(groupedPresets).filter( (key) => key !== "custom" && groupedPresets[key]?.length, ); }, [groupedPresets]); // 判断是否显示端点测速(仅第三方和自定义类别) const shouldShowSpeedTest = category === "third_party" || category === "custom"; // 使用 API Key 链接 hook (Claude) const { shouldShowApiKeyLink: shouldShowClaudeApiKeyLink, websiteUrl: claudeWebsiteUrl, } = useApiKeyLink({ appType: "claude", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); // 使用 API Key 链接 hook (Codex) const { shouldShowApiKeyLink: shouldShowCodexApiKeyLink, websiteUrl: codexWebsiteUrl, } = useApiKeyLink({ appType: "codex", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); // 使用自定义端点 hook const customEndpointsMap = useCustomEndpoints({ appType, selectedPresetId, presetEntries, draftCustomEndpoints, baseUrl, codexBaseUrl, }); // 使用端点测速候选 hook const speedTestEndpoints = useSpeedTestEndpoints({ appType, selectedPresetId, presetEntries, baseUrl, codexBaseUrl, initialData, }); const handlePresetChange = (value: string) => { setSelectedPresetId(value); if (value === "custom") { setActivePreset(null); form.reset(defaultValues); // Codex 自定义模式:重置为空配置 if (appType === "codex") { resetCodexConfig({}, ""); } return; } const entry = presetEntries.find((item) => item.id === value); if (!entry) { return; } setActivePreset({ id: value, category: entry.preset.category, }); if (appType === "codex") { const preset = entry.preset as CodexProviderPreset; const auth = preset.auth ?? {}; const config = preset.config ?? ""; // 重置 Codex 配置 resetCodexConfig(auth, config); // 更新表单其他字段 form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify({ auth, config }, null, 2), }); return; } const preset = entry.preset as ProviderPreset; const config = applyTemplateValues( preset.settingsConfig, preset.templateValues, ); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), }); }; return (
{/* 预设供应商选择(仅新增模式显示) */} {!initialData && ( )} {/* 基础字段 */} {/* Claude 专属字段 */} {appType === "claude" && ( )} {/* Codex 专属字段 */} {appType === "codex" && ( )} {/* 配置编辑器:Claude 使用通用配置编辑器,Codex 使用专用编辑器 */} {appType === "codex" ? ( form.setValue("websiteUrl", url)} onNameChange={(name) => form.setValue("name", name)} isTemplateModalOpen={isCodexTemplateModalOpen} setIsTemplateModalOpen={setIsCodexTemplateModalOpen} /> ) : ( form.setValue("settingsConfig", value)} useCommonConfig={useCommonConfig} onCommonConfigToggle={handleCommonConfigToggle} commonConfigSnippet={commonConfigSnippet} onCommonConfigSnippetChange={handleCommonConfigSnippetChange} commonConfigError={commonConfigError} onEditClick={() => setIsCommonConfigModalOpen(true)} isModalOpen={isCommonConfigModalOpen} onModalClose={() => setIsCommonConfigModalOpen(false)} /> )}
); } export type ProviderFormValues = ProviderFormData & { presetId?: string; presetCategory?: ProviderCategory; meta?: { custom_endpoints?: Record; }; };