package api import ( "encoding/json" "net/http" "strconv" "opencatd-open/internal/store" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // AdminModels GET /api/admin/models — 模型列表(含价格、渠道绑定、定价/禁止状态)。 func (h *Handler) AdminModels(c *gin.Context) { var ms []store.Model if err := h.db.Order("sort ASC, id ASC").Find(&ms).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "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.db.Preload("Channel").Where("model_id = ?", m.ID).Find(&bindings) chs := make([]gin.H, 0, len(bindings)) for _, b := range bindings { if !b.Channel.Enabled { continue } 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(chs) > 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.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.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++ } } } c.JSON(http.StatusOK, gin.H{ "data": 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.db.Model(&store.SystemConfig{}).Where("key = ?", "model_allowlist").Pluck("value", &raw) _ = json.Unmarshal([]byte(raw), &allow) raw = "" h.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/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"` Sort int `json:"sort"` Enabled *bool `json:"enabled"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input: " + err.Error()}) return } m := store.Model{ Name: req.Name, DisplayName: req.DisplayName, InputPrice: req.InputPrice, OutputPrice: req.OutputPrice, CacheReadPrice: req.CacheReadPrice, Sort: req.Sort, Enabled: boolOr(req.Enabled, true), } if err := h.db.Create(&m).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "failed to create model (name may already exist)"}) return } c.JSON(http.StatusCreated, gin.H{"id": m.ID, "name": m.Name}) } // AdminUpdateModel PUT /api/admin/models/:id — 价格/启停/排序。 func (h *Handler) AdminUpdateModel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "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 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input"}) return } var m store.Model if err := h.db.First(&m, id).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "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.db.Model(&m).Updates(updates).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update model"}) return } } c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminDeleteModel DELETE /api/admin/models/:id func (h *Handler) AdminDeleteModel(c *gin.Context) { id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid model id"}) return } res := h.db.Delete(&store.Model{}, id) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete model"}) return } if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "model not found"}) return } h.db.Where("model_id = ?", id).Delete(&store.ChannelModelBinding{}) c.JSON(http.StatusOK, gin.H{"ok": true}) } // AdminDeleteUnusedModels DELETE /api/admin/models/unused — 一键清除未绑定任何渠道的模型。 func (h *Handler) AdminDeleteUnusedModels(c *gin.Context) { var orphans []store.Model if err := h.db.Where("id NOT IN (SELECT DISTINCT model_id FROM channel_model_bindings)").Find(&orphans).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "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.db.Delete(&store.Model{}, ids).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete models"}) return } } c.JSON(http.StatusOK, gin.H{"deleted": names, "count": len(names)}) } // AdminCreateModelBinding POST /api/admin/models/:id/bindings func (h *Handler) AdminCreateModelBinding(c *gin.Context) { modelID, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "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 { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid input: channel_id and upstream_model required"}) return } var m store.Model if err := h.db.First(&m, modelID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "model not found"}) return } var ch store.Channel if err := h.db.First(&ch, req.ChannelID).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "channel not found"}) return } b := store.ChannelModelBinding{ ChannelID: req.ChannelID, ModelID: modelID, UpstreamModel: req.UpstreamModel, Weight: intOr(req.Weight, 1), } if err := h.db.Create(&b).Error; err != nil { c.JSON(http.StatusConflict, gin.H{"error": "binding may already exist"}) return } c.JSON(http.StatusCreated, gin.H{"id": b.ID}) } // AdminDeleteModelBinding DELETE /api/admin/models/:id/bindings/:bid func (h *Handler) AdminDeleteModelBinding(c *gin.Context) { bid, err := strconv.ParseUint(c.Param("bid"), 10, 64) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid binding id"}) return } res := h.db.Delete(&store.ChannelModelBinding{}, bid) if res.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete binding"}) return } if res.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "binding not found"}) return } c.JSON(http.StatusOK, gin.H{"ok": true}) } var _ = gorm.ErrRecordNotFound