|
|
@@ -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;
|
|
|
}
|
|
|
+ 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 (
|
|
|
- localInputs.model_mapping &&
|
|
|
- localInputs.model_mapping !== '' &&
|
|
|
- !verifyJSON(localInputs.model_mapping)
|
|
|
+ parsedModelMapping &&
|
|
|
+ typeof parsedModelMapping === 'object' &&
|
|
|
+ !Array.isArray(parsedModelMapping)
|
|
|
) {
|
|
|
- showInfo(t('模型映射必须是合法的 JSON 格式!'));
|
|
|
- return;
|
|
|
+ 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('模型列表已更新'));
|