import { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { ScrollArea } from '@/components/ui/scroll-area' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command' import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover' import { Checkbox } from '@/components/ui/checkbox' import { Badge } from '@/components/ui/badge' import { Plus, Pencil, Trash2, Save, Eye, EyeOff, Copy, Search, Info, Power, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Check, ChevronsUpDown, Zap, Loader2, CheckCircle2, XCircle, AlertCircle } from 'lucide-react' import { getModelConfig, updateModelConfig, updateModelConfigSection, testProviderConnection, type TestConnectionResult } from '@/lib/config-api' import { useToast } from '@/hooks/use-toast' import { Alert, AlertDescription } from '@/components/ui/alert' import { HelpTooltip } from '@/components/ui/help-tooltip' import { useTour } from '@/components/tour' import { MODEL_ASSIGNMENT_TOUR_ID, modelAssignmentTourSteps, STEP_ROUTE_MAP } from '@/components/tour/tours/model-assignment-tour' import { useNavigate } from '@tanstack/react-router' import { RestartOverlay } from '@/components/restart-overlay' import { RestartProvider, useRestart } from '@/lib/restart-context' import { PROVIDER_TEMPLATES } from './providerTemplates' import type { APIProvider, DeleteConfirmState, FormErrors } from './modelProvider/types' import { cleanProviderData, validateProvider } from './modelProvider/utils' // 主导出组件:包装 RestartProvider export function ModelProviderConfigPage() { return ( ) } // 内部实现组件 function ModelProviderConfigPageContent() { const [providers, setProviders] = useState([]) const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) const [autoSaving, setAutoSaving] = useState(false) const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false) const [editDialogOpen, setEditDialogOpen] = useState(false) const [editingProvider, setEditingProvider] = useState(null) const [editingIndex, setEditingIndex] = useState(null) const [selectedTemplate, setSelectedTemplate] = useState('custom') const [templateComboboxOpen, setTemplateComboboxOpen] = useState(false) const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [deletingIndex, setDeletingIndex] = useState(null) const [showApiKey, setShowApiKey] = useState(false) const [searchQuery, setSearchQuery] = useState('') const [selectedProviders, setSelectedProviders] = useState>(new Set()) const [batchDeleteDialogOpen, setBatchDeleteDialogOpen] = useState(false) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) const [jumpToPage, setJumpToPage] = useState('') // 删除提供商确认对话框状态(合并为单个对象以减少状态变量) const [deleteConfirmState, setDeleteConfirmState] = useState({ isOpen: false, providersToDelete: [], affectedModels: [], pendingProviders: [], context: 'auto', oldProviders: [], }) // 表单验证错误状态 const [formErrors, setFormErrors] = useState({}) // 测试连接状态 const [testingProviders, setTestingProviders] = useState>(new Set()) const [testResults, setTestResults] = useState>(new Map()) const { toast } = useToast() const navigate = useNavigate() const { state: tourState, goToStep, registerTour } = useTour() const { triggerRestart, isRestarting } = useRestart() // 用于防抖的定时器 const autoSaveTimerRef = useRef | null>(null) const initialLoadRef = useRef(true) // 注册 Tour(确保跨页导航时 Tour 仍然可用) useEffect(() => { registerTour(MODEL_ASSIGNMENT_TOUR_ID, modelAssignmentTourSteps) }, [registerTour]) // 监听 Tour 步骤变化,处理页面导航 useEffect(() => { if (tourState.activeTourId === MODEL_ASSIGNMENT_TOUR_ID && tourState.isRunning) { const targetRoute = STEP_ROUTE_MAP[tourState.stepIndex] if (targetRoute && !window.location.pathname.endsWith(targetRoute.replace('/config/', ''))) { navigate({ to: targetRoute }) } } }, [tourState.stepIndex, tourState.activeTourId, tourState.isRunning, navigate]) // 监听 Tour 步骤变化,处理弹窗的打开和关闭 // 提供商弹窗步骤: 3-9 (index 3-9),弹窗外步骤: 0-2 (index 0-2) 和 10+ (index 10+) const prevTourStepRef = useRef(tourState.stepIndex) useEffect(() => { if (tourState.activeTourId === MODEL_ASSIGNMENT_TOUR_ID && tourState.isRunning) { const prevStep = prevTourStepRef.current const currentStep = tourState.stepIndex // 如果从弹窗内步骤 (3-9) 回退到弹窗外步骤 (0-2),关闭弹窗 if (prevStep >= 3 && prevStep <= 9 && currentStep < 3) { setEditDialogOpen(false) } // 如果从弹窗外步骤 (10+) 回退到弹窗内步骤 (3-9),重新打开弹窗 // 这处理了从模型管理页面第 11 步点击"上一步"回到提供商弹窗的情况 if (prevStep >= 10 && currentStep >= 3 && currentStep <= 9) { // 需要打开空白弹窗以便 Tour 可以定位到弹窗内的元素 setFormErrors({}) setSelectedTemplate('custom') setEditingProvider({ name: '', base_url: '', api_key: '', client_type: 'openai', max_retry: 2, timeout: 30, retry_interval: 10, }) setEditingIndex(null) setShowApiKey(false) setEditDialogOpen(true) } prevTourStepRef.current = currentStep } }, [tourState.stepIndex, tourState.activeTourId, tourState.isRunning]) // 处理 Tour 中需要用户点击才能继续的步骤 useEffect(() => { if (tourState.activeTourId !== MODEL_ASSIGNMENT_TOUR_ID || !tourState.isRunning) return const handleTourClick = (e: MouseEvent) => { const target = e.target as HTMLElement const currentStep = tourState.stepIndex // Step 3 (index 2): 点击添加提供商按钮 if (currentStep === 2 && target.closest('[data-tour="add-provider-button"]')) { setTimeout(() => goToStep(3), 300) } // Step 10 (index 9): 点击取消按钮(关闭提供商弹窗) else if (currentStep === 9 && target.closest('[data-tour="provider-cancel-button"]')) { setTimeout(() => goToStep(10), 300) } } document.addEventListener('click', handleTourClick, true) return () => document.removeEventListener('click', handleTourClick, true) }, [tourState, goToStep]) // 加载配置 useEffect(() => { loadConfig() }, []) const loadConfig = async () => { try { setLoading(true) const result = await getModelConfig() if (!result.success) { toast({ title: '加载失败', description: result.error, variant: 'destructive', }) setLoading(false) return } const config = result.data setProviders((config.api_providers as APIProvider[]) || []) setHasUnsavedChanges(false) initialLoadRef.current = false } catch (error) { console.error('加载配置失败:', error) } finally { setLoading(false) } } // 重启麦麦 const handleRestart = async () => { await triggerRestart() } // 保存并重启 const handleSaveAndRestart = async () => { try { setSaving(true) if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } // 清理 providers 数据:将 null 值转换为默认值 const cleanedProviders = providers.map(provider => ({ ...provider, max_retry: provider.max_retry ?? 2, timeout: provider.timeout ?? 30, retry_interval: provider.retry_interval ?? 10, })) // 检查删除提供商的影响 const { shouldProceed } = await checkDeleteProviderImpact(cleanedProviders, 'restart') if (!shouldProceed) { // 需要用户确认,等待确认对话框 setSaving(false) return } const resultGet = await getModelConfig() if (!resultGet.success) { toast({ title: '保存失败', description: resultGet.error, variant: 'destructive', }) setSaving(false) return } const config = resultGet.data // 获取所有有效的 provider 名称 const validProviderNames = new Set(cleanedProviders.map(p => p.name)) // 过滤掉引用已删除 provider 的模型 const originalModels = (config.models as any[]) || [] const filteredModels = originalModels.filter((model: any) => { return validProviderNames.has(model.api_provider) }) config.api_providers = cleanedProviders config.models = filteredModels const resultUpdate = await updateModelConfig(config) if (!resultUpdate.success) { toast({ title: '保存失败', description: resultUpdate.error, variant: 'destructive', }) setSaving(false) return } setHasUnsavedChanges(false) toast({ title: '保存成功', description: '正在重启麦麦...', }) await handleRestart() } catch (error) { console.error('保存配置失败:', error) toast({ title: '保存失败', description: (error as Error).message, variant: 'destructive', }) setSaving(false) } } // 检查删除提供商的影响 const checkDeleteProviderImpact = useCallback(async ( newProviders: APIProvider[], context: 'auto' | 'manual' | 'restart' = 'auto' ) => { try { const result = await getModelConfig() if (!result.success) { console.error('加载配置失败:', result.error) return { shouldProceed: true, providers: newProviders } } const config = result.data const oldProviderNames = new Set(providers.map(p => p.name)) const newProviderNames = new Set(newProviders.map(p => p.name)) // 找出被删除的提供商 const deletedProviders = Array.from(oldProviderNames).filter( name => !newProviderNames.has(name) ) if (deletedProviders.length === 0) { // 没有删除提供商,直接保存 return { shouldProceed: true, providers: newProviders } } // 检查受影响的模型 const models = (config.models as any[]) || [] const affected = models.filter((m: any) => deletedProviders.includes(m.api_provider) ) if (affected.length === 0) { // 没有受影响的模型,直接删除 return { shouldProceed: true, providers: newProviders } } // 有受影响的模型,需要用户确认 setDeleteConfirmState({ isOpen: true, providersToDelete: deletedProviders, affectedModels: affected, pendingProviders: newProviders, context, oldProviders: [...providers], }) return { shouldProceed: false, providers: newProviders } } catch (error) { console.error('检查删除影响失败:', error) return { shouldProceed: true, providers: newProviders } } }, [providers]) // 确认删除提供商及其关联的模型 const handleConfirmDeleteProvider = async () => { try { const savingFlag = deleteConfirmState.context === 'auto' ? setAutoSaving : setSaving savingFlag(true) setDeleteConfirmState(prev => ({ ...prev, isOpen: false })) const resultGet = await getModelConfig() if (!resultGet.success) { toast({ title: '加载失败', description: resultGet.error, variant: 'destructive', }) savingFlag(false) return } const config = resultGet.data // 清理 providers 数据 const cleanedProviders = deleteConfirmState.pendingProviders.map(cleanProviderData) // 获取有效的 provider 名称 const validProviderNames = new Set(cleanedProviders.map(p => p.name)) // 过滤掉引用已删除 provider 的模型 const originalModels = (config.models as any[]) || [] const filteredModels = originalModels.filter((model: any) => { return validProviderNames.has(model.api_provider) }) // 获取被削除的模型名称 const deletedModelNames = new Set( deleteConfirmState.affectedModels.map((m: any) => m.name) ) // 从任务配置中移除这些模型 const modelTaskConfig = config.model_task_config as any if (modelTaskConfig) { Object.keys(modelTaskConfig).forEach(taskName => { const task = modelTaskConfig[taskName] if (task && Array.isArray(task.model_list)) { task.model_list = task.model_list.filter( (modelName: string) => !deletedModelNames.has(modelName) ) } }) } // 更新配置 config.api_providers = cleanedProviders config.models = filteredModels config.model_task_config = modelTaskConfig const resultUpdate = await updateModelConfig(config) if (!resultUpdate.success) { toast({ title: '保存失败', description: resultUpdate.error, variant: 'destructive', }) savingFlag(false) return } // 更新本地状态 setProviders(deleteConfirmState.pendingProviders) setHasUnsavedChanges(false) toast({ title: '删除成功', description: `已删除 ${deleteConfirmState.providersToDelete.length} 个提供商和 ${deleteConfirmState.affectedModels.length} 个关联模型`, }) // 清理状态 setDeleteConfirmState({ isOpen: false, providersToDelete: [], affectedModels: [], pendingProviders: [], context: 'auto', oldProviders: [], }) setSelectedProviders(new Set()) // 清除选中状态(批量删除时) // 根据上下文执行后续操作 if (deleteConfirmState.context === 'restart') { // 如果是保存并重启,继续执行重启流程 await handleRestart() } } catch (error) { console.error('删除失败:', error) toast({ title: '删除失败', description: (error as Error).message, variant: 'destructive', }) } finally { if (deleteConfirmState.context === 'auto') { setAutoSaving(false) } else { setSaving(false) } } } // 取消删除提供商 const handleCancelDeleteProvider = () => { // 恢复到删除前的 providers 状态 if (deleteConfirmState.oldProviders.length > 0) { setProviders(deleteConfirmState.oldProviders) } // 清理状态 setDeleteConfirmState({ isOpen: false, providersToDelete: [], affectedModels: [], pendingProviders: [], context: 'auto', oldProviders: [], }) setHasUnsavedChanges(false) } // 自动保存函数(使用增量 API) const autoSaveProviders = useCallback(async (newProviders: APIProvider[]) => { if (initialLoadRef.current) return // 初始加载时不自动保存 // 检查删除影响 const { shouldProceed } = await checkDeleteProviderImpact(newProviders, 'auto') if (!shouldProceed) { // 需要用户确认,对话框已打开 setHasUnsavedChanges(true) return } try { setAutoSaving(true) // 清理 providers 数据:将 null 值转换为默认值 const cleanedProviders = newProviders.map(cleanProviderData) const result = await updateModelConfigSection('api_providers', cleanedProviders) if (!result.success) { console.error('自动保存失败:', result.error) toast({ title: '自动保存失败', description: result.error, variant: 'destructive', }) setHasUnsavedChanges(true) return } setHasUnsavedChanges(false) } catch (error) { console.error('自动保存失败:', error) toast({ title: '自动保存失败', description: (error as Error).message, variant: 'destructive', }) setHasUnsavedChanges(true) } finally { setAutoSaving(false) } }, [providers, checkDeleteProviderImpact]) // 监听 providers 变化,触发自动保存(带防抖) useEffect(() => { if (initialLoadRef.current) return setHasUnsavedChanges(true) // 清除之前的定时器 if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } // 设置新的定时器(2秒后自动保存) autoSaveTimerRef.current = setTimeout(() => { autoSaveProviders(providers) }, 2000) // 清理函数 return () => { if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } } }, [providers, autoSaveProviders]) // 保存配置(手动保存,保存完整配置) const saveConfig = async () => { try { setSaving(true) // 先取消自动保存定时器 if (autoSaveTimerRef.current) { clearTimeout(autoSaveTimerRef.current) } // 清理 providers 数据:将 null 值转换为默认值 const cleanedProviders = providers.map(cleanProviderData) // 检查删除提供商的影响 const { shouldProceed } = await checkDeleteProviderImpact(cleanedProviders, 'manual') if (!shouldProceed) { // 需要用户确认,等待确认对话框 setSaving(false) return } const resultGet = await getModelConfig() if (!resultGet.success) { toast({ title: '保存失败', description: resultGet.error, variant: 'destructive', }) setSaving(false) return } const config = resultGet.data // 获取所有有效的 provider 名称 const validProviderNames = new Set(cleanedProviders.map(p => p.name)) // 过滤掉引用已删除 provider 的模型 const originalModels = (config.models as any[]) || [] const filteredModels = originalModels.filter((model: any) => { const isValid = validProviderNames.has(model.api_provider) if (!isValid) { console.warn(`模型 "${model.name}" 引用了已删除的提供商 "${model.api_provider}"、将被移除`) } return isValid }) // 如果有模型被移除、显示警告 if (originalModels.length !== filteredModels.length) { const removedCount = originalModels.length - filteredModels.length toast({ title: '注意', description: `已自动移除 ${removedCount} 个引用已删除提供商的模型`, variant: 'default', }) } console.log('发送的 providers 数据:', cleanedProviders) config.api_providers = cleanedProviders config.models = filteredModels console.log('完整配置数据:', config) const resultUpdate = await updateModelConfig(config) if (!resultUpdate.success) { toast({ title: '保存失败', description: resultUpdate.error, variant: 'destructive', }) setSaving(false) return } setHasUnsavedChanges(false) toast({ title: '保存成功', description: '模型提供商配置已保存', }) } catch (error) { console.error('保存配置失败:', error) toast({ title: '保存失败', description: (error as Error).message, variant: 'destructive', }) } finally { setSaving(false) } } // 打开编辑对话框 const openEditDialog = (provider: APIProvider | null, index: number | null) => { // 清除表单验证错误 setFormErrors({}) if (provider) { // 编辑现有提供商 - 检测匹配的模板 const matchedTemplate = PROVIDER_TEMPLATES.find( t => t.base_url === provider.base_url && t.client_type === provider.client_type ) setSelectedTemplate(matchedTemplate?.id || 'custom') setEditingProvider(provider) } else { // 新建提供商 - 默认使用自定义模板 setSelectedTemplate('custom') setEditingProvider({ name: '', base_url: '', api_key: '', client_type: 'openai', max_retry: 2, timeout: 30, retry_interval: 10, }) } setEditingIndex(index) setShowApiKey(false) setEditDialogOpen(true) } // 处理模板选择变化 const handleTemplateChange = useCallback((templateId: string) => { setSelectedTemplate(templateId) setTemplateComboboxOpen(false) const template = PROVIDER_TEMPLATES.find(t => t.id === templateId) if (template && template.id !== 'custom') { // 应用模板配置 setEditingProvider(prev => ({ ...prev!, name: template.name, base_url: template.base_url, client_type: template.client_type, })) } else if (template?.id === 'custom') { // 切换到自定义模板 - 清空URL和客户端类型(保留其他字段) setEditingProvider(prev => ({ ...prev!, name: '', base_url: '', client_type: 'openai', })) } }, []) // 判断当前是否使用模板(非自定义) const isUsingTemplate = useMemo(() => { return selectedTemplate !== 'custom' }, [selectedTemplate]) // 复制 API Key const copyApiKey = useCallback(async () => { if (!editingProvider?.api_key) return try { await navigator.clipboard.writeText(editingProvider.api_key) toast({ title: '复制成功', description: 'API Key 已复制到剪贴板', }) } catch { toast({ title: '复制失败', description: '无法访问剪贴板', variant: 'destructive', }) } }, [editingProvider?.api_key, toast]) // 保存编辑 const handleSaveEdit = () => { if (!editingProvider) return // 验证必填项(传入现有提供商列表和当前编辑索引用于重复检查) const { isValid, errors } = validateProvider(editingProvider, providers, editingIndex) if (!isValid) { setFormErrors(errors) return } // 清除错误状态 setFormErrors({}) // 填充空值的默认值 const providerToSave = cleanProviderData(editingProvider) if (editingIndex !== null) { // 更新现有提供商 const newProviders = [...providers] newProviders[editingIndex] = providerToSave setProviders(newProviders) } else { // 添加新提供商 setProviders([...providers, providerToSave]) } setEditDialogOpen(false) setEditingProvider(null) setEditingIndex(null) } // 处理编辑对话框关闭 const handleEditDialogClose = (open: boolean) => { if (!open && editingProvider) { // 关闭时填充默认值 const updatedProvider = { ...editingProvider, max_retry: editingProvider.max_retry ?? 2, timeout: editingProvider.timeout ?? 30, retry_interval: editingProvider.retry_interval ?? 10, } setEditingProvider(updatedProvider) } setEditDialogOpen(open) } // 打开删除确认对话框 const openDeleteDialog = (index: number) => { setDeletingIndex(index) setDeleteDialogOpen(true) } // 确认删除提供商 const handleConfirmDelete = async () => { if (deletingIndex !== null) { const newProviders = providers.filter((_, i) => i !== deletingIndex) // 检查删除影响 const { shouldProceed } = await checkDeleteProviderImpact(newProviders, 'manual') if (shouldProceed) { // 没有影响,直接删除 setProviders(newProviders) toast({ title: '删除成功', description: '提供商已从列表中移除', }) } // 如果 shouldProceed = false,对话框会自动打开,等待用户确认 } setDeleteDialogOpen(false) setDeletingIndex(null) } // 切换单个提供商选择 const toggleProviderSelection = (index: number) => { const newSelected = new Set(selectedProviders) if (newSelected.has(index)) { newSelected.delete(index) } else { newSelected.add(index) } setSelectedProviders(newSelected) } // 全选/取消全选 const toggleSelectAll = () => { if (selectedProviders.size === filteredProviders.length) { setSelectedProviders(new Set()) } else { const allIndices = filteredProviders.map((_, idx) => providers.findIndex(p => p === filteredProviders[idx]) ) setSelectedProviders(new Set(allIndices)) } } // 打开批量删除确认对话框 const openBatchDeleteDialog = () => { if (selectedProviders.size === 0) { toast({ title: '提示', description: '请先选择要删除的提供商', variant: 'default', }) return } setBatchDeleteDialogOpen(true) } // 确认批量删除 const handleConfirmBatchDelete = async () => { const newProviders = providers.filter((_, index) => !selectedProviders.has(index)) // 检查删除影响 const { shouldProceed } = await checkDeleteProviderImpact(newProviders, 'manual') if (shouldProceed) { // 没有影响,直接删除 setProviders(newProviders) setSelectedProviders(new Set()) toast({ title: '批量删除成功', description: `已删除 ${selectedProviders.size} 个提供商`, }) } // 如果 shouldProceed = false,对话框会自动打开,等待用户确认 setBatchDeleteDialogOpen(false) } // 过滤提供商列表(使用 useMemo 优化性能) const filteredProviders = useMemo(() => { if (!searchQuery) return providers const query = searchQuery.toLowerCase() return providers.filter((provider) => ( provider.name.toLowerCase().includes(query) || provider.base_url.toLowerCase().includes(query) || provider.client_type.toLowerCase().includes(query) )) }, [providers, searchQuery]) // 分页逻辑(使用 useMemo 优化性能) const { totalPages, paginatedProviders } = useMemo(() => { const total = Math.ceil(filteredProviders.length / pageSize) const paginated = filteredProviders.slice( (page - 1) * pageSize, page * pageSize ) return { totalPages: total, paginatedProviders: paginated } }, [filteredProviders, page, pageSize]) // 页码跳转 const handleJumpToPage = useCallback(() => { const targetPage = parseInt(jumpToPage) if (targetPage >= 1 && targetPage <= totalPages) { setPage(targetPage) setJumpToPage('') } }, [jumpToPage, totalPages]) // 测试单个提供商连接 const handleTestConnection = async (providerName: string) => { // 标记正在测试 setTestingProviders(prev => new Set(prev).add(providerName)) try { const result = await testProviderConnection(providerName) if (!result.success) { toast({ title: '测试失败', description: result.error, variant: 'destructive', }) return } const testResult = result.data setTestResults(prev => new Map(prev).set(providerName, testResult)) // 显示结果 toast if (testResult.network_ok) { if (testResult.api_key_valid === true) { toast({ title: '连接正常', description: `${providerName} 网络连接正常、API Key 有效 (${testResult.latency_ms}ms)`, }) } else if (testResult.api_key_valid === false) { toast({ title: '连接正常但 Key 无效', description: `${providerName} 网络连接正常、但 API Key 无效或已过期`, variant: 'destructive', }) } else { toast({ title: '网络连接正常', description: `${providerName} 可以访问 (${testResult.latency_ms}ms)`, }) } } else { toast({ title: '连接失败', description: testResult.error || '无法连接到提供商', variant: 'destructive', }) } } catch (error) { toast({ title: '测试失败', description: (error as Error).message, variant: 'destructive', }) } finally { setTestingProviders(prev => { const newSet = new Set(prev) newSet.delete(providerName) return newSet }) } } // 批量测试所有提供商 const handleTestAllConnections = async () => { for (const provider of providers) { await handleTestConnection(provider.name) } } // 渲染测试状态指示器 const renderTestStatus = (providerName: string) => { const isTesting = testingProviders.has(providerName) const result = testResults.get(providerName) if (isTesting) { return ( 测试中 ) } if (!result) return null if (result.network_ok) { if (result.api_key_valid === true) { return ( 正常 ) } else if (result.api_key_valid === false) { return ( Key无效 ) } else { return ( 可访问 ) } } else { return ( 离线 ) } } if (loading) { return (

加载中...

) } return (
{/* 页面标题 */}

AI模型厂商配置

管理 AI 模型厂商的 API 配置

{selectedProviders.size > 0 && ( )} 确认重启麦麦?

{hasUnsavedChanges ? '当前有未保存的配置更改。点击确认将先保存配置,然后重启麦麦使新配置生效。重启过程中麦麦将暂时离线。' : '即将重启麦麦主程序。重启过程中麦麦将暂时离线,配置将在重启后生效。' }

取消 {hasUnsavedChanges ? '保存并重启' : '确认重启'}
{/* 重启提示 */} 配置更新后需要重启麦麦才能生效。你可以点击右上角的"保存并重启"按钮一键完成保存和重启。 {/* 搜索框 */}
setSearchQuery(e.target.value)} className="pl-9" />
{searchQuery && (

找到 {filteredProviders.length} 个结果

)}
{/* 提供商列表 - 移动端卡片视图 */}
{filteredProviders.length === 0 ? (
{searchQuery ? '未找到匹配的提供商' : '暂无提供商配置,点击"添加提供商"开始配置'}
) : ( paginatedProviders.map((provider, displayIndex) => { const actualIndex = providers.findIndex(p => p === provider) return (

{provider.name}

{renderTestStatus(provider.name)}

{provider.base_url}

客户端类型

{provider.client_type}

最大重试

{provider.max_retry}

超时(秒)

{provider.timeout}

重试间隔(秒)

{provider.retry_interval}

) }) )}
{/* 提供商列表 - 桌面端表格视图 */}
0} onCheckedChange={toggleSelectAll} /> 状态 名称 基础URL 客户端类型 最大重试 超时(秒) 重试间隔(秒) 操作 {paginatedProviders.length === 0 ? ( {searchQuery ? '未找到匹配的提供商' : '暂无提供商配置,点击"添加提供商"开始配置'} ) : ( paginatedProviders.map((provider, displayIndex) => { const actualIndex = providers.findIndex(p => p === provider) return ( toggleProviderSelection(actualIndex)} /> {renderTestStatus(provider.name) || ( 未测试 )} {provider.name} {provider.base_url} {provider.client_type} {provider.max_retry} {provider.timeout} {provider.retry_interval}
) }) )}
{/* 分页 - 增强版 */} {filteredProviders.length > 0 && (
显示 {(page - 1) * pageSize + 1} 到{' '} {Math.min(page * pageSize, filteredProviders.length)} 条,共 {filteredProviders.length} 条
setJumpToPage(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleJumpToPage()} placeholder={page.toString()} className="w-16 h-8 text-center" min={1} max={totalPages} />
)}
{/* 编辑对话框 */} {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" />
{ setEditingProvider((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" />
{ setEditingProvider((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" />
{ setEditingProvider((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) setEditingProvider((prev) => prev ? { ...prev, max_retry: val } : null ) }} placeholder="默认: 2" />
{ const val = e.target.value === '' ? null : parseInt(e.target.value) setEditingProvider((prev) => prev ? { ...prev, timeout: val } : null ) }} placeholder="默认: 30" />
{ const val = e.target.value === '' ? null : parseInt(e.target.value) setEditingProvider((prev) => prev ? { ...prev, retry_interval: val } : null ) }} placeholder="默认: 10" />
{/* 删除确认对话框 */} 确认删除 确定要删除提供商 "{deletingIndex !== null ? providers[deletingIndex]?.name : ''}" 吗? 此操作无法撤销。 取消 删除 {/* 批量删除确认对话框 */} 确认批量删除 确定要删除选中的 {selectedProviders.size} 个提供商吗? 此操作无法撤销。 取消 批量删除 {/* 删除提供商影响确认对话框 */} setDeleteConfirmState(prev => ({ ...prev, isOpen: open }))}> 确认删除提供商

您即将删除以下提供商: {deleteConfirmState.providersToDelete.join(', ')}

⚠️ 此操作将同时删除 {deleteConfirmState.affectedModels.length} 个关联的模型:

{deleteConfirmState.affectedModels.map((model: any, idx: number) => (
{model.name} ({model.model_identifier})
))}

这些模型将从模型列表和所有任务分配中移除。此操作无法撤销。

取消 确认删除
{/* 重启遮罩层 */} ) }