import React, { useState, useEffect, useCallback, useRef } from 'react'; import { adminAPI } from '@/api'; import type { ModelProvider, AIModel, ProtocolDef, FetchableModel } from '@/types'; const SVGIcon: React.FC<{ children: React.ReactNode; size?: number }> = ({ children, size = 16 }) => ( {children} ); const formatNumber = (n: number) => { if (n >= 100000000) return (n / 100000000).toFixed(1) + '亿'; if (n >= 10000) return (n / 10000).toFixed(1) + '万'; return n.toLocaleString(); }; const capabilityColors: Record = { text: '#5B5FED', thinking: '#8B5CF6', vision: '#219B5E', tool: '#3B82F6', image: '#D9383A', audio: '#E8922C', asr: '#0EA5E9', ocr: '#F59E0B', embedding: '#10B981', rerank: '#EC4899', video: '#EF4444', }; const capabilityLabels: Record = { text: '文本对话', thinking: '推理思考', vision: '视觉理解', tool: '工具调用', image: '图像生成', audio: '语音合成', asr: '语音识别', ocr: 'OCR识别', embedding: '嵌入向量', rerank: '重排序', video: '视频生成', }; // Test result type interface TestResult { modelId: string; success: boolean; message: string; latency_ms?: number; } // Model editing type interface EditingModel { id?: string; name: string; provider_id: string; api_key: string; endpoint: string; capabilities: string[]; timeout: number; status: string; price_per_million_tokens: number; is_enabled: boolean; } const Models: React.FC = () => { const [providers, setProviders] = useState([]); const [models, setModels] = useState([]); const [protocols, setProtocols] = useState([]); const [expandedProvider, setExpandedProvider] = useState(null); const [fetchedModels, setFetchedModels] = useState([]); const [fetchSource, setFetchSource] = useState(''); const [fetchMessage, setFetchMessage] = useState(''); const [selectedModelIds, setSelectedModelIds] = useState>(new Set()); const [showAddProvider, setShowAddProvider] = useState(false); const [editingProvider, setEditingProvider] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 能力默认模型(Auto 自动模式) const [capabilities, setCapabilities] = useState([]); const [capabilityDefaults, setCapabilityDefaults] = useState>({}); const [capabilityModels, setCapabilityModels] = useState([]); const [savingCaps, setSavingCaps] = useState>(new Set()); // Model test state const [testingModelId, setTestingModelId] = useState(null); const [testResult, setTestResult] = useState(null); const testTimer = useRef(null); // 测试反馈自动消失(6 秒后) useEffect(() => { return () => { if (testTimer.current) clearTimeout(testTimer.current); }; }, []); // Model editor state const [showModelEditor, setShowModelEditor] = useState(false); const [editingModel, setEditingModel] = useState(null); const [modelSaving, setModelSaving] = useState(false); const [providerForm, setProviderForm] = useState({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '', }); const loadData = useCallback(async () => { try { setLoading(true); const [provRes, modelsRes, protoRes, capRes] = await Promise.all([ adminAPI.getProviders(), adminAPI.getAdminModels({ page_size: 100 }), adminAPI.getProtocols(), adminAPI.getCapabilityDefaults(), ]); setProviders(Array.isArray(provRes) ? provRes : (provRes.data || [])); setProtocols(Array.isArray(protoRes) ? protoRes : (protoRes.data || [])); const modelData = modelsRes.data || modelsRes; const rawModels = Array.isArray(modelData) ? modelData : (modelData?.items || []); const mappedModels = (Array.isArray(rawModels) ? rawModels : []).map((m: any) => ({ ...m, providerId: m.providerId || m.provider_id, apiKey: m.apiKey || m.api_key, pricePerMillionTokens: m.pricePerMillionTokens ?? m.price_per_million_tokens ?? 0, isEnabled: m.isEnabled ?? m.is_enabled ?? true, isDefault: m.isDefault ?? m.is_default ?? false, totalCalls: m.totalCalls ?? m.total_calls ?? 0, totalTokens: m.totalTokens ?? m.total_tokens ?? 0, })); setModels(mappedModels); // 能力默认模型 const cd = (capRes.data || capRes) as any; setCapabilities(cd.capabilities || []); setCapabilityDefaults(cd.defaults || {}); setCapabilityModels(cd.models || []); } catch (err: any) { setError('加载数据失败:' + (err.message || err.detail || JSON.stringify(err))); } finally { setLoading(false); } }, []); useEffect(() => { loadData(); }, [loadData]); // Test model connection (single model only) const handleTestConnection = async (modelId: string) => { setTestingModelId(modelId); setTestResult(null); if (testTimer.current) { clearTimeout(testTimer.current); testTimer.current = null; } try { const result = await adminAPI.testModelConnection(modelId) as any; const r = result.success !== undefined ? result : (result.data || result); setTestResult({ modelId, success: !!r.success, message: r.message || '', latency_ms: r.latency_ms }); } catch (err: any) { setTestResult({ modelId, success: false, message: err.message || '测试失败' }); } finally { setTestingModelId(null); // 6 秒后自动收起反馈 testTimer.current = window.setTimeout(() => setTestResult(null), 6000); } }; // Set default model const handleSetDefault = async (modelId: string) => { try { await adminAPI.setDefaultModel(modelId); await loadData(); } catch (err: any) { setError(err.message || '设置失败'); } }; // Provider operations const handleAddProvider = async (e: React.FormEvent) => { e.preventDefault(); if (!providerForm.name.trim()) return; try { setError(null); await adminAPI.addProvider({ name: providerForm.name, protocol: providerForm.protocol, api_key: (providerForm as any).api_key || providerForm.api_key, api_base_url: (providerForm as any).api_base_url || providerForm.api_base_url || undefined, }); setShowAddProvider(false); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); await loadData(); } catch (err: any) { setError(err.message || '添加供应商失败'); } }; const handleUpdateProvider = async (e: React.FormEvent) => { e.preventDefault(); if (!editingProvider || !providerForm.name.trim()) return; try { setError(null); await adminAPI.updateProvider(editingProvider.id, { name: providerForm.name, protocol: providerForm.protocol, api_key: (providerForm as any).api_key || providerForm.api_key, api_base_url: (providerForm as any).api_base_url || providerForm.api_base_url || undefined, }); setEditingProvider(null); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); await loadData(); } catch (err: any) { setError(err.message || '更新供应商失败'); } }; const handleDeleteProvider = async (id: string) => { if (!confirm('确定删除该供应商?')) return; try { await adminAPI.deleteProvider(id); await loadData(); } catch (err: any) { setError(err.message || '删除失败'); } }; // Fetch models from provider API const handleFetchModels = async (providerId: string) => { try { const result = await adminAPI.fetchProviderModels(providerId) as any; setFetchedModels(result.models || result.data?.models || []); setFetchSource(result.source || result.data?.source || 'unknown'); setFetchMessage(result.message || result.data?.message || ''); setExpandedProvider(providerId); setSelectedModelIds(new Set()); } catch (err: any) { setError(err.message || '获取模型列表失败'); } }; // Add selected models const handleAddModels = async (providerId: string) => { if (selectedModelIds.size === 0) return; try { await adminAPI.addModels(providerId, Array.from(selectedModelIds)); setExpandedProvider(null); setSelectedModelIds(new Set()); setFetchedModels([]); await loadData(); } catch (err: any) { setError(err.message || '添加模型失败'); } }; // Toggle model selection const toggleModelSelection = (id: string) => { const newSet = new Set(selectedModelIds); if (newSet.has(id)) newSet.delete(id); else newSet.add(id); setSelectedModelIds(newSet); }; // Open model editor const openModelEditor = (model?: AIModel, providerId?: string) => { if (model) { setEditingModel({ id: model.id, name: model.name, provider_id: (model as any).provider_id || model.providerId || providerId || '', api_key: (model as any).api_key || model.apiKey || '', endpoint: model.endpoint || '', capabilities: model.capabilities || ['text'], timeout: model.timeout || 60, status: model.status || 'paid', price_per_million_tokens: model.pricePerMillionTokens ?? 0, is_enabled: model.isEnabled ?? true, }); } else { setEditingModel({ name: '', provider_id: providerId || '', api_key: '', endpoint: '', capabilities: ['text'], timeout: 60, status: 'paid', price_per_million_tokens: 0, is_enabled: true, }); } setShowModelEditor(true); }; // Save model const handleSaveModel = async (e: React.FormEvent) => { e.preventDefault(); if (!editingModel || !editingModel.name.trim()) return; setModelSaving(true); try { if (editingModel.id) { await adminAPI.updateAdminModel(editingModel.id, editingModel as any); } else { await adminAPI.createAdminModel(editingModel as any); } setShowModelEditor(false); await loadData(); } catch (err: any) { setError(err.message || '保存失败'); } finally { setModelSaving(false); } }; // Delete model const handleDeleteModel = async (modelId: string) => { if (!confirm('确定删除该模型?')) return; try { await adminAPI.deleteAdminModel(modelId); await loadData(); } catch (err: any) { setError(err.message || '删除失败'); } }; // 设置/清除某个能力的默认模型(Auto 自动模式使用) const handleSetCapabilityDefault = async (capId: string, modelId: string | null) => { try { setSavingCaps(prev => new Set(prev).add(capId)); const res = await adminAPI.setCapabilityDefault(capId, modelId) as any; setCapabilityDefaults((res && res.defaults) || { ...capabilityDefaults, [capId]: modelId || undefined } as Record); } catch (err: any) { setError(err.message || '设置失败'); } finally { setSavingCaps(prev => { const n = new Set(prev); n.delete(capId); return n; }); } }; // Toggle capability const toggleCapability = (cap: string) => { if (!editingModel) return; const caps = editingModel.capabilities || []; if (caps.includes(cap)) { editingModel.capabilities = caps.filter(c => c !== cap); } else { editingModel.capabilities = [...caps, cap]; } setEditingModel({ ...editingModel }); }; // Group models by provider const getProviderModels = (providerId: string) => models.filter(m => ((m as any).provider_id || m.providerId) === providerId); const getProviderName = (providerId: string) => { const p = providers.find(p => p.id === providerId); return p ? p.name : providerId; }; return (

