refactor: sync running-container source back to repo for reproducible docker compose build; remove dead code (orphan pipeline.py + 104 stale files from iterative dev) and drop unused app/ mirror dir

This commit is contained in:
qyyfw committed 2026-07-11 06:29:32 +08:00
1 parent 1346fbbd4b
commit 5d7f32db0a
138 files changed
+12846 -84248

No files matched your search

+111 -355
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useCallback } from 'react';
import { adminAPI } from '@/api';
import type { ModelProvider, AIModel, ProtocolDef, FetchableModel } from '@/types';
@@ -15,27 +15,36 @@ const formatNumber = (n: number) => {
};
const capabilityColors: Record<string, string> = {
text: '#5B5FED', thinking: '#8B5CF6', vision: '#219B5E', tool: '#3B82F6',
image: '#D9383A', audio: '#E8922C', asr: '#0EA5E9', ocr: '#F59E0B',
embedding: '#10B981', rerank: '#EC4899', video: '#EF4444',
text: '#5B5FED',
vision: '#219B5E',
audio: '#E8922C',
video: '#D9383A',
image: '#D9383A',
thinking: '#8B5CF6',
tool: '#3B82F6',
};
const capabilityLabels: Record<string, string> = {
text: '文本对话', thinking: '推理思考', vision: '视觉理解', tool: '工具调用',
image: '图像生成', audio: '语音合成', asr: '语音识别', ocr: 'OCR识别',
embedding: '嵌入向量', rerank: '重排序', video: '视频生成',
text: '文本',
vision: '视觉',
audio: '音频',
video: '视频',
image: '图像',
thinking: '思考',
tool: '工具',
};
// Test result type
interface TestResult {
success: boolean;
message: string;
latency_ms?: number;
}
// Model editing type
interface EditingModel {
id?: string;
name: string;
display_name: string;
provider_id: string;
api_key: string;
endpoint: string;
@@ -46,42 +55,6 @@ interface EditingModel {
is_enabled: boolean;
}
// Stats panel
const ModelStatsPanel: React.FC<{ models: AIModel[]; providers: ModelProvider[] }> = ({ models, providers }) => {
const enabled = models.filter(m => m.isEnabled).length;
const defaults = models.filter(m => m.isDefault).length;
const totalCalls = models.reduce((s, m) => s + (m.totalCalls || 0), 0);
const paidModels = models.filter(m => m.status === 'paid').length;
const items = [
{ label: '供应商', value: providers.length, color: '#5B5FED', icon: '🔌' },
{ label: '模型总数', value: models.length, color: '#219B5E', icon: '🤖' },
{ label: '已启用', value: enabled, color: '#16A34A', icon: '✅' },
{ label: '总调用量', value: formatNumber(totalCalls), color: '#E8922C', icon: '📊' },
{ label: '默认模型', value: defaults, color: '#8B5CF6', icon: '⭐' },
{ label: '付费模型', value: paidModels, color: '#EC4899', icon: '💎' },
];
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12, marginBottom: 20 }}>
{items.map((item, i) => (
<div key={i} style={{
background: '#FFFFFF', borderRadius: 10, border: '1px solid rgba(0,0,0,0.08)',
padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 12,
}}>
<span style={{ fontSize: 20 }}>{item.icon}</span>
<div>
<div style={{ fontSize: 11, color: '#6B7280' }}>{item.label}</div>
<div style={{ fontSize: 18, fontWeight: 700, color: item.color, fontFamily: 'var(--font-mono)' }}>
{item.value}
</div>
</div>
</div>
))}
</div>
);
};
const Models: React.FC = () => {
const [providers, setProviders] = useState<ModelProvider[]>([]);
const [models, setModels] = useState<AIModel[]>([]);
@@ -95,19 +68,15 @@ const Models: React.FC = () => {
const [editingProvider, setEditingProvider] = useState<ModelProvider | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [modelSearch, setModelSearch] = useState('');
const [testResults, setTestResults] = useState<Record<string, TestResult>>({});
const [testingIds, setTestingIds] = useState<Set<string>>(new Set());
const [testingAll, setTestingAll] = useState(false);
const [expandedTestResult, setExpandedTestResult] = useState<string | null>(null);
// Model test state
const [testingModelId, setTestingModelId] = useState<string | null>(null);
const [testResult, setTestResult] = useState<TestResult | null>(null);
// Model editor state
const [showModelEditor, setShowModelEditor] = useState(false);
const [editingModel, setEditingModel] = useState<EditingModel | null>(null);
const [modelSaving, setModelSaving] = useState(false);
// 按能力默认模型
const [capDefaults, setCapDefaults] = useState<Record<string, string>>({});
const [capList, setCapList] = useState<any[]>([]);
const [capModels, setCapModels] = useState<any[]>([]);
const [savingCap, setSavingCap] = useState<string | null>(null);
const [providerForm, setProviderForm] = useState({
name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '',
@@ -116,11 +85,10 @@ const Models: React.FC = () => {
const loadData = useCallback(async () => {
try {
setLoading(true);
const [provRes, modelsRes, protoRes, capRes] = await Promise.all([
const [provRes, modelsRes, protoRes] = await Promise.all([
adminAPI.getProviders(),
adminAPI.getAdminModels({ page_size: 100 }),
adminAPI.getProtocols(),
adminAPI.getCapabilityDefaults().catch(() => null),
]);
setProviders(Array.isArray(provRes) ? provRes : (provRes.data || []));
setProtocols(Array.isArray(protoRes) ? protoRes : (protoRes.data || []));
@@ -129,7 +97,6 @@ const Models: React.FC = () => {
const mappedModels = (Array.isArray(rawModels) ? rawModels : []).map((m: any) => ({
...m,
providerId: m.providerId || m.provider_id,
displayName: m.displayName || m.display_name || m.name,
apiKey: m.apiKey || m.api_key,
pricePerMillionTokens: m.pricePerMillionTokens ?? m.price_per_million_tokens ?? 0,
isEnabled: m.isEnabled ?? m.is_enabled ?? true,
@@ -138,12 +105,6 @@ const Models: React.FC = () => {
totalTokens: m.totalTokens ?? m.total_tokens ?? 0,
}));
setModels(mappedModels);
if (capRes) {
const cd = capRes.data || capRes;
setCapList(cd.capabilities || []);
setCapModels(cd.models || []);
setCapDefaults(cd.defaults || {});
}
} catch (err: any) {
setError('加载数据失败:' + (err.message || err.detail || JSON.stringify(err)));
} finally {
@@ -151,104 +112,23 @@ const Models: React.FC = () => {
}
}, []);
// 设置某个能力的默认模型
const handleSetCapDefault = async (capability: string, modelId: string) => {
try {
setSavingCap(capability);
await adminAPI.setCapabilityDefault(capability, modelId || null);
setCapDefaults((prev) => {
const next = { ...prev };
if (modelId) next[capability] = modelId;
else delete next[capability];
return next;
});
} catch (err: any) {
setError('保存失败:' + (err.message || err.detail || JSON.stringify(err)));
} finally {
setSavingCap(null);
}
};
useEffect(() => { loadData(); }, [loadData]);
// Filtered models
const filteredModels = useMemo(() => {
if (!modelSearch.trim()) return models;
const q = modelSearch.toLowerCase();
return models.filter(m =>
m.name.toLowerCase().includes(q) ||
((m as any).display_name || '').toLowerCase().includes(q)
);
}, [models, modelSearch]);
// Test single model connection
// Test model connection
const handleTestConnection = async (modelId: string) => {
setTestingIds(prev => new Set(prev).add(modelId));
setTestingModelId(modelId);
setTestResult(null);
try {
const result = await adminAPI.testModelConnection(modelId) as any;
const testResult: TestResult = result.success !== undefined ? result : (result.data || result as TestResult);
setTestResults(prev => ({ ...prev, [modelId]: testResult }));
setTestResult(result.success !== undefined ? result : (result.data || result as TestResult));
} catch (err: any) {
setTestResults(prev => ({ ...prev, [modelId]: { success: false, message: err.message || '测试失败' } }));
setTestResult({ success: false, message: err.message || '测试失败' });
} finally {
setTestingIds(prev => { const next = new Set(prev); next.delete(modelId); return next; });
setTestingModelId(null);
}
};
// Test all models
const handleTestAll = async () => {
setTestingAll(true);
setTestResults({});
try {
const result = await adminAPI.testAllModels() as any;
const data = result.data || result;
const resultsMap: Record<string, TestResult> = {};
if (data.results) {
for (const r of data.results) {
resultsMap[r.model_id] = {
success: r.success,
message: r.message,
latency_ms: r.latency_ms,
};
}
}
setTestResults(resultsMap);
} catch (err: any) {
setError('批量测试失败: ' + (err.message || ''));
} finally {
setTestingAll(false);
}
};
// Get model type label
const getModelType = (caps: string[] | undefined): string => {
if (!caps || caps.length === 0) return 'text';
const lower = caps.map(c => c.toLowerCase());
if (lower.includes('image')) return 'image';
if (lower.includes('video')) return 'video';
if (lower.includes('audio')) return 'audio';
if (lower.includes('asr') || lower.includes('speech')) return 'asr';
if (lower.includes('ocr')) return 'ocr';
if (lower.includes('embedding')) return 'embedding';
if (lower.includes('rerank')) return 'rerank';
if (lower.includes('vision')) return 'vision';
if (lower.includes('thinking') || lower.includes('tool')) return 'thinking';
return 'text';
};
const modelTypeLabels: Record<string, { label: string; color: string }> = {
text: { label: '文本', color: '#5B5FED' },
thinking: { label: '推理/工具', color: '#8B5CF6' },
image: { label: '图像生成', color: '#D9383A' },
audio: { label: '语音合成', color: '#E8922C' },
asr: { label: '语音识别', color: '#0EA5E9' },
ocr: { label: 'OCR识别', color: '#F59E0B' },
embedding: { label: '嵌入向量', color: '#10B981' },
rerank: { label: '重排序', color: '#EC4899' },
video: { label: '视频生成', color: '#EF4444' },
vision: { label: '视觉理解', color: '#219B5E' },
};
// Set default model
const handleSetDefault = async (modelId: string) => {
try {
await adminAPI.setDefaultModel(modelId);
@@ -258,14 +138,17 @@ const Models: React.FC = () => {
}
};
// 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.api_key, api_base_url: providerForm.api_base_url || undefined,
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: '' });
@@ -282,7 +165,8 @@ const Models: React.FC = () => {
setError(null);
await adminAPI.updateProvider(editingProvider.id, {
name: providerForm.name, protocol: providerForm.protocol,
api_key: providerForm.api_key, api_base_url: providerForm.api_base_url || undefined,
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: '' });
@@ -293,7 +177,7 @@ const Models: React.FC = () => {
};
const handleDeleteProvider = async (id: string) => {
if (!confirm('确定删除该供应商?关联的模型也会受影响。')) return;
if (!confirm('确定删除该供应商?')) return;
try {
await adminAPI.deleteProvider(id);
await loadData();
@@ -302,6 +186,7 @@ const Models: React.FC = () => {
}
};
// Fetch models from provider API
const handleFetchModels = async (providerId: string) => {
try {
const result = await adminAPI.fetchProviderModels(providerId) as any;
@@ -315,6 +200,7 @@ const Models: React.FC = () => {
}
};
// Add selected models
const handleAddModels = async (providerId: string) => {
if (selectedModelIds.size === 0) return;
try {
@@ -328,17 +214,19 @@ const Models: React.FC = () => {
}
};
// Toggle model selection
const toggleModelSelection = (id: string) => {
const next = new Set(selectedModelIds);
next.has(id) ? next.delete(id) : next.add(id);
setSelectedModelIds(next);
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,
display_name: (model as any).display_name || (model as any).displayName || model.name,
provider_id: (model as any).provider_id || model.providerId || providerId || '',
api_key: (model as any).api_key || model.apiKey || '',
endpoint: model.endpoint || '',
@@ -350,7 +238,7 @@ const Models: React.FC = () => {
});
} else {
setEditingModel({
name: '', display_name: '', provider_id: providerId || '',
name: '', provider_id: providerId || '',
api_key: '', endpoint: '',
capabilities: ['text'], timeout: 60,
status: 'paid', price_per_million_tokens: 0, is_enabled: true,
@@ -359,6 +247,7 @@ const Models: React.FC = () => {
setShowModelEditor(true);
};
// Save model
const handleSaveModel = async (e: React.FormEvent) => {
e.preventDefault();
if (!editingModel || !editingModel.name.trim()) return;
@@ -378,6 +267,7 @@ const Models: React.FC = () => {
}
};
// Delete model
const handleDeleteModel = async (modelId: string) => {
if (!confirm('确定删除该模型?')) return;
try {
@@ -388,103 +278,50 @@ const Models: React.FC = () => {
}
};
// Toggle capability
const toggleCapability = (cap: string) => {
if (!editingModel) return;
const caps = editingModel.capabilities || [];
editingModel.capabilities = caps.includes(cap) ? caps.filter(c => c !== cap) : [...caps, cap];
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) =>
filteredModels.filter(m => ((m as any).provider_id || m.providerId) === providerId);
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 (
<div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
<div>
<h2 style={{ margin: 0, fontSize: '20px', fontWeight: 600 }}>大模型配置</h2>
<p style={{ margin: '4px 0 0', color: '#5A6480', fontSize: '13px' }}>
<p style={{ margin: '4px 0 0', color: '#666', fontSize: '13px' }}>
管理 AI 模型供应商、模型配置和连接测试
</p>
</div>
<button onClick={handleTestAll}
disabled={testingAll}
style={btnStyle(testingAll ? '#4B5563' : '#059669')}>
{testingAll ? '批量测试中...' : '⚡ 测试全部'}
</button>
<button onClick={() => { setShowAddProvider(true); setEditingProvider(null); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); }}
<button onClick={() => { setShowAddProvider(true); setEditingProvider(null); setProviderForm({ name: '', protocol: 'openai_compatible', api_key: '', api_base_url: '' }); }}
style={btnStyle('#5B5FED')}>
+ 添加供应商
</button>
</div>
{/* Stats */}
<ModelStatsPanel models={models} providers={providers} />
{/* 按能力指定默认模型 */}
<div style={{ background: '#FFFFFF', borderRadius: '10px', border: '1px solid rgba(0,0,0,0.08)', padding: '20px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 18 }}>🎯</span>
<h3 style={{ margin: 0, fontSize: 16, fontWeight: 600 }}>按能力指定默认模型</h3>
</div>
<p style={{ margin: '4px 0 16px', color: '#6B7280', fontSize: 13 }}>
Auto 模式下,技能 / 应用 / IM Bot 会根据所需能力自动匹配下方指定的默认模型;文本、推理、视觉、工具、OCR 等文本类能力若未单独指定,将回退到全局默认模型。
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 12 }}>
{capList.map((cap) => (
<div key={cap.id} style={{ border: '1px solid #E5E7EB', borderRadius: 8, padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: cap.color }} />
<span style={{ fontWeight: 600, fontSize: 14 }}>{cap.label}</span>
<span style={{ fontSize: 11, color: '#9CA3AF' }}>{cap.id}</span>
</div>
<div style={{ fontSize: 11, color: '#9CA3AF', minHeight: 16 }}>{cap.desc}</div>
<select
value={capDefaults[cap.id] || ''}
disabled={savingCap === cap.id}
onChange={(e) => handleSetCapDefault(cap.id, e.target.value)}
style={{ padding: '7px 10px', border: '1px solid #D1D5DB', borderRadius: 6, fontSize: 13, background: '#fff' }}
>
<option value=''>(未指定 · 文本类回退全局默认)</option>
{capModels
.filter((m) => (m.capabilities || []).includes(cap.id))
.map((m) => (
<option key={m.id} value={m.id}>{m.display_name || m.name}({m.provider_name})</option>
))}
</select>
</div>
))}
</div>
</div>
{error && (
<div style={{ ...alertStyle('#FEE2E2', '#DC2626'), marginBottom: '16px' }}>
{error} <button onClick={() => setError(null)} style={{ float: 'right', background: 'none', border: 'none', cursor: 'pointer', fontSize: '16px' }}>×</button>
</div>
)}
{/* Search */}
<div style={{ marginBottom: 16, display: 'flex', gap: 12, alignItems: 'center' }}>
<div style={{ position: 'relative', flex: 1, maxWidth: 360 }}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#9CA3AF" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)' }}>
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input
type="text" placeholder="搜索模型名称..."
value={modelSearch} onChange={e => setModelSearch(e.target.value)}
style={{ width: '100%', padding: '8px 12px 8px 36px', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '8px', fontSize: '13px', boxSizing: 'border-box' }}
/>
</div>
{modelSearch && (
<span style={{ fontSize: 12, color: '#6B7280' }}>
找到 {filteredModels.length} 个模型
</span>
)}
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '60px', color: '#6B7280' }}>加载中...</div>
<div style={{ textAlign: 'center', padding: '60px', color: '#999' }}>加载中...</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
{providers.map(provider => {
@@ -492,7 +329,8 @@ const Models: React.FC = () => {
const isExpanded = expandedProvider === provider.id;
return (
<div key={provider.id} style={{ background: '#FFFFFF', borderRadius: '10px', border: '1px solid rgba(0,0,0,0.08)', overflow: 'hidden' }}>
<div key={provider.id} style={{ background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB', overflow: 'hidden' }}>
{/* Provider Header */}
<div style={{ padding: '16px 20px', background: '#F9FAFB', borderBottom: '1px solid #E5E7EB', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
@@ -503,9 +341,8 @@ const Models: React.FC = () => {
{provModels.filter(m => m.isDefault).length > 0 && (
<span style={{ fontSize: '11px', padding: '2px 8px', background: '#DCFCE7', color: '#16A34A', borderRadius: '4px' }}>默认</span>
)}
<span style={{ fontSize: '11px', color: '#9CA3AF' }}>{provModels.length} 个模型</span>
</div>
<div style={{ fontSize: '12px', color: '#5A6480', marginTop: '4px' }}>
<div style={{ fontSize: '12px', color: '#666', marginTop: '4px' }}>
API: {(provider as any).api_base_url || provider.apiBaseUrl || '未配置'}
{(provider as any).api_key || provider.apiKey ? ' ✓ 已配置Key' : ' ✗ 未配置Key'}
</div>
@@ -520,6 +357,7 @@ const Models: React.FC = () => {
</div>
</div>
{/* Model Fetch Panel */}
{isExpanded && (
<div style={{ padding: '16px 20px', background: '#FFFBEB', borderBottom: '1px solid #FDE68A' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '12px' }}>
@@ -534,8 +372,7 @@ const Models: React.FC = () => {
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
{fetchedModels.map(m => (
<label key={m.id} style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '13px', cursor: 'pointer',
padding: '4px 8px', background: selectedModelIds.has(m.id) ? 'rgba(108,99,255,0.15)' : 'rgba(17,22,39,0.3)', border: '1px solid rgba(255,255,255,0.1)', borderRadius: '4px' }}>
<label key={m.id} style={{ display: 'flex', alignItems: 'center', gap: '4px', fontSize: '13px', cursor: 'pointer', padding: '4px 8px', background: selectedModelIds.has(m.id) ? '#EDE9FE' : '#fff', border: '1px solid #D1D5DB', borderRadius: '4px' }}>
<input type="checkbox" checked={selectedModelIds.has(m.id)} onChange={() => toggleModelSelection(m.id)} />
{m.name || m.id}
</label>
@@ -544,19 +381,18 @@ const Models: React.FC = () => {
</div>
)}
{/* Models List */}
<div style={{ padding: '0' }}>
{provModels.length === 0 ? (
<div style={{ padding: '24px', textAlign: 'center', color: '#6B7280', fontSize: '13px' }}>
<div style={{ padding: '24px', textAlign: 'center', color: '#999', fontSize: '13px' }}>
暂无模型,点击"获取模型"或"添加模型"添加
</div>
) : (
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#F3F4F6' }}>
<th style={thStyle}>模型名称</th>
<th style={thStyle}>显示名称</th>
<th style={thStyle}>类型</th>
<th style={thStyle}>能力</th>
<th style={thStyle}>模型</th>
<th style={thStyle}>能力</th>
<th style={thStyle}>状态</th>
<th style={thStyle}>价格</th>
<th style={thStyle}>调用量</th>
@@ -568,23 +404,7 @@ const Models: React.FC = () => {
<tr key={model.id} style={{ borderBottom: '1px solid #F3F4F6' }}>
<td style={tdStyle}>
<div style={{ fontWeight: 500 }}>{model.name}</div>
<div style={{ fontSize: 11, color: '#9CA3AF', fontFamily: 'var(--font-mono)' }}>{model.id?.slice(0, 8)}...</div>
</td>
<td style={tdStyle}>
<span style={{ fontSize: 13, color: '#374151' }}>
{(model as any).display_name || (model as any).displayName || '-'}
</span>
{model.isDefault && <span style={{ fontSize: '11px', color: '#16A34A', marginLeft: 6 }}>⭐ 默认</span>}
</td>
<td style={tdStyle}>
<span style={{
fontSize: '11px', padding: '2px 8px', borderRadius: '3px',
background: (modelTypeLabels[getModelType(model.capabilities)].color) + '22',
color: modelTypeLabels[getModelType(model.capabilities)].color,
fontWeight: 500,
}}>
{modelTypeLabels[getModelType(model.capabilities)].label}
</span>
{model.isDefault && <span style={{ fontSize: '11px', color: '#16A34A' }}>⭐ 默认模型</span>}
</td>
<td style={tdStyle}>
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
@@ -601,95 +421,38 @@ const Models: React.FC = () => {
</span>
</td>
<td style={tdStyle}>
<span style={{ fontSize: '12px', color: '#5A6480' }}>
¥{(model.pricePerMillionTokens || 0).toFixed(4)}/M
<span style={{ fontSize: '12px', color: '#666' }}>
¥{model.pricePerMillionTokens?.toFixed(4) || '0.0000'}/M
</span>
</td>
<td style={tdStyle}>
<div style={{ fontSize: '13px', fontWeight: 500, color: '#374151' }}>
<span style={{ fontSize: '12px', color: '#666' }}>
{formatNumber(model.totalCalls || 0)}
</div>
<div style={{ fontSize: '10px', color: '#9CA3AF' }}>
{formatNumber(model.totalTokens || 0)} tokens
</div>
</span>
</td>
<td style={tdStyle}>
<div style={{ display: 'flex', gap: '4px', flexWrap: 'wrap' }}>
<button onClick={() => handleTestConnection(model.id)} disabled={testingIds.has(model.id)}
style={{ ...btnStyle(testingIds.has(model.id) ? '#4B5563' : '#059669', 'small') }}>
{testingIds.has(model.id) ? '测试中...' : '测试'}
{/* Test Connection Button */}
<button onClick={() => handleTestConnection(model.id)} disabled={testingModelId === model.id}
style={{ ...btnStyle(testingModelId === model.id ? '#9CA3AF' : '#059669', 'small') }}>
{testingModelId === model.id ? '测试中...' : '连接测试'}
</button>
{testResults[model.id] && (
<span
onClick={() => setExpandedTestResult(expandedTestResult === model.id ? null : model.id)}
style={{
fontSize: '11px', padding: '4px 8px', cursor: 'pointer',
background: testResults[model.id].success ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
color: testResults[model.id].success ? '#22C55E' : '#EF4444',
borderRadius: '4px', border: `1px solid ${testResults[model.id].success ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
whiteSpace: 'nowrap',
}}>
{testResults[model.id].success ? `✓ ${testResults[model.id].latency_ms || 0}ms` : '✗ 失败'}
</span>
{/* Test Result Popover */}
{testResult && (
<div style={{ position: 'relative' }}>
<span style={{ fontSize: '11px', padding: '4px 8px', background: testResult.success ? '#DCFCE7' : '#FEE2E2', color: testResult.success ? '#16A34A' : '#DC2626', borderRadius: '4px', display: 'inline-block' }}>
{testResult.success ? '✓ 成功' : '✗ 失败'}
{testResult.latency_ms && ` (${testResult.latency_ms}ms)`}
</span>
<div style={{ position: 'absolute', top: '100%', left: 0, zIndex: 10, background: '#fff', border: '1px solid #E5E7EB', borderRadius: '6px', padding: '8px 12px', fontSize: '12px', color: '#333', minWidth: '200px', boxShadow: '0 4px 12px rgba(0,0,0,0.1)', marginTop: '4px' }}>
{testResult.message}
</div>
<button onClick={() => setTestResult(null)} style={{ marginLeft: '8px', fontSize: '11px', color: '#999', background: 'none', border: 'none', cursor: 'pointer' }}>×</button>
</div>
)}
{!model.isDefault && model.isEnabled && (
<button onClick={() => handleSetDefault(model.id)} style={btnStyle('#5B5FED', 'small')}>设为默认</button>
)}
{expandedTestResult === model.id && testResults[model.id] && (
<div style={{
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
background: 'rgba(0,0,0,0.6)', zIndex: 1000,
display: 'flex', alignItems: 'center', justifyContent: 'center',
}} onClick={() => setExpandedTestResult(null)}>
<div style={{
background: '#FFFFFF', borderRadius: 12, padding: 24,
maxWidth: 480, width: '90%',
border: `1px solid ${testResults[model.id].success ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
}} onClick={e => e.stopPropagation()}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
<h3 style={{ margin: 0, fontSize: 16, color: '#E2E8F0' }}>连接测试详情</h3>
<button onClick={() => setExpandedTestResult(null)} style={{ background: 'none', border: 'none', color: '#6B7280', cursor: 'pointer', fontSize: 18 }}>×</button>
</div>
<div style={{ marginBottom: 12 }}>
<span style={{ fontSize: 12, color: '#6B7280' }}>模型</span>
<span style={{ fontSize: 14, color: '#E2E8F0', marginLeft: 8 }}>{model.name}</span>
</div>
<div style={{ marginBottom: 12 }}>
<span style={{ fontSize: 12, color: '#6B7280' }}>类型</span>
<span style={{ fontSize: 14, color: modelTypeLabels[getModelType(model.capabilities)].color, marginLeft: 8 }}>
{modelTypeLabels[getModelType(model.capabilities)].label}
</span>
</div>
<div style={{ marginBottom: 12 }}>
<span style={{ fontSize: 12, color: '#6B7280' }}>状态</span>
<span style={{
fontSize: 14, marginLeft: 8, fontWeight: 600,
color: testResults[model.id].success ? '#22C55E' : '#EF4444',
}}>
{testResults[model.id].success ? '✓ 成功' : '✗ 失败'}
</span>
</div>
{testResults[model.id].latency_ms !== undefined && testResults[model.id].latency_ms !== null && (
<div style={{ marginBottom: 12 }}>
<span style={{ fontSize: 12, color: '#6B7280' }}>延迟</span>
<span style={{ fontSize: 14, color: '#E2E8F0', marginLeft: 8, fontFamily: 'monospace' }}>
{testResults[model.id].latency_ms}ms
</span>
</div>
)}
<div style={{ marginBottom: 16 }}>
<span style={{ fontSize: 12, color: '#6B7280', display: 'block', marginBottom: 4 }}>详情</span>
<div style={{
fontSize: 13, color: '#9CA3AF', lineHeight: 1.6,
background: 'rgba(0,0,0,0.3)', padding: 12, borderRadius: 8,
wordBreak: 'break-word',
}}>
{testResults[model.id].message}
</div>
</div>
</div>
</div>
)}
<button onClick={() => openModelEditor(model as AIModel)} style={btnStyle('#6B7280', 'small')}>编辑</button>
<button onClick={() => handleDeleteModel(model.id)} style={btnStyle('#EF4444', 'small')}>删除</button>
</div>
@@ -705,9 +468,9 @@ const Models: React.FC = () => {
})}
{providers.length === 0 && (
<div style={{ textAlign: 'center', padding: '60px', background: '#FFFFFF', borderRadius: '10px', border: '1px solid rgba(0,0,0,0.08)' }}>
<div style={{ textAlign: 'center', padding: '60px', background: '#fff', borderRadius: '8px', border: '1px solid #E5E7EB' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>🤖</div>
<p style={{ color: '#5A6480', marginBottom: '16px' }}>暂无模型供应商,点击上方按钮添加</p>
<p style={{ color: '#666', marginBottom: '16px' }}>暂无模型供应商,点击上方按钮添加</p>
</div>
)}
</div>
@@ -715,8 +478,8 @@ const Models: React.FC = () => {
{/* Provider Modal */}
{showAddProvider && (
<div style={modalOverlay} onClick={() => { setShowAddProvider(false); setEditingProvider(null); }}>
<div style={modalContent} onClick={e => e.stopPropagation()}>
<div style={modalOverlay}>
<div style={modalContent}>
<h3 style={{ marginTop: 0 }}>{editingProvider ? '编辑供应商' : '添加供应商'}</h3>
<form onSubmit={editingProvider ? handleUpdateProvider : handleAddProvider}>
<div style={formGroup}>
@@ -748,8 +511,8 @@ const Models: React.FC = () => {
{/* Model Editor Modal */}
{showModelEditor && editingModel && (
<div style={modalOverlay} onClick={() => setShowModelEditor(false)}>
<div style={{ ...modalContent, maxWidth: '600px' }} onClick={e => e.stopPropagation()}>
<div style={modalOverlay}>
<div style={{ ...modalContent, maxWidth: '600px' }}>
<h3 style={{ marginTop: 0 }}>{editingModel.id ? '编辑模型' : '添加模型'}</h3>
<form onSubmit={handleSaveModel}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px' }}>
@@ -758,16 +521,12 @@ const Models: React.FC = () => {
<input value={editingModel.name} onChange={e => setEditingModel({ ...editingModel, name: e.target.value })} placeholder="如:DeepSeek-V4-Flash" style={inputStyle} required />
</div>
<div style={formGroup}>
<label style={labelStyle}>显示名称</label>
<input value={editingModel.display_name} onChange={e => setEditingModel({ ...editingModel, display_name: e.target.value })} placeholder="如:DeepSeek V4" style={inputStyle} />
<label style={labelStyle}>供应商</label>
<select value={editingModel.provider_id} onChange={e => setEditingModel({ ...editingModel, provider_id: e.target.value })} style={inputStyle}>
{providers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
</div>
<div style={formGroup}>
<label style={labelStyle}>供应商</label>
<select value={editingModel.provider_id} onChange={e => setEditingModel({ ...editingModel, provider_id: e.target.value })} style={inputStyle}>
{providers.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
</select>
</div>
<div style={formGroup}>
<label style={labelStyle}>API Key</label>
<input type="password" value={editingModel.api_key} onChange={e => setEditingModel({ ...editingModel, api_key: e.target.value })} placeholder="sk-... (留空则使用供应商的Key)" style={inputStyle} />
@@ -780,10 +539,7 @@ const Models: React.FC = () => {
<label style={labelStyle}>模型能力 (可多选)</label>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{Object.entries(capabilityLabels).map(([key, label]) => (
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer',
padding: '4px 10px', background: (editingModel.capabilities || []).includes(key) ? capabilityColors[key] + '22' : '#F3F4F6',
border: `1px solid ${(editingModel.capabilities || []).includes(key) ? capabilityColors[key] : '#D1D5DB'}`,
borderRadius: '4px', fontSize: '13px' }}>
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: '4px', cursor: 'pointer', padding: '4px 10px', background: (editingModel.capabilities || []).includes(key) ? capabilityColors[key] + '22' : '#F3F4F6', border: `1px solid ${(editingModel.capabilities || []).includes(key) ? capabilityColors[key] : '#D1D5DB'}`, borderRadius: '4px', fontSize: '13px' }}>
<input type="checkbox" checked={(editingModel.capabilities || []).includes(key)} onChange={() => toggleCapability(key)} style={{ display: 'none' }} />
<span style={{ color: capabilityColors[key] }}>●</span> {label}
</label>
@@ -829,16 +585,16 @@ const Models: React.FC = () => {
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', whiteSpace: 'nowrap' as const,
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 rgba(255,255,255,0.1)', borderRadius: '6px', fontSize: '13px', boxSizing: 'border-box' };
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: '#FFFFFF', borderRadius: '12px', padding: '24px', width: '480px', maxWidth: '90vw', maxHeight: '90vh', overflow: 'auto', boxSizing: 'border-box' };
const modalContent: React.CSSProperties = { background: '#fff', borderRadius: '12px', padding: '24px', width: '480px', maxWidth: '90vw', maxHeight: '90vh', overflow: 'auto', boxSizing: 'border-box' };
export default Models;