mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-08-12 18:50:58 +00:00
9ccff4efbc
- Sync backend and frontend from GeekAI Plus v4.3.0 - Remove commercial License flows and update open-source deployment defaults - Preserve Docker Compose deployment and bump image tags to v4.3.0 BREAKING CHANGE: commercial License configuration and related endpoints are removed
285 lines
7.1 KiB
Go
285 lines
7.1 KiB
Go
package admin
|
|
|
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
|
|
// * Use of this source code is governed by a Apache-2.0 license
|
|
// * that can be found in the LICENSE file.
|
|
// * @Author yangjian102621@163.com
|
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
|
|
import (
|
|
"geekai/core"
|
|
"geekai/core/middleware"
|
|
"geekai/core/types"
|
|
"geekai/handler"
|
|
"geekai/service/ppt"
|
|
"geekai/store/model"
|
|
"geekai/utils"
|
|
"geekai/utils/resp"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// PPTHandler 管理后台 PPT 生成配置处理器
|
|
type PPTHandler struct {
|
|
handler.BaseHandler
|
|
pptService *ppt.PptService
|
|
}
|
|
|
|
// NewPPTHandler 创建管理后台 PPT 配置处理器
|
|
func NewPPTHandler(app *core.AppServer, db *gorm.DB, pptService *ppt.PptService) *PPTHandler {
|
|
return &PPTHandler{
|
|
BaseHandler: handler.BaseHandler{App: app, DB: db},
|
|
pptService: pptService,
|
|
}
|
|
}
|
|
|
|
// RegisterRoutes 注册 PPT 配置相关路由
|
|
func (h *PPTHandler) RegisterRoutes() {
|
|
rg := h.App.Engine.Group("/api/admin/ppt/")
|
|
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
|
|
{
|
|
rg.GET("config", h.GetConfig)
|
|
rg.POST("config/update", h.UpdateConfig)
|
|
rg.GET("jobs", h.Jobs)
|
|
rg.GET("jobs/:task_id", h.JobDetail)
|
|
rg.GET("jobs/:task_id/export", h.ExportJob)
|
|
rg.GET("stats", h.Stats)
|
|
}
|
|
}
|
|
|
|
// GetConfig 获取 PPT 生成配置
|
|
func (h *PPTHandler) GetConfig(c *gin.Context) {
|
|
var cfg model.Config
|
|
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
// 返回一个默认空配置
|
|
resp.SUCCESS(c, types.PPTConfig{
|
|
OutlineLLMModel: "gpt-4o-mini",
|
|
MaxSlidesPerTask: 30,
|
|
PowerCostPerSlide: 0,
|
|
MaxConcurrentRequests: 3,
|
|
QPSLimit: 1,
|
|
NanoBananaModel: "nano-banana",
|
|
NanoBananaAspectRatio: "16:9",
|
|
SeedreamSize: "1920x1080",
|
|
})
|
|
return
|
|
}
|
|
resp.ERROR(c, "获取配置失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
var pptConfig types.PPTConfig
|
|
err = utils.JsonDecode(cfg.Value, &pptConfig)
|
|
if err != nil {
|
|
resp.ERROR(c, "解析配置失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
resp.SUCCESS(c, pptConfig)
|
|
}
|
|
|
|
// UpdateConfig 更新 PPT 生成配置
|
|
func (h *PPTHandler) UpdateConfig(c *gin.Context) {
|
|
var req types.PPTConfig
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
resp.ERROR(c, "参数错误")
|
|
return
|
|
}
|
|
|
|
// 基础校验
|
|
if req.MaxSlidesPerTask <= 0 {
|
|
resp.ERROR(c, "单个任务最多 PPT 页数必须大于 0")
|
|
return
|
|
}
|
|
|
|
if req.PowerCostPerSlide < 0 {
|
|
resp.ERROR(c, "每张 PPT 图片消耗算力不能小于 0")
|
|
return
|
|
}
|
|
|
|
if req.MaxConcurrentRequests <= 0 {
|
|
req.MaxConcurrentRequests = 3
|
|
}
|
|
if req.QPSLimit <= 0 {
|
|
req.QPSLimit = 1
|
|
}
|
|
|
|
// 根据当前生图提供方做必填校验
|
|
switch req.ActiveImageProvider {
|
|
case types.PPTImageProviderNanoBanana:
|
|
if req.NanoBananaApiURL == "" {
|
|
resp.ERROR(c, "Nano Banana API 地址不能为空")
|
|
return
|
|
}
|
|
if req.NanoBananaApiKey == "" {
|
|
resp.ERROR(c, "Nano Banana API Key 不能为空")
|
|
return
|
|
}
|
|
case types.PPTImageProviderSeedream:
|
|
if req.SeedreamBaseURL == "" {
|
|
resp.ERROR(c, "Seedream Base URL 不能为空")
|
|
return
|
|
}
|
|
if req.SeedreamApiKey == "" {
|
|
resp.ERROR(c, "Seedream API Key 不能为空")
|
|
return
|
|
}
|
|
if req.SeedreamModel == "" {
|
|
resp.ERROR(c, "Seedream 模型 ID 不能为空")
|
|
return
|
|
}
|
|
default:
|
|
// 允许为空,未来可以扩展更多 provider
|
|
}
|
|
|
|
value := utils.JsonEncode(&req)
|
|
var cfg model.Config
|
|
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
|
|
if err != nil {
|
|
if err == gorm.ErrRecordNotFound {
|
|
cfg.Name = types.ConfigKeyPPT
|
|
cfg.Value = value
|
|
if err = h.DB.Create(&cfg).Error; err != nil {
|
|
resp.ERROR(c, "创建配置失败: "+err.Error())
|
|
return
|
|
}
|
|
resp.SUCCESS(c, gin.H{"message": "配置创建成功"})
|
|
return
|
|
}
|
|
resp.ERROR(c, "获取配置失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
cfg.Value = value
|
|
if err = h.DB.Updates(&cfg).Error; err != nil {
|
|
resp.ERROR(c, "更新配置失败: "+err.Error())
|
|
return
|
|
}
|
|
|
|
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
|
|
}
|
|
|
|
// Jobs 管理后台查看 PPT 任务列表(内存任务)
|
|
func (h *PPTHandler) Jobs(c *gin.Context) {
|
|
page := h.GetInt(c, "page", 1)
|
|
pageSize := h.GetInt(c, "page_size", 20)
|
|
filterUserId := h.GetInt(c, "user_id", 0)
|
|
status := h.GetTrim(c, "status")
|
|
|
|
filtered, total := h.pptService.ListAdminJobs(c.Request.Context(), page, pageSize, filterUserId, status)
|
|
|
|
jobs := make([]gin.H, 0, len(filtered))
|
|
for _, t := range filtered {
|
|
job := t.TaskSummaryMap()
|
|
job["user_id"] = t.UserID
|
|
job["error_message"] = t.ErrorMessage
|
|
jobs = append(jobs, job)
|
|
}
|
|
|
|
resp.SUCCESS(c, gin.H{
|
|
"jobs": jobs,
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": pageSize,
|
|
})
|
|
}
|
|
|
|
func buildAdminPPTTaskDetail(task *ppt.Task) gin.H {
|
|
percentage := 0
|
|
if task.Total > 0 {
|
|
percentage = int(float64(task.Completed) / float64(task.Total) * 100)
|
|
}
|
|
|
|
return gin.H{
|
|
"task_id": task.TaskID,
|
|
"user_id": task.UserID,
|
|
"status": task.Status,
|
|
"progress": gin.H{
|
|
"total_slides": task.Total,
|
|
"completed_slides": task.Completed,
|
|
"percentage": percentage,
|
|
},
|
|
"slides": task.Slides,
|
|
"error_message": task.ErrorMessage,
|
|
"content": task.Content,
|
|
"prompt": task.Prompt,
|
|
"title": task.Title,
|
|
"thumb": task.Thumb,
|
|
}
|
|
}
|
|
|
|
// JobDetail 管理后台查看指定 PPT 任务详情
|
|
func (h *PPTHandler) JobDetail(c *gin.Context) {
|
|
taskID := c.Param("task_id")
|
|
if taskID == "" {
|
|
resp.ERROR(c, types.InvalidArgs)
|
|
return
|
|
}
|
|
|
|
task, ok := h.pptService.GetTask(taskID)
|
|
if !ok {
|
|
resp.ERROR(c, "任务不存在")
|
|
return
|
|
}
|
|
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
|
|
resp.SUCCESS(c, buildAdminPPTTaskDetail(task))
|
|
}
|
|
|
|
// ExportJob 管理后台导出 PPT 任务
|
|
func (h *PPTHandler) ExportJob(c *gin.Context) {
|
|
taskID := c.Param("task_id")
|
|
if taskID == "" {
|
|
resp.ERROR(c, types.InvalidArgs)
|
|
return
|
|
}
|
|
ef, ok := ppt.ParseExportFormat(c.Query("format"))
|
|
if !ok {
|
|
resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx")
|
|
return
|
|
}
|
|
|
|
task, exists := h.pptService.GetTask(taskID)
|
|
if !exists {
|
|
resp.ERROR(c, "任务不存在")
|
|
return
|
|
}
|
|
if task.Status != ppt.TaskStatusCompleted {
|
|
resp.ERROR(c, "仅已完成任务可导出")
|
|
return
|
|
}
|
|
|
|
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
|
|
|
|
ossCfg := types.OSSConfig{}
|
|
if h.App.SysConfig != nil {
|
|
ossCfg = h.App.SysConfig.OSS
|
|
}
|
|
data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config)
|
|
if err != nil {
|
|
resp.ERROR(c, err.Error())
|
|
return
|
|
}
|
|
|
|
base := ppt.SanitizeExportBaseName(task.Title, task.TaskID)
|
|
filename := base + ppt.ExportFileExt(ef)
|
|
c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename))
|
|
c.Data(200, ppt.ExportMimeType(ef), data)
|
|
}
|
|
|
|
// Stats PPT 任务统计信息
|
|
func (h *PPTHandler) Stats(c *gin.Context) {
|
|
total, completed, processing, failed, pending := h.pptService.Stats()
|
|
|
|
resp.SUCCESS(c, gin.H{
|
|
"totalTasks": total,
|
|
"completedTasks": completed,
|
|
"processingTasks": processing,
|
|
"failedTasks": failed,
|
|
"pendingTasks": pending,
|
|
})
|
|
}
|