update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

View File

@@ -0,0 +1,265 @@
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)})
}