diff --git a/controller/channel.go b/controller/channel.go index f2b9ad7e..dc4e0cbf 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -318,6 +318,22 @@ func AddChannel(c *gin.Context) { }) return } + toMap := common.StrToMap(addChannelRequest.Channel.Key) + if toMap != nil { + addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(toMap) + } else { + addChannelRequest.Channel.ChannelInfo.MultiKeySize = 0 + } + } else { + cleanKeys := make([]string, 0) + for _, key := range strings.Split(addChannelRequest.Channel.Key, "\n") { + if key == "" { + continue + } + cleanKeys = append(cleanKeys, key) + } + addChannelRequest.Channel.ChannelInfo.MultiKeySize = len(cleanKeys) + addChannelRequest.Channel.Key = strings.Join(cleanKeys, "\n") } keys = []string{addChannelRequest.Channel.Key} case "batch": diff --git a/model/channel.go b/model/channel.go index 755bd0b2..5f460ec8 100644 --- a/model/channel.go +++ b/model/channel.go @@ -1,6 +1,7 @@ package model import ( + "database/sql/driver" "encoding/json" "one-api/common" "strings" @@ -9,11 +10,6 @@ import ( "gorm.io/gorm" ) -type ChannelInfo struct { - MultiKeyMode bool `json:"multi_key_mode"` // 是否多Key模式 - MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status -} - type Channel struct { Id int `json:"id"` Type int `json:"type" gorm:"default:0"` @@ -46,6 +42,23 @@ type Channel struct { ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"` } +type ChannelInfo struct { + MultiKeyMode bool `json:"multi_key_mode"` // 是否多Key模式 + MultiKeyStatusList map[int]int `json:"multi_key_status_list"` // key状态列表,key index -> status + MultiKeySize int `json:"multi_key_size"` // 多Key模式下的key数量 +} + +// Value implements driver.Valuer interface +func (c ChannelInfo) Value() (driver.Value, error) { + return json.Marshal(c) +} + +// Scan implements sql.Scanner interface +func (c *ChannelInfo) Scan(value interface{}) error { + bytesValue, _ := value.([]byte) + return json.Unmarshal(bytesValue, c) +} + func (channel *Channel) GetModels() []string { if channel.Models == "" { return []string{} diff --git a/web/src/components/table/ChannelsTable.js b/web/src/components/table/ChannelsTable.js index f5a78490..6d0c109d 100644 --- a/web/src/components/table/ChannelsTable.js +++ b/web/src/components/table/ChannelsTable.js @@ -22,7 +22,7 @@ import { Clock, AlertTriangle, Coins, - Tags + Tags, Boxes } from 'lucide-react'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js'; @@ -73,7 +73,7 @@ const ChannelsTable = () => { let type2label = undefined; - const renderType = (type) => { + const renderType = (type, multiKeyMode=false) => { if (!type2label) { type2label = new Map(); for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { @@ -81,6 +81,24 @@ const ChannelsTable = () => { } type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' }; } + + if (multiKeyMode) { + return ( + + + {getChannelIcon(type)} + + } + > + {type2label[type]?.label} + + ); + } return ( { ); }; - const renderStatus = (status) => { + const renderMultiKeyStatus = (status, channelInfo) => { + if (!channelInfo || !channelInfo.multi_key_mode) { + return renderStatus(status, channelInfo); + } + + const { multi_key_status_list, multi_key_size } = channelInfo; + const totalCount = multi_key_size || 0; + + // If multi_key_status_list is null, it means all keys are enabled + if (!multi_key_status_list) { + return ( + }> + {t('已启用')}:{totalCount}/{totalCount} + + ); + } + + // Count enabled keys from the status map + const statusValues = Object.values(multi_key_status_list); + const enabledCount = statusValues.filter(s => s === 1).length; + + // Determine status text, color and icon based on enabled ratio + let statusText, statusColor, statusIcon; + const enabledRatio = totalCount > 0 ? enabledCount / totalCount : 0; + + if (enabledCount === totalCount) { + statusText = t('已启用'); + statusColor = 'green'; + statusIcon = ; + } else if (enabledCount === 0) { + statusText = t('已禁用'); + statusColor = 'red'; + statusIcon = ; + } else { + statusText = t('部分启用'); + // Color based on percentage: green (>80%), yellow (20-80%), red (<20%) + if (enabledRatio > 0.8) { + statusColor = 'green'; + } else if (enabledRatio >= 0.2) { + statusColor = 'yellow'; + } else { + statusColor = 'red'; + } + statusIcon = ; + } + + return ( + + {statusText}:{enabledCount}/{totalCount} + + ); + }; + + const renderStatus = (status, channelInfo=undefined) => { + if (channelInfo?.multi_key_mode) { + return renderMultiKeyStatus(status, channelInfo); + } switch (status) { case 1: return ( @@ -297,7 +371,7 @@ const ChannelsTable = () => { dataIndex: 'type', render: (text, record, index) => { if (record.children === undefined) { - return <>{renderType(text)}; + return <>{renderType(text, record.channel_info?.multi_key_mode)}; } else { return <>{renderTagType()}; } @@ -320,12 +394,12 @@ const ChannelsTable = () => { - {renderStatus(text)} + {renderStatus(text, record.channel_info)} ); } else { - return renderStatus(text); + return renderStatus(text, record.channel_info); } }, }, diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index fa77be81..32d2ce49 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -664,7 +664,7 @@ const EditChannel = (props) => { onChange={() => setMergeToSingle(!mergeToSingle)} /> - {t('合并为单通道(多 Key 模式)')} + {t('合并为单通道(多 Key 聚合模式)')} )}