✨ feat(channel): enhance channel handling with multi-key support
This commit is contained in:
@@ -318,6 +318,22 @@ func AddChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
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}
|
keys = []string{addChannelRequest.Channel.Key}
|
||||||
case "batch":
|
case "batch":
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,11 +10,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"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 {
|
type Channel struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Type int `json:"type" gorm:"default:0"`
|
Type int `json:"type" gorm:"default:0"`
|
||||||
@@ -46,6 +42,23 @@ type Channel struct {
|
|||||||
ChannelInfo ChannelInfo `json:"channel_info" gorm:"type:json"`
|
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 {
|
func (channel *Channel) GetModels() []string {
|
||||||
if channel.Models == "" {
|
if channel.Models == "" {
|
||||||
return []string{}
|
return []string{}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
Clock,
|
Clock,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Coins,
|
Coins,
|
||||||
Tags
|
Tags, Boxes
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../../constants/index.js';
|
||||||
@@ -73,7 +73,7 @@ const ChannelsTable = () => {
|
|||||||
|
|
||||||
let type2label = undefined;
|
let type2label = undefined;
|
||||||
|
|
||||||
const renderType = (type) => {
|
const renderType = (type, multiKeyMode=false) => {
|
||||||
if (!type2label) {
|
if (!type2label) {
|
||||||
type2label = new Map();
|
type2label = new Map();
|
||||||
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
|
for (let i = 0; i < CHANNEL_OPTIONS.length; i++) {
|
||||||
@@ -81,6 +81,24 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' };
|
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 (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
size='large'
|
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) {
|
switch (status) {
|
||||||
case 1:
|
case 1:
|
||||||
return (
|
return (
|
||||||
@@ -297,7 +371,7 @@ const ChannelsTable = () => {
|
|||||||
dataIndex: 'type',
|
dataIndex: 'type',
|
||||||
render: (text, record, index) => {
|
render: (text, record, index) => {
|
||||||
if (record.children === undefined) {
|
if (record.children === undefined) {
|
||||||
return <>{renderType(text)}</>;
|
return <>{renderType(text, record.channel_info?.multi_key_mode)}</>;
|
||||||
} else {
|
} else {
|
||||||
return <>{renderTagType()}</>;
|
return <>{renderTagType()}</>;
|
||||||
}
|
}
|
||||||
@@ -320,12 +394,12 @@ const ChannelsTable = () => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
|
content={t('原因:') + reason + t(',时间:') + timestamp2string(time)}
|
||||||
>
|
>
|
||||||
{renderStatus(text)}
|
{renderStatus(text, record.channel_info)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return renderStatus(text);
|
return renderStatus(text, record.channel_info);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -664,7 +664,7 @@ const EditChannel = (props) => {
|
|||||||
onChange={() => setMergeToSingle(!mergeToSingle)}
|
onChange={() => setMergeToSingle(!mergeToSingle)}
|
||||||
/>
|
/>
|
||||||
<Text style={{ fontSize: 12 }} className="ml-2 text-gray-600">
|
<Text style={{ fontSize: 12 }} className="ml-2 text-gray-600">
|
||||||
{t('合并为单通道(多 Key 模式)')}
|
{t('合并为单通道(多 Key 聚合模式)')}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user