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
aeec39a1
authored
Aug 04, 2025
by
CaIon
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
feat: add multi-key management
parent
bb08de0b
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
726 additions
and
60 deletions
+726
-60
controller/channel.go
+258
-0
model/channel.go
+22
-11
model/channel_cache.go
+1
-1
router/api-router.go
+1
-0
web/src/components/table/channels/ChannelsColumnDefs.js
+47
-48
web/src/components/table/channels/ChannelsTable.jsx
+7
-0
web/src/components/table/channels/index.jsx
+7
-0
web/src/components/table/channels/modals/MultiKeyManageModal.jsx
+373
-0
web/src/hooks/channels/useChannelsData.js
+10
-0
No files found.
controller/channel.go
View file @
aeec39a1
...
@@ -1030,3 +1030,261 @@ func CopyChannel(c *gin.Context) {
...
@@ -1030,3 +1030,261 @@ func CopyChannel(c *gin.Context) {
// success
// success
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
gin
.
H
{
"id"
:
clone
.
Id
}})
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
gin
.
H
{
"id"
:
clone
.
Id
}})
}
}
// MultiKeyManageRequest represents the request for multi-key management operations
type
MultiKeyManageRequest
struct
{
ChannelId
int
`json:"channel_id"`
Action
string
`json:"action"`
// "disable_key", "enable_key", "delete_disabled_keys", "get_key_status"
KeyIndex
*
int
`json:"key_index,omitempty"`
// for disable_key and enable_key actions
}
// MultiKeyStatusResponse represents the response for key status query
type
MultiKeyStatusResponse
struct
{
Keys
[]
KeyStatus
`json:"keys"`
}
type
KeyStatus
struct
{
Index
int
`json:"index"`
Status
int
`json:"status"`
// 1: enabled, 2: disabled
DisabledTime
int64
`json:"disabled_time,omitempty"`
Reason
string
`json:"reason,omitempty"`
KeyPreview
string
`json:"key_preview"`
// first 10 chars of key for identification
}
// ManageMultiKeys handles multi-key management operations
func
ManageMultiKeys
(
c
*
gin
.
Context
)
{
request
:=
MultiKeyManageRequest
{}
err
:=
c
.
ShouldBindJSON
(
&
request
)
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
channel
,
err
:=
model
.
GetChannelById
(
request
.
ChannelId
,
true
)
if
err
!=
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"渠道不存在"
,
})
return
}
if
!
channel
.
ChannelInfo
.
IsMultiKey
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"该渠道不是多密钥模式"
,
})
return
}
switch
request
.
Action
{
case
"get_key_status"
:
keys
:=
channel
.
GetKeys
()
var
keyStatusList
[]
KeyStatus
for
i
,
key
:=
range
keys
{
status
:=
1
// default enabled
var
disabledTime
int64
var
reason
string
if
channel
.
ChannelInfo
.
MultiKeyStatusList
!=
nil
{
if
s
,
exists
:=
channel
.
ChannelInfo
.
MultiKeyStatusList
[
i
];
exists
{
status
=
s
}
}
if
status
!=
1
{
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
!=
nil
{
disabledTime
=
channel
.
ChannelInfo
.
MultiKeyDisabledTime
[
i
]
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
!=
nil
{
reason
=
channel
.
ChannelInfo
.
MultiKeyDisabledReason
[
i
]
}
}
// Create key preview (first 10 chars)
keyPreview
:=
key
if
len
(
key
)
>
10
{
keyPreview
=
key
[
:
10
]
+
"..."
}
keyStatusList
=
append
(
keyStatusList
,
KeyStatus
{
Index
:
i
,
Status
:
status
,
DisabledTime
:
disabledTime
,
Reason
:
reason
,
KeyPreview
:
keyPreview
,
})
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
""
,
"data"
:
MultiKeyStatusResponse
{
Keys
:
keyStatusList
},
})
return
case
"disable_key"
:
if
request
.
KeyIndex
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"未指定要禁用的密钥索引"
,
})
return
}
keyIndex
:=
*
request
.
KeyIndex
if
keyIndex
<
0
||
keyIndex
>=
channel
.
ChannelInfo
.
MultiKeySize
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"密钥索引超出范围"
,
})
return
}
if
channel
.
ChannelInfo
.
MultiKeyStatusList
==
nil
{
channel
.
ChannelInfo
.
MultiKeyStatusList
=
make
(
map
[
int
]
int
)
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
==
nil
{
channel
.
ChannelInfo
.
MultiKeyDisabledTime
=
make
(
map
[
int
]
int64
)
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
==
nil
{
channel
.
ChannelInfo
.
MultiKeyDisabledReason
=
make
(
map
[
int
]
string
)
}
channel
.
ChannelInfo
.
MultiKeyStatusList
[
keyIndex
]
=
2
// disabled
channel
.
ChannelInfo
.
MultiKeyDisabledTime
[
keyIndex
]
=
common
.
GetTimestamp
()
channel
.
ChannelInfo
.
MultiKeyDisabledReason
[
keyIndex
]
=
"手动禁用"
err
=
channel
.
Update
()
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
model
.
InitChannelCache
()
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"密钥已禁用"
,
})
return
case
"enable_key"
:
if
request
.
KeyIndex
==
nil
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"未指定要启用的密钥索引"
,
})
return
}
keyIndex
:=
*
request
.
KeyIndex
if
keyIndex
<
0
||
keyIndex
>=
channel
.
ChannelInfo
.
MultiKeySize
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"密钥索引超出范围"
,
})
return
}
// 从状态列表中删除该密钥的记录,使其回到默认启用状态
if
channel
.
ChannelInfo
.
MultiKeyStatusList
!=
nil
{
delete
(
channel
.
ChannelInfo
.
MultiKeyStatusList
,
keyIndex
)
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
!=
nil
{
delete
(
channel
.
ChannelInfo
.
MultiKeyDisabledTime
,
keyIndex
)
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
!=
nil
{
delete
(
channel
.
ChannelInfo
.
MultiKeyDisabledReason
,
keyIndex
)
}
err
=
channel
.
Update
()
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
model
.
InitChannelCache
()
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
"密钥已启用"
,
})
return
case
"delete_disabled_keys"
:
keys
:=
channel
.
GetKeys
()
var
remainingKeys
[]
string
var
deletedCount
int
var
newStatusList
=
make
(
map
[
int
]
int
)
var
newDisabledTime
=
make
(
map
[
int
]
int64
)
var
newDisabledReason
=
make
(
map
[
int
]
string
)
newIndex
:=
0
for
i
,
key
:=
range
keys
{
status
:=
1
// default enabled
if
channel
.
ChannelInfo
.
MultiKeyStatusList
!=
nil
{
if
s
,
exists
:=
channel
.
ChannelInfo
.
MultiKeyStatusList
[
i
];
exists
{
status
=
s
}
}
// 只删除自动禁用(status == 3)的密钥,保留启用(status == 1)和手动禁用(status == 2)的密钥
if
status
==
3
{
deletedCount
++
}
else
{
remainingKeys
=
append
(
remainingKeys
,
key
)
// 保留非自动禁用密钥的状态信息,重新索引
if
status
!=
1
{
newStatusList
[
newIndex
]
=
status
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
!=
nil
{
if
t
,
exists
:=
channel
.
ChannelInfo
.
MultiKeyDisabledTime
[
i
];
exists
{
newDisabledTime
[
newIndex
]
=
t
}
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
!=
nil
{
if
r
,
exists
:=
channel
.
ChannelInfo
.
MultiKeyDisabledReason
[
i
];
exists
{
newDisabledReason
[
newIndex
]
=
r
}
}
}
newIndex
++
}
}
if
deletedCount
==
0
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"没有需要删除的自动禁用密钥"
,
})
return
}
// Update channel with remaining keys
channel
.
Key
=
strings
.
Join
(
remainingKeys
,
"
\n
"
)
channel
.
ChannelInfo
.
MultiKeySize
=
len
(
remainingKeys
)
channel
.
ChannelInfo
.
MultiKeyStatusList
=
newStatusList
channel
.
ChannelInfo
.
MultiKeyDisabledTime
=
newDisabledTime
channel
.
ChannelInfo
.
MultiKeyDisabledReason
=
newDisabledReason
err
=
channel
.
Update
()
if
err
!=
nil
{
common
.
ApiError
(
c
,
err
)
return
}
model
.
InitChannelCache
()
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
true
,
"message"
:
fmt
.
Sprintf
(
"已删除 %d 个自动禁用的密钥"
,
deletedCount
),
"data"
:
deletedCount
,
})
return
default
:
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"success"
:
false
,
"message"
:
"不支持的操作"
,
})
return
}
}
model/channel.go
View file @
aeec39a1
...
@@ -41,6 +41,7 @@ type Channel struct {
...
@@ -41,6 +41,7 @@ type Channel struct {
Priority
*
int64
`json:"priority" gorm:"bigint;default:0"`
Priority
*
int64
`json:"priority" gorm:"bigint;default:0"`
AutoBan
*
int
`json:"auto_ban" gorm:"default:1"`
AutoBan
*
int
`json:"auto_ban" gorm:"default:1"`
OtherInfo
string
`json:"other_info"`
OtherInfo
string
`json:"other_info"`
Settings
string
`json:"settings"`
Tag
*
string
`json:"tag" gorm:"index"`
Tag
*
string
`json:"tag" gorm:"index"`
Setting
*
string
`json:"setting" gorm:"type:text"`
// 渠道额外设置
Setting
*
string
`json:"setting" gorm:"type:text"`
// 渠道额外设置
ParamOverride
*
string
`json:"param_override" gorm:"type:text"`
ParamOverride
*
string
`json:"param_override" gorm:"type:text"`
...
@@ -52,11 +53,13 @@ type Channel struct {
...
@@ -52,11 +53,13 @@ type Channel struct {
}
}
type
ChannelInfo
struct
{
type
ChannelInfo
struct
{
IsMultiKey
bool
`json:"is_multi_key"`
// 是否多Key模式
IsMultiKey
bool
`json:"is_multi_key"`
// 是否多Key模式
MultiKeySize
int
`json:"multi_key_size"`
// 多Key模式下的Key数量
MultiKeySize
int
`json:"multi_key_size"`
// 多Key模式下的Key数量
MultiKeyStatusList
map
[
int
]
int
`json:"multi_key_status_list"`
// key状态列表,key index -> status
MultiKeyStatusList
map
[
int
]
int
`json:"multi_key_status_list"`
// key状态列表,key index -> status
MultiKeyPollingIndex
int
`json:"multi_key_polling_index"`
// 多Key模式下轮询的key索引
MultiKeyDisabledReason
map
[
int
]
string
`json:"multi_key_disabled_reason"`
// key禁用原因列表,key index -> reason
MultiKeyMode
constant
.
MultiKeyMode
`json:"multi_key_mode"`
MultiKeyDisabledTime
map
[
int
]
int64
`json:"multi_key_disabled_time"`
// key禁用时间列表,key index -> time
MultiKeyPollingIndex
int
`json:"multi_key_polling_index"`
// 多Key模式下轮询的key索引
MultiKeyMode
constant
.
MultiKeyMode
`json:"multi_key_mode"`
}
}
// Value implements driver.Valuer interface
// Value implements driver.Valuer interface
...
@@ -70,7 +73,7 @@ func (c *ChannelInfo) Scan(value interface{}) error {
...
@@ -70,7 +73,7 @@ func (c *ChannelInfo) Scan(value interface{}) error {
return
common
.
Unmarshal
(
bytesValue
,
c
)
return
common
.
Unmarshal
(
bytesValue
,
c
)
}
}
func
(
channel
*
Channel
)
g
etKeys
()
[]
string
{
func
(
channel
*
Channel
)
G
etKeys
()
[]
string
{
if
channel
.
Key
==
""
{
if
channel
.
Key
==
""
{
return
[]
string
{}
return
[]
string
{}
}
}
...
@@ -101,7 +104,7 @@ func (channel *Channel) GetNextEnabledKey() (string, int, *types.NewAPIError) {
...
@@ -101,7 +104,7 @@ func (channel *Channel) GetNextEnabledKey() (string, int, *types.NewAPIError) {
}
}
// Obtain all keys (split by \n)
// Obtain all keys (split by \n)
keys
:=
channel
.
g
etKeys
()
keys
:=
channel
.
G
etKeys
()
if
len
(
keys
)
==
0
{
if
len
(
keys
)
==
0
{
// No keys available, return error, should disable the channel
// No keys available, return error, should disable the channel
return
""
,
0
,
types
.
NewError
(
errors
.
New
(
"no keys available"
),
types
.
ErrorCodeChannelNoAvailableKey
)
return
""
,
0
,
types
.
NewError
(
errors
.
New
(
"no keys available"
),
types
.
ErrorCodeChannelNoAvailableKey
)
...
@@ -528,8 +531,8 @@ func CleanupChannelPollingLocks() {
...
@@ -528,8 +531,8 @@ func CleanupChannelPollingLocks() {
})
})
}
}
func
handlerMultiKeyUpdate
(
channel
*
Channel
,
usingKey
string
,
status
int
)
{
func
handlerMultiKeyUpdate
(
channel
*
Channel
,
usingKey
string
,
status
int
,
reason
string
)
{
keys
:=
channel
.
g
etKeys
()
keys
:=
channel
.
G
etKeys
()
if
len
(
keys
)
==
0
{
if
len
(
keys
)
==
0
{
channel
.
Status
=
status
channel
.
Status
=
status
}
else
{
}
else
{
...
@@ -547,6 +550,14 @@ func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) {
...
@@ -547,6 +550,14 @@ func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) {
delete
(
channel
.
ChannelInfo
.
MultiKeyStatusList
,
keyIndex
)
delete
(
channel
.
ChannelInfo
.
MultiKeyStatusList
,
keyIndex
)
}
else
{
}
else
{
channel
.
ChannelInfo
.
MultiKeyStatusList
[
keyIndex
]
=
status
channel
.
ChannelInfo
.
MultiKeyStatusList
[
keyIndex
]
=
status
if
channel
.
ChannelInfo
.
MultiKeyDisabledReason
==
nil
{
channel
.
ChannelInfo
.
MultiKeyDisabledReason
=
make
(
map
[
int
]
string
)
}
if
channel
.
ChannelInfo
.
MultiKeyDisabledTime
==
nil
{
channel
.
ChannelInfo
.
MultiKeyDisabledTime
=
make
(
map
[
int
]
int64
)
}
channel
.
ChannelInfo
.
MultiKeyDisabledReason
[
keyIndex
]
=
reason
channel
.
ChannelInfo
.
MultiKeyDisabledTime
[
keyIndex
]
=
common
.
GetTimestamp
()
}
}
if
len
(
channel
.
ChannelInfo
.
MultiKeyStatusList
)
>=
channel
.
ChannelInfo
.
MultiKeySize
{
if
len
(
channel
.
ChannelInfo
.
MultiKeyStatusList
)
>=
channel
.
ChannelInfo
.
MultiKeySize
{
channel
.
Status
=
common
.
ChannelStatusAutoDisabled
channel
.
Status
=
common
.
ChannelStatusAutoDisabled
...
@@ -569,7 +580,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
...
@@ -569,7 +580,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
}
}
if
channelCache
.
ChannelInfo
.
IsMultiKey
{
if
channelCache
.
ChannelInfo
.
IsMultiKey
{
// 如果是多Key模式,更新缓存中的状态
// 如果是多Key模式,更新缓存中的状态
handlerMultiKeyUpdate
(
channelCache
,
usingKey
,
status
)
handlerMultiKeyUpdate
(
channelCache
,
usingKey
,
status
,
reason
)
//CacheUpdateChannel(channelCache)
//CacheUpdateChannel(channelCache)
//return true
//return true
}
else
{
}
else
{
...
@@ -600,7 +611,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
...
@@ -600,7 +611,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
if
channel
.
ChannelInfo
.
IsMultiKey
{
if
channel
.
ChannelInfo
.
IsMultiKey
{
beforeStatus
:=
channel
.
Status
beforeStatus
:=
channel
.
Status
handlerMultiKeyUpdate
(
channel
,
usingKey
,
status
)
handlerMultiKeyUpdate
(
channel
,
usingKey
,
status
,
reason
)
if
beforeStatus
!=
channel
.
Status
{
if
beforeStatus
!=
channel
.
Status
{
shouldUpdateAbilities
=
true
shouldUpdateAbilities
=
true
}
}
...
...
model/channel_cache.go
View file @
aeec39a1
...
@@ -70,7 +70,7 @@ func InitChannelCache() {
...
@@ -70,7 +70,7 @@ func InitChannelCache() {
//channelsIDM = newChannelId2channel
//channelsIDM = newChannelId2channel
for
i
,
channel
:=
range
newChannelId2channel
{
for
i
,
channel
:=
range
newChannelId2channel
{
if
channel
.
ChannelInfo
.
IsMultiKey
{
if
channel
.
ChannelInfo
.
IsMultiKey
{
channel
.
Keys
=
channel
.
g
etKeys
()
channel
.
Keys
=
channel
.
G
etKeys
()
if
channel
.
ChannelInfo
.
MultiKeyMode
==
constant
.
MultiKeyModePolling
{
if
channel
.
ChannelInfo
.
MultiKeyMode
==
constant
.
MultiKeyModePolling
{
if
oldChannel
,
ok
:=
channelsIDM
[
i
];
ok
{
if
oldChannel
,
ok
:=
channelsIDM
[
i
];
ok
{
// 存在旧的渠道,如果是多key且轮询,保留轮询索引信息
// 存在旧的渠道,如果是多key且轮询,保留轮询索引信息
...
...
router/api-router.go
View file @
aeec39a1
...
@@ -120,6 +120,7 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -120,6 +120,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute
.
POST
(
"/batch/tag"
,
controller
.
BatchSetChannelTag
)
channelRoute
.
POST
(
"/batch/tag"
,
controller
.
BatchSetChannelTag
)
channelRoute
.
GET
(
"/tag/models"
,
controller
.
GetTagModels
)
channelRoute
.
GET
(
"/tag/models"
,
controller
.
GetTagModels
)
channelRoute
.
POST
(
"/copy/:id"
,
controller
.
CopyChannel
)
channelRoute
.
POST
(
"/copy/:id"
,
controller
.
CopyChannel
)
channelRoute
.
POST
(
"/multi_key/manage"
,
controller
.
ManageMultiKeys
)
}
}
tokenRoute
:=
apiRouter
.
Group
(
"/token"
)
tokenRoute
:=
apiRouter
.
Group
(
"/token"
)
tokenRoute
.
Use
(
middleware
.
UserAuth
())
tokenRoute
.
Use
(
middleware
.
UserAuth
())
...
...
web/src/components/table/channels/ChannelsColumnDefs.js
View file @
aeec39a1
...
@@ -210,7 +210,9 @@ export const getChannelsColumns = ({
...
@@ -210,7 +210,9 @@ export const getChannelsColumns = ({
copySelectedChannel
,
copySelectedChannel
,
refresh
,
refresh
,
activePage
,
activePage
,
channels
channels
,
setShowMultiKeyManageModal
,
setCurrentMultiKeyChannel
})
=>
{
})
=>
{
return
[
return
[
{
{
...
@@ -503,36 +505,50 @@ export const getChannelsColumns = ({
...
@@ -503,36 +505,50 @@ export const getChannelsColumns = ({
/
>
/
>
<
/SplitButtonGroup
>
<
/SplitButtonGroup
>
{
record
.
status
===
1
?
(
<
Button
type
=
'danger'
size
=
"small"
onClick
=
{()
=>
manageChannel
(
record
.
id
,
'disable'
,
record
)}
>
{
t
(
'禁用'
)}
<
/Button
>
)
:
(
<
Button
size
=
"small"
onClick
=
{()
=>
manageChannel
(
record
.
id
,
'enable'
,
record
)}
>
{
t
(
'启用'
)}
<
/Button
>
)
}
{
record
.
channel_info
?.
is_multi_key
?
(
{
record
.
channel_info
?.
is_multi_key
?
(
<
SplitButtonGroup
<
SplitButtonGroup
aria
-
label
=
{
t
(
'多密钥渠道操作项目组'
)}
aria
-
label
=
{
t
(
'多密钥渠道操作项目组'
)}
>
>
{
<
Button
record
.
status
===
1
?
(
type
=
'tertiary'
<
Button
size
=
"small"
type
=
'danger'
onClick
=
{()
=>
{
size
=
"small"
setEditingChannel
(
record
);
onClick
=
{()
=>
manageChannel
(
record
.
id
,
'disable'
,
record
)}
setShowEdit
(
true
);
>
}}
{
t
(
'禁用'
)}
>
<
/Button
>
{
t
(
'编辑'
)}
)
:
(
<
/Button
>
<
Button
size
=
"small"
onClick
=
{()
=>
manageChannel
(
record
.
id
,
'enable'
,
record
)}
>
{
t
(
'启用'
)}
<
/Button
>
)
}
<
Dropdown
<
Dropdown
trigger
=
'click'
trigger
=
'click'
position
=
'bottomRight'
position
=
'bottomRight'
menu
=
{[
menu
=
{[
{
{
node
:
'item'
,
node
:
'item'
,
name
:
t
(
'启用全部密钥'
),
name
:
t
(
'多key管理'
),
onClick
:
()
=>
manageChannel
(
record
.
id
,
'enable_all'
,
record
),
onClick
:
()
=>
{
setCurrentMultiKeyChannel
(
record
);
setShowMultiKeyManageModal
(
true
);
},
}
}
]}
]}
>
>
...
@@ -544,35 +560,18 @@ export const getChannelsColumns = ({
...
@@ -544,35 +560,18 @@ export const getChannelsColumns = ({
<
/Dropdown
>
<
/Dropdown
>
<
/SplitButtonGroup
>
<
/SplitButtonGroup
>
)
:
(
)
:
(
record
.
status
===
1
?
(
<
Button
<
Button
type
=
'tertiary'
type
=
'danger'
size
=
"small"
size
=
"small"
onClick
=
{()
=>
{
onClick
=
{()
=>
manageChannel
(
record
.
id
,
'disable'
,
record
)}
setEditingChannel
(
record
);
>
setShowEdit
(
true
);
{
t
(
'禁用'
)}
}}
<
/Button
>
>
)
:
(
{
t
(
'编辑'
)}
<
Button
<
/Button
>
size
=
"small"
onClick
=
{()
=>
manageChannel
(
record
.
id
,
'enable'
,
record
)}
>
{
t
(
'启用'
)}
<
/Button
>
)
)}
)}
<
Button
type
=
'tertiary'
size
=
"small"
onClick
=
{()
=>
{
setEditingChannel
(
record
);
setShowEdit
(
true
);
}}
>
{
t
(
'编辑'
)}
<
/Button
>
<
Dropdown
<
Dropdown
trigger
=
'click'
trigger
=
'click'
position
=
'bottomRight'
position
=
'bottomRight'
...
...
web/src/components/table/channels/ChannelsTable.jsx
View file @
aeec39a1
...
@@ -57,6 +57,9 @@ const ChannelsTable = (channelsData) => {
...
@@ -57,6 +57,9 @@ const ChannelsTable = (channelsData) => {
setEditingTag
,
setEditingTag
,
copySelectedChannel
,
copySelectedChannel
,
refresh
,
refresh
,
// Multi-key management
setShowMultiKeyManageModal
,
setCurrentMultiKeyChannel
,
}
=
channelsData
;
}
=
channelsData
;
// Get all columns
// Get all columns
...
@@ -79,6 +82,8 @@ const ChannelsTable = (channelsData) => {
...
@@ -79,6 +82,8 @@ const ChannelsTable = (channelsData) => {
refresh
,
refresh
,
activePage
,
activePage
,
channels
,
channels
,
setShowMultiKeyManageModal
,
setCurrentMultiKeyChannel
,
});
});
},
[
},
[
t
,
t
,
...
@@ -98,6 +103,8 @@ const ChannelsTable = (channelsData) => {
...
@@ -98,6 +103,8 @@ const ChannelsTable = (channelsData) => {
refresh
,
refresh
,
activePage
,
activePage
,
channels
,
channels
,
setShowMultiKeyManageModal
,
setCurrentMultiKeyChannel
,
]);
]);
// Filter columns based on visibility settings
// Filter columns based on visibility settings
...
...
web/src/components/table/channels/index.jsx
View file @
aeec39a1
...
@@ -30,6 +30,7 @@ import ModelTestModal from './modals/ModelTestModal.jsx';
...
@@ -30,6 +30,7 @@ import ModelTestModal from './modals/ModelTestModal.jsx';
import
ColumnSelectorModal
from
'./modals/ColumnSelectorModal.jsx'
;
import
ColumnSelectorModal
from
'./modals/ColumnSelectorModal.jsx'
;
import
EditChannelModal
from
'./modals/EditChannelModal.jsx'
;
import
EditChannelModal
from
'./modals/EditChannelModal.jsx'
;
import
EditTagModal
from
'./modals/EditTagModal.jsx'
;
import
EditTagModal
from
'./modals/EditTagModal.jsx'
;
import
MultiKeyManageModal
from
'./modals/MultiKeyManageModal.jsx'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
import
{
createCardProPagination
}
from
'../../../helpers/utils'
;
const
ChannelsPage
=
()
=>
{
const
ChannelsPage
=
()
=>
{
...
@@ -54,6 +55,12 @@ const ChannelsPage = () => {
...
@@ -54,6 +55,12 @@ const ChannelsPage = () => {
/>
/>
<
BatchTagModal
{
...
channelsData
}
/>
<
BatchTagModal
{
...
channelsData
}
/>
<
ModelTestModal
{
...
channelsData
}
/>
<
ModelTestModal
{
...
channelsData
}
/>
<
MultiKeyManageModal
visible=
{
channelsData
.
showMultiKeyManageModal
}
onCancel=
{
()
=>
channelsData
.
setShowMultiKeyManageModal
(
false
)
}
channel=
{
channelsData
.
currentMultiKeyChannel
}
onRefresh=
{
channelsData
.
refresh
}
/>
{
/* Main Content */
}
{
/* Main Content */
}
<
CardPro
<
CardPro
...
...
web/src/components/table/channels/modals/MultiKeyManageModal.jsx
0 → 100644
View file @
aeec39a1
/*
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
,
{
useState
,
useEffect
}
from
'react'
;
import
{
useTranslation
}
from
'react-i18next'
;
import
{
Modal
,
Button
,
Table
,
Tag
,
Typography
,
Space
,
Tooltip
,
Popconfirm
,
Empty
,
Spin
,
Banner
}
from
'@douyinfe/semi-ui'
;
import
{
IconRefresh
,
IconDelete
,
IconClose
,
IconSave
,
IconSetting
}
from
'@douyinfe/semi-icons'
;
import
{
API
,
showError
,
showSuccess
,
timestamp2string
}
from
'../../../../helpers/index.js'
;
const
{
Text
,
Title
}
=
Typography
;
const
MultiKeyManageModal
=
({
visible
,
onCancel
,
channel
,
onRefresh
})
=>
{
const
{
t
}
=
useTranslation
();
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
keyStatusList
,
setKeyStatusList
]
=
useState
([]);
const
[
operationLoading
,
setOperationLoading
]
=
useState
({});
// Load key status data
const
loadKeyStatus
=
async
()
=>
{
if
(
!
channel
?.
id
)
return
;
setLoading
(
true
);
try
{
const
res
=
await
API
.
post
(
'/api/channel/multi_key/manage'
,
{
channel_id
:
channel
.
id
,
action
:
'get_key_status'
});
if
(
res
.
data
.
success
)
{
setKeyStatusList
(
res
.
data
.
data
.
keys
||
[]);
}
else
{
showError
(
res
.
data
.
message
);
}
}
catch
(
error
)
{
showError
(
t
(
'获取密钥状态失败'
));
}
finally
{
setLoading
(
false
);
}
};
// Disable a specific key
const
handleDisableKey
=
async
(
keyIndex
)
=>
{
const
operationId
=
`disable_
${
keyIndex
}
`
;
setOperationLoading
(
prev
=>
({
...
prev
,
[
operationId
]:
true
}));
try
{
const
res
=
await
API
.
post
(
'/api/channel/multi_key/manage'
,
{
channel_id
:
channel
.
id
,
action
:
'disable_key'
,
key_index
:
keyIndex
});
if
(
res
.
data
.
success
)
{
showSuccess
(
t
(
'密钥已禁用'
));
await
loadKeyStatus
();
// Reload data
onRefresh
&&
onRefresh
();
// Refresh parent component
}
else
{
showError
(
res
.
data
.
message
);
}
}
catch
(
error
)
{
showError
(
t
(
'禁用密钥失败'
));
}
finally
{
setOperationLoading
(
prev
=>
({
...
prev
,
[
operationId
]:
false
}));
}
};
// Enable a specific key
const
handleEnableKey
=
async
(
keyIndex
)
=>
{
const
operationId
=
`enable_
${
keyIndex
}
`
;
setOperationLoading
(
prev
=>
({
...
prev
,
[
operationId
]:
true
}));
try
{
const
res
=
await
API
.
post
(
'/api/channel/multi_key/manage'
,
{
channel_id
:
channel
.
id
,
action
:
'enable_key'
,
key_index
:
keyIndex
});
if
(
res
.
data
.
success
)
{
showSuccess
(
t
(
'密钥已启用'
));
await
loadKeyStatus
();
// Reload data
onRefresh
&&
onRefresh
();
// Refresh parent component
}
else
{
showError
(
res
.
data
.
message
);
}
}
catch
(
error
)
{
showError
(
t
(
'启用密钥失败'
));
}
finally
{
setOperationLoading
(
prev
=>
({
...
prev
,
[
operationId
]:
false
}));
}
};
// Delete all disabled keys
const
handleDeleteDisabledKeys
=
async
()
=>
{
setOperationLoading
(
prev
=>
({
...
prev
,
delete_disabled
:
true
}));
try
{
const
res
=
await
API
.
post
(
'/api/channel/multi_key/manage'
,
{
channel_id
:
channel
.
id
,
action
:
'delete_disabled_keys'
});
if
(
res
.
data
.
success
)
{
showSuccess
(
res
.
data
.
message
);
await
loadKeyStatus
();
// Reload data
onRefresh
&&
onRefresh
();
// Refresh parent component
}
else
{
showError
(
res
.
data
.
message
);
}
}
catch
(
error
)
{
showError
(
t
(
'删除禁用密钥失败'
));
}
finally
{
setOperationLoading
(
prev
=>
({
...
prev
,
delete_disabled
:
false
}));
}
};
// Effect to load data when modal opens
useEffect
(()
=>
{
if
(
visible
&&
channel
?.
id
)
{
loadKeyStatus
();
}
},
[
visible
,
channel
?.
id
]);
// Get status tag component
const
renderStatusTag
=
(
status
)
=>
{
switch
(
status
)
{
case
1
:
return
<
Tag
color=
'green'
shape=
'circle'
>
{
t
(
'已启用'
)
}
</
Tag
>;
case
2
:
return
<
Tag
color=
'red'
shape=
'circle'
>
{
t
(
'已禁用'
)
}
</
Tag
>;
case
3
:
return
<
Tag
color=
'orange'
shape=
'circle'
>
{
t
(
'自动禁用'
)
}
</
Tag
>;
default
:
return
<
Tag
color=
'grey'
shape=
'circle'
>
{
t
(
'未知状态'
)
}
</
Tag
>;
}
};
// Table columns definition
const
columns
=
[
{
title
:
t
(
'索引'
),
dataIndex
:
'index'
,
render
:
(
text
)
=>
`#
${
text
}
`
,
},
{
title
:
t
(
'密钥预览'
),
dataIndex
:
'key_preview'
,
render
:
(
text
)
=>
(
<
Text
code
style=
{
{
fontSize
:
'12px'
}
}
>
{
text
}
</
Text
>
),
},
{
title
:
t
(
'状态'
),
dataIndex
:
'status'
,
width
:
100
,
render
:
(
status
)
=>
renderStatusTag
(
status
),
},
{
title
:
t
(
'禁用原因'
),
dataIndex
:
'reason'
,
width
:
220
,
render
:
(
reason
,
record
)
=>
{
if
(
record
.
status
===
1
||
!
reason
)
{
return
<
Text
type=
'quaternary'
>
-
</
Text
>;
}
return
(
<
Tooltip
content=
{
reason
}
>
<
Text
style=
{
{
maxWidth
:
'200px'
,
display
:
'block'
}
}
ellipsis
>
{
reason
}
</
Text
>
</
Tooltip
>
);
},
},
{
title
:
t
(
'禁用时间'
),
dataIndex
:
'disabled_time'
,
width
:
150
,
render
:
(
time
,
record
)
=>
{
if
(
record
.
status
===
1
||
!
time
)
{
return
<
Text
type=
'quaternary'
>
-
</
Text
>;
}
return
(
<
Tooltip
content=
{
timestamp2string
(
time
)
}
>
<
Text
style=
{
{
fontSize
:
'12px'
}
}
>
{
timestamp2string
(
time
)
}
</
Text
>
</
Tooltip
>
);
},
},
{
title
:
t
(
'操作'
),
key
:
'action'
,
width
:
120
,
render
:
(
_
,
record
)
=>
(
<
Space
>
{
record
.
status
===
1
?
(
<
Popconfirm
title=
{
t
(
'确定要禁用此密钥吗?'
)
}
content=
{
t
(
'禁用后该密钥将不再被使用'
)
}
onConfirm=
{
()
=>
handleDisableKey
(
record
.
index
)
}
>
<
Button
type=
'danger'
size=
'small'
loading=
{
operationLoading
[
`disable_${record.index}`
]
}
>
{
t
(
'禁用'
)
}
</
Button
>
</
Popconfirm
>
)
:
(
<
Popconfirm
title=
{
t
(
'确定要启用此密钥吗?'
)
}
content=
{
t
(
'启用后该密钥将重新被使用'
)
}
onConfirm=
{
()
=>
handleEnableKey
(
record
.
index
)
}
>
<
Button
type=
'primary'
size=
'small'
loading=
{
operationLoading
[
`enable_${record.index}`
]
}
>
{
t
(
'启用'
)
}
</
Button
>
</
Popconfirm
>
)
}
</
Space
>
),
},
];
// Calculate statistics
const
enabledCount
=
keyStatusList
.
filter
(
key
=>
key
.
status
===
1
).
length
;
const
manualDisabledCount
=
keyStatusList
.
filter
(
key
=>
key
.
status
===
2
).
length
;
const
autoDisabledCount
=
keyStatusList
.
filter
(
key
=>
key
.
status
===
3
).
length
;
const
totalCount
=
keyStatusList
.
length
;
return
(
<
Modal
title=
{
<
Space
>
<
IconSetting
/>
<
span
>
{
t
(
'多密钥管理'
)
}
-
{
channel
?.
name
}
</
span
>
</
Space
>
}
visible=
{
visible
}
onCancel=
{
onCancel
}
width=
{
800
}
height=
{
600
}
footer=
{
<
Space
>
<
Button
onClick=
{
onCancel
}
>
{
t
(
'关闭'
)
}
</
Button
>
<
Button
icon=
{
<
IconRefresh
/>
}
onClick=
{
loadKeyStatus
}
loading=
{
loading
}
>
{
t
(
'刷新'
)
}
</
Button
>
{
autoDisabledCount
>
0
&&
(
<
Popconfirm
title=
{
t
(
'确定要删除所有已自动禁用的密钥吗?'
)
}
content=
{
t
(
'此操作不可撤销,将永久删除已自动禁用的密钥'
)
}
onConfirm=
{
handleDeleteDisabledKeys
}
>
<
Button
type=
'danger'
icon=
{
<
IconDelete
/>
}
loading=
{
operationLoading
.
delete_disabled
}
>
{
t
(
'删除自动禁用密钥'
)
}
</
Button
>
</
Popconfirm
>
)
}
</
Space
>
}
>
<
div
style=
{
{
padding
:
'16px 0'
}
}
>
{
/* Statistics Banner */
}
<
Banner
type=
'info'
style=
{
{
marginBottom
:
'16px'
}
}
description=
{
<
div
>
<
Text
>
{
t
(
'总共
{{
total
}}
个密钥,
{{
enabled
}}
个已启用,
{{
manual
}}
个手动禁用,
{{
auto
}}
个自动禁用'
,
{
total
:
totalCount
,
enabled
:
enabledCount
,
manual
:
manualDisabledCount
,
auto
:
autoDisabledCount
})
}
</
Text
>
{
channel
?.
channel_info
?.
multi_key_mode
&&
(
<
div
style=
{
{
marginTop
:
'4px'
}
}
>
<
Text
type=
'quaternary'
style=
{
{
fontSize
:
'12px'
}
}
>
{
t
(
'多密钥模式'
)
}
:
{
channel
.
channel_info
.
multi_key_mode
===
'random'
?
t
(
'随机'
)
:
t
(
'轮询'
)
}
</
Text
>
</
div
>
)
}
</
div
>
}
/>
{
/* Key Status Table */
}
<
Spin
spinning=
{
loading
}
>
{
keyStatusList
.
length
>
0
?
(
<
Table
columns=
{
columns
}
dataSource=
{
keyStatusList
}
pagination=
{
false
}
size=
'small'
bordered
rowKey=
'index'
style=
{
{
maxHeight
:
'400px'
,
overflow
:
'auto'
}
}
/>
)
:
(
!
loading
&&
(
<
Empty
image=
{
Empty
.
PRESENTED_IMAGE_SIMPLE
}
title=
{
t
(
'暂无密钥数据'
)
}
description=
{
t
(
'请检查渠道配置或刷新重试'
)
}
/>
)
)
}
</
Spin
>
</
div
>
</
Modal
>
);
};
export
default
MultiKeyManageModal
;
\ No newline at end of file
web/src/hooks/channels/useChannelsData.js
View file @
aeec39a1
...
@@ -83,6 +83,10 @@ export const useChannelsData = () => {
...
@@ -83,6 +83,10 @@ export const useChannelsData = () => {
const
[
isProcessingQueue
,
setIsProcessingQueue
]
=
useState
(
false
);
const
[
isProcessingQueue
,
setIsProcessingQueue
]
=
useState
(
false
);
const
[
modelTablePage
,
setModelTablePage
]
=
useState
(
1
);
const
[
modelTablePage
,
setModelTablePage
]
=
useState
(
1
);
// Multi-key management states
const
[
showMultiKeyManageModal
,
setShowMultiKeyManageModal
]
=
useState
(
false
);
const
[
currentMultiKeyChannel
,
setCurrentMultiKeyChannel
]
=
useState
(
null
);
// Refs
// Refs
const
requestCounter
=
useRef
(
0
);
const
requestCounter
=
useRef
(
0
);
const
allSelectingRef
=
useRef
(
false
);
const
allSelectingRef
=
useRef
(
false
);
...
@@ -885,6 +889,12 @@ export const useChannelsData = () => {
...
@@ -885,6 +889,12 @@ export const useChannelsData = () => {
setModelTablePage
,
setModelTablePage
,
allSelectingRef
,
allSelectingRef
,
// Multi-key management states
showMultiKeyManageModal
,
setShowMultiKeyManageModal
,
currentMultiKeyChannel
,
setCurrentMultiKeyChannel
,
// Form
// Form
formApi
,
formApi
,
setFormApi
,
setFormApi
,
...
...
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