mirror of
https://github.com/yangjian102621/geekai.git
synced 2026-04-28 14:04:48 +08:00
增加即梦AI功能页面
This commit is contained in:
@@ -43,9 +43,16 @@ type SmtpConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ApiConfig struct {
|
type ApiConfig struct {
|
||||||
ApiURL string
|
ApiURL string
|
||||||
AppId string
|
AppId string
|
||||||
Token string
|
Token string
|
||||||
|
JimengConfig JimengConfig // 即梦AI配置
|
||||||
|
}
|
||||||
|
|
||||||
|
// JimengConfig 即梦AI配置
|
||||||
|
type JimengConfig struct {
|
||||||
|
AccessKey string // 火山引擎AccessKey
|
||||||
|
SecretKey string // 火山引擎SecretKey
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlipayConfig struct {
|
type AlipayConfig struct {
|
||||||
@@ -170,7 +177,7 @@ type SystemConfig struct {
|
|||||||
|
|
||||||
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
|
||||||
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
|
||||||
TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的大模型 id
|
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
|
||||||
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
177
api/handler/admin/jimeng_handler.go
Normal file
177
api/handler/admin/jimeng_handler.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"geekai/core"
|
||||||
|
"geekai/handler"
|
||||||
|
"geekai/service/jimeng"
|
||||||
|
"geekai/store/model"
|
||||||
|
"geekai/utils/resp"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdminJimengHandler 管理后台即梦AI处理器
|
||||||
|
type AdminJimengHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
jimengService *jimeng.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAdminJimengHandler 创建管理后台即梦AI处理器
|
||||||
|
func NewAdminJimengHandler(app *core.AppServer, jimengService *jimeng.Service) *AdminJimengHandler {
|
||||||
|
return &AdminJimengHandler{
|
||||||
|
BaseHandler: handler.BaseHandler{App: app},
|
||||||
|
jimengService: jimengService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jobs 获取任务列表
|
||||||
|
func (h *AdminJimengHandler) Jobs(c *gin.Context) {
|
||||||
|
page := h.GetInt(c, "page", 1)
|
||||||
|
pageSize := h.GetInt(c, "page_size", 20)
|
||||||
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
|
taskType := h.GetTrim(c, "type")
|
||||||
|
status := h.GetTrim(c, "status")
|
||||||
|
|
||||||
|
var tasks []model.JimengJob
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
session := h.DB.Model(&model.JimengJob{})
|
||||||
|
|
||||||
|
// 构建查询条件
|
||||||
|
if userId > 0 {
|
||||||
|
session = session.Where("user_id = ?", userId)
|
||||||
|
}
|
||||||
|
if taskType != "" {
|
||||||
|
session = session.Where("type = ?", taskType)
|
||||||
|
}
|
||||||
|
if status != "" {
|
||||||
|
session = session.Where("status = ?", status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取总数
|
||||||
|
err := session.Count(&total).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "获取任务数量失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
err = session.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tasks).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "获取任务列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{
|
||||||
|
"jobs": tasks,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobDetail 获取任务详情
|
||||||
|
func (h *AdminJimengHandler) JobDetail(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
jobId, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var job model.JimengJob
|
||||||
|
err = h.DB.Where("id = ?", jobId).First(&job).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "任务不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除任务
|
||||||
|
func (h *AdminJimengHandler) Remove(c *gin.Context) {
|
||||||
|
idStr := c.Param("id")
|
||||||
|
jobId, err := strconv.ParseUint(idStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.DB.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "删除任务失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchRemove 批量删除任务
|
||||||
|
func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
|
||||||
|
var req struct {
|
||||||
|
JobIds []uint `json:"job_ids" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
resp.ERROR(c, "参数错误")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result := h.DB.Where("id IN ?", req.JobIds).Delete(&model.JimengJob{})
|
||||||
|
if result.Error != nil {
|
||||||
|
resp.ERROR(c, "批量删除失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{
|
||||||
|
"message": "批量删除成功",
|
||||||
|
"deleted_count": result.RowsAffected,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats 获取统计信息
|
||||||
|
func (h *AdminJimengHandler) Stats(c *gin.Context) {
|
||||||
|
type StatResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats []StatResult
|
||||||
|
err := h.DB.Model(&model.JimengJob{}).
|
||||||
|
Select("status, COUNT(*) as count").
|
||||||
|
Group("status").
|
||||||
|
Find(&stats).Error
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "获取统计信息失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整理统计数据
|
||||||
|
result := gin.H{
|
||||||
|
"totalTasks": int64(0),
|
||||||
|
"completedTasks": int64(0),
|
||||||
|
"processingTasks": int64(0),
|
||||||
|
"failedTasks": int64(0),
|
||||||
|
"pendingTasks": int64(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stat := range stats {
|
||||||
|
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
|
||||||
|
switch stat.Status {
|
||||||
|
case "completed":
|
||||||
|
result["completedTasks"] = stat.Count
|
||||||
|
case "processing":
|
||||||
|
result["processingTasks"] = stat.Count
|
||||||
|
case "failed":
|
||||||
|
result["failedTasks"] = stat.Count
|
||||||
|
case "pending":
|
||||||
|
result["pendingTasks"] = stat.Count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, result)
|
||||||
|
}
|
||||||
@@ -77,7 +77,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
|
|||||||
Quality: data.Quality,
|
Quality: data.Quality,
|
||||||
Size: data.Size,
|
Size: data.Size,
|
||||||
Style: data.Style,
|
Style: data.Style,
|
||||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||||
Power: chatModel.Power,
|
Power: chatModel.Power,
|
||||||
}
|
}
|
||||||
job := model.DallJob{
|
job := model.DallJob{
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
|
|||||||
Prompt: prompt,
|
Prompt: prompt,
|
||||||
ModelId: 0,
|
ModelId: 0,
|
||||||
ModelName: "dall-e-3",
|
ModelName: "dall-e-3",
|
||||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||||
N: 1,
|
N: 1,
|
||||||
Quality: "standard",
|
Quality: "standard",
|
||||||
Size: "1024x1024",
|
Size: "1024x1024",
|
||||||
|
|||||||
639
api/handler/jimeng_handler.go
Normal file
639
api/handler/jimeng_handler.go
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geekai/core"
|
||||||
|
"geekai/core/types"
|
||||||
|
"geekai/service/jimeng"
|
||||||
|
"geekai/store/model"
|
||||||
|
"geekai/utils/resp"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JimengHandler 即梦AI处理器
|
||||||
|
type JimengHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
jimengService *jimeng.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJimengHandler 创建即梦AI处理器
|
||||||
|
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service) *JimengHandler {
|
||||||
|
return &JimengHandler{
|
||||||
|
BaseHandler: BaseHandler{App: app},
|
||||||
|
jimengService: jimengService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户
|
||||||
|
user, err := h.GetLoginUser(c)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户算力
|
||||||
|
if user.Power < 20 { // 文生图消耗20算力
|
||||||
|
resp.ERROR(c, "算力不足")
|
||||||
|
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.JimengJobTypeTextToImage,
|
||||||
|
Prompt: req.Prompt,
|
||||||
|
Params: params,
|
||||||
|
ReqKey: model.ReqKeyTextToImage,
|
||||||
|
Power: 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 20, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户算力
|
||||||
|
if user.Power < 30 { // 图生图消耗30算力
|
||||||
|
resp.ERROR(c, "算力不足")
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.JimengJobTypeImageToImagePortrait,
|
||||||
|
Prompt: req.Prompt,
|
||||||
|
Params: params,
|
||||||
|
ReqKey: model.ReqKeyImageToImagePortrait,
|
||||||
|
Power: 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "创建任务失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣除用户算力
|
||||||
|
h.subUserPower(user.Id, 30, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户算力
|
||||||
|
if user.Power < 25 { // 图像编辑消耗25算力
|
||||||
|
resp.ERROR(c, "算力不足")
|
||||||
|
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.JimengJobTypeImageEdit,
|
||||||
|
Prompt: req.Prompt,
|
||||||
|
Params: params,
|
||||||
|
ReqKey: model.ReqKeyImageEdit,
|
||||||
|
Power: 25,
|
||||||
|
}
|
||||||
|
|
||||||
|
job, err := h.jimengService.CreateTask(user.Id, taskReq)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("create jimeng image edit task failed: %v", err)
|
||||||
|
resp.ERROR(c, "创建任务失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣除用户算力
|
||||||
|
h.subUserPower(user.Id, 25, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户算力
|
||||||
|
if user.Power < 15 { // 图像特效消耗15算力
|
||||||
|
resp.ERROR(c, "算力不足")
|
||||||
|
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.JimengJobTypeImageEffects,
|
||||||
|
Prompt: "",
|
||||||
|
Params: params,
|
||||||
|
ReqKey: model.ReqKeyImageEffects,
|
||||||
|
Power: 15,
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 15, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户算力
|
||||||
|
if user.Power < 100 { // 文生视频消耗100算力
|
||||||
|
resp.ERROR(c, "算力不足")
|
||||||
|
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.JimengJobTypeTextToVideo,
|
||||||
|
Prompt: req.Prompt,
|
||||||
|
Params: params,
|
||||||
|
ReqKey: model.ReqKeyTextToVideo,
|
||||||
|
Power: 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 100, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户算力
|
||||||
|
if user.Power < 120 { // 图生视频消耗120算力
|
||||||
|
resp.ERROR(c, "算力不足")
|
||||||
|
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.JimengJobTypeImageToVideo,
|
||||||
|
Prompt: req.Prompt,
|
||||||
|
Params: params,
|
||||||
|
ReqKey: model.ReqKeyImageToVideo,
|
||||||
|
Power: 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 120, model.PowerLog{
|
||||||
|
Type: types.PowerConsume,
|
||||||
|
Model: "即梦图生视频",
|
||||||
|
Remark: fmt.Sprintf("任务ID:%d", job.Id),
|
||||||
|
})
|
||||||
|
|
||||||
|
resp.SUCCESS(c, job)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jobs 获取任务列表
|
||||||
|
func (h *JimengHandler) Jobs(c *gin.Context) {
|
||||||
|
user, err := h.GetLoginUser(c)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := h.GetInt(c, "page", 1)
|
||||||
|
pageSize := h.GetInt(c, "page_size", 20)
|
||||||
|
|
||||||
|
jobs, total, err := h.jimengService.GetUserJobs(user.Id, page, pageSize)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("get user jimeng jobs failed: %v", err)
|
||||||
|
resp.ERROR(c, "获取任务列表失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{
|
||||||
|
"jobs": jobs,
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": pageSize,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobId := h.GetInt(c, "id", 0)
|
||||||
|
if jobId == 0 {
|
||||||
|
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, "删除任务失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry 重试任务
|
||||||
|
func (h *JimengHandler) Retry(c *gin.Context) {
|
||||||
|
user, err := h.GetLoginUser(c)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jobIdStr := c.Param("id")
|
||||||
|
jobId, err := strconv.ParseUint(jobIdStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "参数错误")
|
||||||
|
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.JimengJobStatusFailed {
|
||||||
|
resp.ERROR(c, "只有失败的任务才能重试")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置任务状态
|
||||||
|
if err := h.jimengService.UpdateJobStatus(uint(jobId), model.JimengJobStatusPending, ""); err != nil {
|
||||||
|
logger.Errorf("reset job status failed: %v", err)
|
||||||
|
resp.ERROR(c, "重置任务状态失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新推送到队列
|
||||||
|
task := map[string]interface{}{
|
||||||
|
"job_id": jobId,
|
||||||
|
"type": job.Type,
|
||||||
|
}
|
||||||
|
if err := h.jimengService.PushTaskToQueue(task); err != nil {
|
||||||
|
logger.Errorf("push retry task to queue failed: %v", err)
|
||||||
|
resp.ERROR(c, "推送重试任务失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// subUserPower 扣除用户算力
|
||||||
|
func (h *JimengHandler) subUserPower(userId uint, power int, powerLog model.PowerLog) {
|
||||||
|
session := h.DB.Session(&gorm.Session{})
|
||||||
|
|
||||||
|
// 更新用户算力
|
||||||
|
if err := session.Model(&model.User{}).Where("id = ?", userId).UpdateColumn("power", gorm.Expr("power - ?", power)).Error; err != nil {
|
||||||
|
logger.Errorf("update user power failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录算力消费日志
|
||||||
|
powerLog.UserId = userId
|
||||||
|
powerLog.Amount = power
|
||||||
|
powerLog.Mark = types.PowerSub
|
||||||
|
powerLog.CreatedAt = time.Now()
|
||||||
|
if err := session.Create(&powerLog).Error; err != nil {
|
||||||
|
logger.Errorf("create power log failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Commit()
|
||||||
|
}
|
||||||
@@ -160,7 +160,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
|
|||||||
UserId: userId,
|
UserId: userId,
|
||||||
ImgArr: data.ImgArr,
|
ImgArr: data.ImgArr,
|
||||||
Mode: h.App.SysConfig.MjMode,
|
Mode: h.App.SysConfig.MjMode,
|
||||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||||
}
|
}
|
||||||
job := model.MidJourneyJob{
|
job := model.MidJourneyJob{
|
||||||
Type: data.TaskType,
|
Type: data.TaskType,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
|
|||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.AssistantModelId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
@@ -79,7 +79,7 @@ func (h *PromptHandler) Image(c *gin.Context) {
|
|||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.AssistantModelId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
@@ -108,7 +108,7 @@ func (h *PromptHandler) Video(c *gin.Context) {
|
|||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId)
|
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.AssistantModelId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, err.Error())
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
@@ -158,9 +158,9 @@ func (h *PromptHandler) MetaPrompt(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *PromptHandler) getPromptModel() string {
|
func (h *PromptHandler) getPromptModel() string {
|
||||||
if h.App.SysConfig.TranslateModelId > 0 {
|
if h.App.SysConfig.AssistantModelId > 0 {
|
||||||
var chatModel model.ChatModel
|
var chatModel model.ChatModel
|
||||||
h.DB.Where("id", h.App.SysConfig.TranslateModelId).First(&chatModel)
|
h.DB.Where("id", h.App.SysConfig.AssistantModelId).First(&chatModel)
|
||||||
return chatModel.Value
|
return chatModel.Value
|
||||||
}
|
}
|
||||||
return "gpt-4o"
|
return "gpt-4o"
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
|
|||||||
HdSteps: data.HdSteps,
|
HdSteps: data.HdSteps,
|
||||||
},
|
},
|
||||||
UserId: userId,
|
UserId: userId,
|
||||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||||
}
|
}
|
||||||
|
|
||||||
job := model.SdJob{
|
job := model.SdJob{
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
|
|||||||
Type: types.VideoLuma,
|
Type: types.VideoLuma,
|
||||||
Prompt: data.Prompt,
|
Prompt: data.Prompt,
|
||||||
Params: params,
|
Params: params,
|
||||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||||
}
|
}
|
||||||
// 插入数据库
|
// 插入数据库
|
||||||
job := model.VideoJob{
|
job := model.VideoJob{
|
||||||
@@ -181,7 +181,7 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) {
|
|||||||
Type: types.VideoKeLing,
|
Type: types.VideoKeLing,
|
||||||
Prompt: data.Prompt,
|
Prompt: data.Prompt,
|
||||||
Params: params,
|
Params: params,
|
||||||
TranslateModelId: h.App.SysConfig.TranslateModelId,
|
TranslateModelId: h.App.SysConfig.AssistantModelId,
|
||||||
Channel: data.Channel,
|
Channel: data.Channel,
|
||||||
}
|
}
|
||||||
// 插入数据库
|
// 插入数据库
|
||||||
|
|||||||
39
api/main.go
39
api/main.go
@@ -17,6 +17,7 @@ import (
|
|||||||
logger2 "geekai/logger"
|
logger2 "geekai/logger"
|
||||||
"geekai/service"
|
"geekai/service"
|
||||||
"geekai/service/dalle"
|
"geekai/service/dalle"
|
||||||
|
"geekai/service/jimeng"
|
||||||
"geekai/service/mj"
|
"geekai/service/mj"
|
||||||
"geekai/service/oss"
|
"geekai/service/oss"
|
||||||
"geekai/service/payment"
|
"geekai/service/payment"
|
||||||
@@ -140,6 +141,7 @@ func main() {
|
|||||||
fx.Provide(handler.NewProductHandler),
|
fx.Provide(handler.NewProductHandler),
|
||||||
fx.Provide(handler.NewConfigHandler),
|
fx.Provide(handler.NewConfigHandler),
|
||||||
fx.Provide(handler.NewPowerLogHandler),
|
fx.Provide(handler.NewPowerLogHandler),
|
||||||
|
fx.Provide(handler.NewJimengHandler),
|
||||||
|
|
||||||
fx.Provide(admin.NewConfigHandler),
|
fx.Provide(admin.NewConfigHandler),
|
||||||
fx.Provide(admin.NewAdminHandler),
|
fx.Provide(admin.NewAdminHandler),
|
||||||
@@ -153,6 +155,9 @@ func main() {
|
|||||||
fx.Provide(admin.NewOrderHandler),
|
fx.Provide(admin.NewOrderHandler),
|
||||||
fx.Provide(admin.NewChatHandler),
|
fx.Provide(admin.NewChatHandler),
|
||||||
fx.Provide(admin.NewPowerLogHandler),
|
fx.Provide(admin.NewPowerLogHandler),
|
||||||
|
fx.Provide(func(app *core.AppServer, service *jimeng.Service) *admin.AdminJimengHandler {
|
||||||
|
return admin.NewAdminJimengHandler(app, service)
|
||||||
|
}),
|
||||||
|
|
||||||
// 创建服务
|
// 创建服务
|
||||||
fx.Provide(sms.NewSendServiceManager),
|
fx.Provide(sms.NewSendServiceManager),
|
||||||
@@ -203,6 +208,17 @@ func main() {
|
|||||||
s.SyncTaskProgress()
|
s.SyncTaskProgress()
|
||||||
s.DownloadFiles()
|
s.DownloadFiles()
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 即梦AI 服务
|
||||||
|
fx.Provide(func(config *types.AppConfig) *jimeng.Client {
|
||||||
|
return jimeng.NewClient(config.ApiConfig.JimengConfig.AccessKey, config.ApiConfig.JimengConfig.SecretKey)
|
||||||
|
}),
|
||||||
|
fx.Provide(jimeng.NewService),
|
||||||
|
fx.Provide(jimeng.NewConsumer),
|
||||||
|
fx.Invoke(func(consumer *jimeng.Consumer) {
|
||||||
|
consumer.Start()
|
||||||
|
go consumer.MonitorQueue()
|
||||||
|
}),
|
||||||
fx.Provide(service.NewUserService),
|
fx.Provide(service.NewUserService),
|
||||||
fx.Provide(payment.NewAlipayService),
|
fx.Provide(payment.NewAlipayService),
|
||||||
fx.Provide(payment.NewHuPiPay),
|
fx.Provide(payment.NewHuPiPay),
|
||||||
@@ -496,6 +512,29 @@ func main() {
|
|||||||
group.GET("remove", h.Remove)
|
group.GET("remove", h.Remove)
|
||||||
group.GET("publish", h.Publish)
|
group.GET("publish", h.Publish)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// 即梦AI 路由
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) {
|
||||||
|
group := s.Engine.Group("/api/jimeng")
|
||||||
|
group.POST("text-to-image", h.TextToImage)
|
||||||
|
group.POST("image-to-image-portrait", h.ImageToImagePortrait)
|
||||||
|
group.POST("image-edit", h.ImageEdit)
|
||||||
|
group.POST("image-effects", h.ImageEffects)
|
||||||
|
group.POST("text-to-video", h.TextToVideo)
|
||||||
|
group.POST("image-to-video", h.ImageToVideo)
|
||||||
|
group.GET("jobs", h.Jobs)
|
||||||
|
group.GET("pending-count", h.PendingCount)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
group.POST("retry/:id", h.Retry)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/jimeng")
|
||||||
|
group.GET("jobs", h.Jobs)
|
||||||
|
group.GET("job/:id", h.JobDetail)
|
||||||
|
group.DELETE("job/:id", h.Remove)
|
||||||
|
group.POST("batch-remove", h.BatchRemove)
|
||||||
|
group.GET("stats", h.Stats)
|
||||||
|
}),
|
||||||
fx.Provide(admin.NewChatAppTypeHandler),
|
fx.Provide(admin.NewChatAppTypeHandler),
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
|
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
|
||||||
group := s.Engine.Group("/api/admin/app/type")
|
group := s.Engine.Group("/api/admin/app/type")
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
|||||||
// PushTask push a new mj task in to task queue
|
// PushTask push a new mj task in to task queue
|
||||||
func (s *Service) PushTask(task types.DallTask) {
|
func (s *Service) PushTask(task types.DallTask) {
|
||||||
logger.Infof("add a new DALL-E task to the task list: %+v", task)
|
logger.Infof("add a new DALL-E task to the task list: %+v", task)
|
||||||
s.taskQueue.RPush(task)
|
if err := s.taskQueue.RPush(task); err != nil {
|
||||||
|
logger.Errorf("push dall-e task to queue failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Run() {
|
func (s *Service) Run() {
|
||||||
|
|||||||
332
api/service/jimeng/client.go
Normal file
332
api/service/jimeng/client.go
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
package jimeng
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geekai/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientLogger = logger.GetLogger()
|
||||||
|
|
||||||
|
// Client 即梦API客户端
|
||||||
|
type Client struct {
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
region string
|
||||||
|
service string
|
||||||
|
baseURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建即梦API客户端
|
||||||
|
func NewClient(accessKey, secretKey string) *Client {
|
||||||
|
return &Client{
|
||||||
|
accessKey: accessKey,
|
||||||
|
secretKey: secretKey,
|
||||||
|
region: "cn-north-1",
|
||||||
|
service: "cv",
|
||||||
|
baseURL: "https://visual.volcengineapi.com",
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitTask 提交任务
|
||||||
|
func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error) {
|
||||||
|
// 构建请求URL
|
||||||
|
queryParams := map[string]string{
|
||||||
|
"Action": "CVSync2AsyncSubmitTask",
|
||||||
|
"Version": "2022-08-31",
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := c.buildURL(queryParams)
|
||||||
|
|
||||||
|
// 序列化请求体
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal request body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建HTTP请求
|
||||||
|
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create http request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 签名请求
|
||||||
|
if err := c.signRequest(httpReq, reqBody); err != nil {
|
||||||
|
return nil, fmt.Errorf("sign request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send http request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientLogger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result SubmitTaskResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryTask 查询任务
|
||||||
|
func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
|
||||||
|
// 构建请求URL
|
||||||
|
queryParams := map[string]string{
|
||||||
|
"Action": "CVSync2AsyncGetResult",
|
||||||
|
"Version": "2022-08-31",
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := c.buildURL(queryParams)
|
||||||
|
|
||||||
|
// 序列化请求体
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal request body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建HTTP请求
|
||||||
|
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create http request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 签名请求
|
||||||
|
if err := c.signRequest(httpReq, reqBody); err != nil {
|
||||||
|
return nil, fmt.Errorf("sign request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send http request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientLogger.Infof("Jimeng QueryTask Response: %s", string(respBody))
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result QueryTaskResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitSyncTask 提交同步任务(仅用于文生图)
|
||||||
|
func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*QueryTaskResponse, error) {
|
||||||
|
// 构建请求URL
|
||||||
|
queryParams := map[string]string{
|
||||||
|
"Action": "CVProcess",
|
||||||
|
"Version": "2022-08-31",
|
||||||
|
}
|
||||||
|
|
||||||
|
reqURL := c.buildURL(queryParams)
|
||||||
|
|
||||||
|
// 序列化请求体
|
||||||
|
reqBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal request body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建HTTP请求
|
||||||
|
httpReq, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(reqBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create http request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置请求头
|
||||||
|
httpReq.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// 签名请求
|
||||||
|
if err := c.signRequest(httpReq, reqBody); err != nil {
|
||||||
|
return nil, fmt.Errorf("sign request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
resp, err := c.httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("send http request failed: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("read response body failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientLogger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody))
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
var result QueryTaskResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildURL 构建请求URL
|
||||||
|
func (c *Client) buildURL(queryParams map[string]string) string {
|
||||||
|
u, _ := url.Parse(c.baseURL)
|
||||||
|
q := u.Query()
|
||||||
|
for k, v := range queryParams {
|
||||||
|
q.Set(k, v)
|
||||||
|
}
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
return u.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// signRequest 签名请求
|
||||||
|
func (c *Client) signRequest(req *http.Request, body []byte) error {
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
// 设置基本头部
|
||||||
|
req.Header.Set("X-Date", now.Format("20060102T150405Z"))
|
||||||
|
req.Header.Set("Host", req.URL.Host)
|
||||||
|
|
||||||
|
// 计算内容哈希
|
||||||
|
contentHash := sha256.Sum256(body)
|
||||||
|
req.Header.Set("X-Content-Sha256", hex.EncodeToString(contentHash[:]))
|
||||||
|
|
||||||
|
// 构建签名字符串
|
||||||
|
canonicalRequest := c.buildCanonicalRequest(req)
|
||||||
|
credentialScope := fmt.Sprintf("%s/%s/%s/request", now.Format("20060102"), c.region, c.service)
|
||||||
|
stringToSign := fmt.Sprintf("HMAC-SHA256\n%s\n%s\n%s",
|
||||||
|
now.Format("20060102T150405Z"), credentialScope, sha256Hash(canonicalRequest))
|
||||||
|
|
||||||
|
// 计算签名
|
||||||
|
signature := c.calculateSignature(stringToSign, now)
|
||||||
|
|
||||||
|
// 设置Authorization头部
|
||||||
|
authorization := fmt.Sprintf("HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s",
|
||||||
|
c.accessKey, credentialScope, c.getSignedHeaders(req), signature)
|
||||||
|
req.Header.Set("Authorization", authorization)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildCanonicalRequest 构建规范请求
|
||||||
|
func (c *Client) buildCanonicalRequest(req *http.Request) string {
|
||||||
|
// HTTP方法
|
||||||
|
method := req.Method
|
||||||
|
|
||||||
|
// 规范URI
|
||||||
|
uri := req.URL.Path
|
||||||
|
if uri == "" {
|
||||||
|
uri = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 规范查询字符串
|
||||||
|
query := req.URL.Query()
|
||||||
|
var queryParts []string
|
||||||
|
for k, v := range query {
|
||||||
|
for _, val := range v {
|
||||||
|
queryParts = append(queryParts, fmt.Sprintf("%s=%s", url.QueryEscape(k), url.QueryEscape(val)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(queryParts)
|
||||||
|
canonicalQuery := strings.Join(queryParts, "&")
|
||||||
|
|
||||||
|
// 规范头部
|
||||||
|
var headerParts []string
|
||||||
|
headers := make(map[string]string)
|
||||||
|
for k, v := range req.Header {
|
||||||
|
key := strings.ToLower(k)
|
||||||
|
if len(v) > 0 {
|
||||||
|
headers[key] = strings.TrimSpace(v[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerKeys []string
|
||||||
|
for k := range headers {
|
||||||
|
headerKeys = append(headerKeys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(headerKeys)
|
||||||
|
|
||||||
|
for _, k := range headerKeys {
|
||||||
|
headerParts = append(headerParts, fmt.Sprintf("%s:%s", k, headers[k]))
|
||||||
|
}
|
||||||
|
canonicalHeaders := strings.Join(headerParts, "\n") + "\n"
|
||||||
|
|
||||||
|
// 签名头部
|
||||||
|
signedHeaders := c.getSignedHeaders(req)
|
||||||
|
|
||||||
|
// 载荷哈希
|
||||||
|
payloadHash := req.Header.Get("X-Content-Sha256")
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
|
||||||
|
method, uri, canonicalQuery, canonicalHeaders, signedHeaders, payloadHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSignedHeaders 获取签名头部
|
||||||
|
func (c *Client) getSignedHeaders(req *http.Request) string {
|
||||||
|
var headers []string
|
||||||
|
for k := range req.Header {
|
||||||
|
headers = append(headers, strings.ToLower(k))
|
||||||
|
}
|
||||||
|
sort.Strings(headers)
|
||||||
|
return strings.Join(headers, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateSignature 计算签名
|
||||||
|
func (c *Client) calculateSignature(stringToSign string, t time.Time) string {
|
||||||
|
kDate := hmacSha256([]byte("HMAC-SHA256"+c.secretKey), []byte(t.Format("20060102")))
|
||||||
|
kRegion := hmacSha256(kDate, []byte(c.region))
|
||||||
|
kService := hmacSha256(kRegion, []byte(c.service))
|
||||||
|
kSigning := hmacSha256(kService, []byte("request"))
|
||||||
|
signature := hmacSha256(kSigning, []byte(stringToSign))
|
||||||
|
return hex.EncodeToString(signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hmacSha256 计算HMAC-SHA256
|
||||||
|
func hmacSha256(key []byte, data []byte) []byte {
|
||||||
|
h := hmac.New(sha256.New, key)
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sha256Hash 计算SHA256哈希
|
||||||
|
func sha256Hash(data string) string {
|
||||||
|
hash := sha256.Sum256([]byte(data))
|
||||||
|
return hex.EncodeToString(hash[:])
|
||||||
|
}
|
||||||
177
api/service/jimeng/consumer.go
Normal file
177
api/service/jimeng/consumer.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
package jimeng
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"geekai/logger"
|
||||||
|
"geekai/store/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
var jimengLogger = logger.GetLogger()
|
||||||
|
|
||||||
|
// Consumer 即梦任务消费者
|
||||||
|
type Consumer struct {
|
||||||
|
service *Service
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConsumer 创建即梦任务消费者
|
||||||
|
func NewConsumer(service *Service) *Consumer {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &Consumer{
|
||||||
|
service: service,
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动消费者
|
||||||
|
func (c *Consumer) Start() {
|
||||||
|
jimengLogger.Info("Starting Jimeng task consumer...")
|
||||||
|
go c.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止消费者
|
||||||
|
func (c *Consumer) Stop() {
|
||||||
|
jimengLogger.Info("Stopping Jimeng task consumer...")
|
||||||
|
c.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume 消费任务
|
||||||
|
func (c *Consumer) consume() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
jimengLogger.Info("Jimeng task consumer stopped")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c.processTask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processTask 处理任务
|
||||||
|
func (c *Consumer) processTask() {
|
||||||
|
// 从队列中获取任务
|
||||||
|
var task map[string]interface{}
|
||||||
|
if err := c.service.taskQueue.LPop(&task); err != nil {
|
||||||
|
// 队列为空,等待1秒后重试
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析任务
|
||||||
|
jobIdFloat, ok := task["job_id"].(float64)
|
||||||
|
if !ok {
|
||||||
|
jimengLogger.Errorf("invalid job_id in task: %v", task)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jobId := uint(jobIdFloat)
|
||||||
|
|
||||||
|
taskType, ok := task["type"].(string)
|
||||||
|
if !ok {
|
||||||
|
jimengLogger.Errorf("invalid task type in task: %v", task)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jimengLogger.Infof("Processing Jimeng task: job_id=%d, type=%s", jobId, taskType)
|
||||||
|
|
||||||
|
// 处理任务
|
||||||
|
if err := c.service.ProcessTask(jobId); err != nil {
|
||||||
|
jimengLogger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
|
||||||
|
|
||||||
|
// 任务失败,直接标记为失败状态,不进行重试
|
||||||
|
c.service.UpdateJobStatus(jobId, model.JimengJobStatusFailed, err.Error())
|
||||||
|
} else {
|
||||||
|
jimengLogger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskQueueStatus 任务队列状态
|
||||||
|
type TaskQueueStatus struct {
|
||||||
|
QueueLength int `json:"queue_length"`
|
||||||
|
ActiveTasks int `json:"active_tasks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetQueueStatus 获取队列状态
|
||||||
|
func (c *Consumer) GetQueueStatus() (*TaskQueueStatus, error) {
|
||||||
|
// 获取队列长度
|
||||||
|
length, err := c.service.taskQueue.Size()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取活跃任务数(正在处理的任务)
|
||||||
|
activeTasks, err := c.service.GetPendingTaskCount(0) // 0表示所有用户
|
||||||
|
if err != nil {
|
||||||
|
activeTasks = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return &TaskQueueStatus{
|
||||||
|
QueueLength: int(length),
|
||||||
|
ActiveTasks: int(activeTasks),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonitorQueue 监控队列状态
|
||||||
|
func (c *Consumer) MonitorQueue() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second) // 每30秒监控一次
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
status, err := c.GetQueueStatus()
|
||||||
|
if err != nil {
|
||||||
|
jimengLogger.Errorf("get queue status failed: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.QueueLength > 0 || status.ActiveTasks > 0 {
|
||||||
|
jimengLogger.Infof("Jimeng queue status: queue_length=%d, active_tasks=%d",
|
||||||
|
status.QueueLength, status.ActiveTasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushTaskToQueue 推送任务到队列(用于手动重试)
|
||||||
|
func (c *Consumer) PushTaskToQueue(task map[string]interface{}) error {
|
||||||
|
return c.service.taskQueue.RPush(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTaskStats 获取任务统计信息
|
||||||
|
func (c *Consumer) GetTaskStats() (map[string]interface{}, error) {
|
||||||
|
type StatResult struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats []StatResult
|
||||||
|
err := c.service.db.Model(&model.JimengJob{}).
|
||||||
|
Select("status, COUNT(*) as count").
|
||||||
|
Group("status").
|
||||||
|
Find(&stats).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"total": int64(0),
|
||||||
|
"completed": int64(0),
|
||||||
|
"processing": int64(0),
|
||||||
|
"failed": int64(0),
|
||||||
|
"pending": int64(0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stat := range stats {
|
||||||
|
result["total"] = result["total"].(int64) + stat.Count
|
||||||
|
result[stat.Status] = stat.Count
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
633
api/service/jimeng/service.go
Normal file
633
api/service/jimeng/service.go
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
package jimeng
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"geekai/logger"
|
||||||
|
"geekai/store"
|
||||||
|
"geekai/store/model"
|
||||||
|
"geekai/utils"
|
||||||
|
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serviceLogger = logger.GetLogger()
|
||||||
|
|
||||||
|
// Service 即梦服务
|
||||||
|
type Service struct {
|
||||||
|
db *gorm.DB
|
||||||
|
redis *redis.Client
|
||||||
|
taskQueue *store.RedisQueue
|
||||||
|
client *Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewService 创建即梦服务
|
||||||
|
func NewService(db *gorm.DB, redisCli *redis.Client, client *Client) *Service {
|
||||||
|
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
|
||||||
|
return &Service{
|
||||||
|
db: db,
|
||||||
|
redis: redisCli,
|
||||||
|
taskQueue: taskQueue,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTask 创建任务
|
||||||
|
func (s *Service) CreateTask(userId uint, req *CreateTaskRequest) (*model.JimengJob, error) {
|
||||||
|
// 生成任务ID
|
||||||
|
taskId := utils.RandString(20)
|
||||||
|
|
||||||
|
// 序列化任务参数
|
||||||
|
paramsJson, err := json.Marshal(req.Params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("marshal task params failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建任务记录
|
||||||
|
job := &model.JimengJob{
|
||||||
|
UserId: userId,
|
||||||
|
TaskId: taskId,
|
||||||
|
Type: req.Type,
|
||||||
|
ReqKey: req.ReqKey,
|
||||||
|
Prompt: req.Prompt,
|
||||||
|
TaskParams: string(paramsJson),
|
||||||
|
Status: model.JimengJobStatusPending,
|
||||||
|
Power: req.Power,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到数据库
|
||||||
|
if err := s.db.Create(job).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("create jimeng job failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送到任务队列
|
||||||
|
task := map[string]any{
|
||||||
|
"job_id": job.Id,
|
||||||
|
"type": job.Type,
|
||||||
|
}
|
||||||
|
if err := s.taskQueue.RPush(task); err != nil {
|
||||||
|
return nil, fmt.Errorf("push jimeng task to queue failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return job, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessTask 处理任务
|
||||||
|
func (s *Service) ProcessTask(jobId uint) error {
|
||||||
|
// 获取任务记录
|
||||||
|
var job model.JimengJob
|
||||||
|
if err := s.db.First(&job, jobId).Error; err != nil {
|
||||||
|
return fmt.Errorf("get jimeng job failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务状态为处理中
|
||||||
|
if err := s.UpdateJobStatus(job.Id, model.JimengJobStatusProcessing, ""); err != nil {
|
||||||
|
return fmt.Errorf("update job status failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据任务类型处理
|
||||||
|
switch job.Type {
|
||||||
|
case model.JimengJobTypeTextToImage:
|
||||||
|
return s.processTextToImage(&job)
|
||||||
|
case model.JimengJobTypeImageToImagePortrait:
|
||||||
|
return s.processImageToImagePortrait(&job)
|
||||||
|
case model.JimengJobTypeImageEdit:
|
||||||
|
return s.processImageEdit(&job)
|
||||||
|
case model.JimengJobTypeImageEffects:
|
||||||
|
return s.processImageEffects(&job)
|
||||||
|
case model.JimengJobTypeTextToVideo:
|
||||||
|
return s.processTextToVideo(&job)
|
||||||
|
case model.JimengJobTypeImageToVideo:
|
||||||
|
return s.processImageToVideo(&job)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported task type: %s", job.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processTextToImage 处理文生图任务
|
||||||
|
func (s *Service) processTextToImage(job *model.JimengJob) error {
|
||||||
|
// 解析任务参数
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("parse task params failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
req := &SubmitTaskRequest{
|
||||||
|
ReqKey: job.ReqKey,
|
||||||
|
Prompt: job.Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置参数
|
||||||
|
if seed, ok := params["seed"]; ok {
|
||||||
|
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||||
|
req.Seed = seedVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scale, ok := params["scale"]; ok {
|
||||||
|
if scaleVal, ok := scale.(float64); ok {
|
||||||
|
req.Scale = scaleVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if width, ok := params["width"]; ok {
|
||||||
|
if widthVal, ok := width.(float64); ok {
|
||||||
|
req.Width = int(widthVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if height, ok := params["height"]; ok {
|
||||||
|
if heightVal, ok := height.(float64); ok {
|
||||||
|
req.Height = int(heightVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if usePreLlm, ok := params["use_pre_llm"]; ok {
|
||||||
|
if usePreLlmVal, ok := usePreLlm.(bool); ok {
|
||||||
|
req.UsePreLLM = usePreLlmVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
resp, err := s.client.SubmitTask(req)
|
||||||
|
if err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务ID和原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
|
"task_id": resp.Data.TaskId,
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
serviceLogger.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 {
|
||||||
|
// 解析任务参数
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("parse task params failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
req := &SubmitTaskRequest{
|
||||||
|
ReqKey: job.ReqKey,
|
||||||
|
Prompt: job.Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图像输入
|
||||||
|
if imageInput, ok := params["image_input"].(string); ok {
|
||||||
|
req.ImageInput = imageInput
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置其他参数
|
||||||
|
if gpen, ok := params["gpen"]; ok {
|
||||||
|
if gpenVal, ok := gpen.(float64); ok {
|
||||||
|
req.Gpen = gpenVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if skin, ok := params["skin"]; ok {
|
||||||
|
if skinVal, ok := skin.(float64); ok {
|
||||||
|
req.Skin = skinVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if skinUnifi, ok := params["skin_unifi"]; ok {
|
||||||
|
if skinUnifiVal, ok := skinUnifi.(float64); ok {
|
||||||
|
req.SkinUnifi = skinUnifiVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if genMode, ok := params["gen_mode"].(string); ok {
|
||||||
|
req.GenMode = genMode
|
||||||
|
}
|
||||||
|
if width, ok := params["width"]; ok {
|
||||||
|
if widthVal, ok := width.(float64); ok {
|
||||||
|
req.Width = int(widthVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if height, ok := params["height"]; ok {
|
||||||
|
if heightVal, ok := height.(float64); ok {
|
||||||
|
req.Height = int(heightVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if seed, ok := params["seed"]; ok {
|
||||||
|
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||||
|
req.Seed = seedVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
resp, err := s.client.SubmitTask(req)
|
||||||
|
if err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务ID和原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
|
"task_id": resp.Data.TaskId,
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processImageEdit 处理图像编辑任务
|
||||||
|
func (s *Service) processImageEdit(job *model.JimengJob) error {
|
||||||
|
// 解析任务参数
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("parse task params failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
req := &SubmitTaskRequest{
|
||||||
|
ReqKey: job.ReqKey,
|
||||||
|
Prompt: job.Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图像输入
|
||||||
|
if imageUrls, ok := params["image_urls"].([]any); ok {
|
||||||
|
for _, url := range imageUrls {
|
||||||
|
if urlStr, ok := url.(string); ok {
|
||||||
|
req.ImageUrls = append(req.ImageUrls, urlStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if binaryData, ok := params["binary_data_base64"].([]any); ok {
|
||||||
|
for _, data := range binaryData {
|
||||||
|
if dataStr, ok := data.(string); ok {
|
||||||
|
req.BinaryDataBase64 = append(req.BinaryDataBase64, dataStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置其他参数
|
||||||
|
if seed, ok := params["seed"]; ok {
|
||||||
|
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||||
|
req.Seed = seedVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if scale, ok := params["scale"]; ok {
|
||||||
|
if scaleVal, ok := scale.(float64); ok {
|
||||||
|
req.Scale = scaleVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
resp, err := s.client.SubmitTask(req)
|
||||||
|
if err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务ID和原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
|
"task_id": resp.Data.TaskId,
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processImageEffects 处理图像特效任务
|
||||||
|
func (s *Service) processImageEffects(job *model.JimengJob) error {
|
||||||
|
// 解析任务参数
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("parse task params failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
req := &SubmitTaskRequest{
|
||||||
|
ReqKey: job.ReqKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图像输入
|
||||||
|
if imageInput1, ok := params["image_input1"].(string); ok {
|
||||||
|
req.ImageInput1 = imageInput1
|
||||||
|
}
|
||||||
|
if templateId, ok := params["template_id"].(string); ok {
|
||||||
|
req.TemplateId = templateId
|
||||||
|
}
|
||||||
|
if width, ok := params["width"]; ok {
|
||||||
|
if widthVal, ok := width.(float64); ok {
|
||||||
|
req.Width = int(widthVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if height, ok := params["height"]; ok {
|
||||||
|
if heightVal, ok := height.(float64); ok {
|
||||||
|
req.Height = int(heightVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
resp, err := s.client.SubmitTask(req)
|
||||||
|
if err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务ID和原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
|
"task_id": resp.Data.TaskId,
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processTextToVideo 处理文生视频任务
|
||||||
|
func (s *Service) processTextToVideo(job *model.JimengJob) error {
|
||||||
|
// 解析任务参数
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("parse task params failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
req := &SubmitTaskRequest{
|
||||||
|
ReqKey: job.ReqKey,
|
||||||
|
Prompt: job.Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置参数
|
||||||
|
if seed, ok := params["seed"]; ok {
|
||||||
|
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||||
|
req.Seed = seedVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aspectRatio, ok := params["aspect_ratio"].(string); ok {
|
||||||
|
req.AspectRatio = aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
resp, err := s.client.SubmitTask(req)
|
||||||
|
if err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务ID和原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
|
"task_id": resp.Data.TaskId,
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// processImageToVideo 处理图生视频任务
|
||||||
|
func (s *Service) processImageToVideo(job *model.JimengJob) error {
|
||||||
|
// 解析任务参数
|
||||||
|
var params map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(job.TaskParams), ¶ms); err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("parse task params failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建请求
|
||||||
|
req := &SubmitTaskRequest{
|
||||||
|
ReqKey: job.ReqKey,
|
||||||
|
Prompt: job.Prompt,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图像输入
|
||||||
|
if imageUrls, ok := params["image_urls"].([]any); ok {
|
||||||
|
for _, url := range imageUrls {
|
||||||
|
if urlStr, ok := url.(string); ok {
|
||||||
|
req.ImageUrls = append(req.ImageUrls, urlStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if binaryData, ok := params["binary_data_base64"].([]any); ok {
|
||||||
|
for _, data := range binaryData {
|
||||||
|
if dataStr, ok := data.(string); ok {
|
||||||
|
req.BinaryDataBase64 = append(req.BinaryDataBase64, dataStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置其他参数
|
||||||
|
if seed, ok := params["seed"]; ok {
|
||||||
|
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
|
||||||
|
req.Seed = seedVal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if aspectRatio, ok := params["aspect_ratio"].(string); ok {
|
||||||
|
req.AspectRatio = aspectRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交异步任务
|
||||||
|
resp, err := s.client.SubmitTask(req)
|
||||||
|
if err != nil {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务ID和原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
|
||||||
|
"task_id": resp.Data.TaskId,
|
||||||
|
"raw_data": string(rawData),
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
serviceLogger.Errorf("update jimeng job task_id failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始轮询任务状态
|
||||||
|
return s.pollTaskStatus(job.Id, resp.Data.TaskId, job.ReqKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pollTaskStatus 轮询任务状态
|
||||||
|
func (s *Service) pollTaskStatus(jobId uint, taskId, reqKey string) error {
|
||||||
|
maxRetries := 60 // 最大重试次数,60次 * 5秒 = 5分钟
|
||||||
|
retryCount := 0
|
||||||
|
|
||||||
|
for retryCount < maxRetries {
|
||||||
|
time.Sleep(5 * time.Second) // 等待5秒
|
||||||
|
|
||||||
|
// 查询任务状态
|
||||||
|
resp, err := s.client.QueryTask(&QueryTaskRequest{
|
||||||
|
ReqKey: reqKey,
|
||||||
|
TaskId: taskId,
|
||||||
|
ReqJson: `{"return_url":true}`,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
serviceLogger.Errorf("query jimeng task status failed: %v", err)
|
||||||
|
retryCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新原始数据
|
||||||
|
rawData, _ := json.Marshal(resp)
|
||||||
|
s.db.Model(&model.JimengJob{}).Where("id = ?", jobId).Update("raw_data", string(rawData))
|
||||||
|
|
||||||
|
if resp.Code != 10000 {
|
||||||
|
return s.handleTaskError(jobId, fmt.Sprintf("query task failed: %s", resp.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resp.Data.Status {
|
||||||
|
case TaskStatusDone:
|
||||||
|
// 任务完成,更新结果
|
||||||
|
updates := map[string]any{
|
||||||
|
"status": model.JimengJobStatusCompleted,
|
||||||
|
"progress": 100,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置结果URL
|
||||||
|
if len(resp.Data.ImageUrls) > 0 {
|
||||||
|
updates["img_url"] = resp.Data.ImageUrls[0]
|
||||||
|
}
|
||||||
|
if resp.Data.VideoUrl != "" {
|
||||||
|
updates["video_url"] = resp.Data.VideoUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.db.Model(&model.JimengJob{}).Where("id = ?", jobId).Updates(updates).Error
|
||||||
|
|
||||||
|
case TaskStatusInQueue:
|
||||||
|
// 任务在队列中
|
||||||
|
s.UpdateJobProgress(jobId, 10)
|
||||||
|
|
||||||
|
case TaskStatusGenerating:
|
||||||
|
// 任务处理中
|
||||||
|
s.UpdateJobProgress(jobId, 50)
|
||||||
|
|
||||||
|
case TaskStatusNotFound, TaskStatusExpired:
|
||||||
|
// 任务未找到或已过期
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时处理
|
||||||
|
return s.handleTaskError(jobId, "task timeout")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJobStatus 更新任务状态
|
||||||
|
func (s *Service) UpdateJobStatus(jobId uint, status, errMsg string) error {
|
||||||
|
updates := map[string]any{
|
||||||
|
"status": status,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}
|
||||||
|
if errMsg != "" {
|
||||||
|
updates["err_msg"] = errMsg
|
||||||
|
}
|
||||||
|
return s.db.Model(&model.JimengJob{}).Where("id = ?", jobId).Updates(updates).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateJobProgress 更新任务进度
|
||||||
|
func (s *Service) UpdateJobProgress(jobId uint, progress int) error {
|
||||||
|
return s.db.Model(&model.JimengJob{}).Where("id = ?", jobId).Updates(map[string]any{
|
||||||
|
"progress": progress,
|
||||||
|
"updated_at": time.Now(),
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleTaskError 处理任务错误
|
||||||
|
func (s *Service) handleTaskError(jobId uint, errMsg string) error {
|
||||||
|
serviceLogger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
|
||||||
|
return s.UpdateJobStatus(jobId, model.JimengJobStatusFailed, errMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJob 获取任务
|
||||||
|
func (s *Service) GetJob(jobId uint) (*model.JimengJob, error) {
|
||||||
|
var job model.JimengJob
|
||||||
|
if err := s.db.First(&job, jobId).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &job, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserJobs 获取用户任务列表
|
||||||
|
func (s *Service) GetUserJobs(userId uint, page, pageSize int) ([]*model.JimengJob, int64, error) {
|
||||||
|
var jobs []*model.JimengJob
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := s.db.Model(&model.JimengJob{}).Where("user_id = ?", userId)
|
||||||
|
|
||||||
|
// 统计总数
|
||||||
|
if err := query.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页查询
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := query.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&jobs).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingTaskCount 获取用户未完成任务数量
|
||||||
|
func (s *Service) GetPendingTaskCount(userId uint) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := s.db.Model(&model.JimengJob{}).Where("user_id = ? AND status IN (?)", userId,
|
||||||
|
[]string{model.JimengJobStatusPending, model.JimengJobStatusProcessing}).Count(&count).Error
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteJob 删除任务
|
||||||
|
func (s *Service) DeleteJob(jobId uint, userId uint) error {
|
||||||
|
return s.db.Where("id = ? AND user_id = ?", jobId, userId).Delete(&model.JimengJob{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushTaskToQueue 推送任务到队列
|
||||||
|
func (s *Service) PushTaskToQueue(task map[string]interface{}) error {
|
||||||
|
return s.taskQueue.RPush(task)
|
||||||
|
}
|
||||||
163
api/service/jimeng/types.go
Normal file
163
api/service/jimeng/types.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package jimeng
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SubmitTaskRequest 提交任务请求
|
||||||
|
type SubmitTaskRequest struct {
|
||||||
|
ReqKey string `json:"req_key"`
|
||||||
|
// 文生图参数
|
||||||
|
Prompt string `json:"prompt,omitempty"`
|
||||||
|
Seed int64 `json:"seed,omitempty"`
|
||||||
|
Scale float64 `json:"scale,omitempty"`
|
||||||
|
Width int `json:"width,omitempty"`
|
||||||
|
Height int `json:"height,omitempty"`
|
||||||
|
UsePreLLM bool `json:"use_pre_llm,omitempty"`
|
||||||
|
// 图生图参数
|
||||||
|
ImageInput string `json:"image_input,omitempty"`
|
||||||
|
ImageUrls []string `json:"image_urls,omitempty"`
|
||||||
|
BinaryDataBase64 []string `json:"binary_data_base64,omitempty"`
|
||||||
|
Gpen float64 `json:"gpen,omitempty"`
|
||||||
|
Skin float64 `json:"skin,omitempty"`
|
||||||
|
SkinUnifi float64 `json:"skin_unifi,omitempty"`
|
||||||
|
GenMode string `json:"gen_mode,omitempty"`
|
||||||
|
// 图像编辑参数
|
||||||
|
// 图像特效参数
|
||||||
|
ImageInput1 string `json:"image_input1,omitempty"`
|
||||||
|
TemplateId string `json:"template_id,omitempty"`
|
||||||
|
// 视频生成参数
|
||||||
|
AspectRatio string `json:"aspect_ratio,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmitTaskResponse 提交任务响应
|
||||||
|
type SubmitTaskResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
TimeElapsed string `json:"time_elapsed"`
|
||||||
|
Data struct {
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryTaskRequest 查询任务请求
|
||||||
|
type QueryTaskRequest struct {
|
||||||
|
ReqKey string `json:"req_key"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
ReqJson string `json:"req_json,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryTaskResponse 查询任务响应
|
||||||
|
type QueryTaskResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestId string `json:"request_id"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
TimeElapsed string `json:"time_elapsed"`
|
||||||
|
Data struct {
|
||||||
|
AlgorithmBaseResp struct {
|
||||||
|
StatusCode int `json:"status_code"`
|
||||||
|
StatusMessage string `json:"status_message"`
|
||||||
|
} `json:"algorithm_base_resp"`
|
||||||
|
BinaryDataBase64 []string `json:"binary_data_base64"`
|
||||||
|
ImageUrls []string `json:"image_urls"`
|
||||||
|
VideoUrl string `json:"video_url"`
|
||||||
|
RespData string `json:"resp_data"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
LlmResult string `json:"llm_result"`
|
||||||
|
PeResult string `json:"pe_result"`
|
||||||
|
PredictTagsResult string `json:"predict_tags_result"`
|
||||||
|
RephraserResult string `json:"rephraser_result"`
|
||||||
|
VlmResult string `json:"vlm_result"`
|
||||||
|
InferCtx interface{} `json:"infer_ctx"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskStatus 任务状态
|
||||||
|
const (
|
||||||
|
TaskStatusInQueue = "in_queue" // 任务已提交
|
||||||
|
TaskStatusGenerating = "generating" // 任务处理中
|
||||||
|
TaskStatusDone = "done" // 处理完成
|
||||||
|
TaskStatusNotFound = "not_found" // 任务未找到
|
||||||
|
TaskStatusExpired = "expired" // 任务已过期
|
||||||
|
)
|
||||||
|
|
||||||
|
// CreateTaskRequest 创建任务请求
|
||||||
|
type CreateTaskRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Params map[string]interface{} `json:"params"`
|
||||||
|
ReqKey string `json:"req_key"`
|
||||||
|
ImageUrls []string `json:"image_urls,omitempty"`
|
||||||
|
Power int `json:"power,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskInfo 任务信息
|
||||||
|
type TaskInfo struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
UserId uint `json:"user_id"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ReqKey string `json:"req_key"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
TaskParams string `json:"task_params"`
|
||||||
|
ImgURL string `json:"img_url"`
|
||||||
|
VideoURL string `json:"video_url"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ErrMsg string `json:"err_msg"`
|
||||||
|
Power int `json:"power"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogoInfo 水印信息
|
||||||
|
type LogoInfo struct {
|
||||||
|
AddLogo bool `json:"add_logo"`
|
||||||
|
Position int `json:"position"`
|
||||||
|
Language int `json:"language"`
|
||||||
|
Opacity float64 `json:"opacity"`
|
||||||
|
LogoTextContent string `json:"logo_text_content"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReqJsonConfig 查询配置
|
||||||
|
type ReqJsonConfig struct {
|
||||||
|
ReturnUrl bool `json:"return_url"`
|
||||||
|
LogoInfo *LogoInfo `json:"logo_info,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageEffectTemplate 图像特效模板
|
||||||
|
const (
|
||||||
|
TemplateIdFelt3DPolaroid = "felt_3d_polaroid" // 毛毡3d拍立得风格
|
||||||
|
TemplateIdMyWorld = "my_world" // 像素世界风
|
||||||
|
TemplateIdMyWorldUniversal = "my_world_universal" // 像素世界-万物通用版
|
||||||
|
TemplateIdPlasticBubbleFigure = "plastic_bubble_figure" // 盲盒玩偶风
|
||||||
|
TemplateIdPlasticBubbleFigureCartoon = "plastic_bubble_figure_cartoon_text" // 塑料泡罩人偶-文字卡头版
|
||||||
|
TemplateIdFurryDreamDoll = "furry_dream_doll" // 毛绒玩偶风
|
||||||
|
TemplateIdMicroLandscapeMiniWorld = "micro_landscape_mini_world" // 迷你世界玩偶风
|
||||||
|
TemplateIdMicroLandscapeProfessional = "micro_landscape_mini_world_professional" // 微型景观小世界-职业版
|
||||||
|
TemplateIdAcrylicOrnaments = "acrylic_ornaments" // 亚克力挂饰
|
||||||
|
TemplateIdFeltKeychain = "felt_keychain" // 毛毡钥匙扣
|
||||||
|
TemplateIdLofiPixelCharacter = "lofi_pixel_character_mini_card" // Lofi像素人物小卡
|
||||||
|
TemplateIdAngelFigurine = "angel_figurine" // 天使形象手办
|
||||||
|
TemplateIdLyingInFluffyBelly = "lying_in_fluffy_belly" // 躺在毛茸茸肚皮里
|
||||||
|
TemplateIdGlassBall = "glass_ball" // 玻璃球
|
||||||
|
)
|
||||||
|
|
||||||
|
// AspectRatio 视频宽高比
|
||||||
|
const (
|
||||||
|
AspectRatio16_9 = "16:9" // 1280×720
|
||||||
|
AspectRatio9_16 = "9:16" // 720×1280
|
||||||
|
AspectRatio1_1 = "1:1" // 960×960
|
||||||
|
AspectRatio4_3 = "4:3" // 960×720
|
||||||
|
AspectRatio3_4 = "3:4" // 720×960
|
||||||
|
AspectRatio21_9 = "21:9" // 1680×720
|
||||||
|
AspectRatio9_21 = "9:21" // 720×1680
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenMode 生成模式
|
||||||
|
const (
|
||||||
|
GenModeCreative = "creative" // 提示词模式
|
||||||
|
GenModeReference = "reference" // 全参考模式
|
||||||
|
GenModeReferenceChar = "reference_char" // 人物参考模式
|
||||||
|
)
|
||||||
@@ -212,7 +212,9 @@ func (s *Service) DownloadImages() {
|
|||||||
// PushTask push a new mj task in to task queue
|
// PushTask push a new mj task in to task queue
|
||||||
func (s *Service) PushTask(task types.MjTask) {
|
func (s *Service) PushTask(task types.MjTask) {
|
||||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||||
s.taskQueue.RPush(task)
|
if err := s.taskQueue.RPush(task); err != nil {
|
||||||
|
logger.Errorf("push mj task to queue failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SyncTaskProgress 异步拉取任务
|
// SyncTaskProgress 异步拉取任务
|
||||||
|
|||||||
@@ -253,7 +253,9 @@ func (s *Service) checkTaskProgress(apiKey model.ApiKey) (*TaskProgressResp, err
|
|||||||
|
|
||||||
func (s *Service) PushTask(task types.SdTask) {
|
func (s *Service) PushTask(task types.SdTask) {
|
||||||
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
|
||||||
s.taskQueue.RPush(task)
|
if err := s.taskQueue.RPush(task); err != nil {
|
||||||
|
logger.Errorf("push sd task to queue failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
|||||||
|
|
||||||
func (s *Service) PushTask(task types.SunoTask) {
|
func (s *Service) PushTask(task types.SunoTask) {
|
||||||
logger.Infof("add a new Suno task to the task list: %+v", task)
|
logger.Infof("add a new Suno task to the task list: %+v", task)
|
||||||
s.taskQueue.RPush(task)
|
if err := s.taskQueue.RPush(task); err != nil {
|
||||||
|
logger.Errorf("push suno task to queue failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Run() {
|
func (s *Service) Run() {
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
|
|||||||
|
|
||||||
func (s *Service) PushTask(task types.VideoTask) {
|
func (s *Service) PushTask(task types.VideoTask) {
|
||||||
logger.Infof("add a new Video task to the task list: %+v", task)
|
logger.Infof("add a new Video task to the task list: %+v", task)
|
||||||
s.taskQueue.RPush(task)
|
if err := s.taskQueue.RPush(task); err != nil {
|
||||||
|
logger.Errorf("push video task to queue failed: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Run() {
|
func (s *Service) Run() {
|
||||||
|
|||||||
58
api/store/model/jimeng_job.go
Normal file
58
api/store/model/jimeng_job.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JimengJob 即梦AI任务模型
|
||||||
|
type JimengJob struct {
|
||||||
|
Id uint `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||||
|
UserId uint `gorm:"column:user_id;type:int;not null;index;comment:用户ID" json:"user_id"`
|
||||||
|
TaskId string `gorm:"column:task_id;type:varchar(100);not null;index;comment:任务ID" json:"task_id"`
|
||||||
|
Type string `gorm:"column:type;type:varchar(50);not null;comment:任务类型" json:"type"`
|
||||||
|
ReqKey string `gorm:"column:req_key;type:varchar(100);comment:请求Key" json:"req_key"`
|
||||||
|
Prompt string `gorm:"column:prompt;type:text;comment:提示词" json:"prompt"`
|
||||||
|
TaskParams string `gorm:"column:task_params;type:text;comment:任务参数JSON" json:"task_params"`
|
||||||
|
ImgURL string `gorm:"column:img_url;type:varchar(1024);comment:图片或封面URL" json:"img_url"`
|
||||||
|
VideoURL string `gorm:"column:video_url;type:varchar(1024);comment:视频URL" json:"video_url"`
|
||||||
|
RawData string `gorm:"column:raw_data;type:text;comment:原始API响应" json:"raw_data"`
|
||||||
|
Progress int `gorm:"column:progress;type:int;default:0;comment:进度百分比" json:"progress"`
|
||||||
|
Status string `gorm:"column:status;type:varchar(20);default:'pending';comment:任务状态" json:"status"`
|
||||||
|
ErrMsg string `gorm:"column:err_msg;type:varchar(1024);comment:错误信息" json:"err_msg"`
|
||||||
|
Power int `gorm:"column:power;type:int;default:0;comment:消耗算力" json:"power"`
|
||||||
|
CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null;comment:创建时间" json:"created_at"`
|
||||||
|
UpdatedAt time.Time `gorm:"column:updated_at;type:datetime;not null;comment:更新时间" json:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// JimengJobStatus 即梦任务状态常量
|
||||||
|
const (
|
||||||
|
JimengJobStatusPending = "pending"
|
||||||
|
JimengJobStatusProcessing = "processing"
|
||||||
|
JimengJobStatusCompleted = "completed"
|
||||||
|
JimengJobStatusFailed = "failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JimengJobType 即梦任务类型常量
|
||||||
|
const (
|
||||||
|
JimengJobTypeTextToImage = "text_to_image" // 文生图
|
||||||
|
JimengJobTypeImageToImagePortrait = "image_to_image_portrait" // 图生图人像写真
|
||||||
|
JimengJobTypeImageEdit = "image_edit" // 图像编辑
|
||||||
|
JimengJobTypeImageEffects = "image_effects" // 图像特效
|
||||||
|
JimengJobTypeTextToVideo = "text_to_video" // 文生视频
|
||||||
|
JimengJobTypeImageToVideo = "image_to_video" // 图生视频
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReqKey 常量定义
|
||||||
|
const (
|
||||||
|
ReqKeyTextToImage = "high_aes_general_v30l_zt2i" // 文生图
|
||||||
|
ReqKeyImageToImagePortrait = "i2i_portrait_photo" // 图生图人像写真
|
||||||
|
ReqKeyImageEdit = "seededit_v3.0" // 图像编辑
|
||||||
|
ReqKeyImageEffects = "i2i_multi_style_zx2x" // 图像特效
|
||||||
|
ReqKeyTextToVideo = "jimeng_vgfm_t2v_l20" // 文生视频
|
||||||
|
ReqKeyImageToVideo = "jimeng_vgfm_i2v_l20" // 图生视频
|
||||||
|
)
|
||||||
|
|
||||||
|
// TableName 返回数据表名称
|
||||||
|
func (JimengJob) TableName() string {
|
||||||
|
return "chatgpt_jimeng_jobs"
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ package store
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"geekai/utils"
|
"geekai/utils"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -23,15 +24,15 @@ func NewRedisQueue(name string, client *redis.Client) *RedisQueue {
|
|||||||
return &RedisQueue{name: name, client: client, ctx: context.Background()}
|
return &RedisQueue{name: name, client: client, ctx: context.Background()}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *RedisQueue) RPush(value interface{}) {
|
func (q *RedisQueue) RPush(value any) error {
|
||||||
q.client.RPush(q.ctx, q.name, utils.JsonEncode(value))
|
return q.client.RPush(q.ctx, q.name, utils.JsonEncode(value)).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *RedisQueue) LPush(value interface{}) {
|
func (q *RedisQueue) LPush(value any) error {
|
||||||
q.client.LPush(q.ctx, q.name, utils.JsonEncode(value))
|
return q.client.LPush(q.ctx, q.name, utils.JsonEncode(value)).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *RedisQueue) LPop(value interface{}) error {
|
func (q *RedisQueue) LPop(value any) error {
|
||||||
result, err := q.client.BLPop(q.ctx, 0, q.name).Result()
|
result, err := q.client.BLPop(q.ctx, 0, q.name).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -39,10 +40,18 @@ func (q *RedisQueue) LPop(value interface{}) error {
|
|||||||
return utils.JsonDecode(result[1], value)
|
return utils.JsonDecode(result[1], value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *RedisQueue) RPop(value interface{}) error {
|
func (q *RedisQueue) RPop(value any) error {
|
||||||
result, err := q.client.BRPop(q.ctx, 0, q.name).Result()
|
result, err := q.client.BRPop(q.ctx, 0, q.name).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return utils.JsonDecode(result[1], value)
|
return utils.JsonDecode(result[1], value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (q *RedisQueue) Size() (int64, error) {
|
||||||
|
return q.client.LLen(q.ctx, q.name).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *RedisQueue) Clear() error {
|
||||||
|
return q.client.Del(q.ctx, q.name).Err()
|
||||||
|
}
|
||||||
|
|||||||
21
api/store/vo/jimeng_job.go
Normal file
21
api/store/vo/jimeng_job.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
// JimengJob 即梦AI任务VO
|
||||||
|
type JimengJob struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
UserId uint `json:"user_id"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ReqKey string `json:"req_key"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
TaskParams string `json:"task_params"`
|
||||||
|
ImgURL string `json:"img_url"`
|
||||||
|
VideoURL string `json:"video_url"`
|
||||||
|
RawData string `json:"raw_data"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
ErrMsg string `json:"err_msg"`
|
||||||
|
Power int `json:"power"`
|
||||||
|
CreatedAt int64 `json:"created_at"` // 时间戳
|
||||||
|
UpdatedAt int64 `json:"updated_at"` // 时间戳
|
||||||
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
codeLength = 32 // 兑换码长度
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
codeMap = make(map[string]bool)
|
|
||||||
mapMutex = &sync.Mutex{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// GenerateUniqueCode 生成唯一兑换码
|
|
||||||
func GenerateUniqueCode() (string, error) {
|
|
||||||
for {
|
|
||||||
code, err := generateCode()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
mapMutex.Lock()
|
|
||||||
if !codeMap[code] {
|
|
||||||
codeMap[code] = true
|
|
||||||
mapMutex.Unlock()
|
|
||||||
return code, nil
|
|
||||||
}
|
|
||||||
mapMutex.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// generateCode 生成兑换码
|
|
||||||
func generateCode() (string, error) {
|
|
||||||
bytes := make([]byte, codeLength/2) // 因为 hex 编码会使长度翻倍
|
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return hex.EncodeToString(bytes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
for i := 0; i < 10; i++ {
|
|
||||||
code, err := GenerateUniqueCode()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Error generating code:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println("Generated code:", code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 4125778 */
|
font-family: "iconfont"; /* Project id 4125778 */
|
||||||
src: url('iconfont.woff2?t=1752731646117') format('woff2'),
|
src: url('iconfont.woff2?t=1752831319382') format('woff2'),
|
||||||
url('iconfont.woff?t=1752731646117') format('woff'),
|
url('iconfont.woff?t=1752831319382') format('woff'),
|
||||||
url('iconfont.ttf?t=1752731646117') format('truetype');
|
url('iconfont.ttf?t=1752831319382') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,14 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-jimeng2:before {
|
||||||
|
content: "\eabc";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-jimeng:before {
|
||||||
|
content: "\eabb";
|
||||||
|
}
|
||||||
|
|
||||||
.icon-video:before {
|
.icon-video:before {
|
||||||
content: "\e63f";
|
content: "\e63f";
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,20 @@
|
|||||||
"css_prefix_text": "icon-",
|
"css_prefix_text": "icon-",
|
||||||
"description": "",
|
"description": "",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "42693930",
|
||||||
|
"name": "即梦AI-02",
|
||||||
|
"font_class": "jimeng2",
|
||||||
|
"unicode": "eabc",
|
||||||
|
"unicode_decimal": 60092
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"icon_id": "42693927",
|
||||||
|
"name": "即梦AI-01",
|
||||||
|
"font_class": "jimeng",
|
||||||
|
"unicode": "eabb",
|
||||||
|
"unicode_decimal": 60091
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "1283",
|
"icon_id": "1283",
|
||||||
"name": "视频",
|
"name": "视频",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
291
web/src/components/ImageUpload.vue
Normal file
291
web/src/components/ImageUpload.vue
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<div class="image-upload">
|
||||||
|
<div class="upload-list" v-if="imageList.length > 0">
|
||||||
|
<div v-for="(image, index) in imageList" :key="index" class="upload-item">
|
||||||
|
<el-image
|
||||||
|
:src="image"
|
||||||
|
:preview-src-list="imageList"
|
||||||
|
:initial-index="index"
|
||||||
|
fit="cover"
|
||||||
|
class="upload-image"
|
||||||
|
/>
|
||||||
|
<div class="upload-overlay">
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
:icon="Delete"
|
||||||
|
size="small"
|
||||||
|
circle
|
||||||
|
@click="removeImage(index)"
|
||||||
|
class="remove-btn"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传按钮 -->
|
||||||
|
<div v-if="!multiple || imageList.length < maxCount" class="upload-btn">
|
||||||
|
<el-upload
|
||||||
|
:auto-upload="true"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="handleUpload"
|
||||||
|
accept="image/*"
|
||||||
|
class="uploader"
|
||||||
|
>
|
||||||
|
<div class="upload-placeholder">
|
||||||
|
<el-icon :size="20"><Plus /></el-icon>
|
||||||
|
<span>上传图片</span>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 初始上传区域 -->
|
||||||
|
<div v-else class="upload-area">
|
||||||
|
<el-upload
|
||||||
|
:auto-upload="true"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="handleUpload"
|
||||||
|
accept="image/*"
|
||||||
|
class="uploader"
|
||||||
|
>
|
||||||
|
<div class="upload-placeholder">
|
||||||
|
<el-icon :size="40"><Plus /></el-icon>
|
||||||
|
<div class="upload-text">
|
||||||
|
<p>点击上传图片</p>
|
||||||
|
<p class="upload-tip">支持 JPG、PNG 格式,最大 10MB</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 上传进度 -->
|
||||||
|
<el-progress
|
||||||
|
v-if="uploading"
|
||||||
|
:percentage="uploadProgress"
|
||||||
|
:stroke-width="4"
|
||||||
|
class="upload-progress"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { httpPost } from '@/utils/http'
|
||||||
|
import { Delete, Plus } from '@element-plus/icons-vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: [String, Array],
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
multiple: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
maxCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'upload-success'])
|
||||||
|
|
||||||
|
// 上传状态
|
||||||
|
const uploading = ref(false)
|
||||||
|
const uploadProgress = ref(0)
|
||||||
|
|
||||||
|
// 图片列表
|
||||||
|
const imageList = computed({
|
||||||
|
get() {
|
||||||
|
if (props.multiple) {
|
||||||
|
return Array.isArray(props.modelValue) ? props.modelValue : []
|
||||||
|
} else {
|
||||||
|
return props.modelValue ? [props.modelValue] : []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
if (props.multiple) {
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
} else {
|
||||||
|
emit('update:modelValue', value[0] || '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理上传
|
||||||
|
const handleUpload = async (uploadFile) => {
|
||||||
|
const file = uploadFile.file
|
||||||
|
|
||||||
|
// 检查文件类型
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
ElMessage.error('请选择图片文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小 (10MB)
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
ElMessage.error('图片大小不能超过 10MB')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查数量限制
|
||||||
|
if (props.multiple && imageList.value.length >= props.maxCount) {
|
||||||
|
ElMessage.error(`最多只能上传 ${props.maxCount} 张图片`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploading.value = true
|
||||||
|
uploadProgress.value = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
|
||||||
|
// 模拟上传进度
|
||||||
|
const progressTimer = setInterval(() => {
|
||||||
|
if (uploadProgress.value < 90) {
|
||||||
|
uploadProgress.value += 10
|
||||||
|
}
|
||||||
|
}, 100)
|
||||||
|
|
||||||
|
const response = await httpPost('/api/upload', formData)
|
||||||
|
|
||||||
|
clearInterval(progressTimer)
|
||||||
|
uploadProgress.value = 100
|
||||||
|
|
||||||
|
const imageUrl = response.data.url
|
||||||
|
|
||||||
|
// 更新图片列表
|
||||||
|
if (props.multiple) {
|
||||||
|
const newList = [...imageList.value, imageUrl]
|
||||||
|
imageList.value = newList
|
||||||
|
} else {
|
||||||
|
imageList.value = [imageUrl]
|
||||||
|
}
|
||||||
|
|
||||||
|
emit('upload-success', imageUrl)
|
||||||
|
ElMessage.success('上传成功')
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('上传失败: ' + (error.message || '网络错误'))
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
uploadProgress.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除图片
|
||||||
|
const removeImage = (index) => {
|
||||||
|
const newList = [...imageList.value]
|
||||||
|
newList.splice(index, 1)
|
||||||
|
imageList.value = newList
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.image-upload
|
||||||
|
width 100%
|
||||||
|
|
||||||
|
.upload-list
|
||||||
|
display flex
|
||||||
|
flex-wrap wrap
|
||||||
|
gap 10px
|
||||||
|
|
||||||
|
.upload-item
|
||||||
|
position relative
|
||||||
|
width 100px
|
||||||
|
height 100px
|
||||||
|
border-radius 6px
|
||||||
|
overflow hidden
|
||||||
|
border 1px solid #dcdfe6
|
||||||
|
|
||||||
|
.upload-image
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
|
||||||
|
.upload-overlay
|
||||||
|
position absolute
|
||||||
|
top 0
|
||||||
|
left 0
|
||||||
|
right 0
|
||||||
|
bottom 0
|
||||||
|
background rgba(0, 0, 0, 0.5)
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
opacity 0
|
||||||
|
transition opacity 0.3s
|
||||||
|
|
||||||
|
.remove-btn
|
||||||
|
background rgba(245, 108, 108, 0.8)
|
||||||
|
border none
|
||||||
|
color white
|
||||||
|
|
||||||
|
&:hover .upload-overlay
|
||||||
|
opacity 1
|
||||||
|
|
||||||
|
.upload-btn
|
||||||
|
width 100px
|
||||||
|
height 100px
|
||||||
|
border 2px dashed #dcdfe6
|
||||||
|
border-radius 6px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
cursor pointer
|
||||||
|
transition all 0.3s
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color #409eff
|
||||||
|
color #409eff
|
||||||
|
|
||||||
|
.uploader
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
|
||||||
|
.upload-placeholder
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
align-items center
|
||||||
|
gap 5px
|
||||||
|
font-size 12px
|
||||||
|
color #8c939d
|
||||||
|
|
||||||
|
.upload-area
|
||||||
|
border 2px dashed #dcdfe6
|
||||||
|
border-radius 6px
|
||||||
|
padding 40px
|
||||||
|
text-align center
|
||||||
|
cursor pointer
|
||||||
|
transition all 0.3s
|
||||||
|
|
||||||
|
&:hover
|
||||||
|
border-color #409eff
|
||||||
|
|
||||||
|
.uploader
|
||||||
|
width 100%
|
||||||
|
|
||||||
|
.upload-placeholder
|
||||||
|
display flex
|
||||||
|
flex-direction column
|
||||||
|
align-items center
|
||||||
|
gap 10px
|
||||||
|
color #8c939d
|
||||||
|
|
||||||
|
.upload-text
|
||||||
|
p
|
||||||
|
margin 5px 0
|
||||||
|
|
||||||
|
.upload-tip
|
||||||
|
font-size 12px
|
||||||
|
color #c0c4cc
|
||||||
|
|
||||||
|
.upload-progress
|
||||||
|
margin-top 10px
|
||||||
|
|
||||||
|
:deep(.el-upload)
|
||||||
|
width 100%
|
||||||
|
height 100%
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
justify-content center
|
||||||
|
</style>
|
||||||
@@ -159,6 +159,11 @@ const items = [
|
|||||||
index: '/admin/medias',
|
index: '/admin/medias',
|
||||||
title: '音视频记录',
|
title: '音视频记录',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'image',
|
||||||
|
index: '/admin/jimeng',
|
||||||
|
title: '即梦AI任务',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,12 @@ const routes = [
|
|||||||
meta: { title: '视频创作中心' },
|
meta: { title: '视频创作中心' },
|
||||||
component: () => import('@/views/Video.vue'),
|
component: () => import('@/views/Video.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'jimeng',
|
||||||
|
path: '/jimeng',
|
||||||
|
meta: { title: '即梦AI' },
|
||||||
|
component: () => import('@/views/Jimeng.vue'),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -252,6 +258,12 @@ const routes = [
|
|||||||
meta: { title: '音视频管理' },
|
meta: { title: '音视频管理' },
|
||||||
component: () => import('@/views/admin/records/Medias.vue'),
|
component: () => import('@/views/admin/records/Medias.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/jimeng',
|
||||||
|
name: 'admin-jimeng',
|
||||||
|
meta: { title: '即梦AI管理' },
|
||||||
|
component: () => import('@/views/admin/JimengJobs.vue'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/powerLog',
|
path: '/admin/powerLog',
|
||||||
name: 'admin-power-log',
|
name: 'admin-power-log',
|
||||||
|
|||||||
513
web/src/store/jimeng.js
Normal file
513
web/src/store/jimeng.js
Normal file
@@ -0,0 +1,513 @@
|
|||||||
|
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||||
|
// * 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 nodata from '@/assets/img/no-data.png'
|
||||||
|
import { checkSession } from '@/store/cache'
|
||||||
|
import { closeLoading, showLoading, 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 { defineStore } from 'pinia'
|
||||||
|
import { computed, 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 loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const list = ref([])
|
||||||
|
const noData = ref(true)
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = ref(20)
|
||||||
|
const total = ref(0)
|
||||||
|
const taskPulling = ref(false)
|
||||||
|
const pullHandler = ref(null)
|
||||||
|
const taskFilter = ref('all')
|
||||||
|
const currentList = ref([])
|
||||||
|
|
||||||
|
// 用户信息
|
||||||
|
const isLogin = ref(false)
|
||||||
|
const userPower = ref(100)
|
||||||
|
|
||||||
|
// 视频预览
|
||||||
|
const showDialog = ref(false)
|
||||||
|
const currentVideoUrl = ref('')
|
||||||
|
|
||||||
|
// 功能分类配置
|
||||||
|
const categories = [
|
||||||
|
{ key: 'image_generation', name: '图片生成' },
|
||||||
|
{ key: 'image_editing', name: 'AI修图' },
|
||||||
|
{ key: 'image_effects', name: '图像特效' },
|
||||||
|
{ key: 'video_generation', name: '视频生成' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 功能配置
|
||||||
|
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 textToImageParams = reactive({
|
||||||
|
prompt: '',
|
||||||
|
size: '1328x1328',
|
||||||
|
scale: 2.5,
|
||||||
|
seed: -1,
|
||||||
|
use_pre_llm: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const imageToImageParams = reactive({
|
||||||
|
image_input: '',
|
||||||
|
prompt: '演唱会现场的合照,闪光灯拍摄',
|
||||||
|
size: '1328x1328',
|
||||||
|
gpen: 0.4,
|
||||||
|
skin: 0.3,
|
||||||
|
skin_unifi: 0,
|
||||||
|
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]
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentFunctions = computed(() => {
|
||||||
|
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 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)
|
||||||
|
if (categoryFunctions.length > 0) {
|
||||||
|
if (category === 'image_generation') {
|
||||||
|
activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : 'text_to_image'
|
||||||
|
} else if (category === 'video_generation') {
|
||||||
|
activeFunction.value = useImageInput.value ? 'image_to_video' : 'text_to_video'
|
||||||
|
} else {
|
||||||
|
activeFunction.value = categoryFunctions[0].key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换输入模式
|
||||||
|
const switchInputMode = () => {
|
||||||
|
if (activeCategory.value === 'image_generation') {
|
||||||
|
activeFunction.value = useImageInput.value ? 'image_to_image_portrait' : '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)
|
||||||
|
return func ? func.name : type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务状态文本
|
||||||
|
const getTaskStatusText = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': '等待中',
|
||||||
|
'processing': '处理中',
|
||||||
|
'completed': '已完成',
|
||||||
|
'failed': '失败'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态类型
|
||||||
|
const getStatusType = (status) => {
|
||||||
|
const typeMap = {
|
||||||
|
'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)
|
||||||
|
)
|
||||||
|
} else if (taskFilter.value === 'video') {
|
||||||
|
currentList.value = list.value.filter(item =>
|
||||||
|
['text_to_video', 'image_to_video'].includes(item.type)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务列表
|
||||||
|
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
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
list.value = response.data.jobs || []
|
||||||
|
total.value = response.data.total || 0
|
||||||
|
noData.value = list.value.length === 0
|
||||||
|
updateCurrentList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取任务列表失败:', error)
|
||||||
|
showMessageError('获取任务列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交任务
|
||||||
|
const submitTask = async () => {
|
||||||
|
if (!isLogin.value) {
|
||||||
|
showMessageError('请先登录')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userPower.value < currentPowerCost.value) {
|
||||||
|
showMessageError('算力不足')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
submitting.value = true
|
||||||
|
let apiUrl = ''
|
||||||
|
let requestData = {}
|
||||||
|
|
||||||
|
switch (activeFunction.value) {
|
||||||
|
case 'text_to_image':
|
||||||
|
apiUrl = '/api/jimeng/text-to-image'
|
||||||
|
requestData = {
|
||||||
|
prompt: textToImageParams.prompt,
|
||||||
|
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 = {
|
||||||
|
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,
|
||||||
|
skin: imageToImageParams.skin,
|
||||||
|
skin_unifi: imageToImageParams.skin_unifi,
|
||||||
|
gen_mode: imageToImageParams.gen_mode,
|
||||||
|
seed: imageToImageParams.seed,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'image_edit':
|
||||||
|
apiUrl = '/api/jimeng/image-edit'
|
||||||
|
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,
|
||||||
|
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,
|
||||||
|
aspect_ratio: textToVideoParams.aspect_ratio,
|
||||||
|
seed: textToVideoParams.seed,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'image_to_video':
|
||||||
|
apiUrl = '/api/jimeng/image-to-video'
|
||||||
|
requestData = {
|
||||||
|
image_urls: imageToVideoParams.image_urls,
|
||||||
|
prompt: imageToVideoParams.prompt,
|
||||||
|
aspect_ratio: imageToVideoParams.aspect_ratio,
|
||||||
|
seed: imageToVideoParams.seed,
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await httpPost(apiUrl, requestData)
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
showMessageOK('任务提交成功')
|
||||||
|
// 重新获取任务列表
|
||||||
|
await fetchData(1)
|
||||||
|
// 开始轮询
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交任务失败:', error)
|
||||||
|
showMessageError(error.message || '提交任务失败')
|
||||||
|
} finally {
|
||||||
|
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 {
|
||||||
|
const response = await httpPost(`/api/jimeng/retry/${taskId}`)
|
||||||
|
if (response.data) {
|
||||||
|
showMessageOK('重试任务已提交')
|
||||||
|
await fetchData(page.value)
|
||||||
|
startPolling()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重试任务失败:', error)
|
||||||
|
showMessageError(error.message || '重试任务失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
const removeJob = async (item) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await httpGet('/api/jimeng/remove', { id: item.id })
|
||||||
|
if (response.data) {
|
||||||
|
showMessageOK('删除成功')
|
||||||
|
await fetchData(page.value)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除任务失败:', error)
|
||||||
|
showMessageError(error.message || '删除任务失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 播放视频
|
||||||
|
const playVideo = (item) => {
|
||||||
|
currentVideoUrl.value = item.video_url
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载文件
|
||||||
|
const downloadFile = (item) => {
|
||||||
|
const url = item.video_url || item.img_url
|
||||||
|
if (url) {
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = url
|
||||||
|
link.download = `jimeng_${item.id}.${item.video_url ? 'mp4' : 'jpg'}`
|
||||||
|
link.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
const cleanup = () => {
|
||||||
|
stopPolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回所有状态和方法
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
activeCategory,
|
||||||
|
activeFunction,
|
||||||
|
useImageInput,
|
||||||
|
loading,
|
||||||
|
submitting,
|
||||||
|
list,
|
||||||
|
noData,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
taskFilter,
|
||||||
|
currentList,
|
||||||
|
isLogin,
|
||||||
|
userPower,
|
||||||
|
showDialog,
|
||||||
|
currentVideoUrl,
|
||||||
|
nodata,
|
||||||
|
|
||||||
|
// 配置
|
||||||
|
categories,
|
||||||
|
functions,
|
||||||
|
currentFunctions,
|
||||||
|
|
||||||
|
// 参数
|
||||||
|
textToImageParams,
|
||||||
|
imageToImageParams,
|
||||||
|
imageEditParams,
|
||||||
|
imageEffectsParams,
|
||||||
|
textToVideoParams,
|
||||||
|
imageToVideoParams,
|
||||||
|
|
||||||
|
// 计算属性
|
||||||
|
currentFunction,
|
||||||
|
needsPrompt,
|
||||||
|
needsImage,
|
||||||
|
needsMultipleImages,
|
||||||
|
currentPowerCost,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
init,
|
||||||
|
switchCategory,
|
||||||
|
switchFunction,
|
||||||
|
switchInputMode,
|
||||||
|
getCurrentPowerCost,
|
||||||
|
getFunctionName,
|
||||||
|
getTaskStatusText,
|
||||||
|
getStatusType,
|
||||||
|
switchTaskFilter,
|
||||||
|
updateCurrentList,
|
||||||
|
fetchData,
|
||||||
|
submitTask,
|
||||||
|
getPendingCount,
|
||||||
|
startPolling,
|
||||||
|
stopPolling,
|
||||||
|
retryTask,
|
||||||
|
removeJob,
|
||||||
|
playVideo,
|
||||||
|
downloadFile,
|
||||||
|
cleanup,
|
||||||
|
|
||||||
|
// 工具函数
|
||||||
|
substr,
|
||||||
|
replaceImg,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -255,3 +255,8 @@ export function isChrome() {
|
|||||||
const userAgent = navigator.userAgent.toLowerCase()
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
return /chrome/.test(userAgent) && !/edg/.test(userAgent)
|
return /chrome/.test(userAgent) && !/edg/.test(userAgent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 格式化日期时间
|
||||||
|
export function formatDateTime(timestamp, format = 'yyyy-MM-dd HH:mm:ss') {
|
||||||
|
return dateFormat(timestamp, format)
|
||||||
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
|
<el-popover placement="right-end" trigger="hover" v-if="loginUser.id">
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<li class="menu-list-item flex-center-col">
|
<li class="menu-list-item flex-center-col">
|
||||||
<i class="iconfont icon-config" />
|
<i class="iconfont icon-user-circle" />
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
@@ -97,6 +97,11 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
<div v-else class="mb-2 flex justify-center">
|
||||||
|
<el-button @click="store.setShowLoginDialog(true)" type="primary" size="small">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
<div class="menu-bot-item">
|
<div class="menu-bot-item">
|
||||||
<a @click="router.push('/')" class="link-button">
|
<a @click="router.push('/')" class="link-button">
|
||||||
<i class="iconfont icon-house"></i>
|
<i class="iconfont icon-house"></i>
|
||||||
@@ -109,14 +114,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-scrollbar class="right-main">
|
<el-scrollbar class="right-main">
|
||||||
<div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
<!-- <div class="topheader" v-if="loginUser.id === undefined || !loginUser.id">
|
||||||
<el-button
|
<el-button
|
||||||
@click="router.push('/login')"
|
@click="router.push('/login')"
|
||||||
class="btn-go animate__animated animate__pulse animate__infinite"
|
class="btn-go animate__animated animate__pulse animate__infinite"
|
||||||
round
|
round
|
||||||
>登录</el-button
|
>登录</el-button
|
||||||
>
|
>
|
||||||
</div>
|
</div> -->
|
||||||
<div class="content custom-scroll">
|
<div class="content custom-scroll">
|
||||||
<router-view :key="routerViewKey" v-slot="{ Component }">
|
<router-view :key="routerViewKey" v-slot="{ Component }">
|
||||||
<transition name="move" mode="out-in">
|
<transition name="move" mode="out-in">
|
||||||
@@ -281,7 +286,9 @@ const logout = function () {
|
|||||||
httpGet('/api/user/logout')
|
httpGet('/api/user/logout')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
removeUserToken()
|
removeUserToken()
|
||||||
router.push('/login')
|
// 刷新组件
|
||||||
|
routerViewKey.value += 1
|
||||||
|
loginUser.value = {}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
ElMessage.error('注销失败!')
|
ElMessage.error('注销失败!')
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
class="nav-item-box"
|
class="nav-item-box"
|
||||||
@click="router.push(item.url)"
|
@click="router.push(item.url)"
|
||||||
>
|
>
|
||||||
<i :class="'iconfont ' + iconMap[item.url]"></i>
|
<i :class="'iconfont ' + item.icon"></i>
|
||||||
<div>{{ item.name }}</div>
|
<div>{{ item.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
</el-space>
|
</el-space>
|
||||||
@@ -107,20 +107,6 @@ const githubURL = ref(import.meta.env.VITE_GITHUB_URL)
|
|||||||
const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
|
const giteeURL = ref(import.meta.env.VITE_GITEE_URL)
|
||||||
const navs = ref([])
|
const navs = ref([])
|
||||||
|
|
||||||
const iconMap = ref({
|
|
||||||
'/chat': 'icon-chat',
|
|
||||||
'/mj': 'icon-mj',
|
|
||||||
'/sd': 'icon-sd',
|
|
||||||
'/dalle': 'icon-dalle',
|
|
||||||
'/images-wall': 'icon-image',
|
|
||||||
'/suno': 'icon-suno',
|
|
||||||
'/xmind': 'icon-xmind',
|
|
||||||
'/apps': 'icon-app',
|
|
||||||
'/member': 'icon-vip-user',
|
|
||||||
'/invite': 'icon-share',
|
|
||||||
'/luma': 'icon-luma',
|
|
||||||
})
|
|
||||||
|
|
||||||
const displayedChars = ref([])
|
const displayedChars = ref([])
|
||||||
const initAnimation = ref('')
|
const initAnimation = ref('')
|
||||||
let timer = null // 定时器句柄
|
let timer = null // 定时器句柄
|
||||||
|
|||||||
799
web/src/views/Jimeng.vue
Normal file
799
web/src/views/Jimeng.vue
Normal file
@@ -0,0 +1,799 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page-jimeng">
|
||||||
|
<!-- 左侧参数设置面板 -->
|
||||||
|
<div class="params-panel">
|
||||||
|
<h2>即梦AI</h2>
|
||||||
|
|
||||||
|
<!-- 功能分类按钮组 -->
|
||||||
|
<div class="category-buttons">
|
||||||
|
<div class="category-label">
|
||||||
|
<el-icon><Star /></el-icon>
|
||||||
|
功能分类
|
||||||
|
</div>
|
||||||
|
<div class="category-grid">
|
||||||
|
<div
|
||||||
|
v-for="category in store.categories"
|
||||||
|
:key="category.key"
|
||||||
|
:class="['category-btn', { active: store.activeCategory === category.key }]"
|
||||||
|
@click="store.switchCategory(category.key)"
|
||||||
|
>
|
||||||
|
<div class="category-icon">
|
||||||
|
<i :class="getCategoryIcon(category.key)"></i>
|
||||||
|
</div>
|
||||||
|
<div class="category-name">{{ category.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能开关 -->
|
||||||
|
<div class="function-switch" v-if="store.activeCategory === 'image_generation' || store.activeCategory === 'video_generation'">
|
||||||
|
<div class="switch-label">
|
||||||
|
<el-icon><Switch /></el-icon>
|
||||||
|
生成模式
|
||||||
|
</div>
|
||||||
|
<div class="switch-container">
|
||||||
|
<div class="switch-info">
|
||||||
|
<div class="switch-title">
|
||||||
|
{{ store.useImageInput ? (store.activeCategory === 'image_generation' ? '图生图' : '图生视频') : (store.activeCategory === 'image_generation' ? '文生图' : '文生视频') }}
|
||||||
|
</div>
|
||||||
|
<div class="switch-desc">
|
||||||
|
{{ store.useImageInput ? '使用图片作为输入' : '使用文字作为输入' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-switch
|
||||||
|
v-model="store.useImageInput"
|
||||||
|
@change="store.switchInputMode"
|
||||||
|
size="large"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 参数容器 -->
|
||||||
|
<div class="params-container">
|
||||||
|
<!-- 文生图 -->
|
||||||
|
<div v-if="store.activeFunction === 'text_to_image'" class="function-panel">
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">提示词:</span>
|
||||||
|
<el-tooltip content="输入你想要的图片内容描述" placement="right">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-input
|
||||||
|
v-model="store.textToImageParams.prompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
|
placeholder="请输入图片描述,越详细越好"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">图片尺寸:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-select v-model="store.textToImageParams.size" placeholder="选择尺寸">
|
||||||
|
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||||
|
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||||
|
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||||
|
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">创意度:</span>
|
||||||
|
<el-slider v-model="store.textToImageParams.scale" :min="1" :max="10" :step="0.5" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">种子值:</span>
|
||||||
|
<el-input-number v-model="store.textToImageParams.seed" :min="-1" :max="999999" size="small" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group flex justify-between">
|
||||||
|
<span class="label">智能优化提示词</span>
|
||||||
|
<el-switch v-model="store.textToImageParams.use_pre_llm" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图生图 -->
|
||||||
|
<div v-if="store.activeFunction === 'image_to_image_portrait'" class="function-panel">
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">上传图片:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<ImageUpload v-model="store.imageToImageParams.image_input" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">提示词:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-input
|
||||||
|
v-model="store.imageToImageParams.prompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
|
placeholder="描述你想要的图片效果"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">图片尺寸:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-select v-model="store.imageToImageParams.size" placeholder="选择尺寸">
|
||||||
|
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||||
|
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||||
|
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||||
|
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">GPEN强度:</span>
|
||||||
|
<el-slider v-model="store.imageToImageParams.gpen" :min="0" :max="1" :step="0.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">肌肤质感:</span>
|
||||||
|
<el-slider v-model="store.imageToImageParams.skin" :min="0" :max="1" :step="0.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">种子值:</span>
|
||||||
|
<el-input-number v-model="store.imageToImageParams.seed" :min="-1" :max="999999" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图像编辑 -->
|
||||||
|
<div v-if="store.activeFunction === 'image_edit'" class="function-panel">
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">上传图片:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<ImageUpload v-model="store.imageEditParams.image_urls" :multiple="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">编辑提示词:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-input
|
||||||
|
v-model="store.imageEditParams.prompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
|
placeholder="描述你想要的编辑效果"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">编辑强度:</span>
|
||||||
|
<el-slider v-model="store.imageEditParams.scale" :min="0" :max="1" :step="0.1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">种子值:</span>
|
||||||
|
<el-input-number v-model="store.imageEditParams.seed" :min="-1" :max="999999" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图像特效 -->
|
||||||
|
<div v-if="store.activeFunction === 'image_effects'" class="function-panel">
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">上传图片:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<ImageUpload v-model="store.imageEffectsParams.image_input1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">特效模板:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-select v-model="store.imageEffectsParams.template_id" placeholder="选择特效模板">
|
||||||
|
<el-option label="经典特效" value="classic" />
|
||||||
|
<el-option label="艺术风格" value="artistic" />
|
||||||
|
<el-option label="现代科技" value="modern" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">输出尺寸:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-select v-model="store.imageEffectsParams.size" placeholder="选择尺寸">
|
||||||
|
<el-option label="1328x1328 (正方形)" value="1328x1328" />
|
||||||
|
<el-option label="1024x1024 (正方形)" value="1024x1024" />
|
||||||
|
<el-option label="1024x768 (横版)" value="1024x768" />
|
||||||
|
<el-option label="768x1024 (竖版)" value="768x1024" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文生视频 -->
|
||||||
|
<div v-if="store.activeFunction === 'text_to_video'" class="function-panel">
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">提示词:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-input
|
||||||
|
v-model="store.textToVideoParams.prompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
|
placeholder="描述你想要的视频内容"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">视频比例:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-select v-model="store.textToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||||
|
<el-option label="16:9 (横版)" value="16:9" />
|
||||||
|
<el-option label="9:16 (竖版)" value="9:16" />
|
||||||
|
<el-option label="1:1 (正方形)" value="1:1" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">种子值:</span>
|
||||||
|
<el-input-number v-model="store.textToVideoParams.seed" :min="-1" :max="999999" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 图生视频 -->
|
||||||
|
<div v-if="store.activeFunction === 'image_to_video'" class="function-panel">
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">上传图片:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<ImageUpload v-model="store.imageToVideoParams.image_urls" :multiple="true" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">提示词:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-input
|
||||||
|
v-model="store.imageToVideoParams.prompt"
|
||||||
|
type="textarea"
|
||||||
|
:autosize="{ minRows: 3, maxRows: 5 }"
|
||||||
|
placeholder="描述你想要的视频效果"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-line pt">
|
||||||
|
<span class="label">视频比例:</span>
|
||||||
|
</div>
|
||||||
|
<div class="param-line">
|
||||||
|
<el-select v-model="store.imageToVideoParams.aspect_ratio" placeholder="选择比例">
|
||||||
|
<el-option label="16:9 (横版)" value="16:9" />
|
||||||
|
<el-option label="9:16 (竖版)" value="9:16" />
|
||||||
|
<el-option label="1:1 (正方形)" value="1:1" />
|
||||||
|
</el-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="item-group">
|
||||||
|
<span class="label">种子值:</span>
|
||||||
|
<el-input-number v-model="store.imageToVideoParams.seed" :min="-1" :max="999999" size="small" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 算力显示 -->
|
||||||
|
<div class="text-info">
|
||||||
|
<el-tag type="primary">当前算力: {{ store.userPower }}</el-tag>
|
||||||
|
<el-tag type="warning">消耗: {{ store.currentPowerCost }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 提交按钮 -->
|
||||||
|
<div class="submit-btn">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="store.submitTask"
|
||||||
|
:loading="store.submitting"
|
||||||
|
:disabled="!store.isLogin || store.userPower < store.currentPowerCost"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
立即生成 ({{ store.currentPowerCost }}<i class="iconfont icon-vip2"></i>)
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧任务列表 -->
|
||||||
|
<div class="main-content" v-loading="store.loading">
|
||||||
|
<div class="works-header">
|
||||||
|
<h2 class="h-title">你的作品</h2>
|
||||||
|
<div class="filter-buttons">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button
|
||||||
|
:type="store.taskFilter === 'all' ? 'primary' : 'default'"
|
||||||
|
@click="store.switchTaskFilter('all')"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
全部
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
:type="store.taskFilter === 'image' ? 'primary' : 'default'"
|
||||||
|
@click="store.switchTaskFilter('image')"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
图片
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
:type="store.taskFilter === 'video' ? 'primary' : 'default'"
|
||||||
|
@click="store.switchTaskFilter('video')"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
视频
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-list">
|
||||||
|
<div class="list-box" v-if="!store.noData">
|
||||||
|
<div v-for="item in store.currentList" :key="item.id" class="task-item">
|
||||||
|
<div class="task-left">
|
||||||
|
<div class="task-preview">
|
||||||
|
<el-image
|
||||||
|
v-if="item.img_url"
|
||||||
|
:src="item.img_url"
|
||||||
|
fit="cover"
|
||||||
|
class="preview-image"
|
||||||
|
/>
|
||||||
|
<video
|
||||||
|
v-else-if="item.video_url"
|
||||||
|
:src="item.video_url"
|
||||||
|
class="preview-video"
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
<div v-else class="preview-placeholder">
|
||||||
|
<el-icon><Picture /></el-icon>
|
||||||
|
<span>{{ store.getTaskStatusText(item.status) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-center">
|
||||||
|
<div class="task-info">
|
||||||
|
<el-tag size="small" :type="store.getStatusType(item.status)">
|
||||||
|
{{ store.getTaskStatusText(item.status) }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag size="small">{{ store.getFunctionName(item.type) }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<div class="task-prompt">
|
||||||
|
{{ store.substr(item.prompt, 200) }}
|
||||||
|
</div>
|
||||||
|
<div class="task-meta">
|
||||||
|
<span>{{ dateFormat(item.created_at) }}</span>
|
||||||
|
<span v-if="item.power">{{ item.power }}算力</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="task-right">
|
||||||
|
<div class="task-actions">
|
||||||
|
<el-button
|
||||||
|
v-if="item.status === 'failed'"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="store.retryTask(item.id)"
|
||||||
|
>
|
||||||
|
重试
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="item.video_url || item.img_url"
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
@click="store.downloadFile(item)"
|
||||||
|
>
|
||||||
|
下载
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-if="item.video_url"
|
||||||
|
type="default"
|
||||||
|
size="small"
|
||||||
|
@click="store.playVideo(item)"
|
||||||
|
>
|
||||||
|
播放
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="store.removeJob(item)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-empty
|
||||||
|
v-else
|
||||||
|
:image="store.nodata"
|
||||||
|
description="暂无任务,快去创建吧!"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="pagination" v-if="store.total > store.pageSize">
|
||||||
|
<el-pagination
|
||||||
|
background
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
:current-page="store.page"
|
||||||
|
:page-size="store.pageSize"
|
||||||
|
:total="store.total"
|
||||||
|
@current-change="store.fetchData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 视频预览对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="store.showDialog"
|
||||||
|
title="视频预览"
|
||||||
|
width="70%"
|
||||||
|
center
|
||||||
|
>
|
||||||
|
<video
|
||||||
|
:src="store.currentVideoUrl"
|
||||||
|
controls
|
||||||
|
style="width: 100%; max-height: 60vh;"
|
||||||
|
>
|
||||||
|
您的浏览器不支持视频播放
|
||||||
|
</video>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useJimengStore } from '@/store/jimeng'
|
||||||
|
import { dateFormat } from '@/utils/libs'
|
||||||
|
import ImageUpload from '@/components/ImageUpload.vue'
|
||||||
|
import { InfoFilled, Star, Switch, Picture } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const store = useJimengStore()
|
||||||
|
|
||||||
|
// 获取分类图标
|
||||||
|
const getCategoryIcon = (category) => {
|
||||||
|
const iconMap = {
|
||||||
|
'image_generation': 'iconfont icon-image',
|
||||||
|
'image_editing': 'iconfont icon-edit',
|
||||||
|
'image_effects': 'iconfont icon-magic',
|
||||||
|
'video_generation': 'iconfont icon-video'
|
||||||
|
}
|
||||||
|
return iconMap[category] || 'iconfont icon-image'
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.init()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
store.cleanup()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.page-jimeng {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: var(--chat-bg);
|
||||||
|
|
||||||
|
// 左侧参数面板
|
||||||
|
.params-panel {
|
||||||
|
min-width: 380px;
|
||||||
|
max-width: 380px;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
color: #333;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 功能分类按钮组
|
||||||
|
.category-buttons {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
|
||||||
|
.category-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #5865f2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
.category-btn {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 10px;
|
||||||
|
border: 2px solid #f0f0f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #5865f2;
|
||||||
|
background: #f8f9ff;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
border-color: #5865f2;
|
||||||
|
background: linear-gradient(135deg, #5865f2 0%, #7289da 100%);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(88, 101, 242, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 功能开关
|
||||||
|
.function-switch {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
|
||||||
|
.switch-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #5865f2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
|
||||||
|
.switch-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.switch-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数容器
|
||||||
|
.params-container {
|
||||||
|
.function-panel {
|
||||||
|
.param-line {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
&.pt {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
|
||||||
|
.label {
|
||||||
|
margin-right: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-info {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f0f8ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
border-left: 4px solid #5865f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧主要内容区域
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--chat-bg);
|
||||||
|
color: var(--text-theme-color);
|
||||||
|
|
||||||
|
.works-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.h-title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-theme-color);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
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;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
.el-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
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-right {
|
||||||
|
.task-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式设计
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-jimeng {
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.params-panel {
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -115,12 +115,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Luma 特有参数设置 -->
|
<!-- Luma 特有参数设置 -->
|
||||||
<div class="item-group">
|
<div class="item-group flex justify-between">
|
||||||
<span class="label">循环参考图</span>
|
<span class="label">循环参考图</span>
|
||||||
<el-switch v-model="store.lumaParams.loop" size="small" />
|
<el-switch v-model="store.lumaParams.loop" size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="item-group">
|
<div class="item-group flex justify-between">
|
||||||
<span class="label">提示词优化</span>
|
<span class="label">提示词优化</span>
|
||||||
<el-switch v-model="store.lumaParams.expand_prompt" size="small" />
|
<el-switch v-model="store.lumaParams.expand_prompt" size="small" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
543
web/src/views/admin/JimengJobs.vue
Normal file
543
web/src/views/admin/JimengJobs.vue
Normal file
@@ -0,0 +1,543 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>即梦AI任务管理</h2>
|
||||||
|
<p>管理所有用户的即梦AI任务,查看任务详情和统计信息</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索筛选 -->
|
||||||
|
<el-card class="filter-card" shadow="never">
|
||||||
|
<el-form :model="queryForm" ref="queryFormRef" :inline="true" label-width="80px">
|
||||||
|
<el-form-item label="用户ID">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.user_id"
|
||||||
|
placeholder="请输入用户ID"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务类型">
|
||||||
|
<el-select v-model="queryForm.type" placeholder="请选择任务类型" clearable style="width: 150px">
|
||||||
|
<el-option label="文生图" value="text_to_image" />
|
||||||
|
<el-option label="图生图" value="image_to_image_portrait" />
|
||||||
|
<el-option label="图像编辑" value="image_edit" />
|
||||||
|
<el-option label="图像特效" value="image_effects" />
|
||||||
|
<el-option label="文生视频" value="text_to_video" />
|
||||||
|
<el-option label="图生视频" value="image_to_video" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="任务状态">
|
||||||
|
<el-select v-model="queryForm.status" placeholder="请选择状态" clearable style="width: 120px">
|
||||||
|
<el-option label="等待中" value="pending" />
|
||||||
|
<el-option label="处理中" value="processing" />
|
||||||
|
<el-option label="已完成" value="completed" />
|
||||||
|
<el-option label="失败" value="failed" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery" :loading="loading">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
搜索
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="resetQuery">
|
||||||
|
<el-icon><Refresh /></el-icon>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
<el-button type="danger" @click="handleBatchDelete" :disabled="!multipleSelection.length">
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
批量删除
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 统计信息 -->
|
||||||
|
<el-row :gutter="20" class="stats-row">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number">{{ stats.totalTasks }}</div>
|
||||||
|
<div class="stat-label">总任务数</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number success">{{ stats.completedTasks }}</div>
|
||||||
|
<div class="stat-label">已完成</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number warning">{{ stats.processingTasks }}</div>
|
||||||
|
<div class="stat-label">处理中</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-number danger">{{ stats.failedTasks }}</div>
|
||||||
|
<div class="stat-label">失败</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 任务列表 -->
|
||||||
|
<el-card class="table-card">
|
||||||
|
<el-table
|
||||||
|
:data="taskList"
|
||||||
|
v-loading="loading"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
stripe
|
||||||
|
border
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="user_id" label="用户ID" width="80" />
|
||||||
|
<el-table-column prop="type" label="任务类型" width="120">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag size="small">{{ getTaskTypeName(scope.row.type) }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="prompt" label="提示词" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="getStatusColor(scope.row.status)" size="small">
|
||||||
|
{{ getStatusName(scope.row.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="progress" label="进度" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-progress :percentage="scope.row.progress" :stroke-width="4" />
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="power" label="算力" width="80" />
|
||||||
|
<el-table-column prop="created_at" label="创建时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDateTime(scope.row.created_at) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="150" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="handleViewDetail(scope.row)"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-container">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.page"
|
||||||
|
v-model:page-size="pagination.size"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
:total="pagination.total"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 任务详情对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="detailDialog.visible"
|
||||||
|
:title="`任务详情 - ${detailDialog.data.id}`"
|
||||||
|
width="800px"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div class="detail-content" v-if="detailDialog.data">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="任务ID">{{ detailDialog.data.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户ID">{{ detailDialog.data.user_id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="任务类型">{{ getTaskTypeName(detailDialog.data.type) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">
|
||||||
|
<el-tag :type="getStatusColor(detailDialog.data.status)">
|
||||||
|
{{ getStatusName(detailDialog.data.status) }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="进度">{{ detailDialog.data.progress }}%</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="算力消耗">{{ detailDialog.data.power }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ formatDateTime(detailDialog.data.created_at) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">{{ formatDateTime(detailDialog.data.updated_at) }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<div class="detail-section">
|
||||||
|
<h4>提示词</h4>
|
||||||
|
<div class="prompt-content">{{ detailDialog.data.prompt || '无' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="detailDialog.data.task_params">
|
||||||
|
<h4>任务参数</h4>
|
||||||
|
<el-input
|
||||||
|
v-model="detailDialog.data.task_params"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
readonly
|
||||||
|
class="params-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="detailDialog.data.err_msg">
|
||||||
|
<h4>错误信息</h4>
|
||||||
|
<el-alert :title="detailDialog.data.err_msg" type="error" :closable="false" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="detailDialog.data.img_url || detailDialog.data.video_url">
|
||||||
|
<h4>生成结果</h4>
|
||||||
|
<div class="result-content">
|
||||||
|
<div v-if="detailDialog.data.img_url" class="result-item">
|
||||||
|
<label>图片:</label>
|
||||||
|
<el-image
|
||||||
|
:src="detailDialog.data.img_url"
|
||||||
|
:preview-src-list="[detailDialog.data.img_url]"
|
||||||
|
fit="cover"
|
||||||
|
style="width: 100px; height: 100px; border-radius: 4px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="detailDialog.data.video_url" class="result-item">
|
||||||
|
<label>视频:</label>
|
||||||
|
<video
|
||||||
|
:src="detailDialog.data.video_url"
|
||||||
|
controls
|
||||||
|
style="width: 200px; height: 150px; border-radius: 4px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-section" v-if="detailDialog.data.raw_data">
|
||||||
|
<h4>原始响应数据</h4>
|
||||||
|
<el-input
|
||||||
|
v-model="formattedRawData"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
readonly
|
||||||
|
class="raw-data-content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import { Search, Refresh, Delete } from '@element-plus/icons-vue'
|
||||||
|
import { formatDateTime } from '@/utils/libs'
|
||||||
|
import { httpGet, httpPost } from '@/utils/http'
|
||||||
|
|
||||||
|
// 查询表单
|
||||||
|
const queryForm = reactive({
|
||||||
|
user_id: '',
|
||||||
|
type: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页信息
|
||||||
|
const pagination = reactive({
|
||||||
|
page: 1,
|
||||||
|
size: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 数据
|
||||||
|
const taskList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const multipleSelection = ref([])
|
||||||
|
const queryFormRef = ref(null)
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
const stats = reactive({
|
||||||
|
totalTasks: 0,
|
||||||
|
completedTasks: 0,
|
||||||
|
processingTasks: 0,
|
||||||
|
failedTasks: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 详情对话框
|
||||||
|
const detailDialog = reactive({
|
||||||
|
visible: false,
|
||||||
|
data: {}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 格式化原始数据
|
||||||
|
const formattedRawData = computed(() => {
|
||||||
|
if (!detailDialog.data.raw_data) return ''
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(detailDialog.data.raw_data), null, 2)
|
||||||
|
} catch (error) {
|
||||||
|
return detailDialog.data.raw_data
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取任务类型名称
|
||||||
|
const getTaskTypeName = (type) => {
|
||||||
|
const typeMap = {
|
||||||
|
'text_to_image': '文生图',
|
||||||
|
'image_to_image_portrait': '图生图',
|
||||||
|
'image_edit': '图像编辑',
|
||||||
|
'image_effects': '图像特效',
|
||||||
|
'text_to_video': '文生视频',
|
||||||
|
'image_to_video': '图生视频'
|
||||||
|
}
|
||||||
|
return typeMap[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态名称
|
||||||
|
const getStatusName = (status) => {
|
||||||
|
const statusMap = {
|
||||||
|
'pending': '等待中',
|
||||||
|
'processing': '处理中',
|
||||||
|
'completed': '已完成',
|
||||||
|
'failed': '失败'
|
||||||
|
}
|
||||||
|
return statusMap[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态颜色
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colorMap = {
|
||||||
|
'pending': '',
|
||||||
|
'processing': 'warning',
|
||||||
|
'completed': 'success',
|
||||||
|
'failed': 'danger'
|
||||||
|
}
|
||||||
|
return colorMap[status] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取任务列表
|
||||||
|
const getTaskList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.page,
|
||||||
|
page_size: pagination.size,
|
||||||
|
...queryForm
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await httpGet('/api/admin/jimeng/jobs', params)
|
||||||
|
taskList.value = response.data.jobs || []
|
||||||
|
pagination.total = response.data.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取任务列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取统计信息
|
||||||
|
const getStats = async () => {
|
||||||
|
try {
|
||||||
|
const response = await httpGet('/api/admin/jimeng/stats')
|
||||||
|
Object.assign(stats, response.data)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取统计信息失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询
|
||||||
|
const handleQuery = () => {
|
||||||
|
pagination.page = 1
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置查询
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields()
|
||||||
|
Object.assign(queryForm, {
|
||||||
|
user_id: '',
|
||||||
|
type: '',
|
||||||
|
status: ''
|
||||||
|
})
|
||||||
|
pagination.page = 1
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择变化
|
||||||
|
const handleSelectionChange = (selection) => {
|
||||||
|
multipleSelection.value = selection
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查看详情
|
||||||
|
const handleViewDetail = async (row) => {
|
||||||
|
try {
|
||||||
|
const response = await httpGet(`/api/admin/jimeng/job/${row.id}`)
|
||||||
|
detailDialog.data = response.data
|
||||||
|
detailDialog.visible = true
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取任务详情失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除任务
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm('确定要删除这个任务吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
await httpPost(`/api/admin/jimeng/job/${row.id}`, {}, { method: 'DELETE' })
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
getTaskList()
|
||||||
|
getStats()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (!multipleSelection.value.length) {
|
||||||
|
ElMessage.warning('请选择要删除的任务')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除选中的 ${multipleSelection.value.length} 个任务吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
const jobIds = multipleSelection.value.map(item => item.id)
|
||||||
|
await httpPost('/api/admin/jimeng/batch-remove', { job_ids: jobIds })
|
||||||
|
ElMessage.success('批量删除成功')
|
||||||
|
getTaskList()
|
||||||
|
getStats()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error('批量删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size) => {
|
||||||
|
pagination.size = size
|
||||||
|
pagination.page = 1
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page) => {
|
||||||
|
pagination.page = page
|
||||||
|
getTaskList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
getTaskList()
|
||||||
|
getStats()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="stylus" scoped>
|
||||||
|
.app-container
|
||||||
|
padding 20px
|
||||||
|
|
||||||
|
.page-header
|
||||||
|
margin-bottom 20px
|
||||||
|
|
||||||
|
h2
|
||||||
|
margin 0 0 8px 0
|
||||||
|
color #303133
|
||||||
|
|
||||||
|
p
|
||||||
|
margin 0
|
||||||
|
color #606266
|
||||||
|
font-size 14px
|
||||||
|
|
||||||
|
.filter-card
|
||||||
|
margin-bottom 20px
|
||||||
|
|
||||||
|
.stats-row
|
||||||
|
margin-bottom 20px
|
||||||
|
|
||||||
|
.stat-card
|
||||||
|
.stat-item
|
||||||
|
text-align center
|
||||||
|
padding 20px
|
||||||
|
|
||||||
|
.stat-number
|
||||||
|
font-size 28px
|
||||||
|
font-weight bold
|
||||||
|
color #303133
|
||||||
|
margin-bottom 8px
|
||||||
|
|
||||||
|
&.success
|
||||||
|
color #67c23a
|
||||||
|
|
||||||
|
&.warning
|
||||||
|
color #e6a23c
|
||||||
|
|
||||||
|
&.danger
|
||||||
|
color #f56c6c
|
||||||
|
|
||||||
|
.stat-label
|
||||||
|
font-size 14px
|
||||||
|
color #909399
|
||||||
|
|
||||||
|
.table-card
|
||||||
|
.pagination-container
|
||||||
|
margin-top 20px
|
||||||
|
display flex
|
||||||
|
justify-content center
|
||||||
|
|
||||||
|
.detail-content
|
||||||
|
.detail-section
|
||||||
|
margin-bottom 20px
|
||||||
|
|
||||||
|
h4
|
||||||
|
margin 0 0 10px 0
|
||||||
|
color #303133
|
||||||
|
font-size 16px
|
||||||
|
|
||||||
|
.prompt-content
|
||||||
|
background #f5f7fa
|
||||||
|
padding 12px
|
||||||
|
border-radius 4px
|
||||||
|
color #606266
|
||||||
|
line-height 1.6
|
||||||
|
|
||||||
|
.params-content, .raw-data-content
|
||||||
|
font-family monospace
|
||||||
|
|
||||||
|
.result-content
|
||||||
|
.result-item
|
||||||
|
margin-bottom 10px
|
||||||
|
display flex
|
||||||
|
align-items center
|
||||||
|
gap 10px
|
||||||
|
|
||||||
|
label
|
||||||
|
font-weight bold
|
||||||
|
color #303133
|
||||||
|
min-width 50px
|
||||||
|
</style>
|
||||||
@@ -169,10 +169,10 @@
|
|||||||
<el-form-item>
|
<el-form-item>
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="label-title">
|
<div class="label-title">
|
||||||
默认翻译模型
|
系统辅助AI模型
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
effect="dark"
|
effect="dark"
|
||||||
content="选择一个默认模型来翻译提示词"
|
content="用来辅助用户生成提示词,翻译的AI模型,默认使用 gpt-4o-mini"
|
||||||
raw-content
|
raw-content
|
||||||
placement="right"
|
placement="right"
|
||||||
>
|
>
|
||||||
@@ -183,9 +183,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<el-select
|
<el-select
|
||||||
v-model.number="system['translate_model_id']"
|
v-model.number="system['assistant_model_id']"
|
||||||
:filterable="true"
|
:filterable="true"
|
||||||
placeholder="选择一个默认模型来翻译提示词"
|
placeholder="选择一个系统辅助AI模型"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-option
|
<el-option
|
||||||
|
|||||||
Reference in New Issue
Block a user