Skip to content

Commit a465597

Browse files
authored
Merge pull request #2277 from seefs001/feature/model_list_fetch
feat: 二次确认添加重定向前模型 && 重定向后模式视为已有模型
2 parents dbfcb44 + 7a2bd38 commit a465597

File tree

2 files changed

+218
-11
lines changed

2 files changed

+218
-11
lines changed

web/src/components/table/channels/modals/EditChannelModal.jsx

Lines changed: 158 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,30 @@ const EditChannelModal = (props) => {
190190
const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
191191
const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户
192192
const [doubaoApiEditUnlocked, setDoubaoApiEditUnlocked] = useState(false); // 豆包渠道自定义 API 地址隐藏入口
193+
const redirectModelList = useMemo(() => {
194+
const mapping = inputs.model_mapping;
195+
if (typeof mapping !== 'string') return [];
196+
const trimmed = mapping.trim();
197+
if (!trimmed) return [];
198+
try {
199+
const parsed = JSON.parse(trimmed);
200+
if (
201+
!parsed ||
202+
typeof parsed !== 'object' ||
203+
Array.isArray(parsed)
204+
) {
205+
return [];
206+
}
207+
const values = Object.values(parsed)
208+
.map((value) =>
209+
typeof value === 'string' ? value.trim() : undefined,
210+
)
211+
.filter((value) => value);
212+
return Array.from(new Set(values));
213+
} catch (error) {
214+
return [];
215+
}
216+
}, [inputs.model_mapping]);
193217

194218
// 密钥显示状态
195219
const [keyDisplayState, setKeyDisplayState] = useState({
@@ -220,6 +244,8 @@ const EditChannelModal = (props) => {
220244
];
221245
const formContainerRef = useRef(null);
222246
const doubaoApiClickCountRef = useRef(0);
247+
const initialModelsRef = useRef([]);
248+
const initialModelMappingRef = useRef('');
223249

224250
// 2FA状态更新辅助函数
225251
const updateTwoFAState = (updates) => {
@@ -595,6 +621,10 @@ const EditChannelModal = (props) => {
595621
system_prompt: data.system_prompt,
596622
system_prompt_override: data.system_prompt_override || false,
597623
});
624+
initialModelsRef.current = (data.models || [])
625+
.map((model) => (model || '').trim())
626+
.filter(Boolean);
627+
initialModelMappingRef.current = data.model_mapping || '';
598628
// console.log(data);
599629
} else {
600630
showError(message);
@@ -830,6 +860,13 @@ const EditChannelModal = (props) => {
830860
}
831861
}, [props.visible, channelId]);
832862

863+
useEffect(() => {
864+
if (!isEdit) {
865+
initialModelsRef.current = [];
866+
initialModelMappingRef.current = '';
867+
}
868+
}, [isEdit, props.visible]);
869+
833870
// 统一的模态框重置函数
834871
const resetModalState = () => {
835872
formApiRef.current?.reset();
@@ -903,6 +940,80 @@ const EditChannelModal = (props) => {
903940
})();
904941
};
905942

