diff --git a/controller/channel.go b/controller/channel.go
index d9e4d422..7756e18f 100644
--- a/controller/channel.go
+++ b/controller/channel.go
@@ -71,6 +71,13 @@ func parseStatusFilter(statusParam string) int {
}
}
+func clearChannelInfo(channel *model.Channel) {
+ if channel.ChannelInfo.IsMultiKey {
+ channel.ChannelInfo.MultiKeyDisabledReason = nil
+ channel.ChannelInfo.MultiKeyDisabledTime = nil
+ }
+}
+
func GetAllChannels(c *gin.Context) {
pageInfo := common.GetPageQuery(c)
channelData := make([]*model.Channel, 0)
@@ -145,6 +152,10 @@ func GetAllChannels(c *gin.Context) {
}
}
+ for _, datum := range channelData {
+ clearChannelInfo(datum)
+ }
+
countQuery := model.DB.Model(&model.Channel{})
if statusFilter == common.ChannelStatusEnabled {
countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
@@ -371,6 +382,10 @@ func SearchChannels(c *gin.Context) {
pagedData := channelData[startIdx:endIdx]
+ for _, datum := range pagedData {
+ clearChannelInfo(datum)
+ }
+
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -394,6 +409,9 @@ func GetChannel(c *gin.Context) {
common.ApiError(c, err)
return
}
+ if channel != nil {
+ clearChannelInfo(channel)
+ }
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -827,6 +845,7 @@ func UpdateChannel(c *gin.Context) {
}
model.InitChannelCache()
channel.Key = ""
+ clearChannelInfo(&channel.Channel)
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -1030,3 +1049,409 @@ 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
+ Page int `json:"page,omitempty"` // for get_key_status pagination
+ PageSize int `json:"page_size,omitempty"` // for get_key_status pagination
+ Status *int `json:"status,omitempty"` // for get_key_status filtering: 1=enabled, 2=manual_disabled, 3=auto_disabled, nil=all
+}
+
+// MultiKeyStatusResponse represents the response for key status query
+type MultiKeyStatusResponse struct {
+ Keys []KeyStatus `json:"keys"`
+ Total int `json:"total"`
+ Page int `json:"page"`
+ PageSize int `json:"page_size"`
+ TotalPages int `json:"total_pages"`
+ // Statistics
+ EnabledCount int `json:"enabled_count"`
+ ManualDisabledCount int `json:"manual_disabled_count"`
+ AutoDisabledCount int `json:"auto_disabled_count"`
+}
+
+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()
+
+ // Default pagination parameters
+ page := request.Page
+ pageSize := request.PageSize
+ if page <= 0 {
+ page = 1
+ }
+ if pageSize <= 0 {
+ pageSize = 50 // Default page size
+ }
+
+ // Statistics for all keys (unchanged by filtering)
+ var enabledCount, manualDisabledCount, autoDisabledCount int
+
+ // Build all key status data first
+ var allKeyStatusList []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
+ }
+ }
+
+ // Count for statistics (all keys)
+ switch status {
+ case 1:
+ enabledCount++
+ case 2:
+ manualDisabledCount++
+ case 3:
+ autoDisabledCount++
+ }
+
+ 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] + "..."
+ }
+
+ allKeyStatusList = append(allKeyStatusList, KeyStatus{
+ Index: i,
+ Status: status,
+ DisabledTime: disabledTime,
+ Reason: reason,
+ KeyPreview: keyPreview,
+ })
+ }
+
+ // Apply status filter if specified
+ var filteredKeyStatusList []KeyStatus
+ if request.Status != nil {
+ for _, keyStatus := range allKeyStatusList {
+ if keyStatus.Status == *request.Status {
+ filteredKeyStatusList = append(filteredKeyStatusList, keyStatus)
+ }
+ }
+ } else {
+ filteredKeyStatusList = allKeyStatusList
+ }
+
+ // Calculate pagination based on filtered results
+ filteredTotal := len(filteredKeyStatusList)
+ totalPages := (filteredTotal + pageSize - 1) / pageSize
+ if totalPages == 0 {
+ totalPages = 1
+ }
+ if page > totalPages {
+ page = totalPages
+ }
+
+ // Calculate range for current page
+ start := (page - 1) * pageSize
+ end := start + pageSize
+ if end > filteredTotal {
+ end = filteredTotal
+ }
+
+ // Get the page data
+ var pageKeyStatusList []KeyStatus
+ if start < filteredTotal {
+ pageKeyStatusList = filteredKeyStatusList[start:end]
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "success": true,
+ "message": "",
+ "data": MultiKeyStatusResponse{
+ Keys: pageKeyStatusList,
+ Total: filteredTotal, // Total of filtered results
+ Page: page,
+ PageSize: pageSize,
+ TotalPages: totalPages,
+ EnabledCount: enabledCount, // Overall statistics
+ ManualDisabledCount: manualDisabledCount, // Overall statistics
+ AutoDisabledCount: autoDisabledCount, // Overall statistics
+ },
+ })
+ 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
+
+ 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 "enable_all_keys":
+ // 清空所有禁用状态,使所有密钥回到默认启用状态
+ var enabledCount int
+ if channel.ChannelInfo.MultiKeyStatusList != nil {
+ enabledCount = len(channel.ChannelInfo.MultiKeyStatusList)
+ }
+
+ channel.ChannelInfo.MultiKeyStatusList = make(map[int]int)
+ channel.ChannelInfo.MultiKeyDisabledTime = make(map[int]int64)
+ channel.ChannelInfo.MultiKeyDisabledReason = make(map[int]string)
+
+ 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 个密钥", enabledCount),
+ })
+ return
+
+ case "disable_all_keys":
+ // 禁用所有启用的密钥
+ 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)
+ }
+
+ var disabledCount int
+ for i := 0; i < channel.ChannelInfo.MultiKeySize; i++ {
+ status := 1 // default enabled
+ if s, exists := channel.ChannelInfo.MultiKeyStatusList[i]; exists {
+ status = s
+ }
+
+ // 只禁用当前启用的密钥
+ if status == 1 {
+ channel.ChannelInfo.MultiKeyStatusList[i] = 2 // disabled
+ disabledCount++
+ }
+ }
+
+ if disabledCount == 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "没有可禁用的密钥",
+ })
+ return
+ }
+
+ 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 个密钥", disabledCount),
+ })
+ 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..280781f1 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,omitempty"` // key禁用原因列表,key index -> reason
+ MultiKeyDisabledTime map[int]int64 `json:"multi_key_disabled_time,omitempty"` // 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 ? (
-