import { useState, useEffect, useCallback, useMemo } from "react"; import { useDarkMode } from "@/hooks/useDarkMode"; import { useTranslation } from "react-i18next"; import { Eye, EyeOff, RefreshCw } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { FullScreenPanel } from "@/components/common/FullScreenPanel"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { ProviderIcon } from "@/components/ProviderIcon"; import JsonEditor from "@/components/JsonEditor"; import type { UniversalProvider, UniversalProviderModels } from "@/types"; import { universalProviderPresets, createUniversalProviderFromPreset, type UniversalProviderPreset, } from "@/config/universalProviderPresets"; import { deepClone } from "@/utils/deepClone"; interface UniversalProviderFormModalProps { isOpen: boolean; onClose: () => void; onSave: (provider: UniversalProvider) => void; onSaveAndSync?: (provider: UniversalProvider) => void; editingProvider?: UniversalProvider | null; initialPreset?: UniversalProviderPreset | null; } export function UniversalProviderFormModal({ isOpen, onClose, onSave, onSaveAndSync, editingProvider, initialPreset, }: UniversalProviderFormModalProps) { const isDarkMode = useDarkMode(); const { t } = useTranslation(); const isEditMode = !!editingProvider; // 表单状态 const [selectedPreset, setSelectedPreset] = useState(null); const [name, setName] = useState(""); const [baseUrl, setBaseUrl] = useState(""); const [apiKey, setApiKey] = useState(""); const [showApiKey, setShowApiKey] = useState(false); const [websiteUrl, setWebsiteUrl] = useState(""); const [notes, setNotes] = useState(""); // 应用启用状态 const [claudeEnabled, setClaudeEnabled] = useState(true); const [codexEnabled, setCodexEnabled] = useState(true); const [geminiEnabled, setGeminiEnabled] = useState(true); // 模型配置 const [models, setModels] = useState({}); // 保存并同步确认弹窗 const [syncConfirmOpen, setSyncConfirmOpen] = useState(false); const [pendingProvider, setPendingProvider] = useState(null); // 初始化表单 useEffect(() => { if (editingProvider) { // 编辑模式:加载现有数据 setName(editingProvider.name); setBaseUrl(editingProvider.baseUrl); setApiKey(editingProvider.apiKey); setWebsiteUrl(editingProvider.websiteUrl || ""); setNotes(editingProvider.notes || ""); setClaudeEnabled(editingProvider.apps.claude); setCodexEnabled(editingProvider.apps.codex); setGeminiEnabled(editingProvider.apps.gemini); setModels(editingProvider.models || {}); // 尝试匹配预设 const preset = universalProviderPresets.find( (p) => p.providerType === editingProvider.providerType, ); setSelectedPreset(preset || null); } else { // 新建模式:使用传入的预设或默认选择第一个预设 const defaultPreset = initialPreset || universalProviderPresets[0]; setSelectedPreset(defaultPreset); setName(defaultPreset.name); setBaseUrl(""); setApiKey(""); setWebsiteUrl(defaultPreset.websiteUrl || ""); setNotes(""); setClaudeEnabled(defaultPreset.defaultApps.claude); setCodexEnabled(defaultPreset.defaultApps.codex); setGeminiEnabled(defaultPreset.defaultApps.gemini); setModels(deepClone(defaultPreset.defaultModels)); } }, [editingProvider, initialPreset, isOpen]); // 选择预设 const handlePresetSelect = useCallback( (preset: UniversalProviderPreset) => { setSelectedPreset(preset); if (!isEditMode) { setName(preset.name); setClaudeEnabled(preset.defaultApps.claude); setCodexEnabled(preset.defaultApps.codex); setGeminiEnabled(preset.defaultApps.gemini); setModels(deepClone(preset.defaultModels)); } }, [isEditMode], ); // 更新模型配置 const updateModel = useCallback( (app: "claude" | "codex" | "gemini", field: string, value: string) => { setModels((prev) => ({ ...prev, [app]: { ...(prev[app] || {}), [field]: value, }, })); }, [], ); // 计算 Claude 配置 JSON 预览 const claudeConfigJson = useMemo(() => { if (!claudeEnabled) return null; const model = models.claude?.model || "claude-sonnet-4-20250514"; const haiku = models.claude?.haikuModel || "claude-haiku-4-20250514"; const sonnet = models.claude?.sonnetModel || "claude-sonnet-4-20250514"; const opus = models.claude?.opusModel || "claude-sonnet-4-20250514"; return { env: { ANTHROPIC_BASE_URL: baseUrl, ANTHROPIC_AUTH_TOKEN: apiKey, ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: opus, }, }; }, [claudeEnabled, baseUrl, apiKey, models.claude]); // 计算 Codex 配置 JSON 预览 const codexConfigJson = useMemo(() => { if (!codexEnabled) return null; const model = models.codex?.model || "gpt-5.5"; const reasoningEffort = models.codex?.reasoningEffort || "high"; // 确保 base_url 以 /v1 结尾(Codex 使用 OpenAI 兼容 API) const codexBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl.replace(/\/+$/, "")}/v1`; const configToml = `model_provider = "custom" model = "${model}" model_reasoning_effort = "${reasoningEffort}" disable_response_storage = true [model_providers.custom] name = "NewAPI" base_url = "${codexBaseUrl}" wire_api = "responses" requires_openai_auth = true`; return { auth: { OPENAI_API_KEY: apiKey, }, config: configToml, }; }, [codexEnabled, baseUrl, apiKey, models.codex]); // 计算 Gemini 配置 JSON 预览 const geminiConfigJson = useMemo(() => { if (!geminiEnabled) return null; const model = models.gemini?.model || "gemini-2.5-pro"; return { env: { GOOGLE_GEMINI_BASE_URL: baseUrl, GEMINI_API_KEY: apiKey, GEMINI_MODEL: model, }, }; }, [geminiEnabled, baseUrl, apiKey, models.gemini]); // 提交表单 const handleSubmit = useCallback(() => { if (!name.trim() || !baseUrl.trim() || !apiKey.trim()) { return; } const provider: UniversalProvider = editingProvider ? { ...editingProvider, name: name.trim(), baseUrl: baseUrl.trim(), apiKey: apiKey.trim(), websiteUrl: websiteUrl.trim() || undefined, notes: notes.trim() || undefined, apps: { claude: claudeEnabled, codex: codexEnabled, gemini: geminiEnabled, }, models, } : createUniversalProviderFromPreset( selectedPreset || universalProviderPresets[0], crypto.randomUUID(), baseUrl.trim(), apiKey.trim(), name.trim(), ); // 如果是新建,更新应用启用状态和模型 if (!editingProvider) { provider.apps = { claude: claudeEnabled, codex: codexEnabled, gemini: geminiEnabled, }; provider.models = models; provider.websiteUrl = websiteUrl.trim() || undefined; provider.notes = notes.trim() || undefined; } onSave(provider); onClose(); }, [ editingProvider, name, baseUrl, apiKey, websiteUrl, notes, claudeEnabled, codexEnabled, geminiEnabled, models, selectedPreset, onSave, onClose, ]); // 构建 provider 对象的辅助函数 const buildProvider = useCallback((): UniversalProvider | null => { if (!name.trim() || !baseUrl.trim() || !apiKey.trim()) { return null; } const provider: UniversalProvider = editingProvider ? { ...editingProvider, name: name.trim(), baseUrl: baseUrl.trim(), apiKey: apiKey.trim(), websiteUrl: websiteUrl.trim() || undefined, notes: notes.trim() || undefined, apps: { claude: claudeEnabled, codex: codexEnabled, gemini: geminiEnabled, }, models, } : createUniversalProviderFromPreset( selectedPreset || universalProviderPresets[0], crypto.randomUUID(), baseUrl.trim(), apiKey.trim(), name.trim(), ); // 如果是新建,更新应用启用状态和模型 if (!editingProvider) { provider.apps = { claude: claudeEnabled, codex: codexEnabled, gemini: geminiEnabled, }; provider.models = models; provider.websiteUrl = websiteUrl.trim() || undefined; provider.notes = notes.trim() || undefined; } return provider; }, [ editingProvider, name, baseUrl, apiKey, websiteUrl, notes, claudeEnabled, codexEnabled, geminiEnabled, models, selectedPreset, ]); // 打开保存并同步确认弹窗 const handleSaveAndSyncClick = useCallback(() => { const provider = buildProvider(); if (!provider || !onSaveAndSync) return; setPendingProvider(provider); setSyncConfirmOpen(true); }, [buildProvider, onSaveAndSync]); // 确认保存并同步 const confirmSaveAndSync = useCallback(() => { if (!pendingProvider || !onSaveAndSync) return; onSaveAndSync(pendingProvider); setSyncConfirmOpen(false); setPendingProvider(null); onClose(); }, [pendingProvider, onSaveAndSync, onClose]); const footer = ( <> {isEditMode && onSaveAndSync ? ( ) : ( )} ); return (
{/* 预设选择(仅新建模式) */} {!isEditMode && (
{universalProviderPresets.map((preset) => ( ))}
{selectedPreset?.description && (

{selectedPreset.description}

)}
)} {/* 基本信息 */}
setName(e.target.value)} placeholder={t("universalProvider.namePlaceholder", { defaultValue: "例如:我的 NewAPI", })} />
setBaseUrl(e.target.value)} placeholder="https://api.example.com" />
setApiKey(e.target.value)} placeholder="sk-..." className="pr-10" />
setWebsiteUrl(e.target.value)} placeholder={t("universalProvider.websiteUrlPlaceholder", { defaultValue: "https://example.com(可选,用于在列表中显示)", })} />
setNotes(e.target.value)} placeholder={t("universalProvider.notesPlaceholder", { defaultValue: "可选:添加备注信息", })} />
{/* 应用启用 */}
Claude Code
OpenAI Codex
Gemini CLI
{/* 模型配置 */}
{/* Claude 模型 */} {claudeEnabled && (
Claude
updateModel("claude", "model", e.target.value) } placeholder="claude-sonnet-4-20250514" />
updateModel("claude", "haikuModel", e.target.value) } placeholder="claude-haiku-4-20250514" />
updateModel("claude", "sonnetModel", e.target.value) } placeholder="claude-sonnet-4-20250514" />
updateModel("claude", "opusModel", e.target.value) } placeholder="claude-sonnet-4-20250514" />
)} {/* Codex 模型 */} {codexEnabled && (
Codex
updateModel("codex", "model", e.target.value) } placeholder="gpt-5.5" />
updateModel("codex", "reasoningEffort", e.target.value) } placeholder="high" />
)} {/* Gemini 模型 */} {geminiEnabled && (
Gemini
updateModel("gemini", "model", e.target.value) } placeholder="gemini-2.5-pro" />
)}
{/* 配置 JSON 预览 */} {isEditMode && (claudeEnabled || codexEnabled || geminiEnabled) && (

{t("universalProvider.configJsonPreviewHint", { defaultValue: "以下是将要同步到各应用的配置内容(仅覆盖显示的字段,保留其他自定义配置)", })}

{/* Claude JSON */} {claudeConfigJson && (
Claude
{}} height={180} darkMode={isDarkMode} />
)} {/* Codex JSON */} {codexConfigJson && (
Codex
{}} height={280} darkMode={isDarkMode} />
)} {/* Gemini JSON */} {geminiConfigJson && (
Gemini
{}} height={140} darkMode={isDarkMode} />
)}
)}
{/* 保存并同步确认弹窗 */} { setSyncConfirmOpen(false); setPendingProvider(null); }} />
); }