feat: 补全模型能力选项(11种) + 按能力指定默认模型 + Auto自动模式
This commit is contained in:
1 parent
176cb35b54
commit
4c372746d5
6 files changed
+1408
-966
No files matched your search
@@ -15,13 +15,15 @@ const formatNumber = (n: number) => {
|
||||
};
|
||||
|
||||
const capabilityColors: Record<string, string> = {
|
||||
text: '#5B5FED', vision: '#219B5E', audio: '#E8922C',
|
||||
video: '#D9383A', image: '#D9383A', thinking: '#8B5CF6', tool: '#3B82F6',
|
||||
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<string, string> = {
|
||||
text: '文本', vision: '视觉', audio: '音频',
|
||||
video: '视频', image: '图像', thinking: '思考', tool: '工具',
|
||||
text: '文本对话', thinking: '推理思考', vision: '视觉理解', tool: '工具调用',
|
||||
image: '图像生成', audio: '语音合成', asr: '语音识别', ocr: 'OCR识别',
|
||||
embedding: '嵌入向量', rerank: '重排序', video: '视频生成',
|
||||
};
|
||||
|
||||
interface TestResult {
|
||||
@@ -101,6 +103,11 @@ const Models: React.FC = () => {
|
||||
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: '',
|
||||
@@ -109,10 +116,11 @@ const Models: React.FC = () => {
|
||||
const loadData = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const [provRes, modelsRes, protoRes] = await Promise.all([
|
||||
const [provRes, modelsRes, protoRes, capRes] = 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 || []));
|
||||
@@ -130,6 +138,12 @@ 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 {
|
||||
@@ -137,6 +151,24 @@ 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
|
||||
@@ -193,17 +225,27 @@ const Models: React.FC = () => {
|
||||
if (!caps || caps.length === 0) return 'text';
|
||||
const lower = caps.map(c => c.toLowerCase());
|
||||
if (lower.includes('image')) return 'image';
|
||||
if (lower.includes('audio')) return 'audio';
|
||||
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' },
|
||||
video: { label: '视频', color: '#EC4899' },
|
||||
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' },
|
||||
};
|
||||
|
||||
@@ -379,6 +421,42 @@ const Models: React.FC = () => {
|
||||
{/* 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>
|
||||
|
||||
Reference in new issue
Block a user