import { useCallback, useMemo, useState } from 'react' import { Check, ChevronsUpDown, Copy, Eye, EyeOff } from 'lucide-react' import { Button } from '@/components/ui/button' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { HelpTooltip } from '@/components/ui/help-tooltip' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { ScrollArea } from '@/components/ui/scroll-area' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { useToast } from '@/hooks/use-toast' import { PROVIDER_TEMPLATES } from '../providerTemplates' import type { APIProvider, FormErrors } from './types' import { validateProvider } from './utils' interface ProviderFormProps { open: boolean onOpenChange: (open: boolean) => void editingProvider: APIProvider | null editingIndex: number | null providers: APIProvider[] onSave: (provider: APIProvider, index: number | null) => void tourState: { isRunning: boolean } } export function ProviderForm({ open, onOpenChange, editingProvider, editingIndex, providers, onSave, tourState, }: ProviderFormProps) { const [formErrors, setFormErrors] = useState({}) const [selectedTemplate, setSelectedTemplate] = useState('custom') const [templateComboboxOpen, setTemplateComboboxOpen] = useState(false) const [showApiKey, setShowApiKey] = useState(false) const [localProvider, setLocalProvider] = useState(editingProvider) const { toast } = useToast() // 同步外部状态到本地 if (editingProvider !== localProvider && open) { setLocalProvider(editingProvider) setFormErrors({}) setShowApiKey(false) // 检测匹配的模板 if (editingProvider) { const matchedTemplate = PROVIDER_TEMPLATES.find( t => t.base_url === editingProvider.base_url && t.client_type === editingProvider.client_type ) setSelectedTemplate(matchedTemplate?.id || 'custom') } else { setSelectedTemplate('custom') } } const isUsingTemplate = useMemo(() => selectedTemplate !== 'custom', [selectedTemplate]) const handleTemplateChange = useCallback((templateId: string) => { setSelectedTemplate(templateId) setTemplateComboboxOpen(false) const template = PROVIDER_TEMPLATES.find(t => t.id === templateId) if (template && template.id !== 'custom') { setLocalProvider(prev => ({ ...prev!, name: template.name, base_url: template.base_url, client_type: template.client_type, })) } else if (template?.id === 'custom') { setLocalProvider(prev => ({ ...prev!, name: '', base_url: '', client_type: 'openai', })) } }, []) const copyApiKey = useCallback(async () => { if (!localProvider?.api_key) return try { await navigator.clipboard.writeText(localProvider.api_key) toast({ title: '复制成功', description: 'API Key 已复制到剪贴板', }) } catch { toast({ title: '复制失败', description: '无法访问剪贴板', variant: 'destructive', }) } }, [localProvider?.api_key, toast]) const handleSaveEdit = () => { if (!localProvider) return const { isValid, errors } = validateProvider(localProvider, providers, editingIndex) if (!isValid) { setFormErrors(errors) return } setFormErrors({}) onSave(localProvider, editingIndex) } return ( {editingIndex !== null ? '编辑提供商' : '添加提供商'} 配置 API 提供商的连接信息和参数
{ e.preventDefault(); handleSaveEdit(); }} autoComplete="off">
未找到匹配的模板 {PROVIDER_TEMPLATES.map((template) => ( handleTemplateChange(template.id)} > {template.display_name} ))}

选择预设模板可自动填充 URL 和客户端类型,支持搜索

提供商名称

为这个 API 提供商设置一个便于识别的名称,用于在模型配置中引用。

  • 推荐使用厂商官方名称,如 DeepSeek、OpenAI
  • 名称需要唯一,不能与现有提供商重复
} side="right" maxWidth="350px" />
{ setLocalProvider((prev) => prev ? { ...prev, name: e.target.value } : null ) if (formErrors.name) { setFormErrors((prev) => ({ ...prev, name: undefined })) } }} placeholder="例如: DeepSeek, SiliconFlow" className={formErrors.name ? 'border-destructive focus-visible:ring-destructive' : ''} /> {formErrors.name && (

{formErrors.name}

)}

API 基础地址

提供商的 API 端点基础 URL,通常以 /v1 结尾。

  • OpenAI 格式:https://api.openai.com/v1
  • DeepSeek:https://api.deepseek.com
  • 硅基流动:https://api.siliconflow.cn/v1
  • 选择模板会自动填充正确的 URL
} side="right" maxWidth="400px" />
{ setLocalProvider((prev) => prev ? { ...prev, base_url: e.target.value } : null ) if (formErrors.base_url) { setFormErrors((prev) => ({ ...prev, base_url: undefined })) } }} placeholder="https://api.example.com/v1" disabled={isUsingTemplate} className={`${isUsingTemplate ? 'bg-muted cursor-not-allowed' : ''} ${formErrors.base_url ? 'border-destructive focus-visible:ring-destructive' : ''}`} /> {formErrors.base_url && (

{formErrors.base_url}

)} {isUsingTemplate && !formErrors.base_url && (

使用模板时 URL 不可编辑,切换到"自定义"以手动配置

)}

API 密钥

从提供商平台获取的身份验证密钥。

  • 通常以 sk- 开头
  • 请妥善保管,不要泄露给他人
  • 可以点击眼睛图标切换显示/隐藏
  • 点击复制图标可快速复制密钥
} side="right" maxWidth="350px" />
{ setLocalProvider((prev) => prev ? { ...prev, api_key: e.target.value } : null ) if (formErrors.api_key) { setFormErrors((prev) => ({ ...prev, api_key: undefined })) } }} placeholder="sk-..." className={`flex-1 ${formErrors.api_key ? 'border-destructive focus-visible:ring-destructive' : ''}`} />
{formErrors.api_key && (

{formErrors.api_key}

)}

API 客户端类型

指定与提供商通信时使用的 API 协议格式。

  • OpenAI:兼容 OpenAI API 格式的提供商
  • Gemini:Google Gemini 专用格式
  • 大部分第三方提供商都兼容 OpenAI 格式
} side="right" maxWidth="350px" />
{isUsingTemplate && (

使用模板时客户端类型不可编辑,切换到"自定义"以手动配置

)}
{ const val = e.target.value === '' ? null : parseInt(e.target.value) setLocalProvider((prev) => prev ? { ...prev, max_retry: val } : null ) }} placeholder="默认: 2" />
{ const val = e.target.value === '' ? null : parseInt(e.target.value) setLocalProvider((prev) => prev ? { ...prev, timeout: val } : null ) }} placeholder="默认: 30" />
{ const val = e.target.value === '' ? null : parseInt(e.target.value) setLocalProvider((prev) => prev ? { ...prev, retry_interval: val } : null ) }} placeholder="默认: 10" />
) }