| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import React, { useEffect, useState, useRef } from 'react'; |
| import { |
| Button, |
| Table, |
| Modal, |
| Form, |
| Input, |
| InputNumber, |
| Space, |
| Popconfirm, |
| Typography, |
| Banner, |
| Card, |
| Spin, |
| Tag, |
| } from '@douyinfe/semi-ui'; |
| import { API, showError, showSuccess, showWarning } from '../../../helpers'; |
| import { useTranslation } from 'react-i18next'; |
| import { |
| IconPlus, |
| IconEdit, |
| IconDelete, |
| IconRefresh, |
| } from '@douyinfe/semi-icons'; |
|
|
| const { Title, Text } = Typography; |
|
|
| export default function SettingModelMapping() { |
| const { t } = useTranslation(); |
| |
| const [loading, setLoading] = useState(false); |
| const [mappings, setMappings] = useState({}); |
| const [modalVisible, setModalVisible] = useState(false); |
| const [editingMapping, setEditingMapping] = useState(null); |
| const [models, setModels] = useState([{ model: '', priorities: 0 }]); |
| const [virtualModelError, setVirtualModelError] = useState(''); |
| const [modelErrors, setModelErrors] = useState([]); |
| const formApiRef = useRef(); |
|
|
| |
| const fetchMappings = async () => { |
| try { |
| setLoading(true); |
| const res = await API.get('/api/model_mapping/'); |
| if (res.data.success) { |
| setMappings(res.data.data.mapping || {}); |
| } else { |
| showError(res.data.message); |
| } |
| } catch (error) { |
| showError('获取模型映射配置失败:' + error.message); |
| } finally { |
| setLoading(false); |
| } |
| }; |
|
|
| useEffect(() => { |
| fetchMappings(); |
| }, []); |
|
|
| |
| useEffect(() => { |
| validateModels(); |
| }, [models]); |
|
|
| |
| const validateVirtualModel = (value) => { |
| const trimmedValue = value.trim(); |
| if (!trimmedValue) { |
| setVirtualModelError(''); |
| return; |
| } |
| |
| if (!editingMapping && mappings[trimmedValue]) { |
| setVirtualModelError(`虚拟模型名 '${trimmedValue}' 已存在`); |
| } else if (editingMapping && editingMapping !== trimmedValue && mappings[trimmedValue]) { |
| setVirtualModelError(`虚拟模型名 '${trimmedValue}' 已存在`); |
| } else { |
| setVirtualModelError(''); |
| } |
| }; |
|
|
| |
| const validateModels = () => { |
| const errors = []; |
| const modelNames = new Set(); |
| const duplicates = new Set(); |
| |
| models.forEach((model, index) => { |
| const trimmedModel = model.model.trim(); |
| if (trimmedModel) { |
| if (modelNames.has(trimmedModel)) { |
| duplicates.add(trimmedModel); |
| errors[index] = `模型名重复: ${trimmedModel}`; |
| } else { |
| modelNames.add(trimmedModel); |
| errors[index] = ''; |
| } |
| } else { |
| errors[index] = ''; |
| } |
| }); |
| |
| |
| models.forEach((model, index) => { |
| const trimmedModel = model.model.trim(); |
| if (duplicates.has(trimmedModel) && !errors[index]) { |
| errors[index] = `模型名重复: ${trimmedModel}`; |
| } |
| }); |
| |
| setModelErrors(errors); |
| }; |
|
|
| |
| const getTableData = () => { |
| return Object.entries(mappings).map(([virtualModel, items]) => ({ |
| key: virtualModel, |
| virtualModel, |
| models: items || [], |
| })); |
| }; |
|
|
| |
| const handleSaveMapping = async () => { |
| try { |
| const values = await formApiRef.current.validate(); |
| const { virtualModel } = values; |
|
|
| |
| const processedModels = models |
| .map((model) => ({ |
| model: model.model.trim(), |
| priorities: parseInt(model.priorities) || 0, |
| })) |
| .filter((model) => model.model); |
|
|
| |
| const trimmedVirtualModel = virtualModel.trim(); |
| if (!editingMapping && mappings[trimmedVirtualModel]) { |
| showError(`虚拟模型名 '${trimmedVirtualModel}' 已存在,请使用不同的名称`); |
| return; |
| } |
| |
| |
| if (editingMapping && editingMapping !== trimmedVirtualModel && mappings[trimmedVirtualModel]) { |
| showError(`虚拟模型名 '${trimmedVirtualModel}' 已存在,请使用不同的名称`); |
| return; |
| } |
|
|
| |
| const modelNames = new Set(); |
| const duplicateModels = []; |
| |
| for (let i = 0; i < processedModels.length; i++) { |
| const modelName = processedModels[i].model; |
| if (modelNames.has(modelName)) { |
| duplicateModels.push(modelName); |
| } else { |
| modelNames.add(modelName); |
| } |
| } |
| |
| if (duplicateModels.length > 0) { |
| showError(`实际模型名重复: ${duplicateModels.join(', ')}`); |
| return; |
| } |
|
|
| |
| if (processedModels.length === 0) { |
| showError('至少需要配置一个实际模型'); |
| return; |
| } |
|
|
| const newMappings = { ...mappings }; |
|
|
| |
| if (editingMapping && editingMapping !== trimmedVirtualModel) { |
| delete newMappings[editingMapping]; |
| } |
|
|
| newMappings[trimmedVirtualModel] = processedModels; |
|
|
| const res = await API.put('/api/model_mapping/', { |
| mapping: newMappings, |
| }); |
| if (res.data.success) { |
| showSuccess(res.data.message || '保存成功'); |
| setMappings(newMappings); |
| setModalVisible(false); |
| if (formApiRef.current) { |
| formApiRef.current.reset(); |
| } |
| setEditingMapping(null); |
| setModels([{ model: '', priorities: 0 }]); |
| setVirtualModelError(''); |
| setModelErrors([]); |
| } else { |
| showError(res.data.message); |
| } |
| } catch (error) { |
| if (error.errorFields) { |
| showWarning('请检查表单输入'); |
| } else { |
| showError('保存失败:' + error.message); |
| } |
| } |
| }; |
|
|
| |
| const handleDeleteMapping = async (virtualModel) => { |
| try { |
| const newMappings = { ...mappings }; |
| delete newMappings[virtualModel]; |
|
|
| const res = await API.put('/api/model_mapping/', { |
| mapping: newMappings, |
| }); |
| if (res.data.success) { |
| showSuccess('删除成功'); |
| setMappings(newMappings); |
| } else { |
| showError(res.data.message); |
| } |
| } catch (error) { |
| showError('删除失败:' + error.message); |
| } |
| }; |
|
|
| |
| const handleReload = async () => { |
| try { |
| const res = await API.post('/api/model_mapping/reload'); |
| if (res.data.success) { |
| showSuccess(res.data.message); |
| await fetchMappings(); |
| } else { |
| showError(res.data.message); |
| } |
| } catch (error) { |
| showError('重新加载失败:' + error.message); |
| } |
| }; |
|
|
| |
| const openEditModal = (virtualModel = null) => { |
| setEditingMapping(virtualModel); |
| setModalVisible(true); |
| setVirtualModelError(''); |
| setModelErrors([]); |
|
|
| |
| setTimeout(() => { |
| if (formApiRef.current) { |
| if (virtualModel && mappings[virtualModel]) { |
| formApiRef.current.setValues({ |
| virtualModel, |
| }); |
| setModels(mappings[virtualModel] || [{ model: '', priorities: 0 }]); |
| } else { |
| formApiRef.current.reset(); |
| setModels([{ model: '', priorities: 0 }]); |
| } |
| } |
| }, 100); |
| }; |
|
|
| |
| const addModel = () => { |
| setModels([...models, { model: '', priorities: 0 }]); |
| }; |
|
|
| |
| const removeModel = (index) => { |
| if (models.length > 1) { |
| const newModels = models.filter((_, i) => i !== index); |
| setModels(newModels); |
| } |
| }; |
|
|
| |
| const updateModel = (index, field, value) => { |
| const newModels = [...models]; |
| newModels[index][field] = value; |
| setModels(newModels); |
| }; |
|
|
| |
| const copyVirtualModel = (virtualModel) => { |
| navigator.clipboard |
| .writeText(virtualModel) |
| .then(() => { |
| showSuccess(`已复制: ${virtualModel}`); |
| }) |
| .catch(() => { |
| |
| const textarea = document.createElement('textarea'); |
| textarea.value = virtualModel; |
| document.body.appendChild(textarea); |
| textarea.select(); |
| document.execCommand('copy'); |
| document.body.removeChild(textarea); |
| showSuccess(`已复制: ${virtualModel}`); |
| }); |
| }; |
|
|
| const columns = [ |
| { |
| title: '虚拟模型名', |
| dataIndex: 'virtualModel', |
| key: 'virtualModel', |
| width: 200, |
| render: (virtualModel) => ( |
| <div style={{ display: 'flex', alignItems: 'center' }}> |
| <Tag |
| color='violet' |
| size='large' |
| style={{ |
| fontSize: '13px', |
| fontWeight: '500', |
| padding: '4px 12px', |
| borderRadius: '6px', |
| cursor: 'pointer', |
| userSelect: 'none', |
| transition: 'all 0.2s', |
| }} |
| onClick={() => copyVirtualModel(virtualModel)} |
| onMouseEnter={(e) => { |
| e.target.style.transform = 'scale(1.02)'; |
| e.target.style.boxShadow = '0 2px 8px rgba(0,0,0,0.15)'; |
| }} |
| onMouseLeave={(e) => { |
| e.target.style.transform = 'scale(1)'; |
| e.target.style.boxShadow = 'none'; |
| }} |
| title='点击复制模型名' |
| > |
| {virtualModel} |
| </Tag> |
| </div> |
| ), |
| }, |
| { |
| title: '实际模型映射', |
| dataIndex: 'models', |
| key: 'models', |
| render: (models) => { |
| |
| const maxPriority = Math.max(...models.map(model => model.priorities)); |
| |
| return ( |
| <Space wrap> |
| {models.map((model, index) => { |
| // 判断是否为最高优先级 |
| const isHighestPriority = model.priorities === maxPriority; |
| |
| return ( |
| <Tag |
| key={index} |
| color={ |
| isHighestPriority |
| ? 'green' |
| : model.priorities >= 8 |
| ? 'blue' |
| : model.priorities >= 5 |
| ? 'orange' |
| : 'grey' |
| } |
| style={ |
| isHighestPriority |
| ? { fontWeight: 'bold' } |
| : {} |
| } |
| > |
| {model.model} (优先级: {model.priorities}) |
| </Tag> |
| ); |
| })} |
| </Space> |
| ); |
| }, |
| }, |
| { |
| title: '操作', |
| key: 'action', |
| width: 150, |
| render: (_, record) => ( |
| <Space> |
| <Button |
| icon={<IconEdit />} |
| size='small' |
| onClick={() => openEditModal(record.virtualModel)} |
| > |
| 编辑 |
| </Button> |
| <Popconfirm |
| title='确定要删除这个模型映射吗?' |
| onConfirm={() => handleDeleteMapping(record.virtualModel)} |
| position='leftTop' |
| > |
| <Button icon={<IconDelete />} type='danger' size='small'> |
| 删除 |
| </Button> |
| </Popconfirm> |
| </Space> |
| ), |
| }, |
| ]; |
|
|
| return ( |
| <> |
| <Spin spinning={loading}> |
| <Form style={{ marginBottom: 15 }}> |
| <Form.Section text='模型映射配置'> |
| <Banner |
| type='info' |
| description='配置虚拟模型名到实际模型的映射关系。支持多个实际模型,按优先级和轮询策略选择。' |
| style={{ marginBottom: 16 }} |
| /> |
| |
| <Space style={{ marginBottom: 16 }}> |
| <Button |
| type='primary' |
| icon={<IconPlus />} |
| onClick={() => openEditModal()} |
| > |
| 添加映射 |
| </Button> |
| <Button icon={<IconRefresh />} onClick={handleReload}> |
| 重新加载 |
| </Button> |
| </Space> |
| |
| <div style={{ position: 'relative', overflow: 'visible' }}> |
| <Table |
| columns={columns} |
| dataSource={getTableData()} |
| pagination={false} |
| size='small' |
| empty={ |
| <div style={{ textAlign: 'center', padding: '20px' }}> |
| <Text type='secondary'>暂无模型映射配置</Text> |
| </div> |
| } |
| /> |
| </div> |
| </Form.Section> |
| </Form> |
| </Spin> |
| |
| {/* 添加/编辑模态框 */} |
| <Modal |
| title={editingMapping ? '编辑模型映射' : '添加模型映射'} |
| visible={modalVisible} |
| onOk={handleSaveMapping} |
| onCancel={() => { |
| setModalVisible(false); |
| setEditingMapping(null); |
| setModels([{ model: '', priorities: 0 }]); |
| setVirtualModelError(''); |
| setModelErrors([]); |
| if (formApiRef.current) { |
| formApiRef.current.reset(); |
| } |
| }} |
| width={600} |
| > |
| <Form |
| getFormApi={(formAPI) => (formApiRef.current = formAPI)} |
| labelPosition='left' |
| labelWidth={120} |
| > |
| <Form.Input |
| field='virtualModel' |
| label='虚拟模型名' |
| placeholder='输入虚拟模型名,如: gpt-4' |
| rules={[ |
| { required: true, message: '请输入虚拟模型名' }, |
| { type: 'string', message: '虚拟模型名必须是字符串' }, |
| ]} |
| disabled={false} |
| onChange={validateVirtualModel} |
| validateStatus={virtualModelError ? 'error' : ''} |
| helpText={virtualModelError} |
| /> |
| <div style={{ marginBottom: 16 }}> |
| <Text strong>实际模型列表:</Text> |
| {models.map((model, index) => ( |
| <div |
| key={index} |
| style={{ |
| display: 'flex', |
| alignItems: 'center', |
| marginTop: 8, |
| marginBottom: 8, |
| }} |
| > |
| <div style={{ display: 'flex', flexDirection: 'column', marginRight: 8 }}> |
| <Input |
| value={model.model} |
| placeholder='实际模型名' |
| style={{ |
| width: 300, |
| borderColor: modelErrors[index] ? '#ff4d4f' : undefined |
| }} |
| onChange={(value) => updateModel(index, 'model', value)} |
| /> |
| {modelErrors[index] && ( |
| <Text |
| type="danger" |
| size="small" |
| style={{ fontSize: '12px', marginTop: '2px' }} |
| > |
| {modelErrors[index]} |
| </Text> |
| )} |
| </div> |
| <InputNumber |
| value={model.priorities} |
| placeholder='优先级' |
| min={0} |
| style={{ width: 100, marginRight: 8, alignSelf: 'flex-start' }} |
| onChange={(value) => updateModel(index, 'priorities', value)} |
| /> |
| <Button |
| type='danger' |
| size='small' |
| onClick={() => removeModel(index)} |
| disabled={models.length === 1} |
| style={{ alignSelf: 'flex-start' }} |
| > |
| 删除 |
| </Button> |
| </div> |
| ))} |
| <Button |
| onClick={addModel} |
| icon={<IconPlus />} |
| size='small' |
| style={{ marginTop: 8 }} |
| > |
| 添加模型 |
| </Button> |
| </div> |
| </Form> |
| </Modal> |
| </> |
| ); |
| } |
|
|