diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 25e0f95b..4b46444d 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -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({ @@ -3044,6 +3068,7 @@ const EditChannelModal = (props) => { visible={modelModalVisible} models={fetchedModels} selected={inputs.models} + redirectModels={redirectModelList} onConfirm={(selectedModels) => { handleInputChange('models', selectedModels); showSuccess(t('模型列表已更新')); diff --git a/web/src/components/table/channels/modals/ModelSelectModal.jsx b/web/src/components/table/channels/modals/ModelSelectModal.jsx index 0fe98167..21ac768c 100644 --- a/web/src/components/table/channels/modals/ModelSelectModal.jsx +++ b/web/src/components/table/channels/modals/ModelSelectModal.jsx @@ -17,7 +17,7 @@ along with this program. If not, see . 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 = ({
{categoryData.models.map((model) => ( - {model} + + {model} + {redirectOnlySet.has(normalizeModelName(model)) && ( + + + + )} + ))}