diff --git a/controller/channel.go b/controller/channel.go
index d9e4d422..a2ee5743 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -1030,3 +1030,261 @@ func CopyChannel(c *gin.Context) {
// success
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
+ }
+}
diff --git a/model/channel.go b/model/channel.go
index bcffc102..502171fa 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -41,6 +41,7 @@ type Channel struct {
Priority *int64 `json:"priority" gorm:"bigint;default:0"`
AutoBan *int `json:"auto_ban" gorm:"default:1"`
OtherInfo string `json:"other_info"`
+ Settings string `json:"settings"`
Tag *string `json:"tag" gorm:"index"`
Setting *string `json:"setting" gorm:"type:text"` // 渠道额外设置
ParamOverride *string `json:"param_override" gorm:"type:text"`
@@ -52,11 +53,13 @@ type Channel struct {
}
type ChannelInfo struct {
- IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式
- MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量
- MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status
- MultiKeyPollingIndex int `json:"multi_key_polling_index"` // 多Key模式下轮询的key索引
- MultiKeyMode constant.MultiKeyMode `json:"multi_key_mode"`
+ IsMultiKey bool `json:"is_multi_key"` // 是否多Key模式
+ MultiKeySize int `json:"multi_key_size"` // 多Key模式下的Key数量
+ MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status
+ MultiKeyDisabledReason map[int]string `json:"multi_key_disabled_reason"` // key禁用原因列表,key index -> reason
+ 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
@@ -70,7 +73,7 @@ func (c *ChannelInfo) Scan(value interface{}) error {
return common.Unmarshal(bytesValue, c)
}
-func (channel *Channel) getKeys() []string {
+func (channel *Channel) GetKeys() []string {
if channel.Key == "" {
return []string{}
}
@@ -101,7 +104,7 @@ func (channel *Channel) GetNextEnabledKey() (string, int, *types.NewAPIError) {
}
// Obtain all keys (split by \n)
- keys := channel.getKeys()
+ keys := channel.GetKeys()
if len(keys) == 0 {
// No keys available, return error, should disable the channel
return "", 0, types.NewError(errors.New("no keys available"), types.ErrorCodeChannelNoAvailableKey)
@@ -528,8 +531,8 @@ func CleanupChannelPollingLocks() {
})
}
-func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) {
- keys := channel.getKeys()
+func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int, reason string) {
+ keys := channel.GetKeys()
if len(keys) == 0 {
channel.Status = status
} else {
@@ -547,6 +550,14 @@ func handlerMultiKeyUpdate(channel *Channel, usingKey string, status int) {
delete(channel.ChannelInfo.MultiKeyStatusList, keyIndex)
} else {
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 {
channel.Status = common.ChannelStatusAutoDisabled
@@ -569,7 +580,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
}
if channelCache.ChannelInfo.IsMultiKey {
// 如果是多Key模式,更新缓存中的状态
- handlerMultiKeyUpdate(channelCache, usingKey, status)
+ handlerMultiKeyUpdate(channelCache, usingKey, status, reason)
//CacheUpdateChannel(channelCache)
//return true
} else {
@@ -600,7 +611,7 @@ func UpdateChannelStatus(channelId int, usingKey string, status int, reason stri
if channel.ChannelInfo.IsMultiKey {
beforeStatus := channel.Status
- handlerMultiKeyUpdate(channel, usingKey, status)
+ handlerMultiKeyUpdate(channel, usingKey, status, reason)
if beforeStatus != channel.Status {
shouldUpdateAbilities = true
}
diff --git a/model/channel_cache.go b/model/channel_cache.go
index ecd87607..6ca23cf9 100644
--- a/model/channel_cache.go
+++ b/model/channel_cache.go
@@ -70,7 +70,7 @@ func InitChannelCache() {
//channelsIDM = newChannelId2channel
for i, channel := range newChannelId2channel {
if channel.ChannelInfo.IsMultiKey {
- channel.Keys = channel.getKeys()
+ channel.Keys = channel.GetKeys()
if channel.ChannelInfo.MultiKeyMode == constant.MultiKeyModePolling {
if oldChannel, ok := channelsIDM[i]; ok {
// 存在旧的渠道,如果是多key且轮询,保留轮询索引信息
diff --git a/router/api-router.go b/router/api-router.go
index bc49803a..12846012 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -120,6 +120,7 @@ func SetApiRouter(router *gin.Engine) {
channelRoute.POST("/batch/tag", controller.BatchSetChannelTag)
channelRoute.GET("/tag/models", controller.GetTagModels)
channelRoute.POST("/copy/:id", controller.CopyChannel)
+ channelRoute.POST("/multi_key/manage", controller.ManageMultiKeys)
}
tokenRoute := apiRouter.Group("/token")
tokenRoute.Use(middleware.UserAuth())
diff --git a/web/src/components/table/channels/ChannelsColumnDefs.js b/web/src/components/table/channels/ChannelsColumnDefs.js
index beb5fe55..18cb5700 100644
--- a/web/src/components/table/channels/ChannelsColumnDefs.js
+++ b/web/src/components/table/channels/ChannelsColumnDefs.js
@@ -210,7 +210,9 @@ export const getChannelsColumns = ({
copySelectedChannel,
refresh,
activePage,
- channels
+ channels,
+ setShowMultiKeyManageModal,
+ setCurrentMultiKeyChannel
}) => {
return [
{
@@ -503,47 +505,7 @@ export const getChannelsColumns = ({
/>
- {record.channel_info?.is_multi_key ? (
-