import { useEffect, useMemo, useState, useCallback } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery, useQueryClient } from "@tanstack/react-query"; 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 { buildLocalProxyRequestOverrides, formatRequestOverrideObject, } from "@/lib/requestOverrides"; import { providersApi, settingsApi, type AppId } from "@/lib/api"; import { useDarkMode } from "@/hooks/useDarkMode"; import type { ProviderCategory, ProviderMeta, ProviderTestConfig, ClaudeApiFormat, CodexApiFormat, CodexCatalogModel, CodexChatReasoning, 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, rebaseOpenClawSuggestedDefaults, type OpenClawProviderPreset, type OpenClawSuggestedDefaults, } from "@/config/openclawProviderPresets"; import { hermesProviderPresets, type HermesProviderPreset, } from "@/config/hermesProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; import { OpenClawFormFields } from "./OpenClawFormFields"; import { HermesFormFields } from "./HermesFormFields"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; import { applyTemplateValues, hasApiKeyField, } from "@/utils/providerConfigUtils"; import { mergeProviderMeta } from "@/utils/providerMetaUtils"; import { extractCodexWireApi, setCodexWireApi, setCodexModelName as setCodexModelNameInConfig, } from "@/utils/providerConfigUtils"; import { isNonNegativeDecimalString } from "@/types/usage"; import { getCodexCustomTemplate } from "@/config/codexTemplates"; import CodexConfigEditor from "./CodexConfigEditor"; import { CommonConfigEditor } from "./CommonConfigEditor"; import GeminiConfigEditor from "./GeminiConfigEditor"; import JsonEditor from "@/components/JsonEditor"; import { Label } from "@/components/ui/label"; import { ProviderPresetSelector } from "./ProviderPresetSelector"; import { BasicFormFields } from "./BasicFormFields"; import { ClaudeFormFields } from "./ClaudeFormFields"; import { ClaudeDesktopProviderForm } from "./ClaudeDesktopProviderForm"; import { CodexFormFields } from "./CodexFormFields"; import { GeminiFormFields } from "./GeminiFormFields"; import { OmoFormFields } from "./OmoFormFields"; import { parseOmoOtherFieldsObject } from "@/types/omo"; import { ProviderAdvancedConfig, type PricingModelSourceOption, } from "./ProviderAdvancedConfig"; import { useProviderCategory, useApiKeyState, useBaseUrlState, useModelState, useCodexConfigState, useApiKeyLink, useTemplateValues, useCommonConfigSnippet, useCodexCommonConfig, useSpeedTestEndpoints, useCodexTomlValidation, useGeminiConfigState, useGeminiCommonConfig, useOmoModelSource, useOpencodeFormState, useOmoDraftState, useOpenclawFormState, useHermesFormState, useCopilotAuth, useCodexOauth, } from "./hooks"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { useSettingsQuery } from "@/lib/query"; import { CLAUDE_DEFAULT_CONFIG, CODEX_DEFAULT_CONFIG, GEMINI_DEFAULT_CONFIG, OPENCODE_DEFAULT_CONFIG, OPENCLAW_DEFAULT_CONFIG, normalizePricingSource, } from "./helpers/opencodeFormUtils"; import { HERMES_DEFAULT_CONFIG } from "./hooks/useHermesFormState"; import { resolveManagedAccountId } from "@/lib/authBinding"; import { useOpenClawLiveProviderIds } from "@/hooks/useOpenClaw"; import { useHermesLiveProviderIds } from "@/hooks/useHermes"; type PresetEntry = { id: string; preset: | ProviderPreset | CodexProviderPreset | GeminiProviderPreset | OpenCodeProviderPreset | OpenClawProviderPreset | HermesProviderPreset; }; const codexApiFormatFromWireApi = ( wireApi: string | undefined, ): CodexApiFormat | undefined => { switch (wireApi?.trim().toLowerCase()) { case "chat": case "chat_completions": case "chat-completions": case "openai_chat": case "openai-chat": return "openai_chat"; case "responses": case "openai_responses": case "openai-responses": return "openai_responses"; default: return undefined; } }; // 从已保存的 settingsConfig 推断 Codex 模型映射条目数(用于决定本地路由初始开关)。 const codexCatalogCountFromSettings = (settingsConfig: unknown): number => { if (settingsConfig && typeof settingsConfig === "object") { const models = (settingsConfig as { modelCatalog?: { models?: unknown } }) .modelCatalog?.models; return Array.isArray(models) ? models.length : 0; } return 0; }; export const normalizeCodexCatalogModelsForSave = ( models: CodexCatalogModel[], ): CodexCatalogModel[] => { const seen = new Set(); const normalized: CodexCatalogModel[] = []; for (const item of models) { const model = item.model.trim(); if (!model || seen.has(model)) continue; seen.add(model); const displayName = item.displayName?.trim(); const rawContextWindow = String(item.contextWindow ?? "").replace( /[^\d]/g, "", ); const contextWindow = rawContextWindow ? Number.parseInt(rawContextWindow, 10) : undefined; const inputModalities = item.inputModalities?.filter( (m) => typeof m === "string" && m.trim(), ); const baseInstructions = item.baseInstructions?.trim(); normalized.push({ model, ...(displayName ? { displayName } : {}), ...(contextWindow && contextWindow > 0 ? { contextWindow } : {}), // Native Responses profile overrides (ignored by the chat/proxy profile). ...(typeof item.supportsParallelToolCalls === "boolean" ? { supportsParallelToolCalls: item.supportsParallelToolCalls } : {}), ...(inputModalities && inputModalities.length > 0 ? { inputModalities } : {}), ...(baseInstructions ? { baseInstructions } : {}), }); } return normalized; }; const normalizeCodexChatReasoningForSave = ( value?: CodexChatReasoning, ): CodexChatReasoning | undefined => { const supportsEffort = value?.supportsEffort === true; const supportsThinking = value?.supportsThinking === true || supportsEffort; const hasExplicitConfig = value && Object.keys(value).length > 0; if (!supportsThinking && !supportsEffort) { return hasExplicitConfig ? { supportsThinking: false, supportsEffort: false, thinkingParam: "none", effortParam: "none", outputFormat: value?.outputFormat ?? "auto", } : undefined; } return { supportsThinking, supportsEffort, thinkingParam: supportsThinking ? (value?.thinkingParam ?? "thinking") : "none", effortParam: supportsEffort ? (value?.effortParam ?? "reasoning_effort") : "none", effortValueMode: supportsEffort ? (value?.effortValueMode ?? "passthrough") : undefined, outputFormat: value?.outputFormat ?? "auto", }; }; type LocalProxyRequestOverridesBuildResult = ReturnType< typeof buildLocalProxyRequestOverrides >; export interface ProviderFormProps { appId: AppId; providerId?: string; submitLabel: string; onSubmit: (values: ProviderFormValues) => Promise | void; onCancel: () => void; onUniversalPresetSelect?: (preset: UniversalProviderPreset) => void; onManageUniversalProviders?: () => void; onSubmittingChange?: (isSubmitting: boolean) => void; initialData?: { name?: string; websiteUrl?: string; notes?: string; settingsConfig?: Record; category?: ProviderCategory; meta?: ProviderMeta; icon?: string; iconColor?: string; }; showButtons?: boolean; isProxyTakeover?: boolean; } export function ProviderForm(props: ProviderFormProps) { if (props.appId === "claude-desktop") { return ; } return ; } function ProviderFormFull({ appId, providerId, submitLabel, onSubmit, onCancel, onUniversalPresetSelect, onManageUniversalProviders, onSubmittingChange, initialData, showButtons = true, isProxyTakeover = false, }: ProviderFormProps) { if (appId === "claude-desktop") { throw new Error("ProviderFormFull should not receive claude-desktop"); } const { t } = useTranslation(); const isEditMode = Boolean(initialData); const queryClient = useQueryClient(); const { data: settingsData } = useSettingsQuery(); const showCommonConfigNotice = settingsData != null && settingsData.commonConfigConfirmed !== true; const isDarkMode = useDarkMode(); const handleCommonConfigConfirm = async () => { try { if (settingsData) { const { webdavSync: _, ...rest } = settingsData; await settingsApi.save({ ...rest, commonConfigConfirmed: true }); await queryClient.invalidateQueries({ queryKey: ["settings"] }); } } catch (error) { console.error("Failed to save commonConfigConfirmed:", error); } }; 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 supportsFullUrl = appId === "claude" || appId === "codex"; const [localIsFullUrl, setLocalIsFullUrl] = useState(() => { if (!supportsFullUrl) return false; return initialData?.meta?.isFullUrl ?? false; }); const [testConfig, setTestConfig] = useState( () => initialData?.meta?.testConfig ?? { 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; useEffect(() => { setSelectedPresetId(initialData ? null : "custom"); setActivePreset(null); if (!initialData) { setDraftCustomEndpoints([]); } setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true); setLocalIsFullUrl( supportsFullUrl ? (initialData?.meta?.isFullUrl ?? false) : false, ); setTestConfig(initialData?.meta?.testConfig ?? { enabled: false }); setPricingConfig({ enabled: initialData?.meta?.costMultiplier !== undefined || initialData?.meta?.pricingModelSource !== undefined, costMultiplier: initialData?.meta?.costMultiplier, pricingModelSource: normalizePricingSource( initialData?.meta?.pricingModelSource, ), }); setCodexChatReasoning(initialData?.meta?.codexChatReasoning ?? {}); setCustomUserAgent(initialData?.meta?.customUserAgent ?? ""); setLocalProxyHeadersOverride( formatRequestOverrideObject( initialData?.meta?.localProxyRequestOverrides?.headers, ), ); setLocalProxyBodyOverride( formatRequestOverrideObject( initialData?.meta?.localProxyRequestOverrides?.body, ), ); }, [appId, initialData, supportsFullUrl]); 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 : appId === "hermes" ? HERMES_DEFAULT_CONFIG : CLAUDE_DEFAULT_CONFIG, icon: initialData?.icon ?? "", iconColor: initialData?.iconColor ?? "", }), [initialData, appId], ); const form = useForm({ resolver: zodResolver(providerSchema), defaultValues, mode: "onSubmit", }); const { isSubmitting } = form.formState; const handleSettingsConfigChange = useCallback( (config: string) => { form.setValue("settingsConfig", config); }, [form], ); const [localApiKeyField, setLocalApiKeyField] = useState( () => { if (appId !== "claude") return "ANTHROPIC_AUTH_TOKEN"; if (initialData?.meta?.apiKeyField) return initialData.meta.apiKeyField; // Infer from existing config env const env = (initialData?.settingsConfig as Record) ?.env as Record | undefined; if (env?.ANTHROPIC_API_KEY !== undefined) return "ANTHROPIC_API_KEY"; return "ANTHROPIC_AUTH_TOKEN"; }, ); // 软校验:收集"业务约束"类问题(空值/缺项),由用户决定是否仍要保存 const [softIssues, setSoftIssues] = useState(null); const [pendingFormValues, setPendingFormValues] = useState(null); const [ pendingLocalProxyRequestOverridesResult, setPendingLocalProxyRequestOverridesResult, ] = useState(null); // 确认框走的提交路径绕过了 react-hook-form 的 isSubmitting,单独追踪 const [isConfirmSubmitting, setIsConfirmSubmitting] = useState(false); useEffect(() => { onSubmittingChange?.(isSubmitting || isConfirmSubmitting); }, [isSubmitting, isConfirmSubmitting, onSubmittingChange]); const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey, } = useApiKeyState({ initialConfig: form.getValues("settingsConfig"), onConfigChange: handleSettingsConfigChange, selectedPresetId, category, appType: appId, apiKeyField: appId === "claude" ? localApiKeyField : undefined, }); const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ appType: appId, category, settingsConfig: form.getValues("settingsConfig"), codexConfig: "", onSettingsConfigChange: handleSettingsConfigChange, onCodexConfigChange: () => {}, }); const { claudeModel, defaultHaikuModel, defaultHaikuModelName, defaultSonnetModel, defaultSonnetModelName, defaultOpusModel, defaultOpusModelName, defaultFableModel, defaultFableModelName, handleModelChange, } = useModelState({ settingsConfig: form.getValues("settingsConfig"), onConfigChange: handleSettingsConfigChange, }); const [localApiFormat, setLocalApiFormat] = useState(() => { if (appId !== "claude") return "anthropic"; return initialData?.meta?.apiFormat ?? "anthropic"; }); const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { setLocalApiFormat(format); }, []); const handleApiKeyFieldChange = useCallback( (field: ClaudeApiKeyField) => { const prev = localApiKeyField; setLocalApiKeyField(field); // Swap the env key name in settingsConfig try { const raw = form.getValues("settingsConfig"); const config = JSON.parse(raw || "{}"); if (config?.env && prev in config.env) { const value = config.env[prev]; delete config.env[prev]; config.env[field] = value; const updated = JSON.stringify(config, null, 2); form.setValue("settingsConfig", updated); handleSettingsConfigChange(updated); } } catch { // ignore parse errors during editing } }, [localApiKeyField, form, handleSettingsConfigChange], ); // Copilot OAuth 认证状态(仅 Claude 应用需要) const { isAuthenticated: isCopilotAuthenticated } = useCopilotAuth(); // Codex OAuth 认证状态(ChatGPT Plus/Pro 反代) const { isAuthenticated: isCodexOauthAuthenticated } = useCodexOauth(); // 选中的 GitHub 账号 ID(多账号支持) const [selectedGitHubAccountId, setSelectedGitHubAccountId] = useState< string | null >(() => resolveManagedAccountId(initialData?.meta, "github_copilot")); // 选中的 ChatGPT 账号 ID(Codex OAuth 多账号支持) const [selectedCodexAccountId, setSelectedCodexAccountId] = useState< string | null >(() => resolveManagedAccountId(initialData?.meta, "codex_oauth")); const [codexFastMode, setCodexFastMode] = useState( () => initialData?.meta?.codexFastMode ?? false, ); const [codexChatReasoning, setCodexChatReasoning] = useState( () => initialData?.meta?.codexChatReasoning ?? {}, ); const [customUserAgent, setCustomUserAgent] = useState( () => initialData?.meta?.customUserAgent ?? "", ); const [localProxyHeadersOverride, setLocalProxyHeadersOverride] = useState(() => formatRequestOverrideObject( initialData?.meta?.localProxyRequestOverrides?.headers, ), ); const [localProxyBodyOverride, setLocalProxyBodyOverride] = useState( () => formatRequestOverrideObject( initialData?.meta?.localProxyRequestOverrides?.body, ), ); const { codexAuth, codexConfig, codexApiKey, codexBaseUrl, codexCatalogModels, codexAuthError, setCodexAuth, setCodexConfig, setCodexCatalogModels, handleCodexApiKeyChange, handleCodexBaseUrlChange, handleCodexConfigChange: originalHandleCodexConfigChange, resetCodexConfig, } = useCodexConfigState({ initialData }); const initialCodexApiFormat: CodexApiFormat = initialData?.meta?.apiFormat === "openai_chat" ? "openai_chat" : initialData?.meta?.apiFormat === "openai_responses" ? "openai_responses" : (codexApiFormatFromWireApi( extractCodexWireApi( typeof initialData?.settingsConfig?.config === "string" ? initialData.settingsConfig.config : "", ), ) ?? "openai_responses"); const [localCodexApiFormat, setLocalCodexApiFormat] = useState(initialCodexApiFormat); // 本地路由(接管)开关 —— 纯模型映射门控,与上游格式完全独立。 // 没有独立持久化字段,初值仅按「是否已配置模型映射」推断(有 catalog 即视为 // 接管已开)。只在 useState 初始化与预设重置点设置,跟 localCodexApiFormat // 对称,避免漂移。 const [codexTakeoverEnabled, setCodexTakeoverEnabled] = useState( () => // Native (openai_responses) providers run DIRECT (no proxy takeover) yet // still carry a modelCatalog so cc-switch can generate model-catalogs.json. // So a catalog must NOT auto-enable takeover for native providers. initialCodexApiFormat !== "openai_responses" && codexCatalogCountFromSettings(initialData?.settingsConfig) > 0, ); const { configError: codexConfigError, debouncedValidate } = useCodexTomlValidation(); const handleCodexConfigChange = useCallback( (value: string) => { originalHandleCodexConfigChange(value); debouncedValidate(value); }, [originalHandleCodexConfigChange, debouncedValidate], ); const handleCodexApiFormatChange = useCallback( (format: CodexApiFormat) => { setLocalCodexApiFormat(format); // wire_api is always "responses" for Codex; format controls proxy-layer conversion setCodexConfig((prev) => { const updated = setCodexWireApi(prev, "responses"); debouncedValidate(updated); return updated; }); }, [setCodexConfig, debouncedValidate], ); useEffect(() => { if (appId === "codex" && !initialData && selectedPresetId === "custom") { const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); setCodexChatReasoning({}); setCodexTakeoverEnabled(false); } }, [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, })); } else if (appId === "hermes") { return hermesProviderPresets.map((preset, index) => ({ id: `hermes-${index}`, preset, })); } return providerPresets .filter((p) => !p.hidden) .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: handleSettingsConfigChange, }); const { useCommonConfig, commonConfigSnippet, commonConfigError, handleCommonConfigToggle, handleCommonConfigSnippetChange, isExtracting: isClaudeExtracting, handleExtract: handleClaudeExtract, } = useCommonConfigSnippet({ settingsConfig: form.getValues("settingsConfig"), onConfigChange: handleSettingsConfigChange, initialData: appId === "claude" ? initialData : undefined, initialEnabled: appId === "claude" ? initialData?.meta?.commonConfigEnabled : undefined, selectedPresetId: selectedPresetId ?? undefined, enabled: appId === "claude", }); const { useCommonConfig: useCodexCommonConfigFlag, commonConfigSnippet: codexCommonConfigSnippet, commonConfigError: codexCommonConfigError, handleCommonConfigToggle: handleCodexCommonConfigToggle, handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange, isExtracting: isCodexExtracting, handleExtract: handleCodexExtract, clearCommonConfigError: clearCodexCommonConfigError, } = useCodexCommonConfig({ codexConfig, onConfigChange: handleCodexConfigChange, initialData: appId === "codex" ? initialData : undefined, initialEnabled: appId === "codex" ? initialData?.meta?.commonConfigEnabled : undefined, selectedPresetId: selectedPresetId ?? undefined, }); const { geminiEnv, geminiConfig, geminiApiKey, geminiBaseUrl, geminiModel, envError, configError: geminiConfigError, handleGeminiApiKeyChange: originalHandleGeminiApiKeyChange, handleGeminiBaseUrlChange: originalHandleGeminiBaseUrlChange, handleGeminiModelChange: originalHandleGeminiModelChange, handleGeminiEnvChange, handleGeminiConfigChange, resetGeminiConfig, envStringToObj, envObjToString, } = 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], ); const { useCommonConfig: useGeminiCommonConfigFlag, commonConfigSnippet: geminiCommonConfigSnippet, commonConfigError: geminiCommonConfigError, handleCommonConfigToggle: handleGeminiCommonConfigToggle, handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange, isExtracting: isGeminiExtracting, handleExtract: handleGeminiExtract, clearCommonConfigError: clearGeminiCommonConfigError, } = useGeminiCommonConfig({ envValue: geminiEnv, onEnvChange: handleGeminiEnvChange, envStringToObj, envObjToString, initialData: appId === "gemini" ? initialData : undefined, initialEnabled: appId === "gemini" ? initialData?.meta?.commonConfigEnabled : undefined, selectedPresetId: selectedPresetId ?? undefined, }); // ── Extracted hooks: OpenCode / OMO / OpenClaw ───────────────────── const { omoModelOptions, omoModelVariantsMap, omoPresetMetaMap, existingOpencodeKeys, } = useOmoModelSource({ isOmoCategory: isAnyOmoCategory, providerId }); const { data: opencodeLiveProviderIds = [], isLoading: isOpencodeLiveProviderIdsLoading, } = useQuery({ queryKey: ["opencodeLiveProviderIds"], queryFn: () => providersApi.getOpenCodeLiveProviderIds(), enabled: appId === "opencode" && !isAnyOmoCategory, }); 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, isEditMode, appId, category, }); const openclawForm = useOpenclawFormState({ initialData, appId, providerId, onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), getSettingsConfig: () => form.getValues("settingsConfig"), }); const { data: openclawLiveProviderIds = [], isLoading: isOpenclawLiveProviderIdsLoading, } = useOpenClawLiveProviderIds(appId === "openclaw"); const hermesForm = useHermesFormState({ initialData, appId, providerId, onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), getSettingsConfig: () => form.getValues("settingsConfig"), }); const { data: hermesLiveProviderIds = [], isLoading: isHermesLiveProviderIdsLoading, } = useHermesLiveProviderIds(appId === "hermes"); const additiveExistingProviderKeys = useMemo(() => { if (appId === "opencode" && !isAnyOmoCategory) { return Array.from( new Set( [...existingOpencodeKeys, ...opencodeLiveProviderIds].filter( (key) => key !== providerId, ), ), ); } if (appId === "openclaw") { return Array.from( new Set( [ ...openclawForm.existingOpenclawKeys, ...openclawLiveProviderIds, ].filter((key) => key !== providerId), ), ); } if (appId === "hermes") { return Array.from( new Set( [...hermesForm.existingHermesKeys, ...hermesLiveProviderIds].filter( (key) => key !== providerId, ), ), ); } return []; }, [ appId, existingOpencodeKeys, hermesForm.existingHermesKeys, hermesLiveProviderIds, isAnyOmoCategory, openclawForm.existingOpenclawKeys, openclawLiveProviderIds, opencodeLiveProviderIds, providerId, ]); const isProviderKeyLockStateLoading = useMemo(() => { if (!isEditMode) return false; if (appId === "opencode" && !isAnyOmoCategory) { return isOpencodeLiveProviderIdsLoading; } if (appId === "openclaw") { return isOpenclawLiveProviderIdsLoading; } if (appId === "hermes") { return isHermesLiveProviderIdsLoading; } return false; }, [ appId, isAnyOmoCategory, isEditMode, isHermesLiveProviderIdsLoading, isOpenclawLiveProviderIdsLoading, isOpencodeLiveProviderIdsLoading, ]); const isProviderKeyLocked = useMemo(() => { if (!isEditMode || !providerId) return false; if (appId === "opencode" && !isAnyOmoCategory) { return opencodeLiveProviderIds.includes(providerId); } if (appId === "openclaw") { return openclawLiveProviderIds.includes(providerId); } if (appId === "hermes") { return hermesLiveProviderIds.includes(providerId); } return false; }, [ appId, hermesLiveProviderIds, isAnyOmoCategory, isEditMode, openclawLiveProviderIds, opencodeLiveProviderIds, providerId, ]); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const shouldApplyLocalProxyRequestOverrides = (appId === "claude" || appId === "codex") && category !== "official"; const handleSubmit = async (values: ProviderFormData) => { const overridesResult = shouldApplyLocalProxyRequestOverrides ? buildLocalProxyRequestOverrides( localProxyHeadersOverride, localProxyBodyOverride, ) : {}; if (overridesResult.error) { toast.error( t("providerForm.localProxyRequestOverridesInvalid", { defaultValue: `本地代理请求覆盖格式错误:${overridesResult.error}`, error: overridesResult.error, }), ); return; } // 软性问题(业务约束,用户可选择仍要保存) const issues: string[] = []; // 模板变量未填:A 类(空值) if (appId === "claude" && templateValueEntries.length > 0) { const validation = validateTemplateValues(); if (!validation.isValid && validation.missingField) { issues.push( t("providerForm.fillParameter", { label: validation.missingField.label, defaultValue: `请填写 ${validation.missingField.label}`, }), ); } } // 供应商名空:A 类 if (!values.name.trim()) { issues.push( t("providerForm.fillSupplierName", { defaultValue: "请填写供应商名称", }), ); } const costMultiplier = pricingConfig.costMultiplier?.trim(); if ( pricingConfig.enabled && costMultiplier && !isNonNegativeDecimalString(costMultiplier) ) { toast.error( t("settings.globalProxy.defaultCostMultiplierInvalid", { defaultValue: "成本倍率必须为非负数", }), ); return; } // opencode / openclaw / hermes: providerKey 相关 // A 类(空)归到 issues;B 类(正则不合法 / 重复 / 状态加载中)仍硬拒绝 const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; if (appId === "opencode" && !isAnyOmoCategory) { // providerKey 是 opencode / openclaw / hermes 的主键 ID,空或格式不合法 // 都属于完整性约束,保留硬拒绝(mutations 层也会 throw,软化只会让错误更晦涩) if (!opencodeForm.opencodeProviderKey.trim()) { toast.error(t("opencode.providerKeyRequired")); return; } if (!keyPattern.test(opencodeForm.opencodeProviderKey)) { toast.error(t("opencode.providerKeyInvalid")); return; } if (isProviderKeyLockStateLoading) { toast.error( t("providerForm.providerKeyStatusLoading", { defaultValue: "正在加载供应商标识状态,请稍后再试", }), ); return; } if ( !isProviderKeyLocked && additiveExistingProviderKeys.includes(opencodeForm.opencodeProviderKey) ) { toast.error(t("opencode.providerKeyDuplicate")); return; } if (Object.keys(opencodeForm.opencodeModels).length === 0) { issues.push(t("opencode.modelsRequired")); } } if (appId === "openclaw") { if (!openclawForm.openclawProviderKey.trim()) { toast.error(t("openclaw.providerKeyRequired")); return; } if (!keyPattern.test(openclawForm.openclawProviderKey)) { toast.error(t("openclaw.providerKeyInvalid")); return; } if (isProviderKeyLockStateLoading) { toast.error( t("providerForm.providerKeyStatusLoading", { defaultValue: "正在加载供应商标识状态,请稍后再试", }), ); return; } if ( !isProviderKeyLocked && additiveExistingProviderKeys.includes(openclawForm.openclawProviderKey) ) { toast.error(t("openclaw.providerKeyDuplicate")); return; } } if (appId === "hermes") { if (!hermesForm.hermesProviderKey.trim()) { toast.error(t("hermes.form.providerKeyRequired")); return; } if (!keyPattern.test(hermesForm.hermesProviderKey)) { toast.error(t("hermes.form.providerKeyInvalid")); return; } if (isProviderKeyLockStateLoading) { toast.error( t("providerForm.providerKeyStatusLoading", { defaultValue: "正在加载供应商标识状态,请稍后再试", }), ); return; } if ( !isProviderKeyLocked && additiveExistingProviderKeys.includes(hermesForm.hermesProviderKey) ) { toast.error(t("hermes.form.providerKeyDuplicate")); return; } } // OAuth 未登录:B 类(token 根本不存在,保存了也没法建立) const isCopilotProvider = templatePreset?.providerType === "github_copilot" || initialData?.meta?.providerType === "github_copilot" || baseUrl.includes("githubcopilot.com"); const isCodexOauthProvider = templatePreset?.providerType === "codex_oauth" || initialData?.meta?.providerType === "codex_oauth"; if (isCopilotProvider && !isCopilotAuthenticated) { toast.error( t("copilot.loginRequired", { defaultValue: "请先登录 GitHub Copilot", }), ); return; } if (isCodexOauthProvider && !isCodexOauthAuthenticated) { toast.error( t("codexOauth.loginRequired", { defaultValue: "请先登录 ChatGPT 账号", }), ); return; } // OMO Other Fields JSON:B 类(格式错了保存下去数据就坏了) if ( appId === "opencode" && isAnyOmoCategory && 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; } } catch { toast.error( t("omo.invalidJson", { defaultValue: "Other Fields contains invalid JSON", }), ); return; } } // 非官方供应商端点 / API Key 空:A 类 // cloud_provider(如 Bedrock)通过模板变量处理认证,跳过通用校验 if (category !== "official" && category !== "cloud_provider") { if (appId === "claude") { if (!isCodexOauthProvider && !baseUrl.trim()) { issues.push( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); } if (!isCopilotProvider && !isCodexOauthProvider && !apiKey.trim()) { issues.push( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); } } else if (appId === "codex") { if (!codexBaseUrl.trim()) { issues.push( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); } if (!codexApiKey.trim()) { issues.push( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); } } else if (appId === "gemini") { if (!geminiBaseUrl.trim()) { issues.push( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); } if (!geminiApiKey.trim()) { issues.push( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); } } } if (issues.length > 0) { // 弹确认框让用户决定是否仍要保存 setSoftIssues(issues); setPendingFormValues(values); setPendingLocalProxyRequestOverridesResult(overridesResult); return; } await performSubmit(values, overridesResult); }; const performSubmit = async ( values: ProviderFormData, overridesResult: LocalProxyRequestOverridesBuildResult, ) => { if (overridesResult.error) { toast.error( t("providerForm.localProxyRequestOverridesInvalid", { defaultValue: `本地代理请求覆盖格式错误:${overridesResult.error}`, error: overridesResult.error, }), ); return; } // OAuth / 其它身份识别(与 handleSubmit 保持一致) const isCopilotProvider = templatePreset?.providerType === "github_copilot" || initialData?.meta?.providerType === "github_copilot" || baseUrl.includes("githubcopilot.com"); const isCodexOauthProvider = templatePreset?.providerType === "codex_oauth" || initialData?.meta?.providerType === "codex_oauth"; let settingsConfig: string; if (appId === "codex") { try { const authJson = JSON.parse(codexAuth); let normalizedCodexConfig = category !== "official" && (codexConfig ?? "").trim() ? setCodexWireApi(codexConfig ?? "", "responses") : (codexConfig ?? ""); // Persist the catalog for proxy-takeover (Mode A) OR native-direct // (Mode B, apiFormat=openai_responses). Native providers run without the // proxy but still need cc-switch to generate model-catalogs.json, so the // catalog must survive even when the takeover toggle is off. const normalizedCatalogModels = category !== "official" && (codexTakeoverEnabled || localCodexApiFormat === "openai_responses") ? normalizeCodexCatalogModelsForSave(codexCatalogModels) : []; // Sync first catalog row's model into config.toml so Codex uses it as default if (normalizedCatalogModels.length > 0) { normalizedCodexConfig = setCodexModelNameInConfig( normalizedCodexConfig, normalizedCatalogModels[0].model, ); } const configObj = { auth: authJson, config: normalizedCodexConfig, } as { auth: unknown; config: string; modelCatalog?: { models: CodexCatalogModel[] }; }; if (normalizedCatalogModels.length > 0) { configObj.modelCatalog = { models: normalizedCatalogModels }; } 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 = {}; 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()) { // 格式已在 handleSubmit 前置校验中验证过,此处可以安全解析 const otherFields = parseOmoOtherFieldsObject( omoDraft.omoOtherFieldsStr, ); if (otherFields) { omoConfig.otherFields = otherFields; } } 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; } else if (appId === "hermes") { payload.providerKey = hermesForm.hermesProviderKey; } 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: align preset model refs with the actual submitted provider key. if (activePreset.suggestedDefaults) { payload.suggestedDefaults = appId === "openclaw" && payload.providerKey ? rebaseOpenClawSuggestedDefaults( activePreset.suggestedDefaults, payload.providerKey, ) : 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); // 确定 providerType(新建时从预设获取,编辑时从现有数据获取) const providerType = templatePreset?.providerType || initialData?.meta?.providerType; const nextMeta: ProviderMeta = { ...(baseMeta ?? {}), commonConfigEnabled: appId === "claude" ? useCommonConfig : appId === "codex" ? useCodexCommonConfigFlag : appId === "gemini" ? useGeminiCommonConfigFlag : undefined, endpointAutoSelect, claudeDesktopMode: undefined, // 保存 providerType(用于识别 Copilot / Codex OAuth 等特殊供应商) providerType, authBinding: isCopilotProvider ? { source: "managed_account", authProvider: "github_copilot", accountId: selectedGitHubAccountId ?? undefined, } : isCodexOauthProvider ? { source: "managed_account", authProvider: "codex_oauth", accountId: selectedCodexAccountId ?? undefined, } : undefined, // GitHub Copilot 多账号:保存关联的账号 ID githubAccountId: isCopilotProvider && selectedGitHubAccountId ? selectedGitHubAccountId : undefined, codexFastMode: isCodexOauthProvider ? codexFastMode : undefined, codexChatReasoning: appId === "codex" && category !== "official" && codexTakeoverEnabled && localCodexApiFormat === "openai_chat" ? normalizeCodexChatReasoningForSave(codexChatReasoning) : undefined, customUserAgent: (appId === "claude" || appId === "codex") && category !== "official" ? customUserAgent.trim() || undefined : undefined, localProxyRequestOverrides: shouldApplyLocalProxyRequestOverrides ? overridesResult.overrides : undefined, testConfig: testConfig.enabled ? testConfig : undefined, costMultiplier: pricingConfig.enabled ? pricingConfig.costMultiplier : undefined, pricingModelSource: pricingConfig.enabled && pricingConfig.pricingModelSource !== "inherit" ? pricingConfig.pricingModelSource : undefined, apiFormat: appId === "claude" && category !== "official" ? localApiFormat : appId === "codex" && category !== "official" ? localCodexApiFormat : undefined, apiKeyField: appId === "claude" && category !== "official" && localApiKeyField !== "ANTHROPIC_AUTH_TOKEN" ? localApiKeyField : undefined, isFullUrl: supportsFullUrl && category !== "official" && localIsFullUrl ? true : undefined, }; if (!isCodexOauthProvider && "codexFastMode" in nextMeta) { delete nextMeta.codexFastMode; } payload.meta = nextMeta; await onSubmit(payload); }; 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") || "", }); // 使用 API Key 链接 hook (Hermes) const { shouldShowApiKeyLink: shouldShowHermesApiKeyLink, websiteUrl: hermesWebsiteUrl, isPartner: isHermesPartner, partnerPromotionKey: hermesPartnerPromotionKey, } = useApiKeyLink({ appId: "hermes", 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); setCodexChatReasoning({}); setLocalCodexApiFormat( codexApiFormatFromWireApi(extractCodexWireApi(template.config)) ?? "openai_responses", ); // 自定义模板无模型映射,路由默认关闭 setCodexTakeoverEnabled(false); } if (appId === "gemini") { resetGeminiConfig({}, {}); } if (appId === "opencode") { opencodeForm.resetOpencodeState(); omoDraft.resetOmoDraftState(); } // OpenClaw 自定义模式:重置为空配置 if (appId === "openclaw") { openclawForm.resetOpenclawState(); } if (appId === "hermes") { hermesForm.resetHermesState(); } 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, preset.modelCatalog ?? []); setCodexChatReasoning(preset.codexChatReasoning ?? {}); setLocalCodexApiFormat( preset.apiFormat ?? codexApiFormatFromWireApi(extractCodexWireApi(config)) ?? "openai_responses", ); // 接管开关:原生(openai_responses)预设走直连、不开接管,即便带 catalog //(catalog 用于生成 model-catalogs.json);只有 chat 预设按是否带映射开启。 setCodexTakeoverEnabled( (preset.apiFormat ?? "openai_responses") !== "openai_responses" && (preset.modelCatalog?.length ?? 0) > 0, ); form.reset({ name: preset.nameKey ? t(preset.nameKey) : 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.nameKey ? t(preset.nameKey) : 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.nameKey ? t(preset.nameKey) : 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.nameKey ? t(preset.nameKey) : preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } // Hermes preset handling if (appId === "hermes") { const preset = entry.preset as HermesProviderPreset; const config = preset.settingsConfig; hermesForm.resetHermesState(config); form.reset({ name: preset.nameKey ? t(preset.nameKey) : 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"); } setLocalApiKeyField(preset.apiKeyField ?? "ANTHROPIC_AUTH_TOKEN"); setLocalIsFullUrl(false); form.reset({ name: preset.nameKey ? t(preset.nameKey) : 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={ isProviderKeyLocked || isProviderKeyLockStateLoading } className={ (additiveExistingProviderKeys.includes( opencodeForm.opencodeProviderKey, ) && !isProviderKeyLocked) || (opencodeForm.opencodeProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( opencodeForm.opencodeProviderKey, )) ? "border-destructive" : "" } /> {additiveExistingProviderKeys.includes( opencodeForm.opencodeProviderKey, ) && !isProviderKeyLocked && (

