import { useCallback, useState } from "react"; import { useTranslation } from "react-i18next"; import { Plus } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import type { Provider, CustomEndpoint, UniversalProvider } from "@/types"; import type { AppId } from "@/lib/api"; import { universalProvidersApi } from "@/lib/api"; import { ProviderForm, type ProviderFormValues, } from "@/components/providers/forms/ProviderForm"; import { UniversalProviderFormModal } from "@/components/universal/UniversalProviderFormModal"; import { UniversalProviderPanel } from "@/components/universal"; import { providerPresets } from "@/config/claudeProviderPresets"; import { codexProviderPresets } from "@/config/codexProviderPresets"; import { geminiProviderPresets } from "@/config/geminiProviderPresets"; import { extractCodexBaseUrl } from "@/utils/providerConfigUtils"; import type { OpenClawSuggestedDefaults } from "@/config/openclawProviderPresets"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; interface AddProviderDialogProps { open: boolean; onOpenChange: (open: boolean) => void; appId: AppId; onSubmit: ( provider: Omit & { providerKey?: string; suggestedDefaults?: OpenClawSuggestedDefaults; }, ) => Promise | void; } export function AddProviderDialog({ open, onOpenChange, appId, onSubmit, }: AddProviderDialogProps) { const { t } = useTranslation(); // OpenCode and OpenClaw don't support universal providers const showUniversalTab = appId !== "opencode" && appId !== "openclaw"; const [activeTab, setActiveTab] = useState<"app-specific" | "universal">( "app-specific", ); const [universalFormOpen, setUniversalFormOpen] = useState(false); const [selectedUniversalPreset, setSelectedUniversalPreset] = useState(null); const handleUniversalProviderSave = useCallback( async (provider: UniversalProvider) => { try { await universalProvidersApi.upsert(provider); toast.success( t("universalProvider.addSuccess", { defaultValue: "统一供应商添加成功", }), ); setUniversalFormOpen(false); setSelectedUniversalPreset(null); onOpenChange(false); } catch (error) { console.error( "[AddProviderDialog] Failed to save universal provider", error, ); toast.error( t("universalProvider.addFailed", { defaultValue: "统一供应商添加失败", }), ); } }, [t, onOpenChange], ); const handleUniversalFormClose = useCallback(() => { setUniversalFormOpen(false); setSelectedUniversalPreset(null); }, []); const handleSubmit = useCallback( async (values: ProviderFormValues) => { const parsedConfig = JSON.parse(values.settingsConfig) as Record< string, unknown >; // 构造基础提交数据 const providerData: Omit & { providerKey?: string; suggestedDefaults?: OpenClawSuggestedDefaults; } = { name: values.name.trim(), notes: values.notes?.trim() || undefined, websiteUrl: values.websiteUrl?.trim() || undefined, settingsConfig: parsedConfig, icon: values.icon?.trim() || undefined, iconColor: values.iconColor?.trim() || undefined, ...(values.presetCategory ? { category: values.presetCategory } : {}), ...(values.meta ? { meta: values.meta } : {}), }; // OpenCode/OpenClaw: pass providerKey for ID generation if ( (appId === "opencode" || appId === "openclaw") && values.providerKey ) { providerData.providerKey = values.providerKey; } const hasCustomEndpoints = providerData.meta?.custom_endpoints && Object.keys(providerData.meta.custom_endpoints).length > 0; if (!hasCustomEndpoints && values.presetCategory !== "omo") { const urlSet = new Set(); const addUrl = (rawUrl?: string) => { const url = (rawUrl || "").trim().replace(/\/+$/, ""); if (url && url.startsWith("http")) { urlSet.add(url); } }; if (values.presetId) { if (appId === "claude") { const presets = providerPresets; const presetIndex = parseInt( values.presetId.replace("claude-", ""), ); if ( !isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length ) { const preset = presets[presetIndex]; if (preset?.endpointCandidates) { preset.endpointCandidates.forEach(addUrl); } } } else if (appId === "codex") { const presets = codexProviderPresets; const presetIndex = parseInt(values.presetId.replace("codex-", "")); if ( !isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length ) { const preset = presets[presetIndex]; if (Array.isArray(preset.endpointCandidates)) { preset.endpointCandidates.forEach(addUrl); } } } else if (appId === "gemini") { const presets = geminiProviderPresets; const presetIndex = parseInt( values.presetId.replace("gemini-", ""), ); if ( !isNaN(presetIndex) && presetIndex >= 0 && presetIndex < presets.length ) { const preset = presets[presetIndex]; if (Array.isArray(preset.endpointCandidates)) { preset.endpointCandidates.forEach(addUrl); } } } } if (appId === "claude") { const env = parsedConfig.env as Record | undefined; if (env?.ANTHROPIC_BASE_URL) { addUrl(env.ANTHROPIC_BASE_URL); } } else if (appId === "codex") { const config = parsedConfig.config as string | undefined; if (config) { const extractedBaseUrl = extractCodexBaseUrl(config); if (extractedBaseUrl) { addUrl(extractedBaseUrl); } } } else if (appId === "gemini") { const env = parsedConfig.env as Record | undefined; if (env?.GOOGLE_GEMINI_BASE_URL) { addUrl(env.GOOGLE_GEMINI_BASE_URL); } } else if (appId === "opencode") { const options = parsedConfig.options as | Record | undefined; if (options?.baseURL) { addUrl(options.baseURL); } } else if (appId === "openclaw") { // OpenClaw uses baseUrl directly if (parsedConfig.baseUrl) { addUrl(parsedConfig.baseUrl as string); } } const urls = Array.from(urlSet); if (urls.length > 0) { const now = Date.now(); const customEndpoints: Record = {}; urls.forEach((url) => { customEndpoints[url] = { url, addedAt: now, lastUsed: undefined, }; }); providerData.meta = { ...(providerData.meta ?? {}), custom_endpoints: customEndpoints, }; } } // OpenClaw: pass suggestedDefaults for model registration if (appId === "openclaw" && values.suggestedDefaults) { providerData.suggestedDefaults = values.suggestedDefaults; } await onSubmit(providerData); onOpenChange(false); }, [appId, onSubmit, onOpenChange], ); const footer = !showUniversalTab || activeTab === "app-specific" ? ( <> ) : ( <> ); return ( onOpenChange(false)} footer={footer} > {showUniversalTab ? ( setActiveTab(v as "app-specific" | "universal")} > {t(`apps.${appId}`)} {t("provider.tabProvider")} {t("provider.tabUniversal")} onOpenChange(false)} showButtons={false} /> ) : ( // OpenCode/OpenClaw: directly show form without tabs onOpenChange(false)} showButtons={false} /> )} {showUniversalTab && ( )} ); }