Просмотр исходного кода

✨ feat(channel): enhance channel handling with multi-key support

CaIon 6 месяцев назад
Родитель
Сommit
7403df7e9c

+ 16 - 0
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":

+ 18 - 5
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{}

+ 80 - 6
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 (
+        <Tag
+          size='large'
+          color={type2label[type]?.color}
+          shape='circle'
+          prefixIcon={
+          <div className="flex items-center">
+            <Boxes size={14} className="mr-1" />
+            {getChannelIcon(type)}
+          </div>
+          }
+        >
+          {type2label[type]?.label}
+        </Tag>
+      );
+    }
     return (
       <Tag
         size='large'
@@ -107,7 +125,63 @@ const ChannelsTable = () => {
     );
   };
 
-  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 (
+        <Tag size='large' color='green' shape='circle' prefixIcon={<CheckCircle size={14} />}>
+          {t('已启用')}:{totalCount}/{totalCount}
+        </Tag>
+      );
+    }
+
+    // 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 = <CheckCircle size={14} />;
+    } else if (enabledCount === 0) {
+      statusText = t('已禁用');
+      statusColor = 'red';
+      statusIcon = <XCircle size={14} />;
+    } 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 = <AlertCircle size={14} />;
+    }
+
+    return (
+      <Tag size='large' color={statusColor} shape='circle' prefixIcon={statusIcon}>
+        {statusText}:{enabledCount}/{totalCount}
+      </Tag>
+    );
+  };
+
+  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 = () => {
               <Tooltip
                 content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
               >
-                {renderStatus(text)}
+                {renderStatus(text, record.channel_info)}
               </Tooltip>
             </div>
           );
         } else {
-          return renderStatus(text);
+          return renderStatus(text, record.channel_info);
         }
       },
     },

+ 1 - 1
web/src/pages/Channel/EditChannel.js

@@ -664,7 +664,7 @@ const EditChannel = (props) => {
                           onChange={() => setMergeToSingle(!mergeToSingle)}
                         />
                         <Text style={{ fontSize: 12 }} className="ml-2 text-gray-600">
-                          {t('合并为单通道(多 Key 模式)')}
+                          {t('合并为单通道(多 Key 聚合模式)')}
                         </Text>
                       </div>
                     )}