From 3156701d4e8d059d389e7e5079500b9c8a49f5d8 Mon Sep 17 00:00:00 2001 From: GeekMaster Date: Mon, 21 Jul 2025 20:05:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=B3=E6=A2=A6AI=E7=BB=98=E5=9B=BE=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=89=8D=E7=AB=AF=E9=A1=B5=E9=9D=A2=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/handler/admin/jimeng_handler.go | 19 +- api/handler/jimeng_handler.go | 676 ++++++------------- api/main.go | 13 +- api/service/jimeng/client.go | 11 +- api/service/jimeng/consumer.go | 2 +- api/service/jimeng/service.go | 68 +- web/src/App.vue | 2 +- web/src/assets/css/jimeng.styl | 226 ++++--- web/src/store/jimeng.js | 435 +++++++----- web/src/views/Jimeng.vue | 399 +++++------ web/src/views/admin/jimeng/JimengSetting.vue | 16 +- 11 files changed, 860 insertions(+), 1007 deletions(-) diff --git a/api/handler/admin/jimeng_handler.go b/api/handler/admin/jimeng_handler.go index 6d191789..417e65bc 100644 --- a/api/handler/admin/jimeng_handler.go +++ b/api/handler/admin/jimeng_handler.go @@ -38,7 +38,7 @@ func (h *AdminJimengHandler) RegisterRoutes() { rg.POST("/jobs/batch-remove", h.BatchRemove) rg.GET("/stats", h.Stats) rg.GET("/config", h.GetConfig) - rg.POST("/config", h.UpdateConfig) + rg.POST("/config/update", h.UpdateConfig) } // Jobs 获取任务列表 @@ -241,12 +241,6 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { return } - testErr := h.jimengService.TestConnection(req.AccessKey, req.SecretKey) - if testErr != nil { - resp.ERROR(c, "连接测试失败: "+testErr.Error()) - return - } - // 验证算力配置 if req.Power.TextToImage <= 0 { resp.ERROR(c, "文生图算力必须大于0") @@ -274,10 +268,11 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { } // 保存配置 + tx := h.DB.Begin() value := utils.JsonEncode(&req) config := model.Config{Name: "jimeng", Value: value} - err := h.DB.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error + err := tx.FirstOrCreate(&config, model.Config{Name: "jimeng"}).Error if err != nil { resp.ERROR(c, "保存配置失败: "+err.Error()) return @@ -285,7 +280,7 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { if config.Id > 0 { config.Value = value - err = h.DB.Updates(&config).Error + err = tx.Updates(&config).Error if err != nil { resp.ERROR(c, "更新配置失败: "+err.Error()) return @@ -295,9 +290,11 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) { // 更新服务中的客户端配置 updateErr := h.jimengService.UpdateClientConfig(req.AccessKey, req.SecretKey) if updateErr != nil { - // 配置已保存,但客户端更新失败,记录日志但不返回错误 - logger.Errorf("更新即梦AI客户端配置失败: %v", updateErr) + resp.ERROR(c, updateErr.Error()) + tx.Rollback() + return } + tx.Commit() resp.SUCCESS(c, gin.H{"message": "配置更新成功"}) } diff --git a/api/handler/jimeng_handler.go b/api/handler/jimeng_handler.go index 533e961c..a00f54d1 100644 --- a/api/handler/jimeng_handler.go +++ b/api/handler/jimeng_handler.go @@ -21,504 +21,230 @@ type JimengHandler struct { } // NewJimengHandler 创建即梦AI处理器 -func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service) *JimengHandler { +func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB) *JimengHandler { return &JimengHandler{ - BaseHandler: BaseHandler{App: app}, + BaseHandler: BaseHandler{App: app, DB: db}, jimengService: jimengService, } } +// RegisterRoutes 注册路由,新增统一任务接口 func (h *JimengHandler) RegisterRoutes() { rg := h.App.Engine.Group("/api/jimeng") - rg.POST("text-to-image", h.TextToImage) - rg.POST("image-to-image-portrait", h.ImageToImagePortrait) - rg.POST("image-edit", h.ImageEdit) - rg.POST("image-effects", h.ImageEffects) - rg.POST("text-to-video", h.TextToVideo) - rg.POST("image-to-video", h.ImageToVideo) + rg.POST("task", h.CreateTask) // 只保留统一任务接口 + rg.GET("power-config", h.GetPowerConfig) // 新增算力配置接口 rg.GET("jobs", h.Jobs) - rg.GET("pending-count", h.PendingCount) rg.GET("remove", h.Remove) rg.GET("retry", h.Retry) } -// TextToImage 文生图 -func (h *JimengHandler) TextToImage(c *gin.Context) { - var req struct { - Prompt string `json:"prompt" binding:"required"` - Seed int64 `json:"seed"` - Scale float64 `json:"scale"` - Width int `json:"width"` - Height int `json:"height"` - UsePreLLM bool `json:"use_pre_llm"` - } +// JimengTaskRequest 统一任务请求结构体 +// 支持所有生图和生成视频类型 +type JimengTaskRequest struct { + TaskType string `json:"task_type" binding:"required"` + Prompt string `json:"prompt"` + ImageInput string `json:"image_input"` + ImageUrls []string `json:"image_urls"` + BinaryDataBase64 []string `json:"binary_data_base64"` + Scale float64 `json:"scale"` + Width int `json:"width"` + Height int `json:"height"` + Gpen float64 `json:"gpen"` + Skin float64 `json:"skin"` + SkinUnifi float64 `json:"skin_unifi"` + GenMode string `json:"gen_mode"` + Seed int64 `json:"seed"` + UsePreLLM bool `json:"use_pre_llm"` + TemplateId string `json:"template_id"` + AspectRatio string `json:"aspect_ratio"` +} +// CreateTask 统一任务创建接口 +func (h *JimengHandler) CreateTask(c *gin.Context) { + var req JimengTaskRequest if err := c.ShouldBindJSON(&req); err != nil { resp.ERROR(c, types.InvalidArgs) return } - - // 获取当前用户 + // 新增:除图像特效外,其他任务类型必须有提示词 + if req.TaskType != "image_effects" && req.Prompt == "" { + resp.ERROR(c, "提示词不能为空") + return + } user, err := h.GetLoginUser(c) if err != nil { resp.NotAuth(c) return } - // 获取配置中的算力消耗 - powerCost := h.getPowerFromConfig(model.JMTaskTypeTextToImage) + var powerCost int + var taskType model.JMTaskType + var params map[string]interface{} + var reqKey string + var modelName string - // 检查用户算力 - if user.Power < powerCost { - resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) - return - } - - // 设置默认参数 - if req.Scale == 0 { - req.Scale = 2.5 - } - if req.Width == 0 { - req.Width = 1328 - } - if req.Height == 0 { - req.Height = 1328 - } - if req.Seed == 0 { - req.Seed = -1 - } - - // 构建任务参数 - params := map[string]interface{}{ - "seed": req.Seed, - "scale": req.Scale, - "width": req.Width, - "height": req.Height, - "use_pre_llm": req.UsePreLLM, - } - - // 创建任务 - taskReq := &jimeng.CreateTaskRequest{ - Type: model.JMTaskTypeTextToImage, - Prompt: req.Prompt, - Params: params, - ReqKey: jimeng.ReqKeyTextToImage, - Power: powerCost, - } - - job, err := h.jimengService.CreateTask(user.Id, taskReq) - if err != nil { - logger.Errorf("create jimeng text to image task failed: %v", err) - resp.ERROR(c, "创建任务失败") - return - } - - // 扣除用户算力 - h.subUserPower(user.Id, powerCost, model.PowerLog{ - Type: types.PowerConsume, - Model: "即梦文生图", - Remark: fmt.Sprintf("任务ID:%d", job.Id), - }) - - resp.SUCCESS(c, job) -} - -// ImageToImagePortrait 图生图人像写真 -func (h *JimengHandler) ImageToImagePortrait(c *gin.Context) { - var req struct { - ImageInput string `json:"image_input" binding:"required"` - Prompt string `json:"prompt"` - Width int `json:"width"` - Height int `json:"height"` - Gpen float64 `json:"gpen"` - Skin float64 `json:"skin"` - SkinUnifi float64 `json:"skin_unifi"` - GenMode string `json:"gen_mode"` - Seed int64 `json:"seed"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - resp.ERROR(c, "参数错误: "+err.Error()) - return - } - - // 获取当前用户 - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - // 获取配置中的算力消耗 - powerCost := h.getPowerFromConfig(model.JMTaskTypeImageToImage) - - // 检查用户算力 - if user.Power < powerCost { - resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) - return - } - - // 设置默认参数 - if req.Width == 0 { - req.Width = 1328 - } - if req.Height == 0 { - req.Height = 1328 - } - if req.Gpen == 0 { - req.Gpen = 0.4 - } - if req.Skin == 0 { - req.Skin = 0.3 - } - if req.GenMode == "" { - if req.Prompt != "" { - req.GenMode = jimeng.GenModeCreative - } else { - req.GenMode = jimeng.GenModeReference + switch req.TaskType { + case "text_to_image": + powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToImage) + taskType = model.JMTaskTypeTextToImage + reqKey = jimeng.ReqKeyTextToImage + modelName = "即梦文生图" + if req.Scale == 0 { + req.Scale = 2.5 + } + if req.Width == 0 { + req.Width = 1328 + } + if req.Height == 0 { + req.Height = 1328 + } + if req.Seed == 0 { + req.Seed = -1 + } + params = map[string]interface{}{ + "seed": req.Seed, + "scale": req.Scale, + "width": req.Width, + "height": req.Height, + "use_pre_llm": req.UsePreLLM, + } + case "image_to_image": + powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToImage) + taskType = model.JMTaskTypeImageToImage + reqKey = jimeng.ReqKeyImageToImagePortrait + modelName = "即梦图生图" + if req.Width == 0 { + req.Width = 1328 + } + if req.Height == 0 { + req.Height = 1328 + } + if req.Gpen == 0 { + req.Gpen = 0.4 + } + if req.Skin == 0 { + req.Skin = 0.3 + } + if req.GenMode == "" { + if req.Prompt != "" { + req.GenMode = jimeng.GenModeCreative + } else { + req.GenMode = jimeng.GenModeReference + } + } + if req.Seed == 0 { + req.Seed = -1 } - } - if req.Seed == 0 { - req.Seed = -1 - } - if req.Prompt == "" { - req.Prompt = "演唱会现场的合照,闪光灯拍摄" - } - // 构建任务参数 - params := map[string]interface{}{ - "image_input": req.ImageInput, - "width": req.Width, - "height": req.Height, - "gpen": req.Gpen, - "skin": req.Skin, - "skin_unifi": req.SkinUnifi, - "gen_mode": req.GenMode, - "seed": req.Seed, - } - - // 创建任务 - taskReq := &jimeng.CreateTaskRequest{ - Type: model.JMTaskTypeImageToImage, - Prompt: req.Prompt, - Params: params, - ReqKey: jimeng.ReqKeyImageToImagePortrait, - Power: powerCost, - } - - job, err := h.jimengService.CreateTask(user.Id, taskReq) - if err != nil { - logger.Errorf("create jimeng image to image portrait task failed: %v", err) - resp.ERROR(c, "创建任务失败") + params = map[string]interface{}{ + "image_input": req.ImageInput, + "width": req.Width, + "height": req.Height, + "gpen": req.Gpen, + "skin": req.Skin, + "skin_unifi": req.SkinUnifi, + "gen_mode": req.GenMode, + "seed": req.Seed, + } + case "image_edit": + powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEdit) + taskType = model.JMTaskTypeImageEdit + reqKey = jimeng.ReqKeyImageEdit + modelName = "即梦图像编辑" + if req.Scale == 0 { + req.Scale = 0.5 + } + if req.Seed == 0 { + req.Seed = -1 + } + params = map[string]interface{}{ + "seed": req.Seed, + "scale": req.Scale, + } + if len(req.ImageUrls) > 0 { + params["image_urls"] = req.ImageUrls + } + if len(req.BinaryDataBase64) > 0 { + params["binary_data_base64"] = req.BinaryDataBase64 + } + case "image_effects": + powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEffects) + taskType = model.JMTaskTypeImageEffects + reqKey = jimeng.ReqKeyImageEffects + modelName = "即梦图像特效" + if req.Width == 0 { + req.Width = 1328 + } + if req.Height == 0 { + req.Height = 1328 + } + params = map[string]interface{}{ + "image_input1": req.ImageInput, + "template_id": req.TemplateId, + "width": req.Width, + "height": req.Height, + } + case "text_to_video": + powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToVideo) + taskType = model.JMTaskTypeTextToVideo + reqKey = jimeng.ReqKeyTextToVideo + modelName = "即梦文生视频" + if req.Seed == 0 { + req.Seed = -1 + } + if req.AspectRatio == "" { + req.AspectRatio = jimeng.AspectRatio16_9 + } + params = map[string]interface{}{ + "seed": req.Seed, + "aspect_ratio": req.AspectRatio, + } + case "image_to_video": + powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToVideo) + taskType = model.JMTaskTypeImageToVideo + reqKey = jimeng.ReqKeyImageToVideo + modelName = "即梦图生视频" + if req.Seed == 0 { + req.Seed = -1 + } + params = map[string]interface{}{ + "seed": req.Seed, + "aspect_ratio": req.AspectRatio, + } + if len(req.ImageUrls) > 0 { + params["image_urls"] = req.ImageUrls + } + if len(req.BinaryDataBase64) > 0 { + params["binary_data_base64"] = req.BinaryDataBase64 + } + default: + resp.ERROR(c, "不支持的任务类型") return } - // 扣除用户算力 - h.subUserPower(user.Id, powerCost, model.PowerLog{ - Type: types.PowerConsume, - Model: "即梦图生图", - Remark: fmt.Sprintf("任务ID:%d", job.Id), - }) - - resp.SUCCESS(c, job) -} - -// ImageEdit 图像编辑 -func (h *JimengHandler) ImageEdit(c *gin.Context) { - var req struct { - ImageUrls []string `json:"image_urls"` - BinaryDataBase64 []string `json:"binary_data_base64"` - Prompt string `json:"prompt" binding:"required"` - Seed int64 `json:"seed"` - Scale float64 `json:"scale"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - resp.ERROR(c, "参数错误: "+err.Error()) - return - } - - if len(req.ImageUrls) == 0 && len(req.BinaryDataBase64) == 0 { - resp.ERROR(c, "请提供图片URL或Base64数据") - return - } - - // 获取当前用户 - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - // 获取配置中的算力消耗 - powerCost := h.getPowerFromConfig(model.JMTaskTypeImageEdit) - - // 检查用户算力 if user.Power < powerCost { resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) return } - // 设置默认参数 - if req.Scale == 0 { - req.Scale = 0.5 - } - if req.Seed == 0 { - req.Seed = -1 - } - - // 构建任务参数 - params := map[string]interface{}{ - "seed": req.Seed, - "scale": req.Scale, - } - if len(req.ImageUrls) > 0 { - params["image_urls"] = req.ImageUrls - } - if len(req.BinaryDataBase64) > 0 { - params["binary_data_base64"] = req.BinaryDataBase64 - } - - // 创建任务 taskReq := &jimeng.CreateTaskRequest{ - Type: model.JMTaskTypeImageEdit, + Type: taskType, Prompt: req.Prompt, Params: params, - ReqKey: jimeng.ReqKeyImageEdit, + ReqKey: reqKey, Power: powerCost, } job, err := h.jimengService.CreateTask(user.Id, taskReq) if err != nil { - logger.Errorf("create jimeng image edit task failed: %v", err) + logger.Errorf("create jimeng task failed: %v", err) resp.ERROR(c, "创建任务失败") return } - // 扣除用户算力 h.subUserPower(user.Id, powerCost, model.PowerLog{ Type: types.PowerConsume, - Model: "即梦图像编辑", - Remark: fmt.Sprintf("任务ID:%d", job.Id), - }) - - resp.SUCCESS(c, job) -} - -// ImageEffects 图像特效 -func (h *JimengHandler) ImageEffects(c *gin.Context) { - var req struct { - ImageInput1 string `json:"image_input1" binding:"required"` - TemplateId string `json:"template_id" binding:"required"` - Width int `json:"width"` - Height int `json:"height"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - resp.ERROR(c, "参数错误: "+err.Error()) - return - } - - // 获取当前用户 - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - // 获取配置中的算力消耗 - powerCost := h.getPowerFromConfig(model.JMTaskTypeImageEffects) - - // 检查用户算力 - if user.Power < powerCost { - resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) - return - } - - // 设置默认参数 - if req.Width == 0 { - req.Width = 1328 - } - if req.Height == 0 { - req.Height = 1328 - } - - // 构建任务参数 - params := map[string]interface{}{ - "image_input1": req.ImageInput1, - "template_id": req.TemplateId, - "width": req.Width, - "height": req.Height, - } - - // 创建任务 - taskReq := &jimeng.CreateTaskRequest{ - Type: model.JMTaskTypeImageEffects, - Prompt: "", - Params: params, - ReqKey: jimeng.ReqKeyImageEffects, - Power: powerCost, - } - - job, err := h.jimengService.CreateTask(user.Id, taskReq) - if err != nil { - logger.Errorf("create jimeng image effects task failed: %v", err) - resp.ERROR(c, "创建任务失败") - return - } - - // 扣除用户算力 - h.subUserPower(user.Id, powerCost, model.PowerLog{ - Type: types.PowerConsume, - Model: "即梦图像特效", - Remark: fmt.Sprintf("任务ID:%d", job.Id), - }) - - resp.SUCCESS(c, job) -} - -// TextToVideo 文生视频 -func (h *JimengHandler) TextToVideo(c *gin.Context) { - var req struct { - Prompt string `json:"prompt" binding:"required"` - Seed int64 `json:"seed"` - AspectRatio string `json:"aspect_ratio"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - resp.ERROR(c, "参数错误: "+err.Error()) - return - } - - // 获取当前用户 - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - // 获取配置中的算力消耗 - powerCost := h.getPowerFromConfig(model.JMTaskTypeTextToVideo) - - // 检查用户算力 - if user.Power < powerCost { - resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) - return - } - - // 设置默认参数 - if req.Seed == 0 { - req.Seed = -1 - } - if req.AspectRatio == "" { - req.AspectRatio = jimeng.AspectRatio16_9 - } - - // 构建任务参数 - params := map[string]interface{}{ - "seed": req.Seed, - "aspect_ratio": req.AspectRatio, - } - - // 创建任务 - taskReq := &jimeng.CreateTaskRequest{ - Type: model.JMTaskTypeTextToVideo, - Prompt: req.Prompt, - Params: params, - ReqKey: jimeng.ReqKeyTextToVideo, - Power: powerCost, - } - - job, err := h.jimengService.CreateTask(user.Id, taskReq) - if err != nil { - logger.Errorf("create jimeng text to video task failed: %v", err) - resp.ERROR(c, "创建任务失败") - return - } - - // 扣除用户算力 - h.subUserPower(user.Id, powerCost, model.PowerLog{ - Type: types.PowerConsume, - Model: "即梦文生视频", - Remark: fmt.Sprintf("任务ID:%d", job.Id), - }) - - resp.SUCCESS(c, job) -} - -// ImageToVideo 图生视频 -func (h *JimengHandler) ImageToVideo(c *gin.Context) { - var req struct { - ImageUrls []string `json:"image_urls"` - BinaryDataBase64 []string `json:"binary_data_base64"` - Prompt string `json:"prompt"` - Seed int64 `json:"seed"` - AspectRatio string `json:"aspect_ratio" binding:"required"` - } - - if err := c.ShouldBindJSON(&req); err != nil { - resp.ERROR(c, "参数错误: "+err.Error()) - return - } - - if len(req.ImageUrls) == 0 && len(req.BinaryDataBase64) == 0 { - resp.ERROR(c, "请提供图片URL或Base64数据") - return - } - - // 获取当前用户 - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - // 获取配置中的算力消耗 - powerCost := h.getPowerFromConfig(model.JMTaskTypeImageToVideo) - - // 检查用户算力 - if user.Power < powerCost { - resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) - return - } - - // 设置默认参数 - if req.Seed == 0 { - req.Seed = -1 - } - - // 构建任务参数 - params := map[string]interface{}{ - "seed": req.Seed, - "aspect_ratio": req.AspectRatio, - } - if len(req.ImageUrls) > 0 { - params["image_urls"] = req.ImageUrls - } - if len(req.BinaryDataBase64) > 0 { - params["binary_data_base64"] = req.BinaryDataBase64 - } - - // 创建任务 - taskReq := &jimeng.CreateTaskRequest{ - Type: model.JMTaskTypeImageToVideo, - Prompt: req.Prompt, - Params: params, - ReqKey: jimeng.ReqKeyImageToVideo, - Power: powerCost, - } - - job, err := h.jimengService.CreateTask(user.Id, taskReq) - if err != nil { - logger.Errorf("create jimeng image to video task failed: %v", err) - resp.ERROR(c, "创建任务失败") - return - } - - // 扣除用户算力 - h.subUserPower(user.Id, powerCost, model.PowerLog{ - Type: types.PowerConsume, - Model: "即梦图生视频", + Model: modelName, Remark: fmt.Sprintf("任务ID:%d", job.Id), }) @@ -551,24 +277,6 @@ func (h *JimengHandler) Jobs(c *gin.Context) { }) } -// PendingCount 获取未完成任务数量 -func (h *JimengHandler) PendingCount(c *gin.Context) { - user, err := h.GetLoginUser(c) - if err != nil { - resp.NotAuth(c) - return - } - - count, err := h.jimengService.GetPendingTaskCount(user.Id) - if err != nil { - logger.Errorf("get pending task count failed: %v", err) - resp.ERROR(c, "获取待处理任务数量失败") - return - } - - resp.SUCCESS(c, gin.H{"count": count}) -} - // Remove 删除任务 func (h *JimengHandler) Remove(c *gin.Context) { user, err := h.GetLoginUser(c) @@ -583,6 +291,21 @@ func (h *JimengHandler) Remove(c *gin.Context) { return } + // 获取任务,判断状态 + job, err := h.jimengService.GetJob(uint(jobId)) + if err != nil { + resp.ERROR(c, "任务不存在") + return + } + if job.UserId != user.Id { + resp.ERROR(c, "无权限操作") + return + } + if job.Status != model.JMTaskStatusFailed { + resp.ERROR(c, "只有失败的任务才能删除") + return + } + if err := h.jimengService.DeleteJob(uint(jobId), user.Id); err != nil { logger.Errorf("delete jimeng job failed: %v", err) resp.ERROR(c, "删除任务失败") @@ -709,3 +432,20 @@ func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int { return 10 } } + +// GetPowerConfig 获取即梦各任务类型算力消耗配置 +func (h *JimengHandler) GetPowerConfig(c *gin.Context) { + config, err := h.jimengService.GetConfig() + if err != nil || config == nil { + resp.ERROR(c, "获取算力配置失败") + return + } + resp.SUCCESS(c, gin.H{ + "text_to_image": config.Power.TextToImage, + "image_to_image": config.Power.ImageToImage, + "image_edit": config.Power.ImageEdit, + "image_effects": config.Power.ImageEffects, + "text_to_video": config.Power.TextToVideo, + "image_to_video": config.Power.ImageToVideo, + }) +} diff --git a/api/main.go b/api/main.go index 18dc32c7..6dc7ce50 100644 --- a/api/main.go +++ b/api/main.go @@ -208,21 +208,10 @@ func main() { }), // 即梦AI 服务 - fx.Provide(func(config *types.AppConfig) *jimeng.Client { - // 使用默认配置初始化客户端,后续会从数据库加载 - return jimeng.NewClient("", "") - }), fx.Provide(jimeng.NewService), - fx.Invoke(func(service *jimeng.Service) { - // 从数据库加载配置 - err := service.LoadConfigFromDB() - if err != nil { - logger.Errorf("加载即梦AI配置失败: %v", err) - } - }), fx.Provide(jimeng.NewConsumer), fx.Invoke(func(consumer *jimeng.Consumer) { - consumer.Start() + //consumer.Start() go consumer.MonitorQueue() }), fx.Provide(service.NewUserService), diff --git a/api/service/jimeng/client.go b/api/service/jimeng/client.go index f0e6d54e..0f15104b 100644 --- a/api/service/jimeng/client.go +++ b/api/service/jimeng/client.go @@ -8,11 +8,8 @@ import ( "github.com/volcengine/volc-sdk-golang/base" "github.com/volcengine/volc-sdk-golang/service/visual" - "geekai/logger" ) -var clientLogger = logger.GetLogger() - // Client 即梦API客户端 type Client struct { visual *visual.Visual @@ -80,7 +77,7 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error) return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err) } - clientLogger.Infof("Jimeng SubmitTask Response: %s", string(respBody)) + looger.Infof("Jimeng SubmitTask Response: %s", string(respBody)) // 解析响应 var result SubmitTaskResponse @@ -105,7 +102,7 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) { return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err) } - clientLogger.Infof("Jimeng QueryTask Response: %s", string(respBody)) + looger.Infof("Jimeng QueryTask Response: %s", string(respBody)) // 解析响应 var result QueryTaskResponse @@ -130,7 +127,7 @@ func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, err return nil, fmt.Errorf("submit sync task failed (status: %d): %w", statusCode, err) } - clientLogger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody)) + looger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody)) // 解析响应,同步任务直接返回结果 var result QueryTaskResponse @@ -139,4 +136,4 @@ func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, err } return &result, nil -} \ No newline at end of file +} diff --git a/api/service/jimeng/consumer.go b/api/service/jimeng/consumer.go index 19fdf8e8..c42eff1a 100644 --- a/api/service/jimeng/consumer.go +++ b/api/service/jimeng/consumer.go @@ -55,7 +55,7 @@ func (c *Consumer) consume() { // processTask 处理任务 func (c *Consumer) processTask() { // 从队列中获取任务 - var task map[string]interface{} + var task map[string]any if err := c.service.taskQueue.LPop(&task); err != nil { // 队列为空,等待1秒后重试 time.Sleep(time.Second) diff --git a/api/service/jimeng/service.go b/api/service/jimeng/service.go index d58cf175..62e2c098 100644 --- a/api/service/jimeng/service.go +++ b/api/service/jimeng/service.go @@ -4,11 +4,12 @@ import ( "encoding/json" "fmt" "strconv" + "strings" "time" "gorm.io/gorm" - "geekai/logger" + logger2 "geekai/logger" "geekai/store" "geekai/store/model" "geekai/utils" @@ -18,7 +19,7 @@ import ( "github.com/go-redis/redis/v8" ) -var serviceLogger = logger.GetLogger() +var looger = logger2.GetLogger() // Service 即梦服务 type Service struct { @@ -29,8 +30,16 @@ type Service struct { } // NewService 创建即梦服务 -func NewService(db *gorm.DB, redisCli *redis.Client, client *Client) *Service { +func NewService(db *gorm.DB, redisCli *redis.Client) *Service { taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli) + // 从数据库加载配置 + var config model.Config + db.Where("name = ?", "Jimeng").First(&config) + var jimengConfig types.JimengConfig + if config.Id > 0 { + _ = utils.JsonDecode(config.Value, &jimengConfig) + } + client := NewClient(jimengConfig.AccessKey, jimengConfig.SecretKey) return &Service{ db: db, redis: redisCli, @@ -99,7 +108,7 @@ func (s *Service) ProcessTask(jobId uint) error { case model.JMTaskTypeTextToImage: return s.processTextToImage(&job) case model.JMTaskTypeImageToImage: - return s.processImageToImagePortrait(&job) + return s.processImageToImage(&job) case model.JMTaskTypeImageEdit: return s.processImageEdit(&job) case model.JMTaskTypeImageEffects: @@ -171,15 +180,15 @@ func (s *Service) processTextToImage(job *model.JimengJob) error { "raw_data": string(rawData), "updated_at": time.Now(), }).Error; err != nil { - serviceLogger.Errorf("update jimeng job task_id failed: %v", err) + looger.Errorf("update jimeng job task_id failed: %v", err) } // 开始轮询任务状态 return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey) } -// processImageToImagePortrait 处理图生图人像写真任务 -func (s *Service) processImageToImagePortrait(job *model.JimengJob) error { +// processImageToImage 处理图生图任务 +func (s *Service) processImageToImage(job *model.JimengJob) error { // 解析任务参数 var params map[string]any if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil { @@ -249,7 +258,7 @@ func (s *Service) processImageToImagePortrait(job *model.JimengJob) error { "raw_data": string(rawData), "updated_at": time.Now(), }).Error; err != nil { - serviceLogger.Errorf("update jimeng job task_id failed: %v", err) + looger.Errorf("update jimeng job task_id failed: %v", err) } // 开始轮询任务状态 @@ -315,7 +324,7 @@ func (s *Service) processImageEdit(job *model.JimengJob) error { "raw_data": string(rawData), "updated_at": time.Now(), }).Error; err != nil { - serviceLogger.Errorf("update jimeng job task_id failed: %v", err) + looger.Errorf("update jimeng job task_id failed: %v", err) } // 开始轮询任务状态 @@ -370,7 +379,7 @@ func (s *Service) processImageEffects(job *model.JimengJob) error { "raw_data": string(rawData), "updated_at": time.Now(), }).Error; err != nil { - serviceLogger.Errorf("update jimeng job task_id failed: %v", err) + looger.Errorf("update jimeng job task_id failed: %v", err) } // 开始轮询任务状态 @@ -418,7 +427,7 @@ func (s *Service) processTextToVideo(job *model.JimengJob) error { "raw_data": string(rawData), "updated_at": time.Now(), }).Error; err != nil { - serviceLogger.Errorf("update jimeng job task_id failed: %v", err) + looger.Errorf("update jimeng job task_id failed: %v", err) } // 开始轮询任务状态 @@ -482,7 +491,7 @@ func (s *Service) processImageToVideo(job *model.JimengJob) error { "raw_data": string(rawData), "updated_at": time.Now(), }).Error; err != nil { - serviceLogger.Errorf("update jimeng job task_id failed: %v", err) + looger.Errorf("update jimeng job task_id failed: %v", err) } // 开始轮询任务状态 @@ -505,7 +514,7 @@ func (s *Service) pollTaskStatus(jobId uint, taskId, reqKey string) error { }) if err != nil { - serviceLogger.Errorf("query jimeng task status failed: %v", err) + looger.Errorf("query jimeng task status failed: %v", err) retryCount++ continue } @@ -555,7 +564,7 @@ func (s *Service) pollTaskStatus(jobId uint, taskId, reqKey string) error { return s.handleTaskError(jobId, fmt.Sprintf("task not found or expired: %s", resp.Data.Status)) default: - serviceLogger.Warnf("unknown task status: %s", resp.Data.Status) + looger.Warnf("unknown task status: %s", resp.Data.Status) } retryCount++ @@ -587,7 +596,7 @@ func (s *Service) UpdateJobProgress(jobId uint, progress int) error { // handleTaskError 处理任务错误 func (s *Service) handleTaskError(jobId uint, errMsg string) error { - serviceLogger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg) + looger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg) return s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, errMsg) } @@ -635,13 +644,12 @@ func (s *Service) DeleteJob(jobId uint, userId uint) error { } // PushTaskToQueue 推送任务到队列 -func (s *Service) PushTaskToQueue(task map[string]interface{}) error { +func (s *Service) PushTaskToQueue(task map[string]any) error { return s.taskQueue.RPush(task) } -// TestConnection 测试即梦AI连接 -func (s *Service) TestConnection(accessKey, secretKey string) error { - // 创建临时客户端进行测试 +// testConnection 测试即梦AI连接 +func (s *Service) testConnection(accessKey, secretKey string) error { testClient := NewClient(accessKey, secretKey) // 使用一个简单的查询任务来测试连接 @@ -655,13 +663,12 @@ func (s *Service) TestConnection(accessKey, secretKey string) error { // 即使任务不存在,只要不是认证错误就说明连接正常 if err != nil { // 检查是否是认证错误 - if err.Error() == "unauthorized" || err.Error() == "access denied" { + if strings.Contains(err.Error(), "InvalidAccessKey") { return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确") } // 其他错误(如任务不存在)说明连接正常 return nil } - return nil } @@ -671,9 +678,9 @@ func (s *Service) UpdateClientConfig(accessKey, secretKey string) error { newClient := NewClient(accessKey, secretKey) // 测试新客户端是否可用 - err := s.TestConnection(accessKey, secretKey) + err := s.testConnection(accessKey, secretKey) if err != nil { - return fmt.Errorf("新配置测试失败: %w", err) + return err } // 更新客户端 @@ -709,18 +716,3 @@ func (s *Service) GetConfig() (*types.JimengConfig, error) { return &jimengConfig, nil } - -// LoadConfigFromDB 从数据库加载配置并更新客户端 -func (s *Service) LoadConfigFromDB() error { - config, err := s.GetConfig() - if err != nil { - return err - } - - // 如果配置中有AccessKey和SecretKey,则更新客户端 - if config.AccessKey != "" && config.SecretKey != "" { - return s.UpdateClientConfig(config.AccessKey, config.SecretKey) - } - - return nil -} diff --git a/web/src/App.vue b/web/src/App.vue index 41319f43..89974207 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -91,7 +91,7 @@ html, body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; - --primary-color: #21aa93 + // --primary-color: #21aa93 h1 { font-size: 2em; } /* 通常是 2em */ h2 { font-size: 1.5em; } /* 通常是 1.5em */ diff --git a/web/src/assets/css/jimeng.styl b/web/src/assets/css/jimeng.styl index fb69e673..8d6b043c 100644 --- a/web/src/assets/css/jimeng.styl +++ b/web/src/assets/css/jimeng.styl @@ -10,9 +10,9 @@ margin: 10px; padding: 20px; border-radius: 12px; - background: #ffffff; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); - color: #333; + background: var(--card-bg); + box-shadow: var(--card-shadow, 0 8px 24px rgba(0,0,0,0.12)); + color: var(--text-theme-color); font-size: 14px; overflow: auto; @@ -20,7 +20,7 @@ font-weight: bold; font-size: 20px; text-align: center; - color: #333; + color: var(--text-theme-color); margin-bottom: 30px; } @@ -34,11 +34,11 @@ margin-bottom: 15px; font-size: 16px; font-weight: 600; - color: #333; + color: var(--text-theme-color); .el-icon { margin-right: 8px; - color: #5865f2; + color: var(--primary-color, #5865f2); } } @@ -52,24 +52,36 @@ flex-direction: column; align-items: center; padding: 15px 10px; - border: 2px solid #f0f0f0; + border: 2px solid var(--border-color, #f0f0f0); border-radius: 12px; cursor: pointer; transition: all 0.3s ease; - background: #fafafa; + background: var(--card-bg-secondary, #fafafa); + /* 暗色主题支持 */ + [data-theme="dark"] & { + background: var(--card-bg-secondary-dark, #23242a); + border-color: var(--border-color-dark, #33343a); + } &:hover { - border-color: #5865f2; - background: #f8f9ff; + border-color: var(--primary-color, #5865f2); + background: var(--card-bg-hover, #f8f9ff); + [data-theme="dark"] & { + background: var(--card-bg-hover-dark, #2a2b31); + } transform: translateY(-2px); } &.active { - border-color: #5865f2; - background: linear-gradient(135deg, #5865f2 0%, #7289da 100%); - color: white; + border-color: var(--primary-color, #5865f2); + background: var(--primary-gradient, linear-gradient(135deg, #5865f2 0%, #7289da 100%)); + color: var(--primary-text-on-primary, #fff); + [data-theme="dark"] & { + background: var(--primary-gradient-dark, linear-gradient(135deg, #23242a 0%, #2a2b31 100%)); + color: var(--primary-text-on-primary-dark, #fff); + } transform: translateY(-2px); - box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3); + box-shadow: var(--primary-shadow, 0 4px 12px rgba(88,101,242,0.3)); } .category-icon { @@ -95,11 +107,11 @@ margin-bottom: 15px; font-size: 16px; font-weight: 600; - color: #333; + color: var(--text-theme-color); .el-icon { margin-right: 8px; - color: #5865f2; + color: var(--primary-color, #5865f2); } } @@ -107,10 +119,14 @@ display: flex; align-items: center; justify-content: space-between; - padding: 15px; - border: 1px solid #e0e0e0; + padding: 5px 15px; + border: 1px solid var(--border-color, #e0e0e0); border-radius: 10px; - background: #f9f9f9; + background: var(--card-bg-secondary, #f9f9f9); + [data-theme="dark"] & { + background: var(--card-bg-secondary-dark, #23242a); + border-color: var(--border-color-dark, #33343a); + } .switch-info { flex: 1; @@ -118,13 +134,13 @@ .switch-title { font-size: 14px; font-weight: 600; - color: #333; + color: var(--text-theme-color); margin-bottom: 4px; } .switch-desc { font-size: 12px; - color: #666; + color: var(--text-sub-color, #666); } } } @@ -145,7 +161,7 @@ align-items: center; margin-bottom: 8px; font-weight: 600; - color: #333; + color: var(--text-theme-color); } } @@ -157,7 +173,7 @@ .label { margin-right: 15px; font-weight: 600; - color: #333; + color: var(--text-theme-color); min-width: 80px; } } @@ -165,9 +181,9 @@ .text-info { margin: 20px 0; padding: 15px; - background: #f0f8ff; + background: var(--info-bg, #f0f8ff); border-radius: 8px; - border-left: 4px solid #5865f2; + border-left: 4px solid var(--primary-color, #5865f2); } .submit-btn { @@ -206,80 +222,87 @@ } .task-list { - .list-box { - .task-item { - display: flex; - align-items: center; - padding: 20px; - margin-bottom: 15px; - background: var(--card-bg); - border-radius: 12px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - - .task-left { - margin-right: 20px; - - .task-preview { - width: 120px; - height: 90px; - border-radius: 8px; - overflow: hidden; - background: #f0f0f0; + .task-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 20px; + padding: 10px 0; + } + .task-item { + display: flex; + flex-direction: column; + background: var(--card-bg); + border-radius: 12px; + box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1)); + overflow: hidden; + min-height: 420px; + height: 100%; + transition: box-shadow 0.2s; + &:hover { + box-shadow: 0 4px 24px rgba(88,101,242,0.12); + } + .task-left { + width: 100%; + flex: none; + .task-preview { + width: 100%; + aspect-ratio: 1.2/1; + min-height: 220px; + max-height: 320px; + background: var(--card-bg-secondary, #f0f0f0); + border-radius: 0; + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + .preview-image, .preview-video { + width: 100%; + height: 100%; + object-fit: cover; + } + .preview-placeholder { display: flex; + flex-direction: column; align-items: center; - justify-content: center; - - .preview-image, .preview-video { - width: 100%; - height: 100%; - object-fit: cover; - } - - .preview-placeholder { - display: flex; - flex-direction: column; - align-items: center; - color: #999; - font-size: 12px; - - .el-icon { - font-size: 24px; - margin-bottom: 5px; - } + color: var(--text-disabled-color, #999); + font-size: 16px; + .el-icon, .iconfont { + font-size: 32px; + margin-bottom: 5px; } } } - - .task-center { - flex: 1; - - .task-info { - display: flex; - gap: 8px; - margin-bottom: 10px; - } - - .task-prompt { - font-size: 14px; - color: var(--text-theme-color); - margin-bottom: 8px; - line-height: 1.4; - } - - .task-meta { - display: flex; - gap: 15px; - font-size: 12px; - color: #999; - } + } + .task-center { + flex: none; + padding: 18px 18px 8px 18px; + .task-info { + display: flex; + gap: 8px; + margin-bottom: 10px; } - - .task-right { - .task-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; - } + .task-prompt { + font-size: 14px; + color: var(--text-theme-color); + margin-bottom: 8px; + line-height: 1.4; + word-break: break-all; + } + .task-meta { + display: flex; + gap: 15px; + font-size: 12px; + color: var(--text-disabled-color, #999); + } + } + .task-right { + flex: none; + padding: 0 18px 16px 18px; + .task-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; + justify-content: flex-end; } } } @@ -308,4 +331,21 @@ padding: 15px; } } +} +@media (max-width: 1200px) { + .task-list .task-grid { + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + } +} +@media (max-width: 768px) { + .task-list .task-grid { + grid-template-columns: 1fr; + } + .task-list .task-item { + min-height: 320px; + .task-left .task-preview { + min-height: 160px; + max-height: 220px; + } + } } \ No newline at end of file diff --git a/web/src/store/jimeng.js b/web/src/store/jimeng.js index 33c0b2f9..43aaeb97 100644 --- a/web/src/store/jimeng.js +++ b/web/src/store/jimeng.js @@ -7,40 +7,42 @@ import nodata from '@/assets/img/no-data.png' import { checkSession } from '@/store/cache' -import { closeLoading, showLoading, showMessageError, showMessageOK } from '@/utils/dialog' +import { showMessageError, showMessageOK } from '@/utils/dialog' import { httpGet, httpPost } from '@/utils/http' -import { replaceImg, substr, dateFormat } from '@/utils/libs' -import { ElMessage, ElMessageBox } from 'element-plus' +import { replaceImg, substr } from '@/utils/libs' +import { ElMessageBox } from 'element-plus' import { defineStore } from 'pinia' -import { computed, reactive, ref } from 'vue' +import { computed, nextTick, reactive, ref } from 'vue' export const useJimengStore = defineStore('jimeng', () => { // 当前激活的功能分类和具体功能 const activeCategory = ref('image_generation') const activeFunction = ref('text_to_image') const useImageInput = ref(false) - + + // 新增:全局提示词 + const currentPrompt = ref('') + // 共同状态 const loading = ref(false) const submitting = ref(false) const list = ref([]) const noData = ref(true) const page = ref(1) - const pageSize = ref(20) + const pageSize = ref(10) const total = ref(0) - const taskPulling = ref(false) - const pullHandler = ref(null) const taskFilter = ref('all') const currentList = ref([]) - + const isOver = ref(false) + // 用户信息 const isLogin = ref(false) const userPower = ref(100) - + // 视频预览 const showDialog = ref(false) const currentVideoUrl = ref('') - + // 功能分类配置 const categories = [ { key: 'image_generation', name: '图片生成' }, @@ -48,29 +50,83 @@ export const useJimengStore = defineStore('jimeng', () => { { key: 'image_effects', name: '图像特效' }, { key: 'video_generation', name: '视频生成' }, ] - + + // 新增:动态获取算力消耗配置 + const powerConfig = reactive({}) + // 功能配置 - const functions = [ - { key: 'text_to_image', name: '文生图', category: 'image_generation', needsPrompt: true, needsImage: false, power: 20 }, - { key: 'image_to_image_portrait', name: '图生图', category: 'image_generation', needsPrompt: true, needsImage: true, power: 30 }, - { key: 'image_edit', name: '图像编辑', category: 'image_editing', needsPrompt: true, needsImage: true, multiple: true, power: 25 }, - { key: 'image_effects', name: '图像特效', category: 'image_effects', needsPrompt: false, needsImage: true, power: 15 }, - { key: 'text_to_video', name: '文生视频', category: 'video_generation', needsPrompt: true, needsImage: false, power: 100 }, - { key: 'image_to_video', name: '图生视频', category: 'video_generation', needsPrompt: true, needsImage: true, multiple: true, power: 120 }, - ] - + const functions = reactive([ + { + key: 'text_to_image', + name: '文生图', + category: 'image_generation', + needsPrompt: true, + needsImage: false, + power: 20, + }, + { + key: 'image_to_image', + name: '图生图', + category: 'image_generation', + needsPrompt: true, + needsImage: true, + power: 30, + }, + { + key: 'image_edit', + name: '图像编辑', + category: 'image_editing', + needsPrompt: true, + needsImage: true, + multiple: true, + power: 25, + }, + { + key: 'image_effects', + name: '图像特效', + category: 'image_effects', + needsPrompt: false, + needsImage: true, + power: 15, + }, + { + key: 'text_to_video', + name: '文生视频', + category: 'video_generation', + needsPrompt: true, + needsImage: false, + power: 100, + }, + { + key: 'image_to_video', + name: '图生视频', + category: 'video_generation', + needsPrompt: true, + needsImage: true, + multiple: true, + power: 120, + }, + ]) + + // 动态设置算力消耗 + const setFunctionPowers = (config) => { + functions.forEach((f) => { + if (config[f.key] !== undefined) { + f.power = config[f.key] + } + }) + } + // 各功能的参数 const textToImageParams = reactive({ - prompt: '', size: '1328x1328', scale: 2.5, seed: -1, - use_pre_llm: false, + use_pre_llm: true, }) - + const imageToImageParams = reactive({ image_input: '', - prompt: '演唱会现场的合照,闪光灯拍摄', size: '1328x1328', gpen: 0.4, skin: 0.3, @@ -78,74 +134,70 @@ export const useJimengStore = defineStore('jimeng', () => { gen_mode: 'creative', seed: -1, }) - + const imageEditParams = reactive({ image_urls: [], - prompt: '', scale: 0.5, seed: -1, }) - + const imageEffectsParams = reactive({ image_input1: '', template_id: '', size: '1328x1328', }) - + const textToVideoParams = reactive({ - prompt: '', aspect_ratio: '16:9', seed: -1, }) - + const imageToVideoParams = reactive({ image_urls: [], - prompt: '', aspect_ratio: '16:9', seed: -1, }) - + // 计算属性 const currentFunction = computed(() => { - return functions.find(f => f.key === activeFunction.value) || functions[0] + return functions.find((f) => f.key === activeFunction.value) || functions[0] }) - + const currentFunctions = computed(() => { - return functions.filter(f => f.category === activeCategory.value) + return functions.filter((f) => f.category === activeCategory.value) }) - + const needsPrompt = computed(() => currentFunction.value.needsPrompt) const needsImage = computed(() => currentFunction.value.needsImage) const needsMultipleImages = computed(() => currentFunction.value.multiple) const currentPowerCost = computed(() => currentFunction.value.power) - + // 初始化方法 const init = async () => { try { + // 获取算力消耗配置 + const powerRes = await httpGet('/api/jimeng/power-config') + if (powerRes.data) { + Object.assign(powerConfig, powerRes.data) + setFunctionPowers(powerRes.data) + } const user = await checkSession() isLogin.value = true userPower.value = user.power - // 获取任务列表 await fetchData(1) - - // 检查是否需要开始轮询 - const pendingCount = await getPendingCount() - if (pendingCount > 0) { - startPolling() - } } catch (error) { console.error('初始化失败:', error) } } - + // 切换功能分类 const switchCategory = (category) => { activeCategory.value = category - const categoryFunctions = functions.filter(f => f.category === category) + const categoryFunctions = functions.filter((f) => f.category === category) if (categoryFunctions.length > 0) { if (category === 'image_generation') { - activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image' + activeFunction.value = useImageInput.value ? 'image_to_image' : 'text_to_image' } else if (category === 'video_generation') { activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video' } else { @@ -153,91 +205,106 @@ export const useJimengStore = defineStore('jimeng', () => { } } } - + // 切换输入模式 const switchInputMode = () => { if (activeCategory.value === 'image_generation') { - activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image' + activeFunction.value = useImageInput.value ? 'image_to_image' : 'text_to_image' } else if (activeCategory.value === 'video_generation') { activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video' } } - + // 切换功能 const switchFunction = (functionKey) => { activeFunction.value = functionKey } - + // 获取当前算力消耗 const getCurrentPowerCost = () => { return currentFunction.value.power } - + // 获取功能名称 const getFunctionName = (type) => { - const func = functions.find(f => f.key === type) + const func = functions.find((f) => f.key === type) return func ? func.name : type } - + // 获取任务状态文本 const getTaskStatusText = (status) => { const statusMap = { - 'pending': '等待中', - 'processing': '处理中', - 'completed': '已完成', - 'failed': '失败' + in_queue: '排队中', + generating: '处理中', + success: '成功', + failed: '失败', + canceled: '已取消', } return statusMap[status] || status } - + // 获取状态类型 const getStatusType = (status) => { const typeMap = { - 'pending': 'info', - 'processing': 'warning', - 'completed': 'success', - 'failed': 'danger' + pending: 'info', + processing: 'warning', + completed: 'success', + failed: 'danger', } return typeMap[status] || 'info' } - + // 切换任务筛选 const switchTaskFilter = (filter) => { taskFilter.value = filter updateCurrentList() } - + // 更新当前列表 const updateCurrentList = () => { if (taskFilter.value === 'all') { currentList.value = list.value } else if (taskFilter.value === 'image') { - currentList.value = list.value.filter(item => - ['text_to_image', 'image_to_image_portrait', 'image_edit', 'image_effects'].includes(item.type) + currentList.value = list.value.filter((item) => + ['text_to_image', 'image_to_image_portrait', 'image_edit', 'image_effects'].includes( + item.type + ) ) } else if (taskFilter.value === 'video') { - currentList.value = list.value.filter(item => + currentList.value = list.value.filter((item) => ['text_to_video', 'image_to_video'].includes(item.type) ) } } - + + // 轮询定时器 + let pollHandler = null + // 获取任务列表 const fetchData = async (pageNum = 1) => { try { loading.value = true page.value = pageNum - + const response = await httpGet('/api/jimeng/jobs', { page: pageNum, - page_size: pageSize.value + page_size: pageSize.value, }) - + if (response.data) { list.value = response.data.jobs || [] total.value = response.data.total || 0 noData.value = list.value.length === 0 updateCurrentList() + // 判断是否有未完成任务 + const hasPending = list.value.some( + (item) => item.status === 'in_queue' || item.status === 'processing' + ) + if (hasPending) { + startPolling() + } else { + stopPolling() + } } } catch (error) { console.error('获取任务列表失败:', error) @@ -246,42 +313,53 @@ export const useJimengStore = defineStore('jimeng', () => { loading.value = false } } - + + // 简单轮询逻辑 + const startPolling = () => { + if (pollHandler) return + pollHandler = setInterval(() => { + fetchData(page.value) + }, 5000) + } + + const stopPolling = () => { + if (pollHandler) { + clearInterval(pollHandler) + pollHandler = null + } + } + // 提交任务 const submitTask = async () => { if (!isLogin.value) { showMessageError('请先登录') return } - if (userPower.value < currentPowerCost.value) { showMessageError('算力不足') return } - + // 新增:除图像特效外,其他任务类型必须有提示词 + if (activeFunction.value !== 'image_effects' && !currentPrompt.value) { + showMessageError('提示词不能为空') + return + } try { submitting.value = true - let apiUrl = '' - let requestData = {} - + let requestData = { task_type: activeFunction.value, prompt: currentPrompt.value } switch (activeFunction.value) { case 'text_to_image': - apiUrl = '/api/jimeng/text-to-image' - requestData = { - prompt: textToImageParams.prompt, + Object.assign(requestData, { width: parseInt(textToImageParams.size.split('x')[0]), height: parseInt(textToImageParams.size.split('x')[1]), scale: textToImageParams.scale, seed: textToImageParams.seed, use_pre_llm: textToImageParams.use_pre_llm, - } + }) break - - case 'image_to_image_portrait': - apiUrl = '/api/jimeng/image-to-image-portrait' - requestData = { + case 'image_to_image': + Object.assign(requestData, { image_input: imageToImageParams.image_input, - prompt: imageToImageParams.prompt, width: parseInt(imageToImageParams.size.split('x')[0]), height: parseInt(imageToImageParams.size.split('x')[1]), gpen: imageToImageParams.gpen, @@ -289,57 +367,41 @@ export const useJimengStore = defineStore('jimeng', () => { skin_unifi: imageToImageParams.skin_unifi, gen_mode: imageToImageParams.gen_mode, seed: imageToImageParams.seed, - } + }) break - case 'image_edit': - apiUrl = '/api/jimeng/image-edit' - requestData = { + Object.assign(requestData, { image_urls: imageEditParams.image_urls, - prompt: imageEditParams.prompt, scale: imageEditParams.scale, seed: imageEditParams.seed, - } + }) break - case 'image_effects': - apiUrl = '/api/jimeng/image-effects' - requestData = { - image_input1: imageEffectsParams.image_input1, + Object.assign(requestData, { + image_input: imageEffectsParams.image_input1, template_id: imageEffectsParams.template_id, width: parseInt(imageEffectsParams.size.split('x')[0]), height: parseInt(imageEffectsParams.size.split('x')[1]), - } + }) break - case 'text_to_video': - apiUrl = '/api/jimeng/text-to-video' - requestData = { - prompt: textToVideoParams.prompt, + Object.assign(requestData, { aspect_ratio: textToVideoParams.aspect_ratio, seed: textToVideoParams.seed, - } + }) break - case 'image_to_video': - apiUrl = '/api/jimeng/image-to-video' - requestData = { + Object.assign(requestData, { image_urls: imageToVideoParams.image_urls, - prompt: imageToVideoParams.prompt, aspect_ratio: imageToVideoParams.aspect_ratio, seed: imageToVideoParams.seed, - } + }) break } - - const response = await httpPost(apiUrl, requestData) - + const response = await httpPost('/api/jimeng/task', requestData) if (response.data) { showMessageOK('任务提交成功') - // 重新获取任务列表 await fetchData(1) - // 开始轮询 - startPolling() } } catch (error) { console.error('提交任务失败:', error) @@ -348,42 +410,7 @@ export const useJimengStore = defineStore('jimeng', () => { submitting.value = false } } - - // 获取待处理任务数量 - const getPendingCount = async () => { - try { - const response = await httpGet('/api/jimeng/pending-count') - return response.data?.count || 0 - } catch (error) { - console.error('获取待处理任务数量失败:', error) - return 0 - } - } - - // 开始轮询 - const startPolling = () => { - if (taskPulling.value) return - - taskPulling.value = true - pullHandler.value = setInterval(async () => { - const pendingCount = await getPendingCount() - if (pendingCount > 0) { - await fetchData(page.value) - } else { - stopPolling() - } - }, 3000) - } - - // 停止轮询 - const stopPolling = () => { - if (pullHandler.value) { - clearInterval(pullHandler.value) - pullHandler.value = null - } - taskPulling.value = false - } - + // 重试任务 const retryTask = async (taskId) => { try { @@ -391,14 +418,13 @@ export const useJimengStore = defineStore('jimeng', () => { if (response.data) { showMessageOK('重试任务已提交') await fetchData(page.value) - startPolling() } } catch (error) { console.error('重试任务失败:', error) showMessageError(error.message || '重试任务失败') } } - + // 删除任务 const removeJob = async (item) => { try { @@ -407,7 +433,7 @@ export const useJimengStore = defineStore('jimeng', () => { cancelButtonText: '取消', type: 'warning', }) - + const response = await httpGet('/api/jimeng/remove', { id: item.id }) if (response.data) { showMessageOK('删除成功') @@ -420,13 +446,13 @@ export const useJimengStore = defineStore('jimeng', () => { } } } - + // 播放视频 const playVideo = (item) => { currentVideoUrl.value = item.video_url showDialog.value = true } - + // 下载文件 const downloadFile = (item) => { const url = item.video_url || item.img_url @@ -437,12 +463,68 @@ export const useJimengStore = defineStore('jimeng', () => { link.click() } } - - // 清理 + + // 画同款功能 + const drawSame = (item) => { + // 联动功能开关 + if (item.type === 'text_to_image' || item.type === 'image_to_image') { + activeCategory.value = 'image_generation' + useImageInput.value = item.type === 'image_to_image' + } else if (item.type === 'text_to_video' || item.type === 'image_to_video') { + activeCategory.value = 'video_generation' + useImageInput.value = item.type === 'image_to_video' + } else if (item.type === 'image_edit') { + activeCategory.value = 'image_editing' + } else if (item.type === 'image_effects') { + activeCategory.value = 'image_effects' + } + switchFunction(item.type) + nextTick(() => { + currentPrompt.value = item.prompt + }) + if (item.type === 'text_to_image') { + if (item.width && item.height) { + textToImageParams.size = `${item.width}x${item.height}` + } + if (item.scale) textToImageParams.scale = item.scale + if (item.seed) textToImageParams.seed = item.seed + if (item.use_pre_llm !== undefined) textToImageParams.use_pre_llm = item.use_pre_llm + } else if (item.type === 'image_to_image') { + if (item.image_input) imageToImageParams.image_input = item.image_input + if (item.width && item.height) { + imageToImageParams.size = `${item.width}x${item.height}` + } + if (item.gpen) imageToImageParams.gpen = item.gpen + if (item.skin) imageToImageParams.skin = item.skin + if (item.skin_unifi) imageToImageParams.skin_unifi = item.skin_unifi + if (item.gen_mode) imageToImageParams.gen_mode = item.gen_mode + if (item.seed) imageToImageParams.seed = item.seed + } else if (item.type === 'image_edit') { + if (item.image_urls) imageEditParams.image_urls = item.image_urls + if (item.scale) imageEditParams.scale = item.scale + if (item.seed) imageEditParams.seed = item.seed + } else if (item.type === 'image_effects') { + if (item.image_input1) imageEffectsParams.image_input1 = item.image_input1 + if (item.template_id) imageEffectsParams.template_id = item.template_id + if (item.width && item.height) { + imageEffectsParams.size = `${item.width}x${item.height}` + } + } else if (item.type === 'text_to_video') { + if (item.aspect_ratio) textToVideoParams.aspect_ratio = item.aspect_ratio + if (item.seed) textToVideoParams.seed = item.seed + } else if (item.type === 'image_to_video') { + if (item.image_urls) imageToVideoParams.image_urls = item.image_urls + if (item.aspect_ratio) imageToVideoParams.aspect_ratio = item.aspect_ratio + if (item.seed) imageToVideoParams.seed = item.seed + } + showMessageOK('已填入全部参数,可直接生成同款') + } + + // 页面卸载时清理轮询 const cleanup = () => { stopPolling() } - + // 返回所有状态和方法 return { // 状态 @@ -463,27 +545,28 @@ export const useJimengStore = defineStore('jimeng', () => { showDialog, currentVideoUrl, nodata, - + // 配置 categories, functions, currentFunctions, - + // 参数 + currentPrompt, textToImageParams, imageToImageParams, imageEditParams, imageEffectsParams, textToVideoParams, imageToVideoParams, - + // 计算属性 currentFunction, needsPrompt, needsImage, needsMultipleImages, currentPowerCost, - + // 方法 init, switchCategory, @@ -497,17 +580,33 @@ export const useJimengStore = defineStore('jimeng', () => { updateCurrentList, fetchData, submitTask, - getPendingCount, - startPolling, - stopPolling, retryTask, removeJob, playVideo, downloadFile, cleanup, - + drawSame, + // 工具函数 substr, replaceImg, } -}) \ No newline at end of file +}) + +export const imageSizeOptions = [ + { label: '1:1 (1328x1328)', value: '1328x1328' }, + { label: '3:2 (1584x1056)', value: '1584x1056' }, + { label: '2:3 (1056x1584)', value: '1056x1584' }, + { label: '4:3 (1472x1104)', value: '1472x1104' }, + { label: '3:4 (1104x1472)', value: '1104x1472' }, + { label: '16:9 (1664x936)', value: '1664x936' }, + { label: '9:16 (936x1664)', value: '936x1664' }, + { label: '21:9 (2016x864)', value: '2016x864' }, + { label: '9:21 (864x2016)', value: '864x2016' }, +] + +export const videoAspectRatioOptions = [ + { label: '1:1 (正方形)', value: '1:1' }, + { label: '16:9 (横版)', value: '16:9' }, + { label: '9:16 (竖版)', value: '9:16' }, +] diff --git a/web/src/views/Jimeng.vue b/web/src/views/Jimeng.vue index 28a13a15..2fc3074f 100644 --- a/web/src/views/Jimeng.vue +++ b/web/src/views/Jimeng.vue @@ -33,21 +33,10 @@
- {{ - store.useImageInput - ? store.activeCategory === 'image_generation' - ? '图生图' - : '图生视频' - : store.activeCategory === 'image_generation' - ? '文生图' - : '文生视频' - }} -
-
- {{ store.useImageInput ? '使用图片作为输入' : '使用文字作为输入' }} + {{ store.activeCategory === 'image_generation' ? '图生图人像写真' : '图生视频' }}
- +
@@ -57,13 +46,10 @@
提示词: - - -
- - - - +
-
- 创意度: - +
+ 创意度 + +
-
- 种子值: - +
智能优化提示词 - +
-
+
上传图片:
@@ -119,7 +103,7 @@
- - - - +
- -
- GPEN强度: - -
- -
- 肌肤质感: - -
- -
- 种子值: - -
@@ -175,7 +141,7 @@
编辑强度:
- -
- 种子值: - -
@@ -225,10 +181,12 @@
- - - - +
@@ -240,7 +198,7 @@
- - - +
- -
- 种子值: - -
@@ -285,7 +236,7 @@
- - - +
- -
- 种子值: - -
-
- - -
- 当前算力: {{ store.userPower }} - 消耗: {{ store.currentPowerCost }}
-
+
-
-
-
-
- -
@@ -469,12 +434,17 @@ + + diff --git a/web/src/views/admin/jimeng/JimengSetting.vue b/web/src/views/admin/jimeng/JimengSetting.vue index e4b17650..b77c4980 100644 --- a/web/src/views/admin/jimeng/JimengSetting.vue +++ b/web/src/views/admin/jimeng/JimengSetting.vue @@ -54,7 +54,6 @@ @@ -78,7 +77,6 @@ @@ -102,7 +100,6 @@ @@ -126,7 +123,6 @@ @@ -150,7 +146,6 @@ @@ -174,7 +169,6 @@ @@ -243,16 +237,10 @@ const saveConfig = async () => { try { await configFormRef.value.validate() saving.value = true - - await httpPost('/api/admin/jimeng/config', { - config: jimengConfig.value, - }) - + await httpPost('/api/admin/jimeng/config/update', jimengConfig.value) ElMessage.success('配置保存成功!') } catch (e) { - if (e.message) { - ElMessage.error('保存失败:' + e.message) - } + ElMessage.error(e.message) } finally { saving.value = false }