Browse Source

Merge pull request #1498 from QuantumNous/multi-key-manage

feat: add multi-key management
Calcium-Ion 7 months ago
parent
commit
c9d4cdc57e

+ 425 - 0
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) {
 func GetAllChannels(c *gin.Context) {
 	pageInfo := common.GetPageQuery(c)
 	pageInfo := common.GetPageQuery(c)
 	channelData := make([]*model.Channel, 0)
 	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{})
 	countQuery := model.DB.Model(&model.Channel{})
 	if statusFilter == common.ChannelStatusEnabled {
 	if statusFilter == common.ChannelStatusEnabled {
 		countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
 		countQuery = countQuery.Where("status = ?", common.ChannelStatusEnabled)
@@ -371,6 +382,10 @@ func SearchChannels(c *gin.Context) {
 
 
 	pagedData := channelData[startIdx:endIdx]
 	pagedData := channelData[startIdx:endIdx]
 
 
+	for _, datum := range pagedData {
+		clearChannelInfo(datum)
+	}
+
 	c.JSON(http.StatusOK, gin.H{
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"success": true,
 		"message": "",
 		"message": "",
@@ -394,6 +409,9 @@ func GetChannel(c *gin.Context) {
 		common.ApiError(c, err)
 		common.ApiError(c, err)
 		return
 		return
 	}
 	}
+	if channel != nil {
+		clearChannelInfo(channel)
+	}
 	c.JSON(http.StatusOK, gin.H{
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"success": true,
 		"message": "",
 		"message": "",
@@ -827,6 +845,7 @@ func UpdateChannel(c *gin.Context) {
 	}
 	}
 	model.InitChannelCache()
 	model.InitChannelCache()
 	channel.Key = ""
 	channel.Key = ""
+	clearChannelInfo(&channel.Channel)
 	c.JSON(http.StatusOK, gin.H{
 	c.JSON(http.StatusOK, gin.H{
 		"success": true,
 		"success": true,
 		"message": "",
 		"message": "",
@@ -1030,3 +1049,409 @@ 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
+	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
+	}
+}

+ 22 - 11
model/channel.go

@@ -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 {
 }
 }
 
 
 type ChannelInfo 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
 // Value implements driver.Valuer interface
@@ -70,7 +73,7 @@ func (c *ChannelInfo) Scan(value interface{}) error {
 	return common.Unmarshal(bytesValue, c)
 	return common.Unmarshal(bytesValue, c)
 }
 }
 
 
-func (channel *Channel) getKeys() []string {
+func (channel *Channel) GetKeys() []string {
 	if channel.Key == "" {
 	if channel.Key == "" {
 		return []string{}
 		return []string{}
 	}
 	}
@@ -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.getKeys()
+	keys := channel.GetKeys()
 	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() {
 	})
 	})
 }
 }
 
 
-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 {
 	if len(keys) == 0 {
 		channel.Status = status
 		channel.Status = status
 	} else {
 	} else {
@@ -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
 		}
 		}
 		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
 
 
 		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
 			}
 			}

+ 1 - 1
model/channel_cache.go

@@ -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.getKeys()
+			channel.Keys = channel.GetKeys()
 			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且轮询,保留轮询索引信息

+ 1 - 0
router/api-router.go

@@ -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())

+ 47 - 48
web/src/components/table/channels/ChannelsColumnDefs.js

@@ -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 = ({
                 />
                 />
               </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('多密钥渠道操作项目组')}
                 >
                 >
-                  {
-                    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>
-                    )
-                  }
+                  <Button
+                    type='tertiary'
+                    size="small"
+                    onClick={() => {
+                      setEditingChannel(record);
+                      setShowEdit(true);
+                    }}
+                  >
+                    {t('编辑')}
+                  </Button>
                   <Dropdown
                   <Dropdown
                     trigger='click'
                     trigger='click'
                     position='bottomRight'
                     position='bottomRight'
                     menu={[
                     menu={[
                       {
                       {
                         node: 'item',
                         node: 'item',
-                        name: t('启用全部密钥'),
-                        onClick: () => manageChannel(record.id, 'enable_all', record),
+                        name: t('多key管理'),
+                        onClick: () => {
+                          setCurrentMultiKeyChannel(record);
+                          setShowMultiKeyManageModal(true);
+                        },
                       }
                       }
                     ]}
                     ]}
                   >
                   >
