From 74cc8c0d56fa5626d9a1db171e2e735b12d073d8 Mon Sep 17 00:00:00 2001 From: fengsilin Date: Tue, 28 Apr 2026 11:40:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(playground):=20=E6=B7=BB=E5=8A=A0=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E9=80=89=E6=8B=A9=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=8C=87=E5=AE=9A=E6=B8=A0=E9=81=93=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 /api/user/model_channels 接口,返回模型可用渠道及默认渠道 - Playground 设置面板添加渠道选择下拉框 - Distribute 中间件支持从请求体读取 channel_id 指定渠道 - 支持 URL 参数 ?model=xxx 直接选择模型 Co-Authored-By: Claude --- controller/playground_channels.go | 62 +++++++++++++++++++ middleware/distributor.go | 10 ++- model/ability.go | 51 +++++++++++++++ router/api-router.go | 1 + .../playground/OptimizedComponents.js | 3 +- .../components/playground/SettingsPanel.jsx | 33 ++++++++++ web/src/constants/playground.constants.js | 1 + web/src/hooks/playground/useDataLoader.js | 16 +++-- .../hooks/playground/usePlaygroundState.js | 3 + web/src/pages/Playground/index.jsx | 41 +++++++++++- 10 files changed, 211 insertions(+), 10 deletions(-) create mode 100644 controller/playground_channels.go diff --git a/controller/playground_channels.go b/controller/playground_channels.go new file mode 100644 index 0000000..89a86c6 --- /dev/null +++ b/controller/playground_channels.go @@ -0,0 +1,62 @@ +package controller + +import ( + "net/http" + + "github.com/QuantumNous/new-api/model" + "github.com/gin-gonic/gin" +) + +func GetModelChannels(c *gin.Context) { + modelName := c.Query("model") + if modelName == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "success": false, + "message": "model parameter is required", + }) + return + } + + userId := c.GetInt("id") + if userId == 0 { + c.JSON(http.StatusUnauthorized, gin.H{ + "success": false, + "message": "unauthorized", + }) + return + } + + userCache, err := model.GetUserCache(userId) + if err != nil || userCache == nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "failed to get user info", + }) + return + } + userGroup := userCache.Group + if userGroup == "" { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "failed to get user group", + }) + return + } + + channels, defaultChannelId, err := model.GetModelChannelsForGroup(modelName, userGroup) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "success": false, + "message": "failed to query channels", + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "data": gin.H{ + "channels": channels, + "default_channel_id": defaultChannelId, + }, + }) +} diff --git a/middleware/distributor.go b/middleware/distributor.go index 59a76dd..053ab21 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -23,19 +23,20 @@ import ( ) type ModelRequest struct { - Model string `json:"model"` - Group string `json:"group,omitempty"` + Model string `json:"model"` + Group string `json:"group,omitempty"` + ChannelId int `json:"channel_id,omitempty"` } func Distribute() func(c *gin.Context) { return func(c *gin.Context) { var channel *model.Channel - channelId, ok := common.GetContextKey(c, constant.ContextKeyTokenSpecificChannelId) modelRequest, shouldSelectChannel, err := getModelRequest(c) if err != nil { abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()})) return } + channelId, ok := common.GetContextKey(c, constant.ContextKeyTokenSpecificChannelId) if ok { id, err := strconv.Atoi(channelId.(string)) if err != nil { @@ -368,6 +369,9 @@ func getModelRequest(c *gin.Context) (*ModelRequest, bool, error) { modelRequest.Model = req.Model modelRequest.Group = req.Group common.SetContextKey(c, constant.ContextKeyTokenGroup, modelRequest.Group) + if req.ChannelId > 0 { + common.SetContextKey(c, constant.ContextKeyTokenSpecificChannelId, strconv.Itoa(req.ChannelId)) + } } if strings.HasPrefix(c.Request.URL.Path, "/v1/responses/compact") && modelRequest.Model != "" { diff --git a/model/ability.go b/model/ability.go index 7911920..cf1ea67 100644 --- a/model/ability.go +++ b/model/ability.go @@ -66,6 +66,57 @@ func GetAbilitiesByChannelId(channelId int) ([]*Ability, error) { return abilities, err } +// GetModelChannelsForGroup 返回指定模型在指定分组下的可用渠道列表及默认渠道ID +func GetModelChannelsForGroup(modelName string, group string) ([]map[string]any, int, error) { + var channelIds []int + err := DB.Model(&Ability{}). + Where("model = ?", modelName). + Where("enabled = ?", true). + Where(commonGroupCol+" = ?", group). + Distinct("channel_id"). + Pluck("channel_id", &channelIds).Error + if err != nil { + return nil, 0, err + } + + if len(channelIds) == 0 { + return []map[string]any{}, 0, nil + } + + type channelInfo struct { + Id int `json:"id"` + Name string `json:"name"` + } + var channels []channelInfo + err = DB.Table("channels"). + Where("id IN ? AND status = ?", channelIds, common.ChannelStatusEnabled). + Select("id, name"). + Find(&channels).Error + if err != nil { + return nil, 0, err + } + + defaultChannelId := 0 + if defaultChId, ok := GetDefaultChannelId(modelName); ok { + for _, id := range channelIds { + if id == defaultChId { + defaultChannelId = defaultChId + break + } + } + } + + result := make([]map[string]any, 0, len(channels)) + for _, ch := range channels { + result = append(result, map[string]any{ + "id": ch.Id, + "name": ch.Name, + }) + } + + return result, defaultChannelId, nil +} + func getPriority(group string, model string, retry int) (int, error) { var priorities []int diff --git a/router/api-router.go b/router/api-router.go index 7c4a360..04cb916 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -75,6 +75,7 @@ func SetApiRouter(router *gin.Engine) { selfRoute.GET("/self/groups", controller.GetUserGroups) selfRoute.GET("/self", controller.GetSelf) selfRoute.GET("/models", controller.GetUserModels) + selfRoute.GET("/model_channels", controller.GetModelChannels) selfRoute.GET("/channels", controller.GetUserChannelsForBinding) selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.DELETE("/self", controller.DeleteSelf) diff --git a/web/src/components/playground/OptimizedComponents.js b/web/src/components/playground/OptimizedComponents.js index ff679c6..1fe4268 100644 --- a/web/src/components/playground/OptimizedComponents.js +++ b/web/src/components/playground/OptimizedComponents.js @@ -74,7 +74,8 @@ export const OptimizedSettingsPanel = React.memo( prevProps.showSettings === nextProps.showSettings && JSON.stringify(prevProps.previewPayload) === JSON.stringify(nextProps.previewPayload) && - JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages) + JSON.stringify(prevProps.messages) === JSON.stringify(nextProps.messages) && + JSON.stringify(prevProps.channels) === JSON.stringify(nextProps.channels) ); }, ); diff --git a/web/src/components/playground/SettingsPanel.jsx b/web/src/components/playground/SettingsPanel.jsx index 3899e59..b9fcc37 100644 --- a/web/src/components/playground/SettingsPanel.jsx +++ b/web/src/components/playground/SettingsPanel.jsx @@ -45,6 +45,7 @@ const SettingsPanel = ({ onCustomRequestBodyChange, previewPayload, messages, + channels = [], }) => { const { t } = useTranslation(); @@ -176,6 +177,38 @@ const SettingsPanel = ({ /> + {/* 渠道选择 */} +
+
+ + {t('渠道')} + + {customRequestMode && ( + + ({t('已在自定义模式中忽略')}) + + )} +
+