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 { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import type { AppId } from "@/lib/api"; import type { ProviderCategory, ProviderMeta, ProviderTestConfig, ProviderProxyConfig, ClaudeApiFormat, ClaudeApiKeyField, } from "@/types"; import { providerPresets, type ProviderPreset, } from "@/config/claudeProviderPresets"; import { codexProviderPresets, type CodexProviderPreset, } from "@/config/codexProviderPresets"; import { geminiProviderPresets, type GeminiProviderPreset, } from "@/config/geminiProviderPresets"; import { opencodeProviderPresets, type OpenCodeProviderPreset, } from "@/config/opencodeProviderPresets"; import { openclawProviderPresets, type OpenClawProviderPreset, type OpenClawSuggestedDefaults, } from "@/config/openclawProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; import { OpenClawFormFields } from "./OpenClawFormFields"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; import { applyTemplateValues, hasApiKeyField, } from "@/utils/providerConfigUtils"; import { mergeProviderMeta } from "@/utils/providerMetaUtils"; import { getCodexCustomTemplate } from "@/config/codexTemplates"; import CodexConfigEditor from "./CodexConfigEditor"; import GeminiConfigEditor from "./GeminiConfigEditor"; import JsonEditor from "@/components/JsonEditor"; import { ClaudeQuickToggles, jsonMergePatch } from "./ClaudeQuickToggles"; import { Label } from "@/components/ui/label"; import { ProviderPresetSelector } from "./ProviderPresetSelector"; import { BasicFormFields } from "./BasicFormFields"; import { ClaudeFormFields } from "./ClaudeFormFields"; import { CodexFormFields } from "./CodexFormFields"; import { GeminiFormFields } from "./GeminiFormFields"; import { OmoFormFields } from "./OmoFormFields"; import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor"; import { parseOmoOtherFieldsObject } from "@/types/omo"; import { ProviderAdvancedConfig, type PricingModelSourceOption, } from "./ProviderAdvancedConfig"; import { useProviderCategory, useApiKeyState, useBaseUrlState, useModelState, useCodexConfigState, useApiKeyLink, useTemplateValues, useSpeedTestEndpoints, useCodexTomlValidation, useGeminiConfigState, useOmoModelSource, useOpencodeFormState, useOmoDraftState, useOpenclawFormState, } from "./hooks"; import { useOmoGlobalConfig, useOmoSlimGlobalConfig } from "@/lib/query/omo"; import { CLAUDE_DEFAULT_CONFIG, CODEX_DEFAULT_CONFIG, GEMINI_DEFAULT_CONFIG, OPENCODE_DEFAULT_CONFIG, OPENCLAW_DEFAULT_CONFIG, normalizePricingSource, } from "./helpers/opencodeFormUtils"; type PresetEntry = { id: string; preset: | ProviderPreset | CodexProviderPreset | GeminiProviderPreset | OpenCodeProviderPreset | OpenClawProviderPreset; }; interface ProviderFormProps { appId: AppId; providerId?: string; submitLabel: string; onSubmit: (values: ProviderFormValues) => void; onCancel: () => void; onUniversalPresetSelect?: (preset: UniversalProviderPreset) => void; onManageUniversalProviders?: () => void; initialData?: { name?: string; websiteUrl?: string; notes?: string; settingsConfig?: Record; category?: ProviderCategory; meta?: ProviderMeta; icon?: string; iconColor?: string; }; showButtons?: boolean; isCurrent?: boolean; } export function ProviderForm({ appId, providerId, submitLabel, onSubmit, onCancel, onUniversalPresetSelect, onManageUniversalProviders, initialData, showButtons = true, isCurrent = false, }: ProviderFormProps) { const { t } = useTranslation(); const isEditMode = Boolean(initialData); const [selectedPresetId, setSelectedPresetId] = useState( initialData ? null : "custom", ); const [activePreset, setActivePreset] = useState<{ id: string; category?: ProviderCategory; isPartner?: boolean; partnerPromotionKey?: string; suggestedDefaults?: OpenClawSuggestedDefaults; } | null>(null); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); const [draftCustomEndpoints, setDraftCustomEndpoints] = useState( () => { if (initialData) return []; return []; }, ); const [endpointAutoSelect, setEndpointAutoSelect] = useState( () => initialData?.meta?.endpointAutoSelect ?? true, ); const [testConfig, setTestConfig] = useState( () => initialData?.meta?.testConfig ?? { enabled: false }, ); const [proxyConfig, setProxyConfig] = useState( () => initialData?.meta?.proxyConfig ?? { enabled: false }, ); const [pricingConfig, setPricingConfig] = useState<{ enabled: boolean; costMultiplier?: string; pricingModelSource: PricingModelSourceOption; }>(() => ({ enabled: initialData?.meta?.costMultiplier !== undefined || initialData?.meta?.pricingModelSource !== undefined, costMultiplier: initialData?.meta?.costMultiplier, pricingModelSource: normalizePricingSource( initialData?.meta?.pricingModelSource, ), })); const { category } = useProviderCategory({ appId, selectedPresetId, isEditMode, initialCategory: initialData?.category, }); const isOmoCategory = appId === "opencode" && category === "omo"; const isOmoSlimCategory = appId === "opencode" && category === "omo-slim"; const isAnyOmoCategory = isOmoCategory || isOmoSlimCategory; const { data: queriedStandardOmoGlobalConfig } = useOmoGlobalConfig(isOmoCategory); const { data: queriedSlimOmoGlobalConfig } = useOmoSlimGlobalConfig(isOmoSlimCategory); const queriedOmoGlobalConfig = isOmoSlimCategory ? queriedSlimOmoGlobalConfig : queriedStandardOmoGlobalConfig; useEffect(() => { setSelectedPresetId(initialData ? null : "custom"); setActivePreset(null); if (!initialData) { setDraftCustomEndpoints([]); } setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true); setTestConfig(initialData?.meta?.testConfig ?? { enabled: false }); setProxyConfig(initialData?.meta?.proxyConfig ?? { enabled: false }); setPricingConfig({ enabled: initialData?.meta?.costMultiplier !== undefined || initialData?.meta?.pricingModelSource !== undefined, costMultiplier: initialData?.meta?.costMultiplier, pricingModelSource: normalizePricingSource( initialData?.meta?.pricingModelSource, ), }); }, [appId, initialData]); const defaultValues: ProviderFormData = useMemo( () => ({ name: initialData?.name ?? "", websiteUrl: initialData?.websiteUrl ?? "", notes: initialData?.notes ?? "", settingsConfig: initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig, null, 2) : appId === "codex" ? CODEX_DEFAULT_CONFIG : appId === "gemini" ? GEMINI_DEFAULT_CONFIG : appId === "opencode" ? OPENCODE_DEFAULT_CONFIG : appId === "openclaw" ? OPENCLAW_DEFAULT_CONFIG : CLAUDE_DEFAULT_CONFIG, icon: initialData?.icon ?? "", iconColor: initialData?.iconColor ?? "", }), [initialData, appId], ); const form = useForm({ resolver: zodResolver(providerSchema), defaultValues, mode: "onSubmit", }); const [localApiFormat, setLocalApiFormat] = useState(() => { if (appId !== "claude") return "anthropic"; return initialData?.meta?.apiFormat ?? "anthropic"; }); const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { setLocalApiFormat(format); }, []); const [localApiKeyField, setLocalApiKeyField] = useState( () => { if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN"; if (initialData?.meta?.apiKeyField) return initialData.meta.apiKeyField; try { const config = initialData?.settingsConfig; if ( config?.env && (config.env as Record).ANTHROPIC_API_KEY !== undefined ) return "ANTHROPIC_API_KEY"; } catch {} return "ANTHROPIC_AUTH_TOKEN"; }, ); const handleApiKeyFieldChange = useCallback( (field: ClaudeApiKeyField) => { setLocalApiKeyField(field); try { const config = JSON.parse(form.getValues("settingsConfig") || "{}") as { env?: Record; }; const env = (config.env ?? {}) as Record; const oldField = field === "ANTHROPIC_API_KEY" ? "ANTHROPIC_AUTH_TOKEN" : "ANTHROPIC_API_KEY"; if (oldField in env) { env[field] = env[oldField]; delete env[oldField]; config.env = env; form.setValue("settingsConfig", JSON.stringify(config, null, 2)); } } catch {} }, [form], ); const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey, } = useApiKeyState({ initialConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), selectedPresetId, category, appType: appId, apiKeyField: appId === "claude" ? localApiKeyField : undefined, }); const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ appType: appId, category, settingsConfig: form.getValues("settingsConfig"), codexConfig: "", onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), onCodexConfigChange: () => {}, }); const { claudeModel, reasoningModel, defaultHaikuModel, defaultSonnetModel, defaultOpusModel, handleModelChange, } = useModelState({ settingsConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), }); const { codexAuth, codexConfig, codexApiKey, codexBaseUrl, codexModelName, codexAuthError, setCodexAuth, handleCodexApiKeyChange, handleCodexBaseUrlChange, handleCodexModelNameChange, handleCodexConfigChange: originalHandleCodexConfigChange, resetCodexConfig, } = useCodexConfigState({ initialData }); const { configError: codexConfigError, debouncedValidate } = useCodexTomlValidation(); const handleCodexConfigChange = useCallback( (value: string) => { originalHandleCodexConfigChange(value); debouncedValidate(value); }, [originalHandleCodexConfigChange, debouncedValidate], ); useEffect(() => { if (appId === "codex" && !initialData && selectedPresetId === "custom") { const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); } }, [appId, initialData, selectedPresetId, resetCodexConfig]); useEffect(() => { form.reset(defaultValues); }, [defaultValues, form]); const presetCategoryLabels: Record = useMemo( () => ({ official: t("providerForm.categoryOfficial", { defaultValue: "官方", }), cn_official: t("providerForm.categoryCnOfficial", { defaultValue: "国内官方", }), aggregator: t("providerForm.categoryAggregation", { defaultValue: "聚合服务", }), third_party: t("providerForm.categoryThirdParty", { defaultValue: "第三方", }), omo: "OMO", }), [t], ); const presetEntries = useMemo(() => { if (appId === "codex") { return codexProviderPresets.map((preset, index) => ({ id: `codex-${index}`, preset, })); } else if (appId === "gemini") { return geminiProviderPresets.map((preset, index) => ({ id: `gemini-${index}`, preset, })); } else if (appId === "opencode") { return opencodeProviderPresets.map((preset, index) => ({ id: `opencode-${index}`, preset, })); } else if (appId === "openclaw") { return openclawProviderPresets.map((preset, index) => ({ id: `openclaw-${index}`, preset, })); } return providerPresets.map((preset, index) => ({ id: `claude-${index}`, preset, })); }, [appId]); const { templateValues, templateValueEntries, selectedPreset: templatePreset, handleTemplateValueChange, validateTemplateValues, } = useTemplateValues({ selectedPresetId: appId === "claude" ? selectedPresetId : null, presetEntries: appId === "claude" ? presetEntries : [], settingsConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), }); const { geminiEnv, geminiConfig, geminiApiKey, geminiBaseUrl, geminiModel, envError, configError: geminiConfigError, handleGeminiApiKeyChange: originalHandleGeminiApiKeyChange, handleGeminiBaseUrlChange: originalHandleGeminiBaseUrlChange, handleGeminiModelChange: originalHandleGeminiModelChange, handleGeminiEnvChange, handleGeminiConfigChange, resetGeminiConfig, envStringToObj, } = useGeminiConfigState({ initialData: appId === "gemini" ? initialData : undefined, }); const updateGeminiEnvField = useCallback( ( key: "GEMINI_API_KEY" | "GOOGLE_GEMINI_BASE_URL" | "GEMINI_MODEL", value: string, ) => { try { const config = JSON.parse(form.getValues("settingsConfig") || "{}") as { env?: Record; }; if (!config.env || typeof config.env !== "object") { config.env = {}; } config.env[key] = value; form.setValue("settingsConfig", JSON.stringify(config, null, 2)); } catch {} }, [form], ); const handleGeminiApiKeyChange = useCallback( (key: string) => { originalHandleGeminiApiKeyChange(key); updateGeminiEnvField("GEMINI_API_KEY", key.trim()); }, [originalHandleGeminiApiKeyChange, updateGeminiEnvField], ); const handleGeminiBaseUrlChange = useCallback( (url: string) => { originalHandleGeminiBaseUrlChange(url); updateGeminiEnvField( "GOOGLE_GEMINI_BASE_URL", url.trim().replace(/\/+$/, ""), ); }, [originalHandleGeminiBaseUrlChange, updateGeminiEnvField], ); const handleGeminiModelChange = useCallback( (model: string) => { originalHandleGeminiModelChange(model); updateGeminiEnvField("GEMINI_MODEL", model.trim()); }, [originalHandleGeminiModelChange, updateGeminiEnvField], ); // ── Extracted hooks: OpenCode / OMO / OpenClaw ───────────────────── const { omoModelOptions, omoModelVariantsMap, omoPresetMetaMap, existingOpencodeKeys, } = useOmoModelSource({ isOmoCategory: isAnyOmoCategory, providerId }); const opencodeForm = useOpencodeFormState({ initialData, appId, providerId, onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), getSettingsConfig: () => form.getValues("settingsConfig"), }); const initialOmoSettings = appId === "opencode" && (initialData?.category === "omo" || initialData?.category === "omo-slim") ? (initialData.settingsConfig as Record | undefined) : undefined; const omoDraft = useOmoDraftState({ initialOmoSettings, queriedOmoGlobalConfig, isEditMode, appId, category, }); const openclawForm = useOpenclawFormState({ initialData, appId, providerId, onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), getSettingsConfig: () => form.getValues("settingsConfig"), }); const handleSubmit = (values: ProviderFormData) => { if (appId === "claude" && templateValueEntries.length > 0) { const validation = validateTemplateValues(); if (!validation.isValid && validation.missingField) { toast.error( t("providerForm.fillParameter", { label: validation.missingField.label, defaultValue: `请填写 ${validation.missingField.label}`, }), ); return; } } if (!values.name.trim()) { toast.error( t("providerForm.fillSupplierName", { defaultValue: "请填写供应商名称", }), ); return; } if (appId === "opencode" && !isAnyOmoCategory) { const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; if (!opencodeForm.opencodeProviderKey.trim()) { toast.error(t("opencode.providerKeyRequired")); return; } if (!keyPattern.test(opencodeForm.opencodeProviderKey)) { toast.error(t("opencode.providerKeyInvalid")); return; } if ( !isEditMode && existingOpencodeKeys.includes(opencodeForm.opencodeProviderKey) ) { toast.error(t("opencode.providerKeyDuplicate")); return; } if (Object.keys(opencodeForm.opencodeModels).length === 0) { toast.error(t("opencode.modelsRequired")); return; } } // OpenClaw: validate provider key if (appId === "openclaw") { const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; if (!openclawForm.openclawProviderKey.trim()) { toast.error(t("openclaw.providerKeyRequired")); return; } if (!keyPattern.test(openclawForm.openclawProviderKey)) { toast.error(t("openclaw.providerKeyInvalid")); return; } if ( !isEditMode && openclawForm.existingOpenclawKeys.includes( openclawForm.openclawProviderKey, ) ) { toast.error(t("openclaw.providerKeyDuplicate")); return; } } // 非官方供应商必填校验:端点和 API Key // cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验 if (category !== "official" && category !== "cloud_provider") { if (appId === "claude") { if (!baseUrl.trim()) { toast.error( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); return; } if (!apiKey.trim()) { toast.error( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); return; } } else if (appId === "codex") { if (!codexBaseUrl.trim()) { toast.error( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); return; } if (!codexApiKey.trim()) { toast.error( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); return; } } else if (appId === "gemini") { if (!geminiBaseUrl.trim()) { toast.error( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); return; } if (!geminiApiKey.trim()) { toast.error( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); return; } } } let settingsConfig: string; if (appId === "codex") { try { const authJson = JSON.parse(codexAuth); const configObj = { auth: authJson, config: codexConfig ?? "", }; settingsConfig = JSON.stringify(configObj); } catch (err) { settingsConfig = values.settingsConfig.trim(); } } else if (appId === "gemini") { try { const envObj = envStringToObj(geminiEnv); const configObj = geminiConfig.trim() ? JSON.parse(geminiConfig) : {}; const combined = { env: envObj, config: configObj, }; settingsConfig = JSON.stringify(combined); } catch (err) { settingsConfig = values.settingsConfig.trim(); } } else if ( appId === "opencode" && (category === "omo" || category === "omo-slim") ) { const omoConfig: Record = {}; omoConfig.useCommonConfig = omoDraft.useOmoCommonConfig; if (Object.keys(omoDraft.omoAgents).length > 0) { omoConfig.agents = omoDraft.omoAgents; } if ( category === "omo" && Object.keys(omoDraft.omoCategories).length > 0 ) { omoConfig.categories = omoDraft.omoCategories; } if (omoDraft.omoOtherFieldsStr.trim()) { try { const otherFields = parseOmoOtherFieldsObject( omoDraft.omoOtherFieldsStr, ); if (!otherFields) { toast.error( t("omo.jsonMustBeObject", { field: t("omo.otherFields", { defaultValue: "Other Config", }), defaultValue: "{{field}} must be a JSON object", }), ); return; } omoConfig.otherFields = otherFields; } catch { toast.error( t("omo.invalidJson", { defaultValue: "Other Fields contains invalid JSON", }), ); return; } } settingsConfig = JSON.stringify(omoConfig); } else { settingsConfig = values.settingsConfig.trim(); } const payload: ProviderFormValues = { ...values, name: values.name.trim(), websiteUrl: values.websiteUrl?.trim() ?? "", settingsConfig, }; if (appId === "opencode") { if (isAnyOmoCategory) { if (!isEditMode) { const prefix = category === "omo" ? "omo" : "omo-slim"; payload.providerKey = `${prefix}-${crypto.randomUUID().slice(0, 8)}`; } } else { payload.providerKey = opencodeForm.opencodeProviderKey; } } else if (appId === "openclaw") { payload.providerKey = openclawForm.openclawProviderKey; } if (isAnyOmoCategory && !payload.presetCategory) { payload.presetCategory = category; } if (activePreset) { payload.presetId = activePreset.id; if (activePreset.category) { payload.presetCategory = activePreset.category; } if (activePreset.isPartner) { payload.isPartner = activePreset.isPartner; } // OpenClaw: 传递预设的 suggestedDefaults 到提交数据 if (activePreset.suggestedDefaults) { payload.suggestedDefaults = activePreset.suggestedDefaults; } } if (!isEditMode && draftCustomEndpoints.length > 0) { const customEndpointsToSave: Record< string, import("@/types").CustomEndpoint > = draftCustomEndpoints.reduce( (acc, url) => { const now = Date.now(); acc[url] = { url, addedAt: now, lastUsed: undefined }; return acc; }, {} as Record, ); const hadEndpoints = initialData?.meta?.custom_endpoints && Object.keys(initialData.meta.custom_endpoints).length > 0; const needsClearEndpoints = hadEndpoints && draftCustomEndpoints.length === 0; let mergedMeta = needsClearEndpoints ? mergeProviderMeta(initialData?.meta, {}) : mergeProviderMeta(initialData?.meta, customEndpointsToSave); if (activePreset?.isPartner) { mergedMeta = { ...(mergedMeta ?? {}), isPartner: true, }; } if (activePreset?.partnerPromotionKey) { mergedMeta = { ...(mergedMeta ?? {}), partnerPromotionKey: activePreset.partnerPromotionKey, }; } if (mergedMeta !== undefined) { payload.meta = mergedMeta; } } const baseMeta: ProviderMeta | undefined = payload.meta ?? (initialData?.meta ? { ...initialData.meta } : undefined); payload.meta = { ...(baseMeta ?? {}), endpointAutoSelect, testConfig: testConfig.enabled ? testConfig : undefined, proxyConfig: proxyConfig.enabled ? proxyConfig : undefined, costMultiplier: pricingConfig.enabled ? pricingConfig.costMultiplier : undefined, pricingModelSource: pricingConfig.enabled && pricingConfig.pricingModelSource !== "inherit" ? pricingConfig.pricingModelSource : undefined, apiFormat: appId === "claude" && category !== "official" ? localApiFormat : undefined, apiKeyField: appId === "claude" && category !== "official" ? localApiKeyField : undefined, }; 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 !== "official" && category !== "cloud_provider"; const { shouldShowApiKeyLink: shouldShowClaudeApiKeyLink, websiteUrl: claudeWebsiteUrl, isPartner: isClaudePartner, partnerPromotionKey: claudePartnerPromotionKey, } = useApiKeyLink({ appId: "claude", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const { shouldShowApiKeyLink: shouldShowCodexApiKeyLink, websiteUrl: codexWebsiteUrl, isPartner: isCodexPartner, partnerPromotionKey: codexPartnerPromotionKey, } = useApiKeyLink({ appId: "codex", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const { shouldShowApiKeyLink: shouldShowGeminiApiKeyLink, websiteUrl: geminiWebsiteUrl, isPartner: isGeminiPartner, partnerPromotionKey: geminiPartnerPromotionKey, } = useApiKeyLink({ appId: "gemini", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const { shouldShowApiKeyLink: shouldShowOpencodeApiKeyLink, websiteUrl: opencodeWebsiteUrl, isPartner: isOpencodePartner, partnerPromotionKey: opencodePartnerPromotionKey, } = useApiKeyLink({ appId: "opencode", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); // 使用 API Key 链接 hook (OpenClaw) const { shouldShowApiKeyLink: shouldShowOpenclawApiKeyLink, websiteUrl: openclawWebsiteUrl, isPartner: isOpenclawPartner, partnerPromotionKey: openclawPartnerPromotionKey, } = useApiKeyLink({ appId: "openclaw", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); // 使用端点测速候选 hook const speedTestEndpoints = useSpeedTestEndpoints({ appId, selectedPresetId, presetEntries, baseUrl, codexBaseUrl, initialData, }); const handlePresetChange = (value: string) => { setSelectedPresetId(value); if (value === "custom") { setActivePreset(null); form.reset(defaultValues); if (appId === "codex") { const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); } if (appId === "gemini") { resetGeminiConfig({}, {}); } if (appId === "opencode") { opencodeForm.resetOpencodeState(); omoDraft.resetOmoDraftState(); } // OpenClaw 自定义模式:重置为空配置 if (appId === "openclaw") { openclawForm.resetOpenclawState(); } return; } const entry = presetEntries.find((item) => item.id === value); if (!entry) { return; } setActivePreset({ id: value, category: entry.preset.category, isPartner: entry.preset.isPartner, partnerPromotionKey: entry.preset.partnerPromotionKey, }); if (appId === "codex") { const preset = entry.preset as CodexProviderPreset; const auth = preset.auth ?? {}; const config = preset.config ?? ""; resetCodexConfig(auth, config); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify({ auth, config }, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } if (appId === "gemini") { const preset = entry.preset as GeminiProviderPreset; const env = (preset.settingsConfig as any)?.env ?? {}; const config = (preset.settingsConfig as any)?.config ?? {}; resetGeminiConfig(env, config); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(preset.settingsConfig, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } if (appId === "opencode") { const preset = entry.preset as OpenCodeProviderPreset; const config = preset.settingsConfig; if (preset.category === "omo" || preset.category === "omo-slim") { omoDraft.resetOmoDraftState(); form.reset({ name: preset.category === "omo" ? "OMO" : "OMO Slim", websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify({}, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } opencodeForm.resetOpencodeState(config); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } // OpenClaw preset handling if (appId === "openclaw") { const preset = entry.preset as OpenClawProviderPreset; const config = preset.settingsConfig; // Update activePreset with suggestedDefaults for OpenClaw setActivePreset({ id: value, category: preset.category, isPartner: preset.isPartner, partnerPromotionKey: preset.partnerPromotionKey, suggestedDefaults: preset.suggestedDefaults, }); openclawForm.resetOpenclawState(config); // Update form fields form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } const preset = entry.preset as ProviderPreset; const config = applyTemplateValues( preset.settingsConfig, preset.templateValues, ); if (preset.apiFormat) { setLocalApiFormat(preset.apiFormat); } else { setLocalApiFormat("anthropic"); } if (preset.apiKeyField) { setLocalApiKeyField(preset.apiKeyField); } else { setLocalApiKeyField("ANTHROPIC_AUTH_TOKEN"); } form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); }; const settingsConfigErrorField = ( ( )} /> ); return (
{!initialData && ( )} opencodeForm.setOpencodeProviderKey( e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""), ) } placeholder={t("opencode.providerKeyPlaceholder")} disabled={isEditMode} className={ (existingOpencodeKeys.includes( opencodeForm.opencodeProviderKey, ) && !isEditMode) || (opencodeForm.opencodeProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( opencodeForm.opencodeProviderKey, )) ? "border-destructive" : "" } /> {existingOpencodeKeys.includes( opencodeForm.opencodeProviderKey, ) && !isEditMode && (

{t("opencode.providerKeyDuplicate")}

)} {opencodeForm.opencodeProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( opencodeForm.opencodeProviderKey, ) && (

{t("opencode.providerKeyInvalid")}

)} {!( existingOpencodeKeys.includes( opencodeForm.opencodeProviderKey, ) && !isEditMode ) && (opencodeForm.opencodeProviderKey.trim() === "" || /^[a-z0-9]+(-[a-z0-9]+)*$/.test( opencodeForm.opencodeProviderKey, )) && (

{t("opencode.providerKeyHint")}

)} ) : appId === "openclaw" ? (
openclawForm.setOpenclawProviderKey( e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""), ) } placeholder={t("openclaw.providerKeyPlaceholder")} disabled={isEditMode} className={ (openclawForm.existingOpenclawKeys.includes( openclawForm.openclawProviderKey, ) && !isEditMode) || (openclawForm.openclawProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( openclawForm.openclawProviderKey, )) ? "border-destructive" : "" } /> {openclawForm.existingOpenclawKeys.includes( openclawForm.openclawProviderKey, ) && !isEditMode && (

{t("openclaw.providerKeyDuplicate")}

)} {openclawForm.openclawProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( openclawForm.openclawProviderKey, ) && (

{t("openclaw.providerKeyInvalid")}

)} {!( openclawForm.existingOpenclawKeys.includes( openclawForm.openclawProviderKey, ) && !isEditMode ) && (openclawForm.openclawProviderKey.trim() === "" || /^[a-z0-9]+(-[a-z0-9]+)*$/.test( openclawForm.openclawProviderKey, )) && (

{t("openclaw.providerKeyHint")}

)}
) : undefined } /> {appId === "claude" && ( )} {appId === "codex" && ( )} {appId === "gemini" && ( )} {appId === "opencode" && !isAnyOmoCategory && ( )} {appId === "opencode" && (category === "omo" || category === "omo-slim") && ( )} {/* OpenClaw 专属字段 */} {appId === "openclaw" && ( )} {/* 配置编辑器:Codex、Claude、Gemini 分别使用不同的编辑器 */} {appId === "codex" ? ( <> {settingsConfigErrorField} ) : appId === "gemini" ? ( <> {settingsConfigErrorField} ) : appId === "opencode" && (category === "omo" || category === "omo-slim") ? ( omoDraft.setIsOmoConfigModalOpen(false)} onSave={omoDraft.handleOmoGlobalConfigSave} isSaving={omoDraft.isOmoSaving} onGlobalConfigStateChange={omoDraft.setOmoGlobalState} globalConfigRef={omoDraft.omoGlobalConfigRef} fieldsKey={omoDraft.omoFieldsKey} isSlim={category === "omo-slim"} /> ) : appId === "opencode" && category !== "omo" && category !== "omo-slim" ? ( <>
form.setValue("settingsConfig", config)} placeholder={`{ "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "https://your-api-endpoint.com", "apiKey": "your-api-key-here" }, "models": {} }`} rows={14} showValidation={true} language="json" />
{settingsConfigErrorField} ) : appId === "openclaw" ? ( <>
form.setValue("settingsConfig", config)} placeholder={`{ "baseUrl": "https://api.example.com/v1", "apiKey": "your-api-key-here", "api": "openai-completions", "models": [] }`} rows={14} showValidation={true} language="json" />
( )} /> ) : ( <>
{isEditMode && isCurrent && ( { try { const cfg = JSON.parse( form.getValues("settingsConfig") || "{}", ); jsonMergePatch(cfg, patch); form.setValue( "settingsConfig", JSON.stringify(cfg, null, 2), ); } catch { // invalid JSON in editor — skip mirror } }} /> )} form.setValue("settingsConfig", value)} placeholder={`{ "env": { "ANTHROPIC_AUTH_TOKEN": "your-api-key-here" } }`} rows={14} showValidation={true} language="json" />

{t( isCurrent ? "claudeConfig.fullSettingsHint" : "claudeConfig.fragmentSettingsHint", )}

{settingsConfigErrorField} )} {!isAnyOmoCategory && appId !== "opencode" && appId !== "openclaw" && ( )} {showButtons && (
)} ); } export type ProviderFormValues = ProviderFormData & { presetId?: string; presetCategory?: ProviderCategory; isPartner?: boolean; meta?: ProviderMeta; providerKey?: string; // OpenCode/OpenClaw: user-defined provider key suggestedDefaults?: OpenClawSuggestedDefaults; // OpenClaw: suggested default model configuration };