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 ? ( - - { - record.status === 1 ? ( - - ) : ( - - ) - } - manageChannel(record.id, 'enable_all', record), - } - ]} - > - + {record.channel_info?.is_multi_key ? ( + + + { + setCurrentMultiKeyChannel(record); + setShowMultiKeyManageModal(true); + }, + } + ]} + > + + )} { setEditingTag, copySelectedChannel, refresh, + // Multi-key management + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel, } = channelsData; // Get all columns @@ -79,6 +82,8 @@ const ChannelsTable = (channelsData) => { refresh, activePage, channels, + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel, }); }, [ t, @@ -98,6 +103,8 @@ const ChannelsTable = (channelsData) => { refresh, activePage, channels, + setShowMultiKeyManageModal, + setCurrentMultiKeyChannel, ]); // Filter columns based on visibility settings diff --git a/web/src/components/table/channels/index.jsx b/web/src/components/table/channels/index.jsx index b0106b4e..66e2d72d 100644 --- a/web/src/components/table/channels/index.jsx +++ b/web/src/components/table/channels/index.jsx @@ -30,6 +30,7 @@ import ModelTestModal from './modals/ModelTestModal.jsx'; import ColumnSelectorModal from './modals/ColumnSelectorModal.jsx'; import EditChannelModal from './modals/EditChannelModal.jsx'; import EditTagModal from './modals/EditTagModal.jsx'; +import MultiKeyManageModal from './modals/MultiKeyManageModal.jsx'; import { createCardProPagination } from '../../../helpers/utils'; const ChannelsPage = () => { @@ -54,6 +55,12 @@ const ChannelsPage = () => { /> + channelsData.setShowMultiKeyManageModal(false)} + channel={channelsData.currentMultiKeyChannel} + onRefresh={channelsData.refresh} + /> {/* Main Content */} . + +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 {t('已启用')}; + case 2: + return {t('已禁用')}; + case 3: + return {t('自动禁用')}; + default: + return {t('未知状态')}; + } + }; + + // Table columns definition + const columns = [ + { + title: t('索引'), + dataIndex: 'index', + render: (text) => `#${text}`, + }, + // { + // title: t('密钥预览'), + // dataIndex: 'key_preview', + // render: (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 -; + } + return ( + + + {reason} + + + ); + }, + }, + { + title: t('禁用时间'), + dataIndex: 'disabled_time', + width: 150, + render: (time, record) => { + if (record.status === 1 || !time) { + return -; + } + return ( + + + {timestamp2string(time)} + + + ); + }, + }, + { + title: t('操作'), + key: 'action', + width: 120, + render: (_, record) => ( + + {record.status === 1 ? ( + + ) : ( + + )} + + ), + }, + ]; + + return ( + + + {t('多密钥管理')} - {channel?.name} + + } + visible={visible} + onCancel={onCancel} + width={800} + height={600} + footer={ + + + + + + + {enabledCount > 0 && ( + + + + )} + + + + + } + > +
+ {/* Statistics Banner */} + + + {t('总共 {{total}} 个密钥,{{enabled}} 个已启用,{{manual}} 个手动禁用,{{auto}} 个自动禁用', { + total: total, + enabled: enabledCount, + manual: manualDisabledCount, + auto: autoDisabledCount + })} + + {channel?.channel_info?.multi_key_mode && ( +
+ + {t('多密钥模式')}: {channel.channel_info.multi_key_mode === 'random' ? t('随机') : t('轮询')} + +
+ )} +
+ } + /> + + {/* Filter Controls */} +
+ {t('状态筛选')}: + + {statusFilter !== null && ( + + {t('当前显示 {{count}} 条筛选结果', { count: total })} + + )} +
+ + {/* Key Status Table */} + + {keyStatusList.length > 0 ? ( + <> + + + {/* Pagination */} + {total > 0 && ( +
+ + {t('显示第 {{start}}-{{end}} 条,共 {{total}} 条', { + start: (currentPage - 1) * pageSize + 1, + end: Math.min(currentPage * pageSize, total), + total: total + })} + + +
+ + {t('每页显示')}: + + + + + t('第 {{current}} / {{total}} 页', { + current: currentPage, + total: totalPages + }) + } + /> +
+
+ )} + + ) : ( + !loading && ( + + ) + )} + + + + ); +}; + +export default MultiKeyManageModal; \ No newline at end of file diff --git a/web/src/hooks/channels/useChannelsData.js b/web/src/hooks/channels/useChannelsData.js index d188c9fe..8f1f8c29 100644 --- a/web/src/hooks/channels/useChannelsData.js +++ b/web/src/hooks/channels/useChannelsData.js @@ -83,6 +83,10 @@ export const useChannelsData = () => { const [isProcessingQueue, setIsProcessingQueue] = useState(false); const [modelTablePage, setModelTablePage] = useState(1); + // Multi-key management states + const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false); + const [currentMultiKeyChannel, setCurrentMultiKeyChannel] = useState(null); + // Refs const requestCounter = useRef(0); const allSelectingRef = useRef(false); @@ -885,6 +889,12 @@ export const useChannelsData = () => { setModelTablePage, allSelectingRef, + // Multi-key management states + showMultiKeyManageModal, + setShowMultiKeyManageModal, + currentMultiKeyChannel, + setCurrentMultiKeyChannel, + // Form formApi, setFormApi,