943+
const confirmMissingModelMappings = (missingModels) =>
944+
new Promise((resolve) => {
945+
const modal = Modal.confirm({
946+
title: t('模型未加入列表,可能无法调用'),
947+
content: (
948+
<div className='text-sm leading-6'>
949+
<div>
950+
{t(
951+
'模型重定向里的下列模型尚未添加到“模型”列表,调用时会因为缺少可用模型而失败:',
952+
)}
953+
</div>
954+
<div className='font-mono text-xs break-all text-red-600 mt-1'>
955+
{missingModels.join(', ')}
956+
</div>
957+
<div className='mt-2'>
958+
{t(
959+
'你可以在“自定义模型名称”处手动添加它们,然后点击填入后再提交,或者直接使用下方操作自动处理。',
960+
)}
961+
</div>
962+
</div>
963+
),
964+
centered: true,
965+
footer: (
966+
<Space align='center' className='w-full justify-end'>
967+
<Button
968+
type='tertiary'
969+
onClick={() => {
970+
modal.destroy();
971+
resolve('cancel');
972+
}}
973+
>
974+
{t('返回修改')}
975+
</Button>
976+
<Button
977+
type='primary'
978+
theme='light'
979+
onClick={() => {
980+
modal.destroy();
981+
resolve('submit');
982+
}}
983+
>
984+
{t('直接提交')}
985+
</Button>
986+
<Button
987+
type='primary'
988+
theme='solid'
989+
onClick={() => {
990+
modal.destroy();
991+
resolve('add');
992+
}}
993+
>
994+
{t('添加后提交')}
995+
</Button>
996+
</Space>
997+
),
998+
});
999+
});
1000+
1001+
const hasModelConfigChanged = (normalizedModels, modelMappingStr) => {
1002+
if (!isEdit) return true;
1003+
const initialModels = initialModelsRef.current;
1004+
if (normalizedModels.length !== initialModels.length) {
1005+
return true;
1006+
}
1007+
for (let i = 0; i < normalizedModels.length; i++) {
1008+
if (normalizedModels[i] !== initialModels[i]) {
1009+
return true;
1010+
}
1011+
}
1012+
const normalizedMapping = (modelMappingStr || '').trim();
1013+
const initialMapping = (initialModelMappingRef.current || '').trim();
1014+
return normalizedMapping !== initialMapping;
1015+
};
1016+
9061017
const submit = async () => {
9071018
const formValues = formApiRef.current ? formApiRef.current.getValues() : {};
9081019
let localInputs = { ...formValues };
@@ -986,14 +1097,55 @@ const EditChannelModal = (props) => {
9861097
showInfo(t('请输入API地址!'));
9871098
return;
9881099
}
1100+
const hasModelMapping =
1101+
typeof localInputs.model_mapping === 'string' &&
1102+
localInputs.model_mapping.trim() !== '';
1103+
let parsedModelMapping = null;
1104+
if (hasModelMapping) {
1105+
if (!verifyJSON(localInputs.model_mapping)) {
1106+
showInfo(t('模型映射必须是合法的 JSON 格式!'));
1107+
return;
1108+
}
1109+
try {
1110+
parsedModelMapping = JSON.parse(localInputs.model_mapping);
1111+
} catch (error) {
1112+
showInfo(t('模型映射必须是合法的 JSON 格式!'));
1113+
return;
1114+
}
1115+
}
1116+
1117+
const normalizedModels = (localInputs.models || [])
1118+
.map((model) => (model || '').trim())
1119+
.filter(Boolean);
1120+
localInputs.models = normalizedModels;
1121+
9891122
if (
990-
localInputs.model_mapping &&
991-
localInputs.model_mapping !== '' &&
992-
!verifyJSON(localInputs.model_mapping)
1123+
parsedModelMapping &&
1124+
typeof parsedModelMapping === 'object' &&
1125+
!Array.isArray(parsedModelMapping)
9931126
) {
994-
showInfo(t('模型映射必须是合法的 JSON 格式!'));
995-
return;
1127+
const modelSet = new Set(normalizedModels);
1128+
const missingModels = Object.keys(parsedModelMapping)
1129+
.map((key) => (key || '').trim())
1130+
.filter((key) => key && !modelSet.has(key));
1131+
const shouldPromptMissing =
1132+
missingModels.length > 0 &&
1133+
hasModelConfigChanged(normalizedModels, localInputs.model_mapping);
1134+
if (shouldPromptMissing) {
1135+
const confirmAction = await confirmMissingModelMappings(missingModels);
1136+
if (confirmAction === 'cancel') {
1137+
return;
1138+
}
1139+
if (confirmAction === 'add') {
1140+
const updatedModels = Array.from(
1141+
new Set([...normalizedModels, ...missingModels]),
1142+
);
1143+
localInputs.models = updatedModels;
1144+
handleInputChange('models', updatedModels);
1145+
}
1146+
}
9961147
}
1148+
9971149
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
9981150
localInputs.base_url = localInputs.base_url.slice(
9991151
0,
@@ -2916,6 +3068,7 @@ const EditChannelModal = (props) => {
29163068
visible={modelModalVisible}
29173069
models={fetchedModels}
29183070
selected={inputs.models}
3071+
redirectModels={redirectModelList}
29193072
onConfirm={(selectedModels) => {
29203073
handleInputChange('models', selectedModels);
29213074
showSuccess(t('模型列表已更新'));

web/src/components/table/channels/modals/ModelSelectModal.jsx

Lines changed: 60 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
For commercial licensing, please contact support@quantumnous.com
1818
*/
1919

20-
import React, { useState, useEffect } from 'react';
20+
import React, { useState, useEffect, useMemo } from 'react';
2121
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
2222
import {
2323
Modal,
@@ -28,19 +28,21 @@ import {
2828
Empty,
2929
Tabs,
3030
Collapse,
31+
Tooltip,
3132
} from '@douyinfe/semi-ui';
3233
import {
3334
IllustrationNoResult,
3435
IllustrationNoResultDark,
3536
} from '@douyinfe/semi-illustrations';
36-
import { IconSearch } from '@douyinfe/semi-icons';
37+
import { IconSearch, IconInfoCircle } from '@douyinfe/semi-icons';
3738
import { useTranslation } from 'react-i18next';
3839
import { getModelCategories } from '../../../../helpers/render';
3940

4041
const ModelSelectModal = ({
4142
visible,
4243
models = [],
4344
selected = [],
45+
redirectModels = [],
4446
onConfirm,
4547
onCancel,
4648
}) => {
@@ -50,15 +52,54 @@ const ModelSelectModal = ({
5052
const [activeTab, setActiveTab] = useState('new');
5153

5254
const isMobile = useIsMobile();
55+
const normalizeModelName = (model) =>
56+
typeof model === 'string' ? model.trim() : '';
57+
const normalizedRedirectModels = useMemo(
58+
() =>
59+
Array.from(
60+
new Set(
61+
(redirectModels || [])
62+
.map((model) => normalizeModelName(model))
63+
.filter(Boolean),
64+
),
65+
),
66+
[redirectModels],
67+
);
68+
const normalizedSelectedSet = useMemo(() => {
69+
const set = new Set();
70+
(selected || []).forEach((model) => {
71+
const normalized = normalizeModelName(model);
72+
if (normalized) {
73+
set.add(normalized);
74+
}
75+
});
76+
return set;
77+
}, [selected]);
78+
const classificationSet = useMemo(() => {
79+
const set = new Set(normalizedSelectedSet);
80+
normalizedRedirectModels.forEach((model) => set.add(model));
81+
return set;
82+
}, [normalizedSelectedSet, normalizedRedirectModels]);
83+
const redirectOnlySet = useMemo(() => {
84+
const set = new Set();
85+
normalizedRedirectModels.forEach((model) => {
86+
if (!normalizedSelectedSet.has(model)) {
87+
set.add(model);
88+
}
89+
});
90+
return set;
91+
}, [normalizedRedirectModels, normalizedSelectedSet]);
5392

5493
const filteredModels = models.filter((m) =>
55-
m.toLowerCase().includes(keyword.toLowerCase()),
94+
String(m || '').toLowerCase().includes(keyword.toLowerCase()),
5695
);
5796

5897
// 分类模型:新获取的模型和已有模型
59-
const newModels = filteredModels.filter((model) => !selected.includes(model));
98+
const isExistingModel = (model) =>
99+
classificationSet.has(normalizeModelName(model));
100+
const newModels = filteredModels.filter((model) => !isExistingModel(model));
60101
const existingModels = filteredModels.filter((model) =>
61-
selected.includes(model),
102+
isExistingModel(model),
62103
);
63104

64105
// 同步外部选中值
@@ -228,7 +269,20 @@ const ModelSelectModal = ({
228269
<div className='grid grid-cols-2 gap-x-4'>
229270
{categoryData.models.map((model) => (
230271
<Checkbox key={model} value={model} className='my-1'>
231-
{model}
272+
<span className='flex items-center gap-2'>
273+
<span>{model}</span>
274+
{redirectOnlySet.has(normalizeModelName(model)) && (
275+
<Tooltip
276+
position='top'
277+
content={t('来自模型重定向,尚未加入模型列表')}
278+
>
279+
<IconInfoCircle
280+
size='small'
281+
className='text-amber-500 cursor-help'
282+
/>
283+
</Tooltip>
284+
)}
285+
</span>
232286
</Checkbox>
233287
))}
234288
</div>

0 commit comments

Comments
 (0)