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
2d4149c5
authored
Aug 02, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: implement key mode for multi-key channels with append/replace options
parent
52f419bc
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
170 additions
and
49 deletions
+170
-49
controller/channel.go
+65
-1
web/src/components/table/channels/modals/EditChannelModal.jsx
+105
-48
No files found.
controller/channel.go
View file @
2d4149c5
...
@@ -669,6 +669,7 @@ func DeleteChannelBatch(c *gin.Context) {
...
@@ -669,6 +669,7 @@ func DeleteChannelBatch(c *gin.Context) {
type
PatchChannel
struct
{
type
PatchChannel
struct
{
model
.
Channel
model
.
Channel
MultiKeyMode
*
string
`json:"multi_key_mode"`
MultiKeyMode
*
string
`json:"multi_key_mode"`
KeyMode
*
string
`json:"key_mode"`
// 多key模式下密钥覆盖或者追加
}
}
func
UpdateChannel
(
c
*
gin
.
Context
)
{
func
UpdateChannel
(
c
*
gin
.
Context
)
{
...
@@ -688,7 +689,7 @@ func UpdateChannel(c *gin.Context) {
...
@@ -688,7 +689,7 @@ func UpdateChannel(c *gin.Context) {
return
return
}
}
// Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request.
// Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request.
originChannel
,
err
:=
model
.
GetChannelById
(
channel
.
Id
,
fals
e
)
originChannel
,
err
:=
model
.
GetChannelById
(
channel
.
Id
,
tru
e
)
if
err
!=
nil
{
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"success"
:
false
,
...
@@ -704,6 +705,69 @@ func UpdateChannel(c *gin.Context) {
...
@@ -704,6 +705,69 @@ func UpdateChannel(c *gin.Context) {
if
channel
.
MultiKeyMode
!=
nil
&&
*
channel
.
MultiKeyMode
!=
""
{
if
channel
.
MultiKeyMode
!=
nil
&&
*
channel
.
MultiKeyMode
!=
""
{
channel
.
ChannelInfo
.
MultiKeyMode
=
constant
.
MultiKeyMode
(
*
channel
.
MultiKeyMode
)
channel
.
ChannelInfo
.
MultiKeyMode
=
constant
.
MultiKeyMode
(
*
channel
.
MultiKeyMode
)
}
}
// 处理多key模式下的密钥追加/覆盖逻辑
if
channel
.
KeyMode
!=
nil
&&
channel
.
ChannelInfo
.
IsMultiKey
{
switch
*
channel
.
KeyMode
{
case
"append"
:
// 追加模式:将新密钥添加到现有密钥列表
if
originChannel
.
Key
!=
""
{
var
newKeys
[]
string
var
existingKeys
[]
string
// 解析现有密钥
if
strings
.
HasPrefix
(
strings
.
TrimSpace
(
originChannel
.
Key
),
"["
)
{
// JSON数组格式
var
arr
[]
json
.
RawMessage
if
err
:=
json
.
Unmarshal
([]
byte
(
strings
.
TrimSpace
(
originChannel
.
Key
)),
&
arr
);
err
==
nil
{
existingKeys
=
make
([]
string
,
len
(
arr
))
for
i
,
v
:=
range
arr
{
existingKeys
[
i
]
=
string
(
v
)
}
}
}
else
{
// 换行分隔格式
existingKeys
=
strings
.
Split
(
strings
.
Trim
(
originChannel
.
Key
,
"
\n
"
),
"
\n
"
)
}
// 处理 Vertex AI 的特殊情况
if
channel
.
Type
==
constant
.
ChannelTypeVertexAi
{
// 尝试解析新密钥为JSON数组
if
strings
.
HasPrefix
(
strings
.
TrimSpace
(
channel
.
Key
),
"["
)
{
array
,
err
:=
getVertexArrayKeys
(
channel
.
Key
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"追加密钥解析失败: "
+
err
.
Error
(),
})
return
}
newKeys
=
array
}
else
{
// 单个JSON密钥
newKeys
=
[]
string
{
channel
.
Key
}
}
// 合并密钥
allKeys
:=
append
(
existingKeys
,
newKeys
...
)
channel
.
Key
=
strings
.
Join
(
allKeys
,
"
\n
"
)
}
else
{
// 普通渠道的处理
inputKeys
:=
strings
.
Split
(
channel
.
Key
,
"
\n
"
)
for
_
,
key
:=
range
inputKeys
{
key
=
strings
.
TrimSpace
(
key
)
if
key
!=
""
{
newKeys
=
append
(
newKeys
,
key
)
}
}
// 合并密钥
allKeys
:=
append
(
existingKeys
,
newKeys
...
)
channel
.
Key
=
strings
.
Join
(
allKeys
,
"
\n
"
)
}
}
case
"replace"
:
// 覆盖模式:直接使用新密钥(默认行为,不需要特殊处理)
}
}
err
=
channel
.
Update
()
err
=
channel
.
Update
()
if
err
!=
nil
{
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
common
.
ApiError
(
c
,
err
)
...
...
web/src/components/table/channels/modals/EditChannelModal.jsx
View file @
2d4149c5
...
@@ -154,6 +154,7 @@ const EditChannelModal = (props) => {
...
@@ -154,6 +154,7 @@ const EditChannelModal = (props) => {
const
[
isMultiKeyChannel
,
setIsMultiKeyChannel
]
=
useState
(
false
);
const
[
isMultiKeyChannel
,
setIsMultiKeyChannel
]
=
useState
(
false
);
const
[
channelSearchValue
,
setChannelSearchValue
]
=
useState
(
''
);
const
[
channelSearchValue
,
setChannelSearchValue
]
=
useState
(
''
);
const
[
useManualInput
,
setUseManualInput
]
=
useState
(
false
);
// 是否使用手动输入模式
const
[
useManualInput
,
setUseManualInput
]
=
useState
(
false
);
// 是否使用手动输入模式
const
[
keyMode
,
setKeyMode
]
=
useState
(
'append'
);
// 密钥模式:replace(覆盖)或 append(追加)
// 渠道额外设置状态
// 渠道额外设置状态
const
[
channelSettings
,
setChannelSettings
]
=
useState
({
const
[
channelSettings
,
setChannelSettings
]
=
useState
({
force_format
:
false
,
force_format
:
false
,
...
@@ -560,6 +561,12 @@ const EditChannelModal = (props) => {
...
@@ -560,6 +561,12 @@ const EditChannelModal = (props) => {
pass_through_body_enabled
:
false
,
pass_through_body_enabled
:
false
,
system_prompt
:
''
,
system_prompt
:
''
,
});
});
// 重置密钥模式状态
setKeyMode
(
'append'
);
// 清空表单中的key_mode字段
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'key_mode'
,
undefined
);
}
}
}
},
[
props
.
visible
,
channelId
]);
},
[
props
.
visible
,
channelId
]);
...
@@ -725,6 +732,7 @@ const EditChannelModal = (props) => {
...
@@ -725,6 +732,7 @@ const EditChannelModal = (props) => {
res
=
await
API
.
put
(
`/api/channel/`
,
{
res
=
await
API
.
put
(
`/api/channel/`
,
{
...
localInputs
,
...
localInputs
,
id
:
parseInt
(
channelId
),
id
:
parseInt
(
channelId
),
key_mode
:
isMultiKeyChannel
?
keyMode
:
undefined
,
// 只在多key模式下传递
});
});
}
else
{
}
else
{
res
=
await
API
.
post
(
`/api/channel/`
,
{
res
=
await
API
.
post
(
`/api/channel/`
,
{
...
@@ -787,55 +795,59 @@ const EditChannelModal = (props) => {
...
@@ -787,55 +795,59 @@ const EditChannelModal = (props) => {
const
batchAllowed
=
!
isEdit
||
isMultiKeyChannel
;
const
batchAllowed
=
!
isEdit
||
isMultiKeyChannel
;
const
batchExtra
=
batchAllowed
?
(
const
batchExtra
=
batchAllowed
?
(
<
Space
>
<
Space
>
<
Checkbox
{
!
isEdit
&&
(
disabled=
{
isEdit
}
<
Checkbox
checked=
{
batch
}
disabled=
{
isEdit
}
onChange=
{
(
e
)
=>
{
checked=
{
batch
}
const
checked
=
e
.
target
.
checked
;
onChange=
{
(
e
)
=>
{
const
checked
=
e
.
target
.
checked
;
if
(
!
checked
&&
vertexFileList
.
length
>
1
)
{
Modal
.
confirm
({
if
(
!
checked
&&
vertexFileList
.
length
>
1
)
{
title
:
t
(
'切换为单密钥模式'
),
Modal
.
confirm
({
content
:
t
(
'将仅保留第一个密钥文件,其余文件将被移除,是否继续?'
),
title
:
t
(
'切换为单密钥模式'
),
onOk
:
()
=>
{
content
:
t
(
'将仅保留第一个密钥文件,其余文件将被移除,是否继续?'
),
const
firstFile
=
vertexFileList
[
0
];
onOk
:
()
=>
{
const
firstKey
=
vertexKeys
[
0
]
?
[
vertexKeys
[
0
]]
:
[];
const
firstFile
=
vertexFileList
[
0
];
const
firstKey
=
vertexKeys
[
0
]
?
[
vertexKeys
[
0
]]
:
[];
setVertexFileList
([
firstFile
]);
setVertexKeys
(
firstKey
);
setVertexFileList
([
firstFile
]);
setVertexKeys
(
firstKey
);
formApiRef
.
current
?.
setValue
(
'vertex_files'
,
[
firstFile
]);
setInputs
((
prev
)
=>
({
...
prev
,
vertex_files
:
[
firstFile
]
}));
formApiRef
.
current
?.
setValue
(
'vertex_files'
,
[
firstFile
]);
setInputs
((
prev
)
=>
({
...
prev
,
vertex_files
:
[
firstFile
]
}));
setBatch
(
false
);
setMultiToSingle
(
false
);
setBatch
(
false
);
setMultiKeyMode
(
'random'
);
setMultiToSingle
(
false
);
},
setMultiKeyMode
(
'random'
);
onCancel
:
()
=>
{
},
setBatch
(
true
);
onCancel
:
()
=>
{
},
setBatch
(
true
);
centered
:
true
,
},
});
centered
:
true
,
return
;
});
}
return
;
}
setBatch
(
checked
);
setBatch
(
checked
);
if
(
!
checked
)
{
if
(
!
checked
)
{
setMultiToSingle
(
false
);
setMultiToSingle
(
false
);
setMultiKeyMode
(
'random'
);
setMultiKeyMode
(
'random'
);
}
else
{
}
else
{
// 批量模式下禁用手动输入,并清空手动输入的内容
// 批量模式下禁用手动输入,并清空手动输入的内容
setUseManualInput
(
false
);
setUseManualInput
(
false
);
if
(
inputs
.
type
===
41
)
{
if
(
inputs
.
type
===
41
)
{
// 清空手动输入的密钥内容
// 清空手动输入的密钥内容
if
(
formApiRef
.
current
)
{
if
(
formApiRef
.
current
)
{
formApiRef
.
current
.
setValue
(
'key'
,
''
);
formApiRef
.
current
.
setValue
(
'key'
,
''
);
}
handleInputChange
(
'key'
,
''
);
}
}
handleInputChange
(
'key'
,
''
);
}
}
}
}
}
}
}
>
>
{
t
(
'批量创建'
)
}
</
Checkbox
>
{
t
(
'批量创建'
)
}
</
Checkbox
>
)
}
{
batch
&&
(
{
batch
&&
(
<
Checkbox
disabled=
{
isEdit
}
checked=
{
multiToSingle
}
onChange=
{
()
=>
{
<
Checkbox
disabled=
{
isEdit
}
checked=
{
multiToSingle
}
onChange=
{
()
=>
{
setMultiToSingle
(
prev
=>
!
prev
);
setMultiToSingle
(
prev
=>
!
prev
);
...
@@ -1032,7 +1044,16 @@ const EditChannelModal = (props) => {
...
@@ -1032,7 +1044,16 @@ const EditChannelModal = (props) => {
autosize
autosize
autoComplete=
'new-password'
autoComplete=
'new-password'
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
extraText=
{
batchExtra
}
extraText=
{
<
div
className=
"flex items-center gap-2"
>
{
isEdit
&&
isMultiKeyChannel
&&
keyMode
===
'append'
&&
(
<
Text
type=
"warning"
size=
"small"
>
{
t
(
'追加模式:新密钥将添加到现有密钥列表的末尾'
)
}
</
Text
>
)
}
{
batchExtra
}
</
div
>
}
showClear
showClear
/>
/>
)
)
...
@@ -1099,6 +1120,11 @@ const EditChannelModal = (props) => {
...
@@ -1099,6 +1120,11 @@ const EditChannelModal = (props) => {
<
Text
type=
"tertiary"
size=
"small"
>
<
Text
type=
"tertiary"
size=
"small"
>
{
t
(
'请输入完整的 JSON 格式密钥内容'
)
}
{
t
(
'请输入完整的 JSON 格式密钥内容'
)
}
</
Text
>
</
Text
>
{
isEdit
&&
isMultiKeyChannel
&&
keyMode
===
'append'
&&
(
<
Text
type=
"warning"
size=
"small"
>
{
t
(
'追加模式:新密钥将添加到现有密钥列表的末尾'
)
}
</
Text
>
)
}
{
batchExtra
}
{
batchExtra
}
</
div
>
</
div
>
}
}
...
@@ -1132,13 +1158,44 @@ const EditChannelModal = (props) => {
...
@@ -1132,13 +1158,44 @@ const EditChannelModal = (props) => {
rules=
{
isEdit
?
[]
:
[{
required
:
true
,
message
:
t
(
'请输入密钥'
)
}]
}
rules=
{
isEdit
?
[]
:
[{
required
:
true
,
message
:
t
(
'请输入密钥'
)
}]
}
autoComplete=
'new-password'
autoComplete=
'new-password'
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
onChange=
{
(
value
)
=>
handleInputChange
(
'key'
,
value
)
}
extraText=
{
batchExtra
}
extraText=
{
<
div
className=
"flex items-center gap-2"
>
{
isEdit
&&
isMultiKeyChannel
&&
keyMode
===
'append'
&&
(
<
Text
type=
"warning"
size=
"small"
>
{
t
(
'追加模式:新密钥将添加到现有密钥列表的末尾'
)
}
</
Text
>
)
}
{
batchExtra
}
</
div
>
}
showClear
showClear
/>
/>
)
}
)
}
</>
</>
)
}
)
}
{
isEdit
&&
isMultiKeyChannel
&&
(
<
Form
.
Select
field=
'key_mode'
label=
{
t
(
'密钥更新模式'
)
}
placeholder=
{
t
(
'请选择密钥更新模式'
)
}
optionList=
{
[
{
label
:
t
(
'追加到现有密钥'
),
value
:
'append'
},
{
label
:
t
(
'覆盖现有密钥'
),
value
:
'replace'
},
]
}
style=
{
{
width
:
'100%'
}
}
value=
{
keyMode
}
onChange=
{
(
value
)
=>
setKeyMode
(
value
)
}
extraText=
{
<
Text
type=
"tertiary"
size=
"small"
>
{
keyMode
===
'replace'
?
t
(
'覆盖模式:将完全替换现有的所有密钥'
)
:
t
(
'追加模式:将新密钥添加到现有密钥列表末尾'
)
}
</
Text
>
}
/>
)
}
{
batch
&&
multiToSingle
&&
(
{
batch
&&
multiToSingle
&&
(
<>
<>
<
Form
.
Select
<
Form
.
Select
...
...
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