大模型配置

管理 AI 模型供应商、模型配置和连接测试

{error && (
{error}
)} {loading ? (
加载中...
) : (
{providers.map(provider => { const provModels = getProviderModels(provider.id); const isExpanded = expandedProvider === provider.id; return (
{/* Provider Header */}
{provider.name} {provider.protocol} {provModels.filter(m => m.isDefault).length > 0 && ( 默认 )}
API: {(provider as any).api_base_url || provider.apiBaseUrl || '未配置'} {(provider as any).api_key || provider.apiKey ? ' ✓ 已配置Key' : ' ✗ 未配置Key'}
{/* Model Fetch Panel */} {isExpanded && (
{fetchMessage || `找到 ${fetchedModels.length} 个模型(来源: ${fetchSource})`} {fetchedModels.length > 0 && ( )}
{fetchedModels.map(m => ( ))}
)} {/* Models List */}
{provModels.length === 0 ? (
暂无模型,点击"获取模型"或"添加模型"添加
) : ( {provModels.map(model => ( ))}
模型 能力 状态 价格 调用量 操作
{model.name}
{model.isDefault && ⭐ 默认模型}
{(model.capabilities || []).map(cap => ( {capabilityLabels[cap] || cap} ))}
{model.isEnabled ? '启用' : '禁用'} ¥{model.pricePerMillionTokens?.toFixed(4) || '0.0000'}/M {formatNumber(model.totalCalls || 0)}
{/* Test Connection Button */} {/* Test Result Popover (only for the tested model) */} {testResult && testResult.modelId === model.id && (
{testResult.success ? '✓ 成功' : '✗ 失败'} {testResult.latency_ms && ` (${testResult.latency_ms}ms)`}
{testResult.message}
)} {!model.isDefault && model.isEnabled && ( )}
)}
); })} {providers.length === 0 && (
🤖

暂无模型供应商,点击上方按钮添加

)}
)} {/* ===== 能力默认模型(Auto 自动模式) ===== */} {capabilities.length > 0 && (

能力默认模型

为每项能力指定默认模型,前台「Auto 自动」将按对话意图自动匹配

选择「Auto 自动」时,系统依据用户输入(如含「画图 / 视频」)自动选用对应能力的默认模型;文本类能力在未单独配置时回退到全局默认模型。

{capabilities.map((cap: any) => { const capModels = capabilityModels.filter( (m: any) => (m.capabilities || []).includes(cap.id) || capabilityDefaults[cap.id] === m.id ); const cur = capabilityDefaults[cap.id] || ''; const saving = savingCaps.has(cap.id); const dotColor = cap.color || capabilityColors[cap.id] || '#6B7280'; return (
{cap.label} {cap.id}
{cap.desc}
); })}
)} {/* Provider Modal */} {showAddProvider && (

{editingProvider ? '编辑供应商' : '添加供应商'}

setProviderForm({ ...providerForm, name: e.target.value })} placeholder="如:SiliconFlow" style={inputStyle} required />
setProviderForm({ ...providerForm, api_base_url: e.target.value })} placeholder="https://api.siliconflow.cn" style={inputStyle} />
setProviderForm({ ...providerForm, api_key: e.target.value })} placeholder="sk-..." style={inputStyle} />
)} {/* Model Editor Modal */} {showModelEditor && editingModel && (

{editingModel.id ? '编辑模型' : '添加模型'}

setEditingModel({ ...editingModel, name: e.target.value })} placeholder="如:DeepSeek-V4-Flash" style={inputStyle} required />
setEditingModel({ ...editingModel, api_key: e.target.value })} placeholder="sk-... (留空则使用供应商的Key)" style={inputStyle} />
setEditingModel({ ...editingModel, endpoint: e.target.value })} placeholder="https://api.siliconflow.cn/v1/chat/completions" style={inputStyle} />
{Object.entries(capabilityLabels).map(([key, label]) => ( ))}
setEditingModel({ ...editingModel, timeout: parseInt(e.target.value) || 60 })} style={inputStyle} />
setEditingModel({ ...editingModel, price_per_million_tokens: parseFloat(e.target.value) || 0 })} style={inputStyle} />
)}
); }; // Styles const btnStyle = (color: string, size: 'normal' | 'small' = 'normal'): React.CSSProperties => ({ background: color, color: '#fff', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: size === 'small' ? '12px' : '14px', padding: size === 'small' ? '4px 10px' : '8px 16px', fontWeight: 500, transition: 'opacity 0.2s', }); const thStyle: React.CSSProperties = { padding: '10px 16px', textAlign: 'left', fontSize: '12px', color: '#6B7280', fontWeight: 600 }; const tdStyle: React.CSSProperties = { padding: '12px 16px', fontSize: '13px', verticalAlign: 'middle' }; const formGroup: React.CSSProperties = { marginBottom: '16px' }; const labelStyle: React.CSSProperties = { display: 'block', marginBottom: '6px', fontSize: '13px', fontWeight: 500, color: '#374151' }; const inputStyle: React.CSSProperties = { width: '100%', padding: '8px 12px', border: '1px solid #D1D5DB', borderRadius: '6px', fontSize: '13px', boxSizing: 'border-box' }; const alertStyle = (bg: string, color: string): React.CSSProperties => ({ background: bg, color, padding: '10px 16px', borderRadius: '6px', fontSize: '13px' }); const modalOverlay: React.CSSProperties = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }; const modalContent: React.CSSProperties = { background: '#fff', borderRadius: '12px', padding: '24px', width: '480px', maxWidth: '90vw', maxHeight: '90vh', overflow: 'auto', boxSizing: 'border-box' }; export default Models;