From f7a5741b3305a872fcfed00445afc8183900ec48 Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Sat, 22 Aug 2026 14:19:45 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E4=BB=85=E6=98=BE=E7=A4=BA=E5=B7=B2=E5=90=AF=E7=94=A8=E6=B8=A0?= =?UTF-8?q?=E9=81=93=E4=B8=AD=E7=9A=84=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/internal/api/user.go | 19 +++++++++++++------ server/internal/channel/channel.go | 14 ++++++++++++++ server/internal/proxy/gateway.go | 9 +++++++-- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/server/internal/api/user.go b/server/internal/api/user.go index bb8f115..01b3202 100644 --- a/server/internal/api/user.go +++ b/server/internal/api/user.go @@ -49,15 +49,22 @@ func (h *Handler) todayUsage(c *gin.Context, userID uint64) gin.H { } // UserModels GET /api/v1/user/models — 控制台可用模型列表(无需 API Key)。 +// 仅返回启用的模型且至少绑定到一个启用且健康的渠道,与 /v1/models 口径一致。 func (h *Handler) UserModels(c *gin.Context) { - var ms []store.Model - if err := h.a.DB.Where("enabled = ?", true).Order("sort ASC, id ASC").Find(&ms).Error; err != nil { + var names []string + if err := h.a.DB.Table("models"). + Joins("JOIN channel_model_bindings ON channel_model_bindings.model_id = models.id"). + Joins("JOIN channels ON channels.id = channel_model_bindings.channel_id"). + Where("models.enabled = ? AND channels.enabled = ? AND channels.health_status = ?", + true, true, store.ChannelHealthHealthy). + Distinct("models.name"). + Order("models.sort ASC, models.id ASC"). + Pluck("models.name", &names).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to load models") return } - out := make([]string, 0, len(ms)) - for _, m := range ms { - out = append(out, m.Name) + if names == nil { + names = []string{} } - resp.OK(c, gin.H{"items": out}) + resp.OK(c, gin.H{"items": names}) } diff --git a/server/internal/channel/channel.go b/server/internal/channel/channel.go index 9659d61..422f9ef 100644 --- a/server/internal/channel/channel.go +++ b/server/internal/channel/channel.go @@ -87,6 +87,20 @@ func (s *Service) loadBound(bindings []store.ChannelModelBinding) []Candidate { return out } +// AvailableModelIDs 返回对外可见的模型 ID:启用的模型且至少绑定到一个启用且健康的渠道。 +// 与 Candidates 的过滤口径一致(enabled + health_status=healthy),避免暴露绑定到已停用渠道的模型。 +func (s *Service) AvailableModelIDs() []uint64 { + var ids []uint64 + s.db.Model(&store.ChannelModelBinding{}). + Joins("JOIN channels ON channels.id = channel_model_bindings.channel_id"). + Joins("JOIN models ON models.id = channel_model_bindings.model_id"). + Where("channels.enabled = ? AND channels.health_status = ?", true, store.ChannelHealthHealthy). + Where("models.enabled = ?", true). + Distinct("channel_model_bindings.model_id"). + Pluck("channel_model_bindings.model_id", &ids) + return ids +} + // Pick 按权重加权随机选一个候选渠道(负载均衡)。 func (s *Service) Pick(cands []Candidate) *store.Channel { if len(cands) == 0 { diff --git a/server/internal/proxy/gateway.go b/server/internal/proxy/gateway.go index 16efcef..3035f40 100644 --- a/server/internal/proxy/gateway.go +++ b/server/internal/proxy/gateway.go @@ -207,10 +207,15 @@ func (g *Gateway) Handle(c *gin.Context) { } } -// models GET /v1/models:返回启用的全局模型(OpenAI 风格)。 +// models GET /v1/models:返回对外可见的模型(启用且至少绑定到一个启用且健康的渠道;OpenAI 风格)。 func (g *Gateway) models(c *gin.Context) { + ids := g.ch.AvailableModelIDs() + if len(ids) == 0 { + c.JSON(http.StatusOK, gin.H{"object": "list", "data": []gin.H{}}) + return + } var ms []store.Model - if err := g.db.Where("enabled = ?", true).Order("sort ASC, id ASC").Find(&ms).Error; err != nil { + if err := g.db.Where("id IN ? AND enabled = ?", ids, true).Order("sort ASC, id ASC").Find(&ms).Error; err != nil { apiError(c, http.StatusInternalServerError, "internal_error", "failed to load models") return }