import { useEffect, useState } from 'react' import { Button, Card, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Tag, } from 'antd' import { CloudServerOutlined, ExperimentOutlined, PlusOutlined, ReloadOutlined, DeleteOutlined, EditOutlined, } from '@ant-design/icons' import { createLLMModelConfig, deleteLLMModelConfig, getLLMModelConfigDetail, getLLMModelConfigs, getLLMProviderCatalog, testLLMModelConfig, updateLLMModelConfig, updateLLMModelConfigStatus, } from '@/api/llmModelConfigs' import ListTable from '@/components/ListTable/ListTable' import PageHeader from '@/components/PageHeader/PageHeader' import Toast from '@/components/Toast/Toast' import './ModelConfigs.css' import '@/pages/System/AdminPages.css' const { Search, TextArea } = Input function buildModelCode(provider, llmModelName) { const providerPart = (provider || 'custom').trim().toLowerCase() const modelPart = (llmModelName || '').trim().toLowerCase() const sanitized = [] let previousSeparator = false for (const char of modelPart) { if (/[a-z0-9]/.test(char)) { sanitized.push(char) previousSeparator = false } else if (!previousSeparator) { sanitized.push('_') previousSeparator = true } } const suffix = sanitized.join('').replace(/^_+|_+$/g, '') || 'model' return `llm_${providerPart}_${suffix}` } function buildModelName(providerMeta, provider, llmModelName) { const label = providerMeta?.label || provider || '自定义模型' const modelPart = (llmModelName || '').trim() if (!modelPart) { return label } return `${label} ${modelPart}` } function formatDateTime(value) { if (!value) return '-' return new Date(value).toLocaleString('zh-CN') } function ModelConfigs() { const [form] = Form.useForm() const [loading, setLoading] = useState(false) const [submitting, setSubmitting] = useState(false) const [testing, setTesting] = useState(false) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(10) const [total, setTotal] = useState(0) const [keyword, setKeyword] = useState('') const [providerFilter, setProviderFilter] = useState(undefined) const [statusFilter, setStatusFilter] = useState(undefined) const [configs, setConfigs] = useState([]) const [providerCatalog, setProviderCatalog] = useState([]) const [modalVisible, setModalVisible] = useState(false) const [editingConfigId, setEditingConfigId] = useState(null) const [autoFillFlags, setAutoFillFlags] = useState({ endpointUrl: true, modelName: true, modelCode: true, }) const providerValue = Form.useWatch('provider', form) const llmModelNameValue = Form.useWatch('llm_model_name', form) useEffect(() => { loadProviderCatalog() }, []) useEffect(() => { loadConfigs() }, [page, pageSize, keyword, providerFilter, statusFilter]) useEffect(() => { if (!modalVisible || !providerValue) { return } const providerMeta = providerCatalog.find((item) => item.value === providerValue) if (!providerMeta) { return } const currentValues = form.getFieldsValue() const nextValues = {} if (autoFillFlags.endpointUrl) { const nextEndpointUrl = providerMeta.default_endpoint_url || '' if (nextEndpointUrl !== currentValues.endpoint_url) { nextValues.endpoint_url = nextEndpointUrl } } if (llmModelNameValue) { if (autoFillFlags.modelName) { const nextModelName = buildModelName(providerMeta, providerValue, llmModelNameValue) if (nextModelName !== currentValues.model_name) { nextValues.model_name = nextModelName } } if (autoFillFlags.modelCode) { const nextModelCode = buildModelCode(providerValue, llmModelNameValue) if (nextModelCode !== currentValues.model_code) { nextValues.model_code = nextModelCode } } } if (Object.keys(nextValues).length > 0) { form.setFieldsValue(nextValues) } }, [modalVisible, providerValue, llmModelNameValue, providerCatalog, autoFillFlags, form]) const loadProviderCatalog = async () => { try { const res = await getLLMProviderCatalog() setProviderCatalog(res.data || []) } catch (error) { console.error('Load LLM provider catalog error:', error) Toast.error('加载提供方列表失败') } } const loadConfigs = async () => { try { setLoading(true) const params = { page, page_size: pageSize, } if (keyword) params.keyword = keyword if (providerFilter) params.provider = providerFilter if (statusFilter !== undefined) params.is_active = statusFilter const res = await getLLMModelConfigs(params) setConfigs(res.data || []) setTotal(res.total || 0) } catch (error) { console.error('Load llm model configs error:', error) Toast.error('加载模型配置失败') } finally { setLoading(false) } } const getProviderMeta = (provider) => providerCatalog.find((item) => item.value === provider) const openCreateModal = () => { const defaultProvider = providerCatalog[0]?.value || 'openai' const defaultEndpointUrl = getProviderMeta(defaultProvider)?.default_endpoint_url || '' setEditingConfigId(null) setAutoFillFlags({ endpointUrl: true, modelName: true, modelCode: true, }) form.setFieldsValue({ provider: defaultProvider, endpoint_url: defaultEndpointUrl, llm_timeout: 120, llm_temperature: 0.7, llm_top_p: 0.9, llm_max_tokens: 2048, is_active: true, is_default: false, description: '', llm_system_prompt: '', model_name: '', model_code: '', llm_model_name: '', api_key: '', }) setModalVisible(true) } const openEditModal = async (record) => { try { const res = await getLLMModelConfigDetail(record.config_id) const detail = res.data const providerMeta = getProviderMeta(detail.provider) setEditingConfigId(record.config_id) setAutoFillFlags({ endpointUrl: !detail.endpoint_url || detail.endpoint_url === (providerMeta?.default_endpoint_url || ''), modelName: !detail.model_name || detail.model_name === buildModelName(providerMeta, detail.provider, detail.llm_model_name), modelCode: !detail.model_code || detail.model_code === buildModelCode(detail.provider, detail.llm_model_name), }) form.setFieldsValue({ ...detail, api_key: detail.api_key || '', }) setModalVisible(true) } catch (error) { console.error('Load llm model config detail error:', error) Toast.error('加载模型配置详情失败') } } const closeModal = () => { setModalVisible(false) setEditingConfigId(null) form.resetFields() } const handleSubmit = async (values) => { try { setSubmitting(true) if (editingConfigId) { await updateLLMModelConfig(editingConfigId, values) Toast.success('模型配置更新成功') } else { await createLLMModelConfig(values) Toast.success('模型配置创建成功') } closeModal() loadConfigs() } catch (error) { Toast.error(error.response?.data?.detail || '保存模型配置失败') } finally { setSubmitting(false) } } const handleDelete = async (record) => { try { await deleteLLMModelConfig(record.config_id) Toast.success('模型配置删除成功') loadConfigs() } catch (error) { Toast.error(error.response?.data?.detail || '删除模型配置失败') } } const handleStatusChange = async (record, checked) => { try { await updateLLMModelConfigStatus(record.config_id, checked) Toast.success(checked ? '模型已启用' : '模型已停用') loadConfigs() } catch (error) { Toast.error(error.response?.data?.detail || '更新状态失败') } } const showTestResult = (result) => { Modal.info({ title: '模型测试成功', width: 620, content: (
提供方:{getProviderMeta(result.provider)?.label || result.provider}
模型:{result.llm_model_name}
延迟:{result.latency_ms} ms
返回预览:{result.preview || '模型已成功返回,但未提取到文本内容。'}
), }) } const handleFormTest = async () => { try { const values = await form.validateFields() setTesting(true) const res = await testLLMModelConfig(values) showTestResult(res.data) } catch (error) { if (error?.errorFields) { return } Toast.error(error.response?.data?.detail || '模型测试失败') } finally { setTesting(false) } } const columns = [ { title: '模型名称', dataIndex: 'model_name', key: 'model_name', width: 240, render: (_, record) => ( {record.model_name} {record.model_code} ), }, { title: '提供方', dataIndex: 'provider', key: 'provider', width: 140, render: (provider) => ( {getProviderMeta(provider)?.label || provider || '-'} ), }, { title: '模型标识', dataIndex: 'llm_model_name', key: 'llm_model_name', width: 180, }, { title: 'Base URL', dataIndex: 'endpoint_url', key: 'endpoint_url', ellipsis: true, render: (value) => value || '-', }, { title: '状态', dataIndex: 'is_active', key: 'is_active', width: 100, render: (value, record) => ( handleStatusChange(record, checked)} /> ), }, { title: '默认', dataIndex: 'is_default', key: 'is_default', width: 90, render: (value) => ( value ? 默认 : ), }, { title: '更新时间', dataIndex: 'updated_at', key: 'updated_at', width: 180, render: (value) => formatDateTime(value), }, { title: '操作', key: 'action', fixed: 'right', width: 170, render: (_, record) => ( handleDelete(record)} > ), }, ] return (
} extra={( )} />
{ setPage(1) setKeyword(event.target.value) }} onSearch={(value) => { setPage(1) setKeyword(value) }} /> { setPage(1) setStatusFilter(value) }} />
{ setPage(nextPage) setPageSize(nextPageSize) }, showTotal: (value) => `共 ${value} 条`, }} />
form.submit()} confirmLoading={submitting} okText={editingConfigId ? '保存修改' : '创建'} cancelText="取消" styles={{ body: { maxHeight: 'calc(80vh - 180px)', overflowY: 'auto', paddingRight: 8, }, }} footer={(_, { OkBtn, CancelBtn }) => ( <> )} >
自动生成规则: 选择提供方后会自动带出默认 `base_url`;填写模型标识后会自动生成“模型名称”和“模型编码”。 如果你手动改过这些字段,后续就不会再被自动覆盖。
{ setAutoFillFlags((current) => ({ ...current, endpointUrl: false })) }} /> { setAutoFillFlags((current) => ({ ...current, modelName: false })) }} /> { setAutoFillFlags((current) => ({ ...current, modelCode: false })) }} />