package api import ( "encoding/json" "errors" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/openteam/server/internal/pkg/resp" "github.com/openteam/server/internal/store" "gorm.io/gorm" ) // AdminModels GET /api/v1/admin/models — 模型列表(含价格、渠道绑定、定价/禁止状态)。 func (h *Handler) AdminModels(c *gin.Context) { var ms []store.Model if err := h.a.DB.Order("sort ASC, id ASC").Find(&ms).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to load models") return } // 全局模型限制策略 allow, deny := h.modelPolicyConfig() out := make([]gin.H, 0, len(ms)) for _, m := range ms { var bindings []store.ChannelModelBinding h.a.DB.Preload("Channel").Where("model_id = ?", m.ID).Find(&bindings) chs := make([]gin.H, 0, len(bindings)) for _, b := range bindings { chs = append(chs, gin.H{ "id": b.ID, "channel_id": b.ChannelID, "channel_name": b.Channel.Name, "upstream_model": b.UpstreamModel, "weight": b.Weight, }) } used := len(bindings) > 0 needsPricing := used && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0 denied := containsStr(deny, m.Name) || (len(allow) > 0 && !containsStr(allow, m.Name)) out = append(out, gin.H{ "id": m.ID, "name": m.Name, "display_name": m.DisplayName, "input_price": m.InputPrice, "output_price": m.OutputPrice, "cache_read_price": m.CacheReadPrice, "enabled": m.Enabled, "sort": m.Sort, "channels": chs, "used": used, "needs_pricing": needsPricing, "denied": denied, }) } // 渠道选中但目录中缺失的模型(孤儿绑定:渠道绑定指向已被删除的模型) var orphans []struct { ChannelName string UpstreamModel string ModelID uint64 } h.a.DB.Raw(`SELECT c.name as channel_name, b.model_id, b.upstream_model FROM channel_model_bindings b LEFT JOIN models m ON m.id = b.model_id LEFT JOIN channels c ON c.id = b.channel_id WHERE m.id IS NULL`).Scan(&orphans) missing := make([]gin.H, 0, len(orphans)) for _, o := range orphans { missing = append(missing, gin.H{ "channel": o.ChannelName, "model_id": o.ModelID, "upstream_model": o.UpstreamModel, }) } // 在用但未定价的模型数(渠道已提供、需定价) unpriced := 0 { var usedBindings []struct { ModelID uint64 } h.a.DB.Model(&store.ChannelModelBinding{}).Distinct("model_id").Scan(&usedBindings) usedIDs := map[uint64]bool{} for _, u := range usedBindings { usedIDs[u.ModelID] = true } for _, m := range ms { if usedIDs[m.ID] && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0 { unpriced++ } } } resp.OK(c, gin.H{ "items": out, "summary": gin.H{ "total": len(ms), "unpriced": unpriced, "missing": missing, "denied_count": len(deny), }, }) } // modelPolicyConfig 读取全局模型允许/禁止列表。 func (h *Handler) modelPolicyConfig() (allow, deny []string) { var raw string h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "model_allowlist").Pluck("value", &raw) _ = json.Unmarshal([]byte(raw), &allow) raw = "" h.a.DB.Model(&store.SystemConfig{}).Where("key = ?", "model_denylist").Pluck("value", &raw) _ = json.Unmarshal([]byte(raw), &deny) return } func containsStr(list []string, s string) bool { for _, v := range list { if v == s { return true } } return false } // AdminCreateModel POST /api/v1/admin/models func (h *Handler) AdminCreateModel(c *gin.Context) { var req struct { Name string `json:"name" binding:"required,min=1,max=128"` DisplayName string `json:"display_name"` InputPrice float64 `json:"input_price"` OutputPrice float64 `json:"output_price"` CacheReadPrice float64 `json:"cache_read_price"` Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input: "+err.Error()) return } m := store.Model{ Name: req.Name, DisplayName: req.DisplayName, InputPrice: req.InputPrice, OutputPrice: req.OutputPrice, CacheReadPrice: req.CacheReadPrice, Enabled: boolOr(req.Enabled, true), } if m.DisplayName == "" { m.DisplayName = m.Name } if err := h.a.DB.Create(&m).Error; err != nil { resp.Fail(c, http.StatusConflict, "failed to create model (name may already exist)") return } resp.Created(c, gin.H{"id": m.ID, "name": m.Name}) } // AdminUpdateModel PUT /api/v1/admin/models/:id — 价格/展示名/启停/排序。 func (h *Handler) AdminUpdateModel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid model id") return } var req struct { DisplayName *string `json:"display_name"` InputPrice *float64 `json:"input_price"` OutputPrice *float64 `json:"output_price"` CacheReadPrice *float64 `json:"cache_read_price"` Enabled *bool `json:"enabled"` Sort *int `json:"sort"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input") return } var m store.Model if err := h.a.DB.First(&m, id).Error; err != nil { resp.Fail(c, http.StatusNotFound, "model not found") return } updates := map[string]any{} if req.DisplayName != nil { updates["display_name"] = *req.DisplayName } if req.InputPrice != nil { updates["input_price"] = *req.InputPrice } if req.OutputPrice != nil { updates["output_price"] = *req.OutputPrice } if req.CacheReadPrice != nil { updates["cache_read_price"] = *req.CacheReadPrice } if req.Enabled != nil { updates["enabled"] = *req.Enabled } if req.Sort != nil { updates["sort"] = *req.Sort } if len(updates) > 0 { if err := h.a.DB.Model(&m).Updates(updates).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to update model") return } } resp.OK(c, gin.H{"ok": true}) } // AdminDeleteModel DELETE /api/v1/admin/models/:id func (h *Handler) AdminDeleteModel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid model id") return } res := h.a.DB.Delete(&store.Model{}, id) if res.Error != nil { resp.Fail(c, http.StatusInternalServerError, "failed to delete model") return } if res.RowsAffected == 0 { resp.Fail(c, http.StatusNotFound, "model not found") return } h.a.DB.Where("model_id = ?", id).Delete(&store.ChannelModelBinding{}) resp.OK(c, gin.H{"ok": true}) } // AdminDeleteUnusedModels DELETE /api/v1/admin/models/unused — 一键清除未绑定任何渠道的模型。 // 这些通常是渠道抽屉里选中过、后来又取消绑定留下的目录条目,客户端无法调用。 func (h *Handler) AdminDeleteUnusedModels(c *gin.Context) { var orphans []store.Model if err := h.a.DB.Where("id NOT IN (SELECT DISTINCT model_id FROM channel_model_bindings)").Find(&orphans).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to load models") return } names := make([]string, 0, len(orphans)) ids := make([]uint64, 0, len(orphans)) for _, m := range orphans { names = append(names, m.Name) ids = append(ids, m.ID) } if len(ids) > 0 { if err := h.a.DB.Delete(&store.Model{}, ids).Error; err != nil { resp.Fail(c, http.StatusInternalServerError, "failed to delete models") return } } resp.OK(c, gin.H{"deleted": names, "count": len(names)}) } // AdminCreateModelBinding POST /api/v1/admin/models/:id/bindings func (h *Handler) AdminCreateModelBinding(c *gin.Context) { modelID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid model id") return } var req struct { ChannelID uint64 `json:"channel_id" binding:"required"` UpstreamModel string `json:"upstream_model" binding:"required"` Weight *int `json:"weight"` } if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, http.StatusBadRequest, "invalid input: channel_id and upstream_model required") return } var m store.Model if err := h.a.DB.First(&m, modelID).Error; err != nil { resp.Fail(c, http.StatusNotFound, "model not found") return } var ch store.Channel if err := h.a.DB.First(&ch, req.ChannelID).Error; err != nil { resp.Fail(c, http.StatusNotFound, "channel not found") return } b := store.ChannelModelBinding{ ChannelID: req.ChannelID, ModelID: modelID, UpstreamModel: req.UpstreamModel, Weight: intOr(req.Weight, 1), } if err := h.a.DB.Create(&b).Error; err != nil { resp.Fail(c, http.StatusConflict, "binding may already exist") return } resp.Created(c, gin.H{"id": b.ID}) } // AdminDeleteModelBinding DELETE /api/v1/admin/models/:id/bindings/:bid func (h *Handler) AdminDeleteModelBinding(c *gin.Context) { bid, err := strconv.ParseUint(c.Param("bid"), 10, 64) if err != nil { resp.Fail(c, http.StatusBadRequest, "invalid binding id") return } res := h.a.DB.Delete(&store.ChannelModelBinding{}, bid) if res.Error != nil { resp.Fail(c, http.StatusInternalServerError, "failed to delete binding") return } if res.RowsAffected == 0 { resp.Fail(c, http.StatusNotFound, "binding not found") return } resp.OK(c, gin.H{"ok": true}) } var _ = errors.Is var _ = gorm.ErrRecordNotFound