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
c0a9934a
authored
Jan 08, 2026
by
Bliod
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: select model in visualized modl mapping
parent
04ff3df5
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
663 additions
and
331 deletions
+663
-331
web/src/components/common/ui/JSONEditor.jsx
+6
-2
web/src/components/table/channels/modals/EditChannelModal.jsx
+462
-329
web/src/components/table/channels/modals/SingleModelSelectModal.jsx
+195
-0
No files found.
web/src/components/common/ui/JSONEditor.jsx
View file @
c0a9934a
...
...
@@ -60,6 +60,7 @@ const JSONEditor = ({
editorType
=
'keyValue'
,
rules
=
[],
formApi
=
null
,
renderStringValueSuffix
,
...
props
})
=>
{
const
{
t
}
=
useTranslation
();
...
...
@@ -335,7 +336,7 @@ const JSONEditor = ({
]);
// 渲染值输入控件(支持嵌套)
const
renderValueInput
=
(
pairId
,
value
)
=>
{
const
renderValueInput
=
(
pairId
,
pairKey
,
value
)
=>
{
const
valueType
=
typeof
value
;
if
(
valueType
===
'boolean'
)
{
...
...
@@ -387,6 +388,7 @@ const JSONEditor = ({
<
Input
placeholder=
{
t
(
'参数值'
)
}
value=
{
String
(
value
)
}
suffix=
{
renderStringValueSuffix
?.({
pairId
,
pairKey
,
value
})
}
onChange=
{
(
newValue
)
=>
{
let
convertedValue
=
newValue
;
if
(
newValue
===
'true'
)
convertedValue
=
true
;
...
...
@@ -470,7 +472,9 @@ const JSONEditor = ({
)
}
</
div
>
</
Col
>
<
Col
span=
{
12
}
>
{
renderValueInput
(
pair
.
id
,
pair
.
value
)
}
</
Col
>
<
Col
span=
{
12
}
>
{
renderValueInput
(
pair
.
id
,
pair
.
key
,
pair
.
value
)
}
</
Col
>
<
Col
span=
{
2
}
>
<
Button
icon=
{
<
IconDelete
/>
}
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
c0a9934a
...
...
@@ -46,6 +46,7 @@ import {
Col
,
Highlight
,
Input
,
Tooltip
,
}
from
'@douyinfe/semi-ui'
;
import
{
getChannelModels
,
...
...
@@ -55,6 +56,7 @@ import {
selectFilter
,
}
from
'../../../../helpers'
;
import
ModelSelectModal
from
'./ModelSelectModal'
;
import
SingleModelSelectModal
from
'./SingleModelSelectModal'
;
import
OllamaModelModal
from
'./OllamaModelModal'
;
import
JSONEditor
from
'../../../common/ui/JSONEditor'
;
import
SecureVerificationModal
from
'../../../common/modals/SecureVerificationModal'
;
...
...
@@ -69,6 +71,7 @@ import {
IconCode
,
IconGlobe
,
IconBolt
,
IconSearch
,
IconChevronUp
,
IconChevronDown
,
}
from
'@douyinfe/semi-icons'
;
...
...
@@ -109,7 +112,7 @@ function type2secretPrompt(type) {
case
33
:
return
'按照如下格式输入:Ak|Sk|Region'
;
case
45
:
return
'请输入渠道对应的鉴权密钥, 豆包语音输入:AppId|AccessToken'
;
return
'请输入渠道对应的鉴权密钥, 豆包语音输入:AppId|AccessToken'
;
case
50
:
return
'按照如下格式输入: AccessKey|SecretKey, 如果上游是New API,则直接输ApiKey'
;
case
51
:
...
...
@@ -181,6 +184,13 @@ const EditChannelModal = (props) => {
const
[
isModalOpenurl
,
setIsModalOpenurl
]
=
useState
(
false
);
const
[
modelModalVisible
,
setModelModalVisible
]
=
useState
(
false
);
const
[
fetchedModels
,
setFetchedModels
]
=
useState
([]);
const
[
modelMappingValueModalVisible
,
setModelMappingValueModalVisible
]
=
useState
(
false
);
const
[
modelMappingValueModalModels
,
setModelMappingValueModalModels
]
=
useState
([]);
const
[
modelMappingValueKey
,
setModelMappingValueKey
]
=
useState
(
''
);
const
[
modelMappingValueSelected
,
setModelMappingValueSelected
]
=
useState
(
''
);
const
[
ollamaModalVisible
,
setOllamaModalVisible
]
=
useState
(
false
);
const
formApiRef
=
useRef
(
null
);
const
[
vertexKeys
,
setVertexKeys
]
=
useState
([]);
...
...
@@ -199,17 +209,11 @@ const EditChannelModal = (props) => {
if
(
!
trimmed
)
return
[];
try
{
const
parsed
=
JSON
.
parse
(
trimmed
);
if
(
!
parsed
||
typeof
parsed
!==
'object'
||
Array
.
isArray
(
parsed
)
)
{
if
(
!
parsed
||
typeof
parsed
!==
'object'
||
Array
.
isArray
(
parsed
))
{
return
[];
}
const
values
=
Object
.
values
(
parsed
)
.
map
((
value
)
=>
typeof
value
===
'string'
?
value
.
trim
()
:
undefined
,
)
.
map
((
value
)
=>
(
typeof
value
===
'string'
?
value
.
trim
()
:
undefined
))
.
filter
((
value
)
=>
value
);
return
Array
.
from
(
new
Set
(
values
));
}
catch
(
error
)
{
...
...
@@ -426,7 +430,11 @@ const EditChannelModal = (props) => {
const
isIonetLocked
=
isIonetChannel
&&
isEdit
;
const
handleInputChange
=
(
name
,
value
)
=>
{
if
(
isIonetChannel
&&
isEdit
&&
[
'type'
,
'key'
,
'base_url'
].
includes
(
name
))
{
if
(
isIonetChannel
&&
isEdit
&&
[
'type'
,
'key'
,
'base_url'
].
includes
(
name
)
)
{
return
;
}
if
(
formApiRef
.
current
)
{
...
...
@@ -730,10 +738,49 @@ const EditChannelModal = (props) => {
if
(
!
silent
)
{
setModelModalVisible
(
true
);
}
setLoading
(
false
);
return
uniqueModels
;
}
else
{
showError
(
t
(
'获取模型列表失败'
));
}
setLoading
(
false
);
return
null
;
};
const
openModelMappingValueModal
=
async
({
pairKey
,
value
})
=>
{
const
mappingKey
=
String
(
pairKey
??
''
).
trim
();
if
(
!
mappingKey
)
return
;
if
(
!
MODEL_FETCHABLE_TYPES
.
has
(
inputs
.
type
))
{
return
;
}
let
modelsToUse
=
fetchedModels
;
if
(
!
Array
.
isArray
(
modelsToUse
)
||
modelsToUse
.
length
===
0
)
{
const
fetched
=
await
fetchUpstreamModelList
(
'models'
,
{
silent
:
true
});
if
(
Array
.
isArray
(
fetched
))
{
modelsToUse
=
fetched
;
}
}
if
(
!
Array
.
isArray
(
modelsToUse
)
||
modelsToUse
.
length
===
0
)
{
showInfo
(
t
(
'暂无模型'
));
return
;
}
const
normalizedModelsToUse
=
Array
.
from
(
new
Set
(
modelsToUse
.
map
((
model
)
=>
String
(
model
??
''
).
trim
()).
filter
(
Boolean
),
),
);
const
currentValue
=
String
(
value
??
''
).
trim
();
setModelMappingValueModalModels
(
normalizedModelsToUse
);
setModelMappingValueKey
(
mappingKey
);
setModelMappingValueSelected
(
normalizedModelsToUse
.
includes
(
currentValue
)
?
currentValue
:
''
,
);
setModelMappingValueModalVisible
(
true
);
};
const
fetchModels
=
async
()
=>
{
...
...
@@ -1673,43 +1720,45 @@ const EditChannelModal = (props) => {
</
div
>
{
isIonetChannel
&&
(
<
Banner
type=
'info'
closeIcon=
{
null
}
className=
'mb-4 rounded-xl'
description=
{
t
(
'此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。'
)
}
>
<
Space
>
{
ionetMetadata
?.
deployment_id
&&
(
<
Button
size=
'small'
theme=
'light'
type=
'primary'
icon=
{
<
IconGlobe
/>
}
onClick=
{
handleOpenIonetDeployment
}
>
{
t
(
'查看关联部署'
)
}
</
Button
>
<
Banner
type=
'info'
closeIcon=
{
null
}
className=
'mb-4 rounded-xl'
description=
{
t
(
'此渠道由 IO.NET 自动同步,类型、密钥和 API 地址已锁定。'
,
)
}
</
Space
>
</
Banner
>
)
}
<
Form
.
Select
field=
'type'
label=
{
t
(
'类型'
)
}
placeholder=
{
t
(
'请选择渠道类型'
)
}
rules=
{
[{
required
:
true
,
message
:
t
(
'请选择渠道类型'
)
}]
}
optionList=
{
channelOptionList
}
style=
{
{
width
:
'100%'
}
}
filter=
{
selectFilter
}
autoClearSearchValue=
{
false
}
searchPosition=
'dropdown'
onSearch=
{
(
value
)
=>
setChannelSearchValue
(
value
)
}
renderOptionItem=
{
renderChannelOption
}
onChange=
{
(
value
)
=>
handleInputChange
(
'type'
,
value
)
}
disabled=
{
isIonetLocked
}
/>
>
<
Space
>
{
ionetMetadata
?.
deployment_id
&&
(
<
Button
size=
'small'
theme=
'light'
type=
'primary'
icon=
{
<
IconGlobe
/>
}
onClick=
{
handleOpenIonetDeployment
}
>
{
t
(
'查看关联部署'
)
}
</
Button
>
)
}
</
Space
>
</
Banner
>
)
}
<
Form
.
Select
field=
'type'
label=
{
t
(
'类型'
)
}
placeholder=
{
t
(
'请选择渠道类型'
)
}
rules=
{
[{
required
:
true
,
message
:
t
(
'请选择渠道类型'
)
}]
}
optionList=
{
channelOptionList
}
style=
{
{
width
:
'100%'
}
}
filter=
{
selectFilter
}
autoClearSearchValue=
{
false
}
searchPosition=
'dropdown'
onSearch=
{
(
value
)
=>
setChannelSearchValue
(
value
)
}
renderOptionItem=
{
renderChannelOption
}
onChange=
{
(
value
)
=>
handleInputChange
(
'type'
,
value
)
}
disabled=
{
isIonetLocked
}
/>
{
inputs
.
type
===
20
&&
(
<
Form
.
Switch
...
...
@@ -1754,7 +1803,10 @@ const EditChannelModal = (props) => {
style=
{
{
width
:
'100%'
}
}
value=
{
inputs
.
aws_key_type
||
'ak_sk'
}
onChange=
{
(
value
)
=>
{
handleChannelOtherSettingsChange
(
'aws_key_type'
,
value
);
handleChannelOtherSettingsChange
(
'aws_key_type'
,
value
,
);
}
}
extraText=
{
t
(
'AK/SK 模式:使用 AccessKey 和 SecretAccessKey;API Key 模式:使用 API Key'
,
...
...
@@ -1834,7 +1886,9 @@ const EditChannelModal = (props) => {
placeholder=
{
inputs
.
type
===
33
?
inputs
.
aws_key_type
===
'api_key'
?
t
(
'请输入 API Key,一行一个,格式:APIKey|Region'
)
?
t
(
'请输入 API Key,一行一个,格式:APIKey|Region'
,
)
:
t
(
'请输入密钥,一行一个,格式:AccessKey|SecretAccessKey|Region'
,
)
...
...
@@ -1849,85 +1903,87 @@ const EditChannelModal = (props) => {
autoComplete=
'new-password'
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
disabled=
{
isIonetLocked
}
extraText=
{
<
div
className=
'flex items-center gap-2 flex-wrap'
>
{
isEdit
&&
isMultiKeyChannel
&&
keyMode
===
'append'
&&
(
<
Text
type=
'warning'
size=
'small'
>
{
t
(
'追加模式:新密钥将添加到现有密钥列表的末尾'
,
)
}
</
Text
>
)
}
{
isEdit
&&
(
<
Button
size=
'small'
type=
'primary'
theme=
'outline'
onClick=
{
handleShow2FAModal
}
>
{
t
(
'查看密钥'
)
}
</
Button
>
)
}
{
batchExtra
}
</
div
>
}
showClear
/>
)
)
:
(
<>
{
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<>
{
!
batch
&&
(
<
div
className=
'flex items-center justify-between mb-3'
>
<
Text
className=
'text-sm font-medium'
>
{
t
(
'密钥输入方式'
)
}
</
Text
>
<
Space
>
<
Button
size=
'small'
type=
{
!
useManualInput
?
'primary'
:
'tertiary'
}
onClick=
{
()
=>
{
setUseManualInput
(
false
);
// 切换到文件上传模式时清空手动输入的密钥
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'key'
,
''
);
}
handleInputChange
(
'key'
,
''
);
}
}
>
{
t
(
'文件上传'
)
}
</
Button
>
extraText=
{
<
div
className=
'flex items-center gap-2 flex-wrap'
>
{
isEdit
&&
isMultiKeyChannel
&&
keyMode
===
'append'
&&
(
<
Text
type=
'warning'
size=
'small'
>
{
t
(
'追加模式:新密钥将添加到现有密钥列表的末尾'
,
)
}
</
Text
>
)
}
{
isEdit
&&
(
<
Button
size=
'small'
type=
{
useManualInput
?
'primary'
:
'tertiary'
}
onClick=
{
()
=>
{
setUseManualInput
(
true
);
// 切换到手动输入模式时清空文件上传相关状态
setVertexKeys
([]);
setVertexFileList
([]);
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'vertex_files'
,
[],
);
}
setInputs
((
prev
)
=>
({
...
prev
,
vertex_files
:
[],
}));
}
}
type=
'primary'
theme=
'outline'
onClick=
{
handleShow2FAModal
}
>
{
t
(
'
手动输入
'
)
}
{
t
(
'
查看密钥
'
)
}
</
Button
>
</
Space
>
)
}
{
batchExtra
}
</
div
>
)
}
}
showClear
/>
)
)
:
(
<>
{
inputs
.
type
===
41
&&
(
inputs
.
vertex_key_type
||
'json'
)
===
'json'
?
(
<>
{
!
batch
&&
(
<
div
className=
'flex items-center justify-between mb-3'
>
<
Text
className=
'text-sm font-medium'
>
{
t
(
'密钥输入方式'
)
}
</
Text
>
<
Space
>
<
Button
size=
'small'
type=
{
!
useManualInput
?
'primary'
:
'tertiary'
}
onClick=
{
()
=>
{
setUseManualInput
(
false
);
// 切换到文件上传模式时清空手动输入的密钥
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'key'
,
''
);
}
handleInputChange
(
'key'
,
''
);
}
}
>
{
t
(
'文件上传'
)
}
</
Button
>
<
Button
size=
'small'
type=
{
useManualInput
?
'primary'
:
'tertiary'
}
onClick=
{
()
=>
{
setUseManualInput
(
true
);
// 切换到手动输入模式时清空文件上传相关状态
setVertexKeys
([]);
setVertexFileList
([]);
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'vertex_files'
,
[],
);
}
setInputs
((
prev
)
=>
({
...
prev
,
vertex_files
:
[],
}));
}
}
>
{
t
(
'手动输入'
)
}
</
Button
>
</
Space
>
</
div
>
)
}
{
batch
&&
(
<
Banner
...
...
@@ -2036,7 +2092,9 @@ const EditChannelModal = (props) => {
inputs
.
type
===
33
?
inputs
.
aws_key_type
===
'api_key'
?
t
(
'请输入 API Key,格式:APIKey|Region'
)
:
t
(
'按照如下格式输入:AccessKey|SecretAccessKey|Region'
)
:
t
(
'按照如下格式输入:AccessKey|SecretAccessKey|Region'
,
)
:
t
(
type2secretPrompt
(
inputs
.
type
))
}
rules=
{
...
...
@@ -2258,86 +2316,86 @@ const EditChannelModal = (props) => {
/>
)
}
{
inputs
.
type
===
3
&&
(
<>
<
Banner
type=
'warning'
description=
{
t
(
'2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."'
,
)
}
className=
'!rounded-lg'
/>
<
div
>
<
Form
.
Input
field=
'base_url'
label=
'AZURE_OPENAI_ENDPOINT'
placeholder=
{
t
(
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
<
div
>
<
Form
.
Input
field=
'other'
label=
{
t
(
'默认 API 版本'
)
}
placeholder=
{
t
(
'请输入默认 API 版本,例如:2025-04-01-preview'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'other'
,
value
)
}
showClear
/>
</
div
>
<
div
>
<
Form
.
Input
field=
'azure_responses_version'
label=
{
t
(
'默认 Responses API 版本,为空则使用上方版本'
,
{
inputs
.
type
===
3
&&
(
<>
<
Banner
type=
'warning'
description=
{
t
(
'2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的"."'
,
)
}
placeholder=
{
t
(
'例如:preview'
)
}
onChange=
{
(
value
)
=>
handleChannelOtherSettingsChange
(
'azure_responses_version'
,
value
,
)
}
showClear
className=
'!rounded-lg'
/>
</
div
>
</>
)
}
<
div
>
<
Form
.
Input
field=
'base_url'
label=
'AZURE_OPENAI_ENDPOINT'
placeholder=
{
t
(
'请输入 AZURE_OPENAI_ENDPOINT,例如:https://docs-test-001.openai.azure.com'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
<
div
>
<
Form
.
Input
field=
'other'
label=
{
t
(
'默认 API 版本'
)
}
placeholder=
{
t
(
'请输入默认 API 版本,例如:2025-04-01-preview'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'other'
,
value
)
}
showClear
/>
</
div
>
<
div
>
<
Form
.
Input
field=
'azure_responses_version'
label=
{
t
(
'默认 Responses API 版本,为空则使用上方版本'
,
)
}
placeholder=
{
t
(
'例如:preview'
)
}
onChange=
{
(
value
)
=>
handleChannelOtherSettingsChange
(
'azure_responses_version'
,
value
,
)
}
showClear
/>
</
div
>
</>
)
}
{
inputs
.
type
===
8
&&
(
<>
<
Banner
type=
'warning'
description=
{
t
(
'如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。'
,
)
}
className=
'!rounded-lg'
/>
<
div
>
<
Form
.
Input
field=
'base_url'
label=
{
t
(
'完整的
Base
URL
,支持变量
{
model
}
'
)
}
placeholder=
{
t
(
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
,
{
inputs
.
type
===
8
&&
(
<>
<
Banner
type=
'warning'
description=
{
t
(
'如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
className=
'!rounded-lg'
/>
</
div
>
</>
)
}
<
div
>
<
Form
.
Input
field=
'base_url'
label=
{
t
(
'完整的
Base
URL
,支持变量
{
model
}
'
)
}
placeholder=
{
t
(
'请输入完整的URL,例如:https://api.openai.com/v1/chat/completions'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
</>
)
}
{
inputs
.
type
===
37
&&
(
<
Banner
...
...
@@ -2366,76 +2424,79 @@ const EditChannelModal = (props) => {
}
showClear
disabled=
{
isIonetLocked
}
extraText=
{
t
(
'对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写'
,
extraText=
{
t
(
'对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写'
,
)
}
/>
</
div
>
)
}
{
inputs
.
type
===
22
&&
(
<
div
>
<
Form
.
Input
field=
'base_url'
label=
{
t
(
'私有部署地址'
)
}
placeholder=
{
t
(
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
)
}
{
inputs
.
type
===
22
&&
(
<
div
>
<
Form
.
Input
field=
'base_url'
label=
{
t
(
'私有部署地址'
)
}
placeholder=
{
t
(
'请输入私有部署地址,格式为:https://fastgpt.run/api/openapi'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
)
}
{
inputs
.
type
===
36
&&
(
<
div
>
<
Form
.
Input
field=
'base_url'
label=
{
t
(
'注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用'
,
)
}
placeholder=
{
t
(
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com'
,
{
inputs
.
type
===
36
&&
(
<
div
>
<
Form
.
Input
field=
'base_url'
label=
{
t
(
'注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用'
,
)
}
placeholder=
{
t
(
'请输入到 /suno 前的路径,通常就是域名,例如:https://api.example.com'
,
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
showClear
disabled=
{
isIonetLocked
}
/>
</
div
>
)
}
{
inputs
.
type
===
45
&&
!
doubaoApiEditUnlocked
&&
(
<
div
>
<
Form
.
Select
field=
'base_url'
label=
{
t
(
'API地址'
)
}
placeholder=
{
t
(
'请选择API地址'
)
}
onChange=
{
(
value
)
=>
{
inputs
.
type
===
45
&&
!
doubaoApiEditUnlocked
&&
(
<
div
>
<
Form
.
Select
field=
'base_url'
label=
{
t
(
'API地址'
)
}
placeholder=
{
t
(
'请选择API地址'
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'base_url'
,
value
)
}
optionList=
{
[
{
value
:
'https://ark.cn-beijing.volces.com'
,
label
:
'https://ark.cn-beijing.volces.com'
,
},
{
value
:
'https://ark.ap-southeast.bytepluses.com'
,
label
:
'https://ark.ap-southeast.bytepluses.com'
,
},
{
value
:
'doubao-coding-plan'
,
label
:
'Doubao Coding Plan'
,
},
]
}
defaultValue=
'https://ark.cn-beijing.volces.com'
disabled=
{
isIonetLocked
}
/>
</
div
>
)
}
}
optionList=
{
[
{
value
:
'https://ark.cn-beijing.volces.com'
,
label
:
'https://ark.cn-beijing.volces.com'
,
},
{
value
:
'https://ark.ap-southeast.bytepluses.com'
,
label
:
'https://ark.ap-southeast.bytepluses.com'
,
},
{
value
:
'doubao-coding-plan'
,
label
:
'Doubao Coding Plan'
,
},
]
}
defaultValue=
'https://ark.cn-beijing.volces.com'
disabled=
{
isIonetLocked
}
/>
</
div
>
)
}
</
Card
>
</
div
>
)
}
...
...
@@ -2531,79 +2592,81 @@ const EditChannelModal = (props) => {
</
Button
>
)
}
{
inputs
.
type
===
4
&&
isEdit
&&
(
<
Button
size=
'small'
type=
'primary'
theme=
'light'
onClick=
{
()
=>
setOllamaModalVisible
(
true
)
}
>
{
t
(
'Ollama 模型管理'
)
}
</
Button
>
)
}
<
Button
size=
'small'
type=
'warning'
onClick=
{
()
=>
handleInputChange
(
'models'
,
[])
}
>
{
t
(
'清除所有模型'
)
}
</
Button
>
<
Button
size=
'small'
type=
'tertiary'
onClick=
{
()
=>
{
if
(
inputs
.
models
.
length
===
0
)
{
showInfo
(
t
(
'没有模型可以复制'
));
return
;
}
try
{
copy
(
inputs
.
models
.
join
(
','
));
showSuccess
(
t
(
'模型列表已复制到剪贴板'
));
}
catch
(
error
)
{
showError
(
t
(
'复制失败'
));
}
}
}
>
{
t
(
'复制所有模型'
)
}
</
Button
>
{
modelGroups
&&
modelGroups
.
length
>
0
&&
modelGroups
.
map
((
group
)
=>
(
<
Button
key=
{
group
.
id
}
size=
'small'
type=
'primary'
onClick=
{
()
=>
{
let
items
=
[];
try
{
if
(
Array
.
isArray
(
group
.
items
))
{
items
=
group
.
items
;
}
else
if
(
typeof
group
.
items
===
'string'
)
{
const
parsed
=
JSON
.
parse
(
group
.
items
||
'[]'
,
);
if
(
Array
.
isArray
(
parsed
))
items
=
parsed
;
}
}
catch
{}
const
current
=
formApiRef
.
current
?.
getValue
(
'models'
)
||
inputs
.
models
||
[];
const
merged
=
Array
.
from
(
new
Set
(
[...
current
,
...
items
]
.
map
((
m
)
=>
(
m
||
''
).
trim
())
.
filter
(
Boolean
),
),
);
handleInputChange
(
'models'
,
merged
);
}
}
theme=
'light'
onClick=
{
()
=>
setOllamaModalVisible
(
true
)
}
>
{
group
.
name
}
{
t
(
'Ollama 模型管理'
)
}
</
Button
>
))
}
</
Space
>
}
/>
)
}
<
Button
size=
'small'
type=
'warning'
onClick=
{
()
=>
handleInputChange
(
'models'
,
[])
}
>
{
t
(
'清除所有模型'
)
}
</
Button
>
<
Button
size=
'small'
type=
'tertiary'
onClick=
{
()
=>
{
if
(
inputs
.
models
.
length
===
0
)
{
showInfo
(
t
(
'没有模型可以复制'
));
return
;
}
try
{
copy
(
inputs
.
models
.
join
(
','
));
showSuccess
(
t
(
'模型列表已复制到剪贴板'
));
}
catch
(
error
)
{
showError
(
t
(
'复制失败'
));
}
}
}
>
{
t
(
'复制所有模型'
)
}
</
Button
>
{
modelGroups
&&
modelGroups
.
length
>
0
&&
modelGroups
.
map
((
group
)
=>
(
<
Button
key=
{
group
.
id
}
size=
'small'
type=
'primary'
onClick=
{
()
=>
{
let
items
=
[];
try
{
if
(
Array
.
isArray
(
group
.
items
))
{
items
=
group
.
items
;
}
else
if
(
typeof
group
.
items
===
'string'
)
{
const
parsed
=
JSON
.
parse
(
group
.
items
||
'[]'
,
);
if
(
Array
.
isArray
(
parsed
))
items
=
parsed
;
}
}
catch
{}
const
current
=
formApiRef
.
current
?.
getValue
(
'models'
)
||
inputs
.
models
||
[];
const
merged
=
Array
.
from
(
new
Set
(
[...
current
,
...
items
]
.
map
((
m
)
=>
(
m
||
''
).
trim
())
.
filter
(
Boolean
),
),
);
handleInputChange
(
'models'
,
merged
);
}
}
>
{
group
.
name
}
</
Button
>
))
}
</
Space
>
}
/>
<
Form
.
Input
field=
'custom_model'
...
...
@@ -2650,6 +2713,27 @@ const EditChannelModal = (props) => {
templateLabel=
{
t
(
'填入模板'
)
}
editorType=
'keyValue'
formApi=
{
formApiRef
.
current
}
renderStringValueSuffix=
{
({
pairKey
,
value
})
=>
{
if
(
!
MODEL_FETCHABLE_TYPES
.
has
(
inputs
.
type
))
{
return
null
;
}
const
disabled
=
!
String
(
pairKey
??
''
).
trim
();
return
(
<
Tooltip
content=
{
t
(
'选择模型'
)
}
>
<
Button
type=
'tertiary'
theme=
'borderless'
size=
'small'
icon=
{
<
IconSearch
size=
{
14
}
/>
}
disabled=
{
disabled
}
onClick=
{
(
e
)
=>
{
e
.
stopPropagation
();
openModelMappingValueModal
({
pairKey
,
value
});
}
}
/>
</
Tooltip
>
);
}
}
extraText=
{
t
(
'键为请求中的模型名称,值为要替换的模型名称'
,
)
}
...
...
@@ -3164,6 +3248,53 @@ const EditChannelModal = (props) => {
onCancel=
{
()
=>
setModelModalVisible
(
false
)
}
/>
<
SingleModelSelectModal
visible=
{
modelMappingValueModalVisible
}
models=
{
modelMappingValueModalModels
}
selected=
{
modelMappingValueSelected
}
onConfirm=
{
(
selectedModel
)
=>
{
const
modelName
=
String
(
selectedModel
??
''
).
trim
();
if
(
!
modelName
)
{
showError
(
t
(
'请先选择模型!'
));
return
;
}
const
mappingKey
=
String
(
modelMappingValueKey
??
''
).
trim
();
if
(
!
mappingKey
)
{
setModelMappingValueModalVisible
(
false
);
return
;
}
let
parsed
=
{};
const
currentMapping
=
inputs
.
model_mapping
;
if
(
typeof
currentMapping
===
'string'
&&
currentMapping
.
trim
())
{
try
{
parsed
=
JSON
.
parse
(
currentMapping
);
}
catch
(
error
)
{
parsed
=
{};
}
}
else
if
(
currentMapping
&&
typeof
currentMapping
===
'object'
&&
!
Array
.
isArray
(
currentMapping
)
)
{
parsed
=
currentMapping
;
}
if
(
!
parsed
||
typeof
parsed
!==
'object'
||
Array
.
isArray
(
parsed
))
{
parsed
=
{};
}
parsed
[
mappingKey
]
=
modelName
;
const
nextMapping
=
JSON
.
stringify
(
parsed
,
null
,
2
);
handleInputChange
(
'model_mapping'
,
nextMapping
);
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'model_mapping'
,
nextMapping
);
}
setModelMappingValueModalVisible
(
false
);
}
}
onCancel=
{
()
=>
setModelMappingValueModalVisible
(
false
)
}
/>
<
OllamaModelModal
visible=
{
ollamaModalVisible
}
onCancel=
{
()
=>
setOllamaModalVisible
(
false
)
}
...
...
@@ -3181,7 +3312,9 @@ const EditChannelModal = (props) => {
?
inputs
.
models
.
map
(
String
)
:
[];
const
incoming
=
modelIds
.
map
(
String
);
const
nextModels
=
Array
.
from
(
new
Set
([...
existingModels
,
...
incoming
]));
const
nextModels
=
Array
.
from
(
new
Set
([...
existingModels
,
...
incoming
]),
);
handleInputChange
(
'models'
,
nextModels
);
if
(
formApiRef
.
current
)
{
...
...
web/src/components/table/channels/modals/SingleModelSelectModal.jsx
0 → 100644
View file @
c0a9934a
/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import
React
,
{
useEffect
,
useMemo
,
useState
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
useIsMobile
}
from
'../../../../hooks/common/useIsMobile'
;
import
{
Collapse
,
Empty
,
Input
,
Modal
,
Radio
,
Typography
,
}
from
'@douyinfe/semi-ui'
;
import
{
IllustrationNoResult
,
IllustrationNoResultDark
,
}
from
'@douyinfe/semi-illustrations'
;
import
{
IconSearch
}
from
'@douyinfe/semi-icons'
;
import
{
getModelCategories
}
from
'../../../../helpers/render'
;
const
SingleModelSelectModal
=
({
visible
,
models
=
[],
selected
=
''
,
onConfirm
,
onCancel
,
})
=>
{
const
{
t
}
=
useTranslation
();
const
isMobile
=
useIsMobile
();
const
normalizeModelName
=
(
model
)
=>
String
(
model
??
''
).
trim
();
const
normalizedModels
=
useMemo
(()
=>
{
const
list
=
Array
.
isArray
(
models
)
?
models
:
[];
return
Array
.
from
(
new
Set
(
list
.
map
(
normalizeModelName
).
filter
(
Boolean
)));
},
[
models
]);
const
[
keyword
,
setKeyword
]
=
useState
(
''
);
const
[
selectedModel
,
setSelectedModel
]
=
useState
(
''
);
useEffect
(()
=>
{
if
(
visible
)
{
setKeyword
(
''
);
setSelectedModel
(
normalizeModelName
(
selected
));
}
},
[
visible
,
selected
]);
const
filteredModels
=
useMemo
(()
=>
{
const
lower
=
keyword
.
trim
().
toLowerCase
();
if
(
!
lower
)
return
normalizedModels
;
return
normalizedModels
.
filter
((
m
)
=>
m
.
toLowerCase
().
includes
(
lower
));
},
[
normalizedModels
,
keyword
]);
const
modelsByCategory
=
useMemo
(()
=>
{
const
categories
=
getModelCategories
(
t
);
const
categorized
=
{};
const
uncategorized
=
[];
filteredModels
.
forEach
((
model
)
=>
{
let
foundCategory
=
false
;
for
(
const
[
key
,
category
]
of
Object
.
entries
(
categories
))
{
if
(
key
!==
'all'
&&
category
.
filter
({
model_name
:
model
}))
{
if
(
!
categorized
[
key
])
{
categorized
[
key
]
=
{
label
:
category
.
label
,
icon
:
category
.
icon
,
models
:
[],
};
}
categorized
[
key
].
models
.
push
(
model
);
foundCategory
=
true
;
break
;
}
}
if
(
!
foundCategory
)
{
uncategorized
.
push
(
model
);
}
});
if
(
uncategorized
.
length
>
0
)
{
categorized
.
other
=
{
label
:
t
(
'其他'
),
icon
:
null
,
models
:
uncategorized
,
};
}
return
categorized
;
},
[
filteredModels
,
t
]);
const
categoryEntries
=
useMemo
(
()
=>
Object
.
entries
(
modelsByCategory
),
[
modelsByCategory
],
);
return
(
<
Modal
header=
{
<
div
className=
'flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4'
>
<
Typography
.
Title
heading=
{
5
}
className=
'm-0'
>
{
t
(
'选择模型'
)
}
</
Typography
.
Title
>
</
div
>
}
visible=
{
visible
}
onOk=
{
()
=>
onConfirm
?.(
selectedModel
)
}
onCancel=
{
onCancel
}
okText=
{
t
(
'确定'
)
}
cancelText=
{
t
(
'取消'
)
}
okButtonProps=
{
{
disabled
:
!
selectedModel
}
}
size=
{
isMobile
?
'full-width'
:
'large'
}
closeOnEsc
maskClosable
centered
>
<
Input
prefix=
{
<
IconSearch
size=
{
14
}
/>
}
placeholder=
{
t
(
'搜索模型'
)
}
value=
{
keyword
}
onChange=
{
(
v
)
=>
setKeyword
(
v
)
}
showClear
/>
<
div
style=
{
{
maxHeight
:
400
,
overflowY
:
'auto'
,
paddingRight
:
8
}
}
>
{
filteredModels
.
length
===
0
?
(
<
Empty
image=
{
<
IllustrationNoResult
style=
{
{
width
:
150
,
height
:
150
}
}
/>
}
darkModeImage=
{
<
IllustrationNoResultDark
style=
{
{
width
:
150
,
height
:
150
}
}
/>
}
description=
{
t
(
'暂无匹配模型'
)
}
style=
{
{
padding
:
30
}
}
/>
)
:
(
<
Radio
.
Group
className=
'w-full'
style=
{
{
width
:
'100%'
}
}
value=
{
selectedModel
}
onChange=
{
(
val
)
=>
{
const
next
=
val
&&
val
.
target
?
val
.
target
.
value
:
val
;
setSelectedModel
(
next
);
}
}
>
<
Collapse
className=
'w-full'
style=
{
{
width
:
'100%'
}
}
defaultActiveKey=
{
[]
}
>
{
categoryEntries
.
map
(([
key
,
categoryData
],
index
)
=>
(
<
Collapse
.
Panel
key=
{
`${key}_${index}`
}
itemKey=
{
`${key}_${index}`
}
header=
{
<
span
className=
'flex items-center gap-2'
>
{
categoryData
.
icon
}
<
span
>
{
categoryData
.
label
}
(
{
categoryData
.
models
.
length
}
)
</
span
>
</
span
>
}
>
<
div
className=
'grid grid-cols-2 gap-x-4'
>
{
categoryData
.
models
.
map
((
model
)
=>
(
<
Radio
key=
{
model
}
value=
{
model
}
className=
'my-1'
>
{
model
}
</
Radio
>
))
}
</
div
>
</
Collapse
.
Panel
>
))
}
</
Collapse
>
</
Radio
.
Group
>
)
}
</
div
>
</
Modal
>
);
};
export
default
SingleModelSelectModal
;
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