@@ -544,35 +560,18 @@ export const getChannelsColumns = ({
                   </Dropdown>
                   </Dropdown>
                 </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>
-                )
+                <Button
+                  type='tertiary'
+                  size="small"
+                  onClick={() => {
+                    setEditingChannel(record);
+                    setShowEdit(true);
+                  }}
+                >
+                  {t('编辑')}
+                </Button>
               )}
               )}
 
 
-              <Button
-                type='tertiary'
-                size="small"
-                onClick={() => {
-                  setEditingChannel(record);
-                  setShowEdit(true);
-                }}
-              >
-                {t('编辑')}
-              </Button>
-
               <Dropdown
               <Dropdown
                 trigger='click'
                 trigger='click'
                 position='bottomRight'
                 position='bottomRight'

+ 7 - 0
web/src/components/table/channels/ChannelsTable.jsx

@@ -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) => {
       refresh,
       refresh,
       activePage,
       activePage,
       channels,
       channels,
+      setShowMultiKeyManageModal,
+      setCurrentMultiKeyChannel,
     });
     });
   }, [
   }, [
     t,
     t,
@@ -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

+ 7 - 0
web/src/components/table/channels/index.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 = () => {
       />
       />
       <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

+ 578 - 0
web/src/components/table/channels/modals/MultiKeyManageModal.jsx

@@ -0,0 +1,578 @@
+/*
+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,
+  Select,
+  Pagination
+} 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({});
+  
+  // Pagination states
+  const [currentPage, setCurrentPage] = useState(1);
+  const [pageSize, setPageSize] = useState(50);
+  const [total, setTotal] = useState(0);
+  const [totalPages, setTotalPages] = useState(0);
+  
+  // Statistics states
+  const [enabledCount, setEnabledCount] = useState(0);
+  const [manualDisabledCount, setManualDisabledCount] = useState(0);
+  const [autoDisabledCount, setAutoDisabledCount] = useState(0);
+
+  // Filter states
+  const [statusFilter, setStatusFilter] = useState(null); // null=all, 1=enabled, 2=manual_disabled, 3=auto_disabled
+
+  // Load key status data
+  const loadKeyStatus = async (page = currentPage, size = pageSize, status = statusFilter) => {
+    if (!channel?.id) return;
+    
+    setLoading(true);
+    try {
+      const requestData = {
+        channel_id: channel.id,
+        action: 'get_key_status',
+        page: page,
+        page_size: size
+      };
+      
+      // Add status filter if specified
+      if (status !== null) {
+        requestData.status = status;
+      }
+      
+      const res = await API.post('/api/channel/multi_key/manage', requestData);
+      
+      if (res.data.success) {
+        const data = res.data.data;
+        setKeyStatusList(data.keys || []);
+        setTotal(data.total || 0);
+        setCurrentPage(data.page || 1);
+        setPageSize(data.page_size || 50);
+        setTotalPages(data.total_pages || 0);
+        
+        // Update statistics (these are always the overall statistics)
+        setEnabledCount(data.enabled_count || 0);
+        setManualDisabledCount(data.manual_disabled_count || 0);
+        setAutoDisabledCount(data.auto_disabled_count || 0);
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      console.error(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(currentPage, pageSize); // Reload current page
+        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(currentPage, pageSize); // Reload current page
+        onRefresh && onRefresh(); // Refresh parent component
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(t('启用密钥失败'));
+    } finally {
+      setOperationLoading(prev => ({ ...prev, [operationId]: false }));
+    }
+  };
+
+  // Enable all disabled keys
+  const handleEnableAll = async () => {
+    setOperationLoading(prev => ({ ...prev, enable_all: true }));
+    
+    try {
+      const res = await API.post('/api/channel/multi_key/manage', {
+        channel_id: channel.id,
+        action: 'enable_all_keys'
+      });
+      
+      if (res.data.success) {
+        showSuccess(res.data.message || t('已启用所有密钥'));
+        // Reset to first page after bulk operation
+        setCurrentPage(1);
+        await loadKeyStatus(1, pageSize);
+        onRefresh && onRefresh(); // Refresh parent component
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(t('启用所有密钥失败'));
+    } finally {
+      setOperationLoading(prev => ({ ...prev, enable_all: false }));
+    }
+  };
+
+  // Disable all enabled keys
+  const handleDisableAll = async () => {
+    setOperationLoading(prev => ({ ...prev, disable_all: true }));
+    
+    try {
+      const res = await API.post('/api/channel/multi_key/manage', {
+        channel_id: channel.id,
+        action: 'disable_all_keys'
+      });
+      
+      if (res.data.success) {
+        showSuccess(res.data.message || t('已禁用所有密钥'));
+        // Reset to first page after bulk operation
+        setCurrentPage(1);
+        await loadKeyStatus(1, pageSize);
+        onRefresh && onRefresh(); // Refresh parent component
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(t('禁用所有密钥失败'));
+    } finally {
+      setOperationLoading(prev => ({ ...prev, disable_all: 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);
+        // Reset to first page after deletion as data structure might change
+        setCurrentPage(1);
+        await loadKeyStatus(1, pageSize);
+        onRefresh && onRefresh(); // Refresh parent component
+      } else {
+        showError(res.data.message);
+      }
+    } catch (error) {
+      showError(t('删除禁用密钥失败'));
+    } finally {
+      setOperationLoading(prev => ({ ...prev, delete_disabled: false }));
+    }
+  };
+
+  // Handle page change
+  const handlePageChange = (page) => {
+    setCurrentPage(page);
+    loadKeyStatus(page, pageSize);
+  };
+
+  // Handle page size change  
+  const handlePageSizeChange = (size) => {
+    setPageSize(size);
+    setCurrentPage(1); // Reset to first page
+    loadKeyStatus(1, size);
+  };
+
+  // Handle status filter change
+  const handleStatusFilterChange = (status) => {
+    setStatusFilter(status);
+    setCurrentPage(1); // Reset to first page when filter changes
+    loadKeyStatus(1, pageSize, status);
+  };
+
+  // Effect to load data when modal opens
+  useEffect(() => {
+    if (visible && channel?.id) {
+      setCurrentPage(1); // Reset to first page when opening
+      loadKeyStatus(1, pageSize);
+    }
+  }, [visible, channel?.id]);
+
+  // Reset pagination when modal closes
+  useEffect(() => {
+    if (!visible) {
+      setCurrentPage(1);
+      setKeyStatusList([]);
+      setTotal(0);
+      setTotalPages(0);
+      setEnabledCount(0);
+      setManualDisabledCount(0);
+      setAutoDisabledCount(0);
+      setStatusFilter(null); // Reset filter
+    }
+  }, [visible]);
+
+  // 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 ? (
+            <Button
+              type='danger'
+              size='small'
+              loading={operationLoading[`disable_${record.index}`]}
+              onClick={() => handleDisableKey(record.index)}
+            >
+              {t('禁用')}
+            </Button>
+          ) : (
+            <Button
+              type='primary'
+              size='small'
+              loading={operationLoading[`enable_${record.index}`]}
+              onClick={() => handleEnableKey(record.index)}
+            >
+              {t('启用')}
+            </Button>
+          )}
+        </Space>
+      ),
+    },
+  ];
+
+  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(currentPage, pageSize)}
+            loading={loading}
+          >
+            {t('刷新')}
+          </Button>
+          <Popconfirm
+            title={t('确定要启用所有密钥吗?')}
+            onConfirm={handleEnableAll}
+            position={'topRight'}
+          >
+            <Button
+              type='primary'
+              loading={operationLoading.enable_all}
+            >
+              {t('启用全部')}
+            </Button>
+          </Popconfirm>
+          {enabledCount > 0 && (
+            <Popconfirm
+              title={t('确定要禁用所有的密钥吗?')}
+              onConfirm={handleDisableAll}
+              okType={'danger'}
+              position={'topRight'}
+            >
+              <Button
+                type='danger'
+                loading={operationLoading.disable_all}
+              >
+                {t('禁用全部')}
+              </Button>
+            </Popconfirm>
+          )}
+          <Popconfirm
+            title={t('确定要删除所有已自动禁用的密钥吗?')}
+            content={t('此操作不可撤销,将永久删除已自动禁用的密钥')}
+            onConfirm={handleDeleteDisabledKeys}
+            okType={'danger'}
+            position={'topRight'}
+          >
+            <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: total,
+                  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>
+          }
+        />
+
+        {/* Filter Controls */}
+        <div style={{ marginBottom: '16px', display: 'flex', alignItems: 'center', gap: '12px' }}>
+          <Text style={{ fontSize: '14px', fontWeight: '500' }}>{t('状态筛选')}:</Text>
+          <Select
+            value={statusFilter}
+            onChange={handleStatusFilterChange}
+            style={{ width: '120px' }}
+            size='small'
+            placeholder={t('全部状态')}
+          >
+            <Select.Option value={null}>{t('全部状态')}</Select.Option>
+            <Select.Option value={1}>{t('已启用')}</Select.Option>
+            <Select.Option value={2}>{t('手动禁用')}</Select.Option>
+            <Select.Option value={3}>{t('自动禁用')}</Select.Option>
+          </Select>
+          {statusFilter !== null && (
+            <Text type='quaternary' style={{ fontSize: '12px' }}>
+              {t('当前显示 {{count}} 条筛选结果', { count: total })}
+            </Text>
+          )}
+        </div>
+
+        {/* Key Status Table */}
+        <Spin spinning={loading}>
+          {keyStatusList.length > 0 ? (
+            <>
+              <Table
+                columns={columns}
+                dataSource={keyStatusList}
+                pagination={false}
+                size='small'
+                bordered
+                rowKey='index'
+                style={{ marginBottom: '16px' }}
+              />
+              
+              {/* Pagination */}
+              {total > 0 && (
+                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+                  <Text type='quaternary' style={{ fontSize: '12px' }}>
+                    {t('显示第 {{start}}-{{end}} 条,共 {{total}} 条', {
+                      start: (currentPage - 1) * pageSize + 1,
+                      end: Math.min(currentPage * pageSize, total),
+                      total: total
+                    })}
+                  </Text>
+                  
+                  <div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
+                    <Text type='quaternary' style={{ fontSize: '12px' }}>
+                      {t('每页显示')}:
+                    </Text>
+                    <Select
+                      value={pageSize}
+                      onChange={handlePageSizeChange}
+                      size='small'
+                      style={{ width: '80px' }}
+                    >
+                      <Select.Option value={50}>50</Select.Option>
+                      <Select.Option value={100}>100</Select.Option>
+                      <Select.Option value={500}>500</Select.Option>
+                      <Select.Option value={1000}>1000</Select.Option>
+                    </Select>
+                    
+                    <Pagination
+                      current={currentPage}
+                      total={total}
+                      pageSize={pageSize}
+                      showSizeChanger={false}
+                      showQuickJumper
+                      size='small'
+                      onChange={handlePageChange}
+                      showTotal={(total, range) => 
+                        t('第 {{current}} / {{total}} 页', {
+                          current: currentPage,
+                          total: totalPages
+                        })
+                      }
+                    />
+                  </div>
+                </div>
+              )}
+            </>
+          ) : (
+            !loading && (
+              <Empty
+                image={Empty.PRESENTED_IMAGE_SIMPLE}
+                title={t('暂无密钥数据')}
+                description={t('请检查渠道配置或刷新重试')}
+              />
+            )
+          )}
+        </Spin>
+      </div>
+    </Modal>
+  );
+};
+
+export default MultiKeyManageModal; 

+ 10 - 0
web/src/hooks/channels/useChannelsData.js

@@ -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 = () => {
     setModelTablePage,
     setModelTablePage,
     allSelectingRef,
     allSelectingRef,
 
 
+    // Multi-key management states
+    showMultiKeyManageModal,
+    setShowMultiKeyManageModal,
+    currentMultiKeyChannel,
+    setCurrentMultiKeyChannel,
+
     // Form
     // Form
     formApi,
     formApi,
     setFormApi,
     setFormApi,