{t("opencode.providerKeyDuplicate")}

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

{t("opencode.providerKeyInvalid")}

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

{isProviderKeyLocked ? t("opencode.providerKeyLockedHint", { defaultValue: "该供应商已添加到应用配置中,供应商标识不可修改", }) : t("opencode.providerKeyHint")}

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

{t("openclaw.providerKeyDuplicate")}

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

{t("openclaw.providerKeyInvalid")}

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

{isProviderKeyLocked ? t("openclaw.providerKeyLockedHint", { defaultValue: "该供应商已添加到应用配置中,供应商标识不可修改", }) : t("openclaw.providerKeyHint")}

)}
) : appId === "hermes" ? (
hermesForm.setHermesProviderKey( e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""), ) } placeholder={t("hermes.form.providerKeyPlaceholder", { defaultValue: "my-provider", })} disabled={ isProviderKeyLocked || isProviderKeyLockStateLoading } className={ (additiveExistingProviderKeys.includes( hermesForm.hermesProviderKey, ) && !isProviderKeyLocked) || (hermesForm.hermesProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test( hermesForm.hermesProviderKey, )) ? "border-destructive" : "" } /> {additiveExistingProviderKeys.includes( hermesForm.hermesProviderKey, ) && !isProviderKeyLocked && (

{t("hermes.form.providerKeyDuplicate")}

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

{t("hermes.form.providerKeyInvalid")}

)} {!( additiveExistingProviderKeys.includes( hermesForm.hermesProviderKey, ) && !isProviderKeyLocked ) && (hermesForm.hermesProviderKey.trim() === "" || /^[a-z0-9]+(-[a-z0-9]+)*$/.test( hermesForm.hermesProviderKey, )) && (

{isProviderKeyLocked ? t("hermes.form.providerKeyLockedHint", { defaultValue: "This provider is in Hermes config; key is locked.", }) : t("hermes.form.providerKeyHint", { defaultValue: "Lowercase letters, numbers, and hyphens only. Used as the provider name in config.yaml.", })}

)}
) : undefined } /> {appId === "claude" && ( )} {appId === "codex" && ( )} {appId === "gemini" && ( )} {appId === "opencode" && !isAnyOmoCategory && ( )} {appId === "opencode" && (category === "omo" || category === "omo-slim") && ( )} {/* OpenClaw 专属字段 */} {appId === "openclaw" && ( )} {/* Hermes 专属字段 */} {appId === "hermes" && ( )} {/* 配置编辑器:Codex、Claude、Gemini 分别使用不同的编辑器 */} {appId === "codex" ? ( <> {settingsConfigErrorField} ) : appId === "gemini" ? ( <> {settingsConfigErrorField} ) : appId === "opencode" && (category === "omo" || category === "omo-slim") ? (
{}} rows={14} showValidation={false} language="json" darkMode={isDarkMode} />
) : 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" darkMode={isDarkMode} />
{settingsConfigErrorField} ) : appId === "openclaw" || appId === "hermes" ? ( <>
form.setValue("settingsConfig", config)} placeholder={ appId === "hermes" ? `{ "name": "my-provider", "base_url": "https://api.example.com/v1", "api_key": "" }` : `{ "baseUrl": "https://api.example.com/v1", "apiKey": "your-api-key-here", "api": "openai-completions", "models": [] }` } rows={14} showValidation={true} language="json" darkMode={isDarkMode} />
( )} /> ) : ( <> form.setValue("settingsConfig", value)} useCommonConfig={useCommonConfig} onCommonConfigToggle={handleCommonConfigToggle} commonConfigSnippet={commonConfigSnippet} onCommonConfigSnippetChange={handleCommonConfigSnippetChange} commonConfigError={commonConfigError} onEditClick={() => setIsCommonConfigModalOpen(true)} isModalOpen={isCommonConfigModalOpen} onModalClose={() => setIsCommonConfigModalOpen(false)} onExtract={handleClaudeExtract} isExtracting={isClaudeExtracting} /> {settingsConfigErrorField} )} {!isAnyOmoCategory && appId !== "opencode" && appId !== "openclaw" && appId !== "hermes" && ( )} {showButtons && (
)} void handleCommonConfigConfirm()} onCancel={() => void handleCommonConfigConfirm()} /> 0} variant="info" title={t("providerForm.softValidation.title", { defaultValue: "配置存在以下问题", })} message={ (softIssues ?? []).map((issue) => `• ${issue}`).join("\n") + "\n\n" + t("providerForm.softValidation.hint", { defaultValue: "仍要保存吗?保存后切换此供应商时可能失败,可以之后再补全。", }) } confirmText={t("providerForm.softValidation.saveAnyway", { defaultValue: "仍要保存", })} cancelText={t("common.cancel")} onConfirm={async () => { if (isConfirmSubmitting) return; const values = pendingFormValues; const overridesResult = pendingLocalProxyRequestOverridesResult; if (!values || !overridesResult) { setSoftIssues(null); setPendingFormValues(null); setPendingLocalProxyRequestOverridesResult(null); return; } setIsConfirmSubmitting(true); try { await performSubmit(values, overridesResult); setSoftIssues(null); setPendingFormValues(null); setPendingLocalProxyRequestOverridesResult(null); } catch (error) { console.error("[ProviderForm] soft-confirm submit failed:", error); // 保留确认框和 pending values,让用户可以重试或取消 } finally { setIsConfirmSubmitting(false); } }} onCancel={() => { if (isConfirmSubmitting) return; setSoftIssues(null); setPendingFormValues(null); setPendingLocalProxyRequestOverridesResult(null); }} /> ); } 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 };