699 lines
20 KiB
React
699 lines
20 KiB
React
|
|
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: (
|
|||
|
|
<div className="model-config-test-result">
|
|||
|
|
<div>提供方:{getProviderMeta(result.provider)?.label || result.provider}</div>
|
|||
|
|
<div>模型:{result.llm_model_name}</div>
|
|||
|
|
<div>延迟:{result.latency_ms} ms</div>
|
|||
|
|
<div>返回预览:{result.preview || '模型已成功返回,但未提取到文本内容。'}</div>
|
|||
|
|
</div>
|
|||
|
|
),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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) => (
|
|||
|
|
<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,
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
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: 'is_default',
|
|||
|
|
key: 'is_default',
|
|||
|
|
width: 90,
|
|||
|
|
render: (value) => (
|
|||
|
|
value ? <Tag color="gold">默认</Tag> : <Tag>否</Tag>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
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">
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
icon={<EditOutlined />}
|
|||
|
|
onClick={() => openEditModal(record)}
|
|||
|
|
>
|
|||
|
|
编辑
|
|||
|
|
</Button>
|
|||
|
|
<Popconfirm
|
|||
|
|
title="确认删除该模型配置?"
|
|||
|
|
description="删除后将无法恢复。"
|
|||
|
|
okText="删除"
|
|||
|
|
cancelText="取消"
|
|||
|
|
onConfirm={() => handleDelete(record)}
|
|||
|
|
>
|
|||
|
|
<Button
|
|||
|
|
type="link"
|
|||
|
|
size="small"
|
|||
|
|
danger
|
|||
|
|
icon={<DeleteOutlined />}
|
|||
|
|
>
|
|||
|
|
删除
|
|||
|
|
</Button>
|
|||
|
|
</Popconfirm>
|
|||
|
|
</Space>
|
|||
|
|
),
|
|||
|
|
},
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="admin-page">
|
|||
|
|
<PageHeader
|
|||
|
|
title="模型配置"
|
|||
|
|
description="管理系统可用的大模型提供方、默认接口地址、参数模板与连通性测试。"
|
|||
|
|
icon={<CloudServerOutlined />}
|
|||
|
|
extra={(
|
|||
|
|
<Button
|
|||
|
|
type="primary"
|
|||
|
|
icon={<PlusOutlined />}
|
|||
|
|
onClick={openCreateModal}
|
|||
|
|
>
|
|||
|
|
新增模型
|
|||
|
|
</Button>
|
|||
|
|
)}
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<Card className="admin-card model-config-card">
|
|||
|
|
<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">
|
|||
|
|
<Button icon={<ReloadOutlined />} onClick={loadConfigs}>
|
|||
|
|
刷新
|
|||
|
|
</Button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<ListTable
|
|||
|
|
rowKey="config_id"
|
|||
|
|
className="model-config-table"
|
|||
|
|
loading={loading}
|
|||
|
|
columns={columns}
|
|||
|
|
dataSource={configs}
|
|||
|
|
scroll={{ x: 1280 }}
|
|||
|
|
pagination={{
|
|||
|
|
current: page,
|
|||
|
|
pageSize,
|
|||
|
|
total,
|
|||
|
|
showSizeChanger: true,
|
|||
|
|
showQuickJumper: true,
|
|||
|
|
onChange: (nextPage, nextPageSize) => {
|
|||
|
|
setPage(nextPage)
|
|||
|
|
setPageSize(nextPageSize)
|
|||
|
|
},
|
|||
|
|
showTotal: (value) => `共 ${value} 条`,
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
<Modal
|
|||
|
|
title={editingConfigId ? '编辑模型配置' : '新增模型配置'}
|
|||
|
|
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`;填写模型标识后会自动生成“模型名称”和“模型编码”。
|
|||
|
|
如果你手动改过这些字段,后续就不会再被自动覆盖。
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<Form
|
|||
|
|
form={form}
|
|||
|
|
layout="vertical"
|
|||
|
|
onFinish={handleSubmit}
|
|||
|
|
initialValues={{
|
|||
|
|
llm_timeout: 120,
|
|||
|
|
llm_temperature: 0.7,
|
|||
|
|
llm_top_p: 0.9,
|
|||
|
|
llm_max_tokens: 2048,
|
|||
|
|
is_active: true,
|
|||
|
|
is_default: false,
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<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: '请输入模型标识或部署名' }]}
|
|||
|
|
>
|
|||
|
|
<Input placeholder="如 gpt-4.1-mini / qwen3.6-plus / claude-3-5-sonnet-latest" />
|
|||
|
|
</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>
|
|||
|
|
|
|||
|
|
<Space style={{ width: '100%' }} size={16} align="start">
|
|||
|
|
<Form.Item
|
|||
|
|
label="Temperature"
|
|||
|
|
name="llm_temperature"
|
|||
|
|
style={{ width: '33%' }}
|
|||
|
|
rules={[{ required: true, message: '请输入 temperature' }]}
|
|||
|
|
>
|
|||
|
|
<InputNumber min={0} max={2} step={0.05} style={{ width: '100%' }} />
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item
|
|||
|
|
label="Top P"
|
|||
|
|
name="llm_top_p"
|
|||
|
|
style={{ width: '33%' }}
|
|||
|
|
rules={[{ required: true, message: '请输入 top_p' }]}
|
|||
|
|
>
|
|||
|
|
<InputNumber min={0} max={1} step={0.05} style={{ width: '100%' }} />
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item
|
|||
|
|
label="Max Tokens"
|
|||
|
|
name="llm_max_tokens"
|
|||
|
|
style={{ width: '33%' }}
|
|||
|
|
rules={[{ required: true, message: '请输入 max_tokens' }]}
|
|||
|
|
>
|
|||
|
|
<InputNumber min={1} max={32768} step={128} style={{ width: '100%' }} />
|
|||
|
|
</Form.Item>
|
|||
|
|
</Space>
|
|||
|
|
|
|||
|
|
<Form.Item label="系统提示词" name="llm_system_prompt">
|
|||
|
|
<TextArea
|
|||
|
|
rows={4}
|
|||
|
|
placeholder="可选。测试模型时会作为 system prompt 一并发送。"
|
|||
|
|
/>
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item label="描述" name="description">
|
|||
|
|
<TextArea rows={2} placeholder="可填写用途、场景、适用业务等说明" />
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Space size={24}>
|
|||
|
|
<Form.Item label="启用状态" name="is_active" valuePropName="checked">
|
|||
|
|
<Switch checkedChildren="启用" unCheckedChildren="停用" />
|
|||
|
|
</Form.Item>
|
|||
|
|
|
|||
|
|
<Form.Item label="设为默认" name="is_default" valuePropName="checked">
|
|||
|
|
<Switch checkedChildren="默认" unCheckedChildren="普通" />
|
|||
|
|
</Form.Item>
|
|||
|
|
</Space>
|
|||
|
|
</Form>
|
|||
|
|
</Modal>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export default ModelConfigs
|