2026-04-08 17:03:57 +00:00
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
|
|
import {
|
|
|
|
|
|
Button,
|
|
|
|
|
|
Card,
|
|
|
|
|
|
Form,
|
|
|
|
|
|
Input,
|
|
|
|
|
|
InputNumber,
|
|
|
|
|
|
Modal,
|
|
|
|
|
|
Popconfirm,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
Radio,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
Select,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
Slider,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
Space,
|
|
|
|
|
|
Switch,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
Tabs,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
Tag,
|
|
|
|
|
|
} from 'antd'
|
|
|
|
|
|
import {
|
|
|
|
|
|
CloudServerOutlined,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
CommentOutlined,
|
|
|
|
|
|
DeploymentUnitOutlined,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const MAX_TOKENS_OPTIONS = [
|
|
|
|
|
|
{ label: '4K', value: 4096 },
|
|
|
|
|
|
{ label: '8K', value: 8192 },
|
|
|
|
|
|
{ label: '16K', value: 16384 },
|
|
|
|
|
|
{ label: '32K', value: 32768 },
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
const DEFAULT_MAX_TOKENS = 8192
|
|
|
|
|
|
|
|
|
|
|
|
const MODEL_TYPE_META = {
|
|
|
|
|
|
chat: {
|
|
|
|
|
|
label: '对话模型',
|
|
|
|
|
|
icon: <CommentOutlined />,
|
|
|
|
|
|
description: '用于知识库对话、问答生成的大语言模型。',
|
|
|
|
|
|
addText: '新增对话模型',
|
|
|
|
|
|
},
|
|
|
|
|
|
embedding: {
|
|
|
|
|
|
label: '向量模型',
|
|
|
|
|
|
icon: <DeploymentUnitOutlined />,
|
|
|
|
|
|
description: '用于文档向量化(ZVec)与语义检索的 Embedding 模型。',
|
|
|
|
|
|
addText: '新增向量模型',
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildModelCode(provider, llmModelName, modelType) {
|
2026-04-08 17:03:57 +00:00
|
|
|
|
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'
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const base = `llm_${providerPart}_${suffix}`
|
|
|
|
|
|
return modelType === 'embedding' ? `emb_${base}` : base
|
2026-04-08 17:03:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const [activeType, setActiveType] = useState('chat')
|
|
|
|
|
|
const [editingType, setEditingType] = useState('chat')
|
2026-04-08 17:03:57 +00:00
|
|
|
|
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)
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const isEmbedding = editingType === 'embedding'
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadProviderCatalog()
|
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadConfigs()
|
2026-07-24 07:37:22 +00:00
|
|
|
|
}, [activeType, page, pageSize, keyword, providerFilter, statusFilter])
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const nextModelCode = buildModelCode(providerValue, llmModelNameValue, editingType)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
if (nextModelCode !== currentValues.model_code) {
|
|
|
|
|
|
nextValues.model_code = nextModelCode
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (Object.keys(nextValues).length > 0) {
|
|
|
|
|
|
form.setFieldsValue(nextValues)
|
|
|
|
|
|
}
|
2026-07-24 07:37:22 +00:00
|
|
|
|
}, [modalVisible, providerValue, llmModelNameValue, providerCatalog, autoFillFlags, editingType, form])
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
model_type: activeType,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const handleTabChange = (key) => {
|
|
|
|
|
|
setActiveType(key)
|
|
|
|
|
|
setPage(1)
|
|
|
|
|
|
setKeyword('')
|
|
|
|
|
|
setProviderFilter(undefined)
|
|
|
|
|
|
setStatusFilter(undefined)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 17:03:57 +00:00
|
|
|
|
const openCreateModal = () => {
|
|
|
|
|
|
const defaultProvider = providerCatalog[0]?.value || 'openai'
|
|
|
|
|
|
const defaultEndpointUrl = getProviderMeta(defaultProvider)?.default_endpoint_url || ''
|
|
|
|
|
|
setEditingConfigId(null)
|
2026-07-24 07:37:22 +00:00
|
|
|
|
setEditingType(activeType)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
setAutoFillFlags({
|
|
|
|
|
|
endpointUrl: true,
|
|
|
|
|
|
modelName: true,
|
|
|
|
|
|
modelCode: true,
|
|
|
|
|
|
})
|
|
|
|
|
|
form.setFieldsValue({
|
2026-07-24 07:37:22 +00:00
|
|
|
|
model_type: activeType,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
provider: defaultProvider,
|
|
|
|
|
|
endpoint_url: defaultEndpointUrl,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
llm_timeout: activeType === 'embedding' ? 60 : 120,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
llm_temperature: 0.7,
|
|
|
|
|
|
llm_top_p: 0.9,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
llm_max_tokens: DEFAULT_MAX_TOKENS,
|
|
|
|
|
|
embedding_dimension: undefined,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
is_active: true,
|
|
|
|
|
|
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
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const detailType = detail.model_type || 'chat'
|
2026-04-08 17:03:57 +00:00
|
|
|
|
const providerMeta = getProviderMeta(detail.provider)
|
|
|
|
|
|
|
|
|
|
|
|
setEditingConfigId(record.config_id)
|
2026-07-24 07:37:22 +00:00
|
|
|
|
setEditingType(detailType)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
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),
|
2026-07-24 07:37:22 +00:00
|
|
|
|
modelCode: !detail.model_code || detail.model_code === buildModelCode(detail.provider, detail.llm_model_name, detailType),
|
2026-04-08 17:03:57 +00:00
|
|
|
|
})
|
|
|
|
|
|
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)
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const payload = { ...values, model_type: editingType }
|
2026-04-08 17:03:57 +00:00
|
|
|
|
if (editingConfigId) {
|
2026-07-24 07:37:22 +00:00
|
|
|
|
await updateLLMModelConfig(editingConfigId, payload)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
Toast.success('模型配置更新成功')
|
|
|
|
|
|
} else {
|
2026-07-24 07:37:22 +00:00
|
|
|
|
await createLLMModelConfig(payload)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
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) => {
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const providerLabel = getProviderMeta(result.provider)?.label || result.provider
|
|
|
|
|
|
const lines = [
|
|
|
|
|
|
`提供方:${providerLabel}`,
|
|
|
|
|
|
`模型:${result.llm_model_name}`,
|
|
|
|
|
|
`延迟:${result.latency_ms} ms`,
|
|
|
|
|
|
]
|
|
|
|
|
|
if (result.dimension) {
|
|
|
|
|
|
lines.push(`向量维度:${result.dimension}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
if (result.preview) {
|
|
|
|
|
|
lines.push(`返回预览:${result.preview}`)
|
|
|
|
|
|
}
|
|
|
|
|
|
const description = (
|
|
|
|
|
|
<div style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
|
|
|
|
|
|
{lines.map((line, index) => (
|
|
|
|
|
|
<div key={index}>{line}</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
Toast.success('模型测试成功', description, 5)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleFormTest = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const values = await form.validateFields()
|
|
|
|
|
|
setTesting(true)
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const res = await testLLMModelConfig({ ...values, model_type: editingType })
|
2026-04-08 17:03:57 +00:00
|
|
|
|
showTestResult(res.data)
|
|
|
|
|
|
} catch (error) {
|
2026-07-24 07:37:22 +00:00
|
|
|
|
// 表单校验未通过:不弹提示,仅高亮表单项
|
|
|
|
|
|
// 接口失败:已由全局请求拦截器统一弹出错误 Toast,这里不重复提示
|
2026-04-08 17:03:57 +00:00
|
|
|
|
} finally {
|
|
|
|
|
|
setTesting(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const getColumns = (type) => {
|
|
|
|
|
|
const baseColumns = [
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '模型名称',
|
|
|
|
|
|
dataIndex: 'model_name',
|
|
|
|
|
|
key: 'model_name',
|
|
|
|
|
|
width: 240,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space direction="vertical" size={2}>
|
|
|
|
|
|
<span>{record.model_name}</span>
|
|
|
|
|
|
<span className="model-config-code">{record.model_code}</span>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '提供方',
|
|
|
|
|
|
dataIndex: 'provider',
|
|
|
|
|
|
key: 'provider',
|
|
|
|
|
|
width: 140,
|
|
|
|
|
|
render: (provider) => (
|
|
|
|
|
|
<Tag color="blue">{getProviderMeta(provider)?.label || provider || '-'}</Tag>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '模型标识',
|
|
|
|
|
|
dataIndex: 'llm_model_name',
|
|
|
|
|
|
key: 'llm_model_name',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
if (type === 'embedding') {
|
|
|
|
|
|
baseColumns.push({
|
|
|
|
|
|
title: '向量维度',
|
|
|
|
|
|
dataIndex: 'embedding_dimension',
|
|
|
|
|
|
key: 'embedding_dimension',
|
|
|
|
|
|
width: 110,
|
|
|
|
|
|
render: (value) => (value ? <Tag color="purple">{value}</Tag> : <Tag>自动</Tag>),
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
baseColumns.push(
|
|
|
|
|
|
{
|
|
|
|
|
|
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) => (
|
|
|
|
|
|
<Switch
|
|
|
|
|
|
checked={value}
|
|
|
|
|
|
checkedChildren="启用"
|
|
|
|
|
|
unCheckedChildren="停用"
|
|
|
|
|
|
onChange={(checked) => handleStatusChange(record, checked)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '更新时间',
|
|
|
|
|
|
dataIndex: 'updated_at',
|
|
|
|
|
|
key: 'updated_at',
|
|
|
|
|
|
width: 180,
|
|
|
|
|
|
render: (value) => formatDateTime(value),
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
title: '操作',
|
|
|
|
|
|
key: 'action',
|
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
|
width: 170,
|
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
|
<Space size="small" className="model-config-actions">
|
2026-04-08 17:03:57 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
2026-07-24 07:37:22 +00:00
|
|
|
|
icon={<EditOutlined />}
|
|
|
|
|
|
onClick={() => openEditModal(record)}
|
2026-04-08 17:03:57 +00:00
|
|
|
|
>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
编辑
|
2026-04-08 17:03:57 +00:00
|
|
|
|
</Button>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<Popconfirm
|
|
|
|
|
|
title="确认删除该模型配置?"
|
|
|
|
|
|
description="删除后将无法恢复。"
|
|
|
|
|
|
okText="删除"
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
onConfirm={() => handleDelete(record)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
type="link"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
danger
|
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
|
>
|
|
|
|
|
|
删除
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
|
|
|
|
|
)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
return baseColumns
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const renderListPanel = (type) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="admin-toolbar">
|
|
|
|
|
|
<div className="admin-toolbar-left">
|
|
|
|
|
|
<Search
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
placeholder="搜索模型名称、编码或模型标识"
|
|
|
|
|
|
value={keyword}
|
|
|
|
|
|
style={{ width: 320 }}
|
|
|
|
|
|
onChange={(event) => {
|
|
|
|
|
|
setPage(1)
|
|
|
|
|
|
setKeyword(event.target.value)
|
|
|
|
|
|
}}
|
|
|
|
|
|
onSearch={(value) => {
|
|
|
|
|
|
setPage(1)
|
|
|
|
|
|
setKeyword(value)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
placeholder="筛选提供方"
|
|
|
|
|
|
style={{ width: 180 }}
|
|
|
|
|
|
value={providerFilter}
|
|
|
|
|
|
options={providerCatalog.map((item) => ({
|
|
|
|
|
|
label: item.label,
|
|
|
|
|
|
value: item.value,
|
|
|
|
|
|
}))}
|
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
|
setPage(1)
|
|
|
|
|
|
setProviderFilter(value)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
allowClear
|
|
|
|
|
|
placeholder="筛选状态"
|
|
|
|
|
|
style={{ width: 140 }}
|
|
|
|
|
|
value={statusFilter}
|
|
|
|
|
|
options={[
|
|
|
|
|
|
{ label: '启用', value: true },
|
|
|
|
|
|
{ label: '停用', value: false },
|
|
|
|
|
|
]}
|
|
|
|
|
|
onChange={(value) => {
|
|
|
|
|
|
setPage(1)
|
|
|
|
|
|
setStatusFilter(value)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="admin-toolbar-right">
|
2026-04-08 17:03:57 +00:00
|
|
|
|
<Button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
icon={<PlusOutlined />}
|
|
|
|
|
|
onClick={openCreateModal}
|
|
|
|
|
|
>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
{MODEL_TYPE_META[type].addText}
|
2026-04-08 17:03:57 +00:00
|
|
|
|
</Button>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<Button icon={<ReloadOutlined />} onClick={loadConfigs}>
|
|
|
|
|
|
刷新
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<ListTable
|
|
|
|
|
|
rowKey="config_id"
|
|
|
|
|
|
className="model-config-table"
|
|
|
|
|
|
loading={loading}
|
|
|
|
|
|
columns={getColumns(type)}
|
|
|
|
|
|
dataSource={configs}
|
|
|
|
|
|
scroll={{ x: 1280 }}
|
|
|
|
|
|
pagination={{
|
|
|
|
|
|
current: page,
|
|
|
|
|
|
pageSize,
|
|
|
|
|
|
total,
|
|
|
|
|
|
showSizeChanger: true,
|
|
|
|
|
|
showQuickJumper: true,
|
|
|
|
|
|
onChange: (nextPage, nextPageSize) => {
|
|
|
|
|
|
setPage(nextPage)
|
|
|
|
|
|
setPageSize(nextPageSize)
|
|
|
|
|
|
},
|
|
|
|
|
|
showTotal: (value) => `共 ${value} 条`,
|
|
|
|
|
|
}}
|
2026-04-08 17:03:57 +00:00
|
|
|
|
/>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
</>
|
|
|
|
|
|
)
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
const tabItems = Object.entries(MODEL_TYPE_META).map(([type, meta]) => ({
|
|
|
|
|
|
key: type,
|
|
|
|
|
|
label: (
|
|
|
|
|
|
<span>
|
|
|
|
|
|
{meta.icon}
|
|
|
|
|
|
{meta.label}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
),
|
|
|
|
|
|
children: (
|
|
|
|
|
|
<div className="model-config-tab-panel">
|
|
|
|
|
|
<p className="model-config-tab-desc">{meta.description}</p>
|
|
|
|
|
|
{activeType === type ? renderListPanel(type) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
),
|
|
|
|
|
|
}))
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
return (
|
|
|
|
|
|
<div className="admin-page">
|
|
|
|
|
|
<PageHeader
|
|
|
|
|
|
title="模型配置"
|
|
|
|
|
|
description="分类管理对话模型与向量模型的提供方、接口地址、参数模板与连通性测试。"
|
|
|
|
|
|
icon={<CloudServerOutlined />}
|
|
|
|
|
|
/>
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<Card className="admin-card model-config-card">
|
|
|
|
|
|
<Tabs
|
|
|
|
|
|
items={tabItems}
|
|
|
|
|
|
activeKey={activeType}
|
|
|
|
|
|
onChange={handleTabChange}
|
|
|
|
|
|
tabPosition="left"
|
|
|
|
|
|
className="model-config-tabs"
|
2026-04-08 17:03:57 +00:00
|
|
|
|
/>
|
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
|
|
<Modal
|
2026-07-24 07:37:22 +00:00
|
|
|
|
title={`${editingConfigId ? '编辑' : '新增'}${MODEL_TYPE_META[editingType].label}`}
|
2026-04-08 17:03:57 +00:00
|
|
|
|
open={modalVisible}
|
|
|
|
|
|
width={860}
|
|
|
|
|
|
className="model-config-modal"
|
|
|
|
|
|
style={{ maxWidth: 'calc(100vw - 32px)' }}
|
|
|
|
|
|
destroyOnClose
|
|
|
|
|
|
onCancel={closeModal}
|
|
|
|
|
|
onOk={() => form.submit()}
|
|
|
|
|
|
confirmLoading={submitting}
|
|
|
|
|
|
okText={editingConfigId ? '保存修改' : '创建'}
|
|
|
|
|
|
cancelText="取消"
|
|
|
|
|
|
styles={{
|
|
|
|
|
|
body: {
|
|
|
|
|
|
maxHeight: 'calc(80vh - 180px)',
|
|
|
|
|
|
overflowY: 'auto',
|
|
|
|
|
|
paddingRight: 8,
|
|
|
|
|
|
},
|
|
|
|
|
|
}}
|
|
|
|
|
|
footer={(_, { OkBtn, CancelBtn }) => (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<CancelBtn />
|
|
|
|
|
|
<Button
|
|
|
|
|
|
icon={<ExperimentOutlined />}
|
|
|
|
|
|
loading={testing}
|
|
|
|
|
|
onClick={handleFormTest}
|
|
|
|
|
|
>
|
|
|
|
|
|
测试模型
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<OkBtn />
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="model-config-modal-hint">
|
|
|
|
|
|
<strong>自动生成规则:</strong> 选择提供方后会自动带出默认 `base_url`;填写模型标识后会自动生成“模型名称”和“模型编码”。
|
|
|
|
|
|
如果你手动改过这些字段,后续就不会再被自动覆盖。
|
2026-07-24 07:37:22 +00:00
|
|
|
|
{isEmbedding && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<br />
|
|
|
|
|
|
<strong>向量维度:</strong> 留空时按模型实际返回维度自动建立向量库;如填写,需与模型输出维度一致。更换不同维度的模型会重建该项目的向量库。
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-04-08 17:03:57 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<Form
|
|
|
|
|
|
form={form}
|
|
|
|
|
|
layout="vertical"
|
|
|
|
|
|
onFinish={handleSubmit}
|
|
|
|
|
|
initialValues={{
|
|
|
|
|
|
llm_timeout: 120,
|
|
|
|
|
|
llm_temperature: 0.7,
|
|
|
|
|
|
llm_top_p: 0.9,
|
2026-07-24 07:37:22 +00:00
|
|
|
|
llm_max_tokens: DEFAULT_MAX_TOKENS,
|
2026-04-08 17:03:57 +00:00
|
|
|
|
is_active: true,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<Form.Item name="model_type" hidden>
|
|
|
|
|
|
<Input />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-04-08 17:03:57 +00:00
|
|
|
|
<Space direction="vertical" style={{ width: '100%' }} size={4}>
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="提供方"
|
|
|
|
|
|
name="provider"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择模型提供方' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
showSearch
|
|
|
|
|
|
placeholder="请选择模型提供方"
|
|
|
|
|
|
optionFilterProp="label"
|
|
|
|
|
|
options={providerCatalog.map((item) => ({
|
|
|
|
|
|
label: item.label,
|
|
|
|
|
|
value: item.value,
|
|
|
|
|
|
}))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="Base URL"
|
|
|
|
|
|
name="endpoint_url"
|
|
|
|
|
|
rules={[{ required: true, message: '请输入 base_url' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="选择提供方后自动带出,也可以手动覆盖"
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setAutoFillFlags((current) => ({ ...current, endpointUrl: false }))
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Space style={{ width: '100%' }} size={16} align="start">
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="模型标识"
|
|
|
|
|
|
name="llm_model_name"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
rules={[{ required: true, message: '请输入模型标识或部署名' }]}
|
|
|
|
|
|
>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<Input
|
|
|
|
|
|
placeholder={
|
|
|
|
|
|
isEmbedding
|
|
|
|
|
|
? '如 text-embedding-3-small / text-embedding-v4 / bge-large-zh'
|
|
|
|
|
|
: '如 gpt-4.1-mini / qwen3.6-plus / claude-3-5-sonnet-latest'
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
2026-04-08 17:03:57 +00:00
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="请求超时(秒)"
|
|
|
|
|
|
name="llm_timeout"
|
|
|
|
|
|
style={{ width: 180 }}
|
|
|
|
|
|
rules={[{ required: true, message: '请输入超时时间' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<InputNumber min={5} max={600} style={{ width: '100%' }} />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Space style={{ width: '100%' }} size={16} align="start">
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="模型名称"
|
|
|
|
|
|
name="model_name"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
rules={[{ required: true, message: '请输入模型名称' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="会根据提供方和模型标识自动生成"
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setAutoFillFlags((current) => ({ ...current, modelName: false }))
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="模型编码"
|
|
|
|
|
|
name="model_code"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
rules={[{ required: true, message: '请输入模型编码' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="会自动生成,支持手动调整"
|
|
|
|
|
|
onChange={() => {
|
|
|
|
|
|
setAutoFillFlags((current) => ({ ...current, modelCode: false }))
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="API Key" name="api_key">
|
|
|
|
|
|
<Input.Password
|
|
|
|
|
|
placeholder="支持留空后稍后补齐;测试非 Ollama 模型时建议填写"
|
|
|
|
|
|
autoComplete="new-password"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
{isEmbedding ? (
|
2026-04-08 17:03:57 +00:00
|
|
|
|
<Form.Item
|
2026-07-24 07:37:22 +00:00
|
|
|
|
label="向量维度"
|
|
|
|
|
|
name="embedding_dimension"
|
|
|
|
|
|
extra="留空则按模型实际返回维度自动建立;填写需与模型输出一致。"
|
2026-04-08 17:03:57 +00:00
|
|
|
|
>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<InputNumber
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
max={8192}
|
|
|
|
|
|
step={1}
|
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
|
placeholder="如 1536 / 1024 / 768,可留空自动识别"
|
|
|
|
|
|
/>
|
2026-04-08 17:03:57 +00:00
|
|
|
|
</Form.Item>
|
2026-07-24 07:37:22 +00:00
|
|
|
|
) : (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<Space style={{ width: '100%' }} size={24} align="start">
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="Temperature"
|
|
|
|
|
|
name="llm_temperature"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
tooltip="数值越高回答越发散,越低越稳定保守"
|
|
|
|
|
|
rules={[{ required: true, message: '请设置 temperature' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Slider
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
max={2}
|
|
|
|
|
|
step={0.05}
|
|
|
|
|
|
marks={{ 0: '0', 0.7: '0.7', 1: '1', 2: '2' }}
|
|
|
|
|
|
tooltip={{ open: undefined }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="Top P"
|
|
|
|
|
|
name="llm_top_p"
|
|
|
|
|
|
style={{ flex: 1 }}
|
|
|
|
|
|
tooltip="核采样阈值,控制候选词的累计概率范围"
|
|
|
|
|
|
rules={[{ required: true, message: '请设置 top_p' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Slider
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
max={1}
|
|
|
|
|
|
step={0.05}
|
|
|
|
|
|
marks={{ 0: '0', 0.5: '0.5', 0.9: '0.9', 1: '1' }}
|
|
|
|
|
|
tooltip={{ open: undefined }}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</Space>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item
|
|
|
|
|
|
label="Max Tokens"
|
|
|
|
|
|
name="llm_max_tokens"
|
|
|
|
|
|
tooltip="单次回复的最大输出长度"
|
|
|
|
|
|
rules={[{ required: true, message: '请选择 max_tokens' }]}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Radio.Group
|
|
|
|
|
|
className="max-tokens-group"
|
|
|
|
|
|
optionType="button"
|
|
|
|
|
|
buttonStyle="solid"
|
|
|
|
|
|
options={MAX_TOKENS_OPTIONS}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
|
|
<Form.Item label="系统提示词" name="llm_system_prompt">
|
|
|
|
|
|
<TextArea
|
|
|
|
|
|
rows={4}
|
|
|
|
|
|
placeholder="可选。对话与测试时会作为 system prompt 一并发送。"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2026-04-08 17:03:57 +00:00
|
|
|
|
|
|
|
|
|
|
<Form.Item label="描述" name="description">
|
|
|
|
|
|
<TextArea rows={2} placeholder="可填写用途、场景、适用业务等说明" />
|
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
2026-07-24 07:37:22 +00:00
|
|
|
|
<Form.Item label="启用状态" name="is_active" valuePropName="checked">
|
|
|
|
|
|
<Switch checkedChildren="启用" unCheckedChildren="停用" />
|
|
|
|
|
|
</Form.Item>
|
2026-04-08 17:03:57 +00:00
|
|
|
|
</Form>
|
|
|
|
|
|
</Modal>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default ModelConfigs
|