import { useEffect, useState } from 'react'; import { ApiOutlined, CheckCircleOutlined, LockOutlined, PlusOutlined, RocketOutlined, StarFilled } from '@ant-design/icons'; import { Button, Modal, Form, Input, Select, Space, Tag, App as AntApp, Empty, Popconfirm, Tooltip } from 'antd'; import { LLMKind, LLMProvider, LLMProviderAPI } from '../api'; const KIND_OPTIONS: { value: LLMKind; label: string; baseUrl: string; hint?: string }[] = [ { value: 'openai', label: 'OpenAI 官方', baseUrl: 'https://api.openai.com/v1' }, { value: 'openai-compatible', label: 'OpenAI 兼容(GLM/通义/DeepSeek/腾讯 Token Plan)', baseUrl: '', hint: '智谱:https://open.bigmodel.cn/api/paas/v4 · 通义:https://dashscope.aliyuncs.com/compatible-mode/v1 · DeepSeek:https://api.deepseek.com' }, { value: 'anthropic', label: 'Anthropic Claude', baseUrl: 'https://api.anthropic.com' }, { value: 'ollama', label: 'Ollama 本地', baseUrl: 'http://localhost:11434' } ]; export default function LLMProvidersPage() { const { message } = AntApp.useApp(); const [list, setList] = useState([]); const [editorOpen, setEditorOpen] = useState(false); const [editing, setEditing] = useState(null); const [form] = Form.useForm(); const [testing, setTesting] = useState(null); const load = async () => { try { setList(await LLMProviderAPI.list()); } catch (e: any) { message.error('加载失败:' + (e?.message ?? e)); } }; useEffect(() => { load(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const openCreate = () => { setEditing(null); form.resetFields(); form.setFieldsValue({ kind: 'openai-compatible', enabled: true, isDefault: false }); setEditorOpen(true); }; const openEdit = (p: LLMProvider) => { setEditing(p); form.setFieldsValue({ name: p.name, kind: p.kind, baseUrl: p.baseUrl, models: p.models?.join(', ') || '', defaultModel: p.defaultModel, enabled: p.enabled, isDefault: p.isDefault }); setEditorOpen(true); }; const onSave = async () => { const v = await form.validateFields(); const payload = { ...v, models: typeof v.models === 'string' ? v.models.split(/[,,]/).map((s: string) => s.trim()).filter(Boolean) : v.models }; try { if (editing) { await LLMProviderAPI.update(editing.id, payload); message.success('已更新'); } else { await LLMProviderAPI.create(payload); message.success('已创建'); } setEditorOpen(false); load(); } catch (e: any) { message.error('保存失败:' + (e?.response?.data?.error ?? e?.message ?? e)); } }; const onDelete = async (p: LLMProvider) => { await LLMProviderAPI.remove(p.id); message.success('已删除'); load(); }; const onTest = async (p: LLMProvider) => { setTesting(p.id); try { const r = await LLMProviderAPI.test(p.id, p.defaultModel); if (r.ok) { message.success(`✅ 连通:${r.model} · 用量 ${(r as any).usage?.TotalTokens ?? '?'} tokens`); } else { message.error('❌ 失败:' + (r.error || 'unknown')); } } catch (e: any) { message.error('测试失败:' + (e?.response?.data?.error ?? e?.message ?? e)); } finally { setTesting(null); } }; const onSetDefault = async (p: LLMProvider) => { await LLMProviderAPI.setDefault(p.id); message.success('已设为默认'); load(); }; return (
模型接入中心

LLM 提供商

把不同的模型能力接入到同一个工作台里。这里不只是存放接口配置,更是管理默认模型、候选模型与稳定连接状态的地方。
{[ { label: '已接入提供商', value: list.length, tone: 'rgba(8, 145, 178, 0.10)', color: 'var(--color-brand)' }, { label: '可用连接数', value: list.filter((item) => item.enabled).length, tone: 'rgba(34, 197, 94, 0.10)', color: 'var(--color-success)' }, { label: '默认模型源', value: list.filter((item) => item.isDefault).length, tone: 'rgba(249, 115, 22, 0.10)', color: 'var(--color-warning)' } ].map((item) => (
{item.label}
{item.value} 实时状态
))}
{list.length === 0 ? (
) : (
{list.map((p) => (
{p.name} {p.isDefault && ( } style={{ background: 'var(--color-success-soft)', color: 'var(--color-success)', borderRadius: 999, margin: 0 }}> 默认 )} {!p.enabled && ( 已禁用 )}
{p.baseUrl}
{!p.isDefault && ( )}
接入类型
{p.kind}
默认模型
{p.defaultModel || '未设置'}
密钥状态
{p.hasApiKey ? ( 已配置 {p.apiKeyMasked} ) : ( 尚未配置 API Key )}
{p.baseUrl}
{p.models?.length > 0 && (
候选模型
{p.models.map((m) => ( {m} ))}
)}
{p.enabled ? '可用于聊天、工作流和测试连接' : '当前已暂停使用,需要重新启用'} onDelete(p)}>
))}
)} setEditorOpen(false)} onOk={onSave} width={600} okText="保存" cancelText="取消" destroyOnHidden >
{ if (changed.kind) { const opt = KIND_OPTIONS.find((o) => o.value === changed.kind); if (opt?.baseUrl && !form.getFieldValue('baseUrl')) { form.setFieldsValue({ baseUrl: opt.baseUrl }); } } }} > k.value === form.getFieldValue('kind'))?.hint} >
); }