Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
6a1cb9eb
authored
Nov 23, 2025
by
Calcium-Ion
Committed by
GitHub
Nov 23, 2025
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2277 from seefs001/feature/model_list_fetch
feat: 二次确认添加重定向前模型 && 重定向后模式视为已有模型
parents
e18e45be
3fc9182d
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
217 additions
and
10 deletions
+217
-10
web/src/components/table/channels/modals/EditChannelModal.jsx
+157
-4
web/src/components/table/channels/modals/ModelSelectModal.jsx
+60
-6
No files found.
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
6a1cb9eb
...
...
@@ -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_m
apping
&&
localInputs
.
model_mapping
!==
'
'
&&
!
verifyJSON
(
localInputs
.
model_m
apping
)
parsedModelM
apping
&&
typeof
parsedModelMapping
===
'object
'
&&
!
Array
.
isArray
(
parsedModelM
apping
)
)
{
showInfo
(
t
(
'模型映射必须是合法的 JSON 格式!'
));
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
(
'模型列表已更新'
));
...
...
web/src/components/table/channels/modals/ModelSelectModal.jsx
View file @
6a1cb9eb
...
...
@@ -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
>
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment