diff --git a/backend/python/app/api/chat.py b/backend/python/app/api/chat.py index 5df16c5..4b51be8 100644 --- a/backend/python/app/api/chat.py +++ b/backend/python/app/api/chat.py @@ -46,9 +46,17 @@ def detect_media_intent(text: str) -> str | None: # 疑问句不触发媒体生成 if t.endswith(("?", "?")) or any(q in t for q in ("吗", "呢", "怎么", "如何", "能否", "是否可以", "可以吗", "会不会", "why", "how")): return None - video_kw = ("视频", "短片", "影片", "vide", "动画视频", "一段视频", "做个视频", "生成视频") - image_kw = ("图片", "图像", "插画", "海报", "照片", "imag", "画一张", "画个", "生成图片", "一张图", "一幅画", "生成一幅") - audio_kw = ("语音", "配音", "朗读", "播报", "念一段", "有声", "tts", "音频", "生成语音") + video_kw = ("视频", "短片", "影片", "vide", "动画视频", "一段视频", "做个视频", "生成视频", + "视频生成", "来段视频", "拍个视频", "做段视频", "视频剪辑") + image_kw = ("图片", "图像", "插画", "海报", "照片", "头像", "壁纸", "漫画", "表情包", "logo", "图标", + "imag", "画一张", "画张", "画一只", "画只", "画个", "画幅", + "来张", "来一张", "来幅", "发张图", "发个图", + "生成图片", "生成一张", "生成一幅", "生成图", "图像生成", + "做一张", "做张图", "做张", "出张图", "出一张图", + "帮我画", "给我画", "请画", "绘制", "作画", "ai绘画", "ai画图", + "画个图", "画张图", "画一幅图", "一张图", "一幅画", "生成一幅") + audio_kw = ("语音", "配音", "朗读", "播报", "念一段", "有声", "tts", "音频", "生成语音", + "读一下", "念一下", "语音合成") if any(k in t for k in video_kw): return "video" if any(k in t for k in image_kw): diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index dc35125..384195e 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -616,6 +616,12 @@ export const adminAPI = { setDefaultModel: (id: string) => put(`/admin/models/${id}/set-default`), + // 按能力默认模型(Auto 自动模式使用) + getCapabilities: () => get('/admin/models/capabilities'), + getCapabilityDefaults: () => get('/admin/models/capability-defaults'), + setCapabilityDefault: (capability: string, modelId: string | null) => + put('/admin/models/capability-defaults', { capability, model_id: modelId }), + // Phase 2: Enhanced model management testModelConnection: (id: string) => post(`/admin/models/${id}/test-connection`), diff --git a/frontend/src/pages/admin/Models.tsx b/frontend/src/pages/admin/Models.tsx index b564d84..a28849b 100644 --- a/frontend/src/pages/admin/Models.tsx +++ b/frontend/src/pages/admin/Models.tsx @@ -15,23 +15,15 @@ const formatNumber = (n: number) => { }; const capabilityColors: Record = { - 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 = { - text: '文本', - vision: '视觉', - audio: '音频', - video: '视频', - image: '图像', - thinking: '思考', - tool: '工具', + text: '文本对话', thinking: '推理思考', vision: '视觉理解', tool: '工具调用', + image: '图像生成', audio: '语音合成', asr: '语音识别', ocr: 'OCR识别', + embedding: '嵌入向量', rerank: '重排序', video: '视频生成', }; // Test result type @@ -69,6 +61,12 @@ const Models: React.FC = () => { 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); @@ -85,10 +83,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(), ]); setProviders(Array.isArray(provRes) ? provRes : (provRes.data || [])); setProtocols(Array.isArray(protoRes) ? protoRes : (protoRes.data || [])); @@ -105,6 +104,11 @@ const Models: React.FC = () => { 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 { @@ -278,6 +282,23 @@ const Models: React.FC = () => { } }; + // 设置/清除某个能力的默认模型(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; @@ -476,6 +497,54 @@ const Models: React.FC = () => { )} + {/* ===== 能力默认模型(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 && (
diff --git a/frontend/src/pages/front/ChatPage.tsx b/frontend/src/pages/front/ChatPage.tsx index 1764f34..bc76f00 100644 --- a/frontend/src/pages/front/ChatPage.tsx +++ b/frontend/src/pages/front/ChatPage.tsx @@ -307,7 +307,7 @@ const ChatPage: React.FC = () => { const { id } = useParams<{ id?: string }>(); const navigate = useNavigate(); const [searchQuery, setSearchQuery] = useState(''); - const [selectedModelId, setSelectedModelId] = useState(''); + const [selectedModelId, setSelectedModelId] = useState('auto'); const [chatModels, setChatModels] = useState([]); const [inputValue, setInputValue] = useState(''); const [sending, setSending] = useState(false); @@ -339,7 +339,7 @@ const ChatPage: React.FC = () => { const models = Array.isArray(data) ? data : (data?.items || []); setChatModels(models); if (models.length > 0 && !selectedModelId) { - setSelectedModelId(models[0].id); + setSelectedModelId('auto'); } }).catch(() => { setChatModels([]); @@ -463,8 +463,12 @@ const ChatPage: React.FC = () => { return [...pinned.filter(c => c.title.toLowerCase().includes(q)), ...unpinned.filter(c => c.title.toLowerCase().includes(q))]; }, [conversations, searchQuery]); - // Selected model info - const selectedModel = chatModels.find(m => m.id === selectedModelId) || chatModels[0] || { id: '', name: '选择模型', status: 'free', provider_name: '' }; + // Selected model info(Auto 自动:按对话意图自动匹配后台各能力默认模型) + const isAutoModel = selectedModelId === 'auto'; + const AUTO_MODEL = { id: 'auto', name: 'Auto 自动', status: 'free', provider_name: '按能力智能匹配', capabilities: [], price_per_million_tokens: 0 }; + const selectedModel = isAutoModel + ? AUTO_MODEL + : (chatModels.find(m => m.id === selectedModelId) || chatModels[0] || AUTO_MODEL); // Scroll to bottom when messages change useEffect(() => { @@ -481,7 +485,7 @@ const ChatPage: React.FC = () => { // Handle new conversation const handleNewConversation = async () => { try { - const res = await chatAPI.createConversation({ title: '新对话', model_id: selectedModelId }); + const res = await chatAPI.createConversation({ title: '新对话', model_id: selectedModelId === 'auto' ? (chatModels[0]?.id || '') : selectedModelId }); const newConv = unwrapData(res); const conv: Conversation = { id: newConv.id, @@ -792,6 +796,17 @@ const ChatPage: React.FC = () => {
} > + {chatModels.filter(m => m.is_enabled !== false).map(model => (