package handler import ( "crypto/subtle" "errors" "net/http" "strings" "github.com/gin-gonic/gin" "gorm.io/gorm" "infogenie-backend/config" "infogenie-backend/internal/database" "infogenie-backend/internal/model" ) type SiteConfigHandler struct{} func NewSiteConfigHandler() *SiteConfigHandler { return &SiteConfigHandler{} } func siteAdminTokenOK(headerToken string) bool { if config.Cfg == nil { return false } expected := strings.TrimSpace(config.Cfg.SiteAdminToken) if expected == "" { return false } return subtle.ConstantTimeCompare([]byte(headerToken), []byte(expected)) == 1 } // Get60sDisabled 公开:返回当前隐藏的 60s 功能 id 列表(与前端 item.id 对应) func (h *SiteConfigHandler) Get60sDisabled(c *gin.Context) { var rows []model.Site60sDisabled if err := database.DB.Order("feature_id").Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } ids := make([]string, 0, len(rows)) for _, r := range rows { ids = append(ids, r.FeatureID) } c.JSON(http.StatusOK, gin.H{"disabled": ids}) } type put60sDisabledBody struct { Disabled []string `json:"disabled"` } // Put60sDisabled 需请求头 X-Site-Admin-Token,与后端环境变量 INFOGENIE_SITE_ADMIN_TOKEN 一致(建议与前端管理员口令相同) func (h *SiteConfigHandler) Put60sDisabled(c *gin.Context) { if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "admin_not_configured", "message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN,禁止写入", }) return } if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden", "message": "站点管理员令牌无效"}) return } var body put60sDisabledBody if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"}) return } if len(body.Disabled) > 512 { c.JSON(http.StatusBadRequest, gin.H{"error": "too_many"}) return } seen := make(map[string]struct{}) clean := make([]string, 0, len(body.Disabled)) for _, raw := range body.Disabled { id := strings.TrimSpace(raw) if id == "" || len(id) > 96 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} clean = append(clean, id) } tx := database.DB.Begin() if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.Site60sDisabled{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } for _, id := range clean { if err := tx.Create(&model.Site60sDisabled{FeatureID: id}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } } if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)}) } // —— 60s API 上游节点(仅管理员可切换)—— type sixtySrcInfo struct { Base string Label string } var sixtyUpstreamRegistry = map[string]sixtySrcInfo{ "self": {Base: "https://60s.api.shumengya.top", Label: "萌芽节点"}, "official": {Base: "https://60s.viki.moe", Label: "官方节点"}, } func resolve60sUpstream(sourceID string) (id string, info sixtySrcInfo) { id = strings.TrimSpace(sourceID) if id == "" { id = "self" } var ok bool info, ok = sixtyUpstreamRegistry[id] if !ok { id = "self" info = sixtyUpstreamRegistry["self"] } return id, info } // Get60sSource 公开:当前站点使用的 60s 上游 base_url(供静态页 iframe 传参) func (h *SiteConfigHandler) Get60sSource(c *gin.Context) { var row model.Site60sUpstream _ = database.DB.First(&row, 1).Error sid, info := resolve60sUpstream(row.SourceID) c.JSON(http.StatusOK, gin.H{ "source_id": sid, "base_url": info.Base, "label": info.Label, }) } type put60sSourceBody struct { SourceID string `json:"source_id"` } // Put60sSource 管理员切换节点:source_id 为 self | official func (h *SiteConfigHandler) Put60sSource(c *gin.Context) { if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" { c.JSON(http.StatusServiceUnavailable, gin.H{"error": "admin_not_configured"}) return } if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden"}) return } var body put60sSourceBody if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"}) return } sid := strings.TrimSpace(body.SourceID) if _, ok := sixtyUpstreamRegistry[sid]; !ok { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_source_id"}) return } var row model.Site60sUpstream err := database.DB.First(&row, 1).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { row = model.Site60sUpstream{ID: 1} } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } } row.SourceID = sid if err := database.DB.Save(&row).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } _, info := resolve60sUpstream(sid) c.JSON(http.StatusOK, gin.H{"ok": true, "source_id": sid, "base_url": info.Base, "label": info.Label}) } // —— AI 应用可见性控制(仅管理员可配置)—— // GetAIModelDisabled 公开:返回当前隐藏的 AI 应用 id 列表(与前端 StaticPageConfig 中 AI_MODEL_APPS 的索引对应) func (h *SiteConfigHandler) GetAIModelDisabled(c *gin.Context) { var rows []model.SiteAIModelDisabled if err := database.DB.Order("app_id").Find(&rows).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } ids := make([]string, 0, len(rows)) for _, r := range rows { ids = append(ids, r.AppID) } c.JSON(http.StatusOK, gin.H{"disabled": ids}) } type putAIModelDisabledBody struct { Disabled []string `json:"disabled"` } // PutAIModelDisabled 需请求头 X-Site-Admin-Token,与后端环境变量 INFOGENIE_SITE_ADMIN_TOKEN 一致 func (h *SiteConfigHandler) PutAIModelDisabled(c *gin.Context) { if config.Cfg == nil || strings.TrimSpace(config.Cfg.SiteAdminToken) == "" { c.JSON(http.StatusServiceUnavailable, gin.H{ "error": "admin_not_configured", "message": "服务端未配置 INFOGENIE_SITE_ADMIN_TOKEN,禁止写入", }) return } if !siteAdminTokenOK(c.GetHeader("X-Site-Admin-Token")) { c.JSON(http.StatusForbidden, gin.H{"error": "forbidden", "message": "站点管理员令牌无效"}) return } var body putAIModelDisabledBody if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_json"}) return } if len(body.Disabled) > 64 { c.JSON(http.StatusBadRequest, gin.H{"error": "too_many"}) return } seen := make(map[string]struct{}) clean := make([]string, 0, len(body.Disabled)) for _, raw := range body.Disabled { id := strings.TrimSpace(raw) if id == "" || len(id) > 96 { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} clean = append(clean, id) } tx := database.DB.Begin() if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.SiteAIModelDisabled{}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } for _, id := range clean { if err := tx.Create(&model.SiteAIModelDisabled{AppID: id}).Error; err != nil { tx.Rollback() c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } } if err := tx.Commit().Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "db_error"}) return } c.JSON(http.StatusOK, gin.H{"ok": true, "count": len(clean)}) }