mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:05:21 +00:00
Merge pull request #2277 from seefs001/feature/model_list_fetch
feat: 二次确认添加重定向前模型 && 重定向后模式视为已有模型
This commit is contained in:
@@ -190,6 +190,30 @@ const EditChannelModal = (props) => {
|
||||
const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
|
||||
const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户
|
||||
const [doubaoApiEditUnlocked, setDoubaoApiEditUnlocked] = useState(false); // 豆包渠道自定义 API 地址隐藏入口
|
||||
const redirectModelList = useMemo(() => {
|
||||
const mapping = inputs.model_mapping;
|
||||
if (typeof mapping !== 'string') return [];
|
||||
const trimmed = mapping.trim();
|
||||
if (!trimmed) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (
|
||||
!parsed ||
|
||||
typeof parsed !== 'object' ||
|
||||
Array.isArray(parsed)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const values = Object.values(parsed)
|
||||
.map((value) =>
|
||||
typeof value === 'string' ? value.trim() : undefined,
|
||||
)
|
||||
.filter((value) => value);
|
||||
return Array.from(new Set(values));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
}, [inputs.model_mapping]);
|
||||
|
||||
// 密钥显示状态
|
||||
const [keyDisplayState, setKeyDisplayState] = useState({
|
||||
@@ -220,6 +244,8 @@ const EditChannelModal = (props) => {
|
||||
];
|
||||
const formContainerRef = useRef(null);
|
||||
const doubaoApiClickCountRef = useRef(0);
|
||||
const initialModelsRef = useRef([]);
|
||||
const initialModelMappingRef = useRef('');
|
||||
|
||||
// 2FA状态更新辅助函数
|
||||
const updateTwoFAState = (updates) => {
|
||||
@@ -595,6 +621,10 @@ const EditChannelModal = (props) => {
|
||||
system_prompt: data.system_prompt,
|
||||
system_prompt_override: data.system_prompt_override || false,
|
||||
});
|
||||
initialModelsRef.current = (data.models || [])
|
||||
.map((model) => (model || '').trim())
|
||||
.filter(Boolean);
|
||||
initialModelMappingRef.current = data.model_mapping || '';
|
||||
// console.log(data);
|
||||
} else {
|
||||
showError(message);
|
||||
@@ -830,6 +860,13 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
}, [props.visible, channelId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEdit) {
|
||||
initialModelsRef.current = [];
|
||||
initialModelMappingRef.current = '';
|
||||
}
|
||||
}, [isEdit, props.visible]);
|
||||
|
||||
// 统一的模态框重置函数
|
||||
const resetModalState = () => {
|
||||
formApiRef.current?.reset();
|
||||
@@ -903,6 +940,80 @@ const EditChannelModal = (props) => {
|
||||
})();
|
||||
};
|
||||
|
||||
const confirmMissingModelMappings = (missingModels) =>
|
||||
new Promise((resolve) => {
|
||||
const modal = Modal.confirm({
|
||||
title: t('模型未加入列表,可能无法调用'),
|
||||
content: (
|
||||
<div className='text-sm leading-6'>
|
||||
<div>
|
||||
{t(
|
||||
'模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:',
|
||||
)}
|
||||
</div>
|
||||
<div className='font-mono text-xs break-all text-red-600 mt-1'>
|
||||
{missingModels.join(', ')}
|
||||
</div>
|
||||
<div className='mt-2'>
|
||||
{t(
|
||||
'你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。',
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
centered: true,
|
||||
footer: (
|
||||
<Space align='center' className='w-full justify-end'>
|
||||
<Button
|
||||
type='tertiary'
|
||||
onClick={() => {
|
||||
modal.destroy();
|
||||
resolve('cancel');
|
||||
}}
|
||||
>
|
||||
{t('返回修改')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
theme='light'
|
||||
onClick={() => {
|
||||
modal.destroy();
|
||||
resolve('submit');
|
||||
}}
|
||||
>
|
||||
{t('直接提交')}
|
||||
</Button>
|
||||
<Button
|
||||
type='primary'
|
||||
theme='solid'
|
||||
onClick={() => {
|
||||
modal.destroy();
|
||||
resolve('add');
|
||||
}}
|
||||
>
|
||||
{t('添加后提交')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
});
|
||||
});
|
||||
|
||||
const hasModelConfigChanged = (normalizedModels, modelMappingStr) => {
|
||||
if (!isEdit) return true;
|
||||
const initialModels = initialModelsRef.current;
|
||||
if (normalizedModels.length !== initialModels.length) {
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < normalizedModels.length; i++) {
|
||||
if (normalizedModels[i] !== initialModels[i]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const normalizedMapping = (modelMappingStr || '').trim();
|
||||
const initialMapping = (initialModelMappingRef.current || '').trim();
|
||||
return normalizedMapping !== initialMapping;
|
||||
};
|
||||
|
||||
const submit = async () => {
|
||||
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
|
||||
let localInputs = { ...formValues };
|
||||
@@ -986,14 +1097,55 @@ const EditChannelModal = (props) => {
|
||||
showInfo(t('请输入API地址!'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
localInputs.model_mapping &&
|
||||
localInputs.model_mapping !== '' &&
|
||||
!verifyJSON(localInputs.model_mapping)
|
||||
) {
|
||||
showInfo(t('模型映射必须是合法的 JSON 格式!'));
|
||||
return;
|
||||
const hasModelMapping =
|
||||
typeof localInputs.model_mapping === 'string' &&
|
||||
localInputs.model_mapping.trim() !== '';
|
||||
let parsedModelMapping = null;
|
||||
if (hasModelMapping) {
|
||||
if (!verifyJSON(localInputs.model_mapping)) {
|
||||
showInfo(t('模型映射必须是合法的 JSON 格式!'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
parsedModelMapping = JSON.parse(localInputs.model_mapping);
|
||||
} catch (error) {
|
||||
showInfo(t('模型映射必须是合法的 JSON 格式!'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedModels = (localInputs.models || [])
|
||||
.map((model) => (model || '').trim())
|
||||
.filter(Boolean);
|
||||
localInputs.models = normalizedModels;
|
||||
|
||||
if (
|
||||
parsedModelMapping &&
|
||||
typeof parsedModelMapping === 'object' &&
|
||||
!Array.isArray(parsedModelMapping)
|
||||
) {
|
||||
const modelSet = new Set(normalizedModels);
|
||||
const missingModels = Object.keys(parsedModelMapping)
|
||||
.map((key) => (key || '').trim())
|
||||
.filter((key) => key && !modelSet.has(key));
|
||||
const shouldPromptMissing =
|
||||
missingModels.length > 0 &&
|
||||
hasModelConfigChanged(normalizedModels, localInputs.model_mapping);
|
||||
if (shouldPromptMissing) {
|
||||
const confirmAction = await confirmMissingModelMappings(missingModels);
|
||||
if (confirmAction === 'cancel') {
|
||||
return;
|
||||
}
|
||||
if (confirmAction === 'add') {
|
||||
const updatedModels = Array.from(
|
||||
new Set([...normalizedModels, ...missingModels]),
|
||||
);
|
||||
localInputs.models = updatedModels;
|
||||
handleInputChange('models', updatedModels);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||
localInputs.base_url = localInputs.base_url.slice(
|
||||
0,
|
||||
@@ -2916,6 +3068,7 @@ const EditChannelModal = (props) => {
|
||||
visible={modelModalVisible}
|
||||
models={fetchedModels}
|
||||
selected={inputs.models}
|
||||
redirectModels={redirectModelList}
|
||||
onConfirm={(selectedModels) => {
|
||||
handleInputChange('models', selectedModels);
|
||||
showSuccess(t('模型列表已更新'));
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||
import {
|
||||
Modal,
|
||||
@@ -28,12 +28,13 @@ import {
|
||||
Empty,
|
||||
Tabs,
|
||||
Collapse,
|
||||
Tooltip,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import {
|
||||
IllustrationNoResult,
|
||||
IllustrationNoResultDark,
|
||||
} from '@douyinfe/semi-illustrations';
|
||||
import { IconSearch } from '@douyinfe/semi-icons';
|
||||
import { IconSearch, IconInfoCircle } from '@douyinfe/semi-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getModelCategories } from '../../../../helpers/render';
|
||||
|
||||
@@ -41,6 +42,7 @@ const ModelSelectModal = ({
|
||||
visible,
|
||||
models = [],
|
||||
selected = [],
|
||||
redirectModels = [],
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}) => {
|
||||
@@ -50,15 +52,54 @@ const ModelSelectModal = ({
|
||||
const [activeTab, setActiveTab] = useState('new');
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
const normalizeModelName = (model) =>
|
||||
typeof model === 'string' ? model.trim() : '';
|
||||
const normalizedRedirectModels = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
new Set(
|
||||
(redirectModels || [])
|
||||
.map((model) => normalizeModelName(model))
|
||||
.filter(Boolean),
|
||||
),
|
||||
),
|
||||
[redirectModels],
|
||||
);
|
||||
const normalizedSelectedSet = useMemo(() => {
|
||||
const set = new Set();
|
||||
(selected || []).forEach((model) => {
|
||||
const normalized = normalizeModelName(model);
|
||||
if (normalized) {
|
||||
set.add(normalized);
|
||||
}
|
||||
});
|
||||
return set;
|
||||
}, [selected]);
|
||||
const classificationSet = useMemo(() => {
|
||||
const set = new Set(normalizedSelectedSet);
|
||||
normalizedRedirectModels.forEach((model) => set.add(model));
|
||||
return set;
|
||||
}, [normalizedSelectedSet, normalizedRedirectModels]);
|
||||
const redirectOnlySet = useMemo(() => {
|
||||
const set = new Set();
|
||||
normalizedRedirectModels.forEach((model) => {
|
||||
if (!normalizedSelectedSet.has(model)) {
|
||||
set.add(model);
|
||||
}
|
||||
});
|
||||
return set;
|
||||
}, [normalizedRedirectModels, normalizedSelectedSet]);
|
||||
|
||||
const filteredModels = models.filter((m) =>
|
||||
m.toLowerCase().includes(keyword.toLowerCase()),
|
||||
String(m || '').toLowerCase().includes(keyword.toLowerCase()),
|
||||
);
|
||||
|
||||
// 分类模型:新获取的模型和已有模型
|
||||
const newModels = filteredModels.filter((model) => !selected.includes(model));
|
||||
const isExistingModel = (model) =>
|
||||
classificationSet.has(normalizeModelName(model));
|
||||
const newModels = filteredModels.filter((model) => !isExistingModel(model));
|
||||
const existingModels = filteredModels.filter((model) =>
|
||||
selected.includes(model),
|
||||
isExistingModel(model),
|
||||
);
|
||||
|
||||
// 同步外部选中值
|
||||
@@ -228,7 +269,20 @@ const ModelSelectModal = ({
|
||||
<div className='grid grid-cols-2 gap-x-4'>
|
||||
{categoryData.models.map((model) => (
|
||||
<Checkbox key={model} value={model} className='my-1'>
|
||||
{model}
|
||||
<span className='flex items-center gap-2'>
|
||||
<span>{model}</span>
|
||||
{redirectOnlySet.has(normalizeModelName(model)) && (
|
||||
<Tooltip
|
||||
position='top'
|
||||
content={t('来自模型重定向,尚未加入模型列表')}
|
||||
>
|
||||
<IconInfoCircle
|
||||
size='small'
|
||||
className='text-amber-500 cursor-help'
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</span>
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user