Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 81 additions & 17 deletions controller/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -954,8 +954,9 @@ func DeleteChannelBatch(c *gin.Context) {

type PatchChannel struct {
model.Channel
MultiKeyMode *string `json:"multi_key_mode"`
KeyMode *string `json:"key_mode"` // 多key模式下密钥覆盖或者追加
MultiKeyMode *string `json:"multi_key_mode"`
KeyMode *string `json:"key_mode"` // 多key模式下密钥覆盖或者追加
KeyStorageMode *string `json:"key_storage_mode"` // single | multi,编辑时转换密钥存储形态
}

type ChannelStatusRequest struct {
Expand Down Expand Up @@ -988,8 +989,37 @@ func UpdateChannel(c *gin.Context) {
return
}
clearChannelReadOnlyFields(&channel, requestData)
convertingStorage := channel.KeyStorageMode != nil && strings.TrimSpace(*channel.KeyStorageMode) != ""
if convertingStorage {
// Serialize conversion with polling/key-management operations before
// reading the source ChannelInfo. Otherwise a request can wait for the
// lock and then apply a stale pre-conversion snapshot.
lock := model.GetChannelPollingLock(channel.Id)
lock.Lock()
defer lock.Unlock()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

var originChannel *model.Channel
if convertingStorage {
// Conversion checks must use a fresh source row while the per-channel
// lock is held. In particular, a partial request may omit type/settings.
originChannel, err = model.GetChannelById(channel.Id, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
}

if channel.Type == constant.ChannelTypeTaskPlugin &&
channelTypeForChecks := channel.Type
if convertingStorage {
if _, provided := requestData["type"]; !provided {
channelTypeForChecks = originChannel.Type
}
}
if channelTypeForChecks == constant.ChannelTypeTaskPlugin &&
!authz.Can(c.GetInt("id"), c.GetInt("role"), authz.TaskPluginBind) {
c.JSON(http.StatusOK, gin.H{
"success": false,
Expand All @@ -1007,13 +1037,15 @@ func UpdateChannel(c *gin.Context) {
return
}
// Preserve existing ChannelInfo to ensure multi-key channels keep correct state even if the client does not send ChannelInfo in the request.
originChannel, err := model.GetChannelById(channel.Id, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
if originChannel == nil {
originChannel, err = model.GetChannelById(channel.Id, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
}
originProxy := originChannel.GetSetting().Proxy
proxyChanged := false
Expand All @@ -1033,12 +1065,34 @@ func UpdateChannel(c *gin.Context) {
}

// If the request explicitly specifies a new MultiKeyMode, apply it on top of the original info.
if channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" {
if convertingStorage {
keyConfig := *originChannel
if _, provided := requestData["type"]; provided {
keyConfig.Type = channel.Type
// A type change without an explicit settings payload must not inherit
// a provider-specific credential mode from the old type.
if keyConfig.Type != originChannel.Type {
keyConfig.OtherSettings = ""
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
if _, provided := requestData["settings"]; provided {
keyConfig.OtherSettings = channel.OtherSettings
}
if err := applyKeyStorageMode(&channel, originChannel, &keyConfig); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
}

if !convertingStorage && channel.MultiKeyMode != nil && *channel.MultiKeyMode != "" {
channel.ChannelInfo.MultiKeyMode = constant.MultiKeyMode(*channel.MultiKeyMode)
}

// 处理多key模式下的密钥追加/覆盖逻辑
if channel.KeyMode != nil && channel.ChannelInfo.IsMultiKey {
if !convertingStorage && channel.KeyMode != nil && channel.ChannelInfo.IsMultiKey {
switch *channel.KeyMode {
case "append":
// 追加模式:将新密钥添加到现有密钥列表
Expand Down Expand Up @@ -1117,7 +1171,14 @@ func UpdateChannel(c *gin.Context) {
// 覆盖模式:直接使用新密钥(默认行为,不需要特殊处理)
}
}
err = channel.Update()
// 密钥形态转换必须把 key 与 channel_info 放进同一事务:
// GORM Updates(struct) 会跳过零值 ChannelInfo,若分两次写入,
// 失败时会留下「标记为多密钥、实际只有一把密钥」或反过来的半成品。
if convertingStorage {
err = channel.UpdateWithConvertedKeyStorage()
} else {
err = channel.Update()
}
if err != nil {
common.ApiError(c, err)
return
Expand Down Expand Up @@ -1538,6 +1599,13 @@ func ManageMultiKeys(c *gin.Context) {
return
}

// Acquire the channel lock before reading ChannelInfo. A conversion can
// replace the key storage shape while this request is waiting; reading
// first would let a stale multi-key snapshot overwrite that conversion.
lock := model.GetChannelPollingLock(request.ChannelId)
lock.Lock()
defer lock.Unlock()

channel, err := model.GetChannelById(request.ChannelId, true)
if err != nil {
c.JSON(http.StatusOK, gin.H{
Expand Down Expand Up @@ -1570,10 +1638,6 @@ func ManageMultiKeys(c *gin.Context) {
})
}

lock := model.GetChannelPollingLock(channel.Id)
lock.Lock()
defer lock.Unlock()

switch request.Action {
case "get_key_status":
keys := channel.GetKeys()
Expand Down
4 changes: 4 additions & 0 deletions controller/channel_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ func channelHasSensitiveChanges(channel *PatchChannel, origin *model.Channel, re
if _, ok := requestData["key_mode"]; ok && channel.KeyMode != nil {
return true
}
if _, ok := requestData["key_storage_mode"]; ok && channel.KeyStorageMode != nil {
return true
}
// Fail closed: any field present in the request that is neither a known
// sensitive field (gated above) nor an explicitly classified non-sensitive
// field must be treated as sensitive. This keeps a newly added channel field
Expand Down Expand Up @@ -71,6 +74,7 @@ var channelSensitiveFields = map[string]struct{}{
"other": {},
"settings": {},
"key_mode": {},
"key_storage_mode": {},
}

// channelOperationalFields lists fields managed by operation endpoints instead
Expand Down
106 changes: 106 additions & 0 deletions controller/channel_key_storage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package controller

import (
"fmt"
"strings"

"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/relaykit/dto"
)

const (
keyStorageModeSingle = "single"
keyStorageModeMulti = "multi"
)

func channelSupportsMultiKey(channel *model.Channel) bool {
if channel.Type == constant.ChannelTypeCodex {
return false
}
if channel.Type == constant.ChannelTypeVertexAi && channel.GetOtherSettings().VertexKeyType == dto.VertexKeyTypeAPIKey {
return false
}
return true
}

func parseUpdateKeyList(channel *model.Channel, key string) ([]string, error) {
if channel.Type == constant.ChannelTypeVertexAi && channel.GetOtherSettings().VertexKeyType != dto.VertexKeyTypeAPIKey {
trimmed := strings.TrimSpace(key)
if strings.HasPrefix(trimmed, "[") {
return getVertexArrayKeys(key)
}
if trimmed == "" {
return nil, fmt.Errorf("密钥不能为空")
}
return []string{trimmed}, nil
}
keys := make([]string, 0)
for _, line := range strings.Split(key, "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
keys = append(keys, line)
}
return keys, nil
}

func applyKeyStorageMode(channel *PatchChannel, origin *model.Channel, keyConfig *model.Channel) error {
if channel.KeyStorageMode == nil {
return nil
}
if keyConfig == nil {
keyConfig = origin
}
mode := strings.TrimSpace(*channel.KeyStorageMode)
if mode == "" {
return nil
}
if mode != keyStorageModeSingle && mode != keyStorageModeMulti {
return fmt.Errorf("不支持的密钥存储模式")
}
if strings.TrimSpace(channel.Key) == "" {
return fmt.Errorf("转换密钥模式必须提供新的密钥")
}
keys, err := parseUpdateKeyList(keyConfig, channel.Key)
if err != nil {
return err
}
switch mode {
case keyStorageModeSingle:
if !origin.ChannelInfo.IsMultiKey {
return fmt.Errorf("渠道已经是单密钥模式")
}
if len(keys) != 1 {
return fmt.Errorf("转换为单密钥时必须只提供一把新密钥")
}
channel.Key = keys[0]
channel.ChannelInfo = model.ChannelInfo{}
return nil
default:
if origin.ChannelInfo.IsMultiKey {
return fmt.Errorf("渠道已经是多密钥模式")
}
if !channelSupportsMultiKey(keyConfig) {
return fmt.Errorf("当前渠道类型不支持多密钥模式")
}
if len(keys) < 2 {
return fmt.Errorf("转换为多密钥时至少需要两把密钥")
}
rotation := constant.MultiKeyModeRandom
if channel.MultiKeyMode != nil {
requested := constant.MultiKeyMode(strings.TrimSpace(*channel.MultiKeyMode))
if requested == constant.MultiKeyModeRandom || requested == constant.MultiKeyModePolling {
rotation = requested
}
}
channel.Key = strings.Join(keys, "\n")
channel.ChannelInfo = model.ChannelInfo{
IsMultiKey: true,
MultiKeySize: len(keys),
MultiKeyMode: rotation,
}
return nil
}
}
Loading