diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c4a39d1..be57588e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ # 更新日志 +## v4.1.7 +* Bug修复:手机邮箱相关的注册问题 [#IB0HS5](https://gitee.com/blackfox/geekai/issues/IB0HS5) +* Bug修复:音乐视频无法下载,思维导图下载后看不清文字[#IB0N2E](https://gitee.com/blackfox/geekai/issues/IB0N2E) +* 功能优化:保存所有AIGC任务的原始信息,程序启动之后自动将未执行的任务加入到 redis 队列 +* 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回 +* 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能 +* Bug修复:修复图片对话的时候,上下文不起作用的Bug +* 功能新增:管理后台新增批量导出兑换码功能 + ## v4.1.6 * 功能新增:**支持OpenAI实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。 * 功能优化:优化MysQL容器配置文档,解决MysQL容器资源占用过高问题 diff --git a/api/core/types/chat.go b/api/core/types/chat.go index 56e2639f..6d636955 100644 --- a/api/core/types/chat.go +++ b/api/core/types/chat.go @@ -16,7 +16,8 @@ type ApiRequest struct { Stream bool `json:"stream,omitempty"` Messages []interface{} `json:"messages,omitempty"` Tools []Tool `json:"tools,omitempty"` - Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台 + Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台 + ResponseFormat interface{} `json:"response_format,omitempty"` // 响应格式 ToolChoice string `json:"tool_choice,omitempty"` diff --git a/api/core/types/config.go b/api/core/types/config.go index b827d1f3..a033c17c 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -142,7 +142,6 @@ type SystemConfig struct { OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间 VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明 - DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型 MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 @@ -164,6 +163,7 @@ type SystemConfig struct { Copyright string `json:"copyright"` // 版权信息 MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本 - EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码 - EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表 + EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码 + EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表 + TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的大模型 id } diff --git a/api/core/types/locked_map.go b/api/core/types/locked_map.go index 5ae764bb..a062a50c 100644 --- a/api/core/types/locked_map.go +++ b/api/core/types/locked_map.go @@ -16,7 +16,7 @@ type MKey interface { string | int | uint } type MValue interface { - *WsClient | *ChatSession | context.CancelFunc | []Message + *WsClient | *ChatSession | context.CancelFunc | []interface{} } type LMap[K MKey, T MValue] struct { lock sync.RWMutex diff --git a/api/core/types/task.go b/api/core/types/task.go index 5dac6443..d822faee 100644 --- a/api/core/types/task.go +++ b/api/core/types/task.go @@ -24,30 +24,31 @@ const ( // MjTask MidJourney 任务 type MjTask struct { - Id uint `json:"id"` // 任务ID - TaskId string `json:"task_id"` // 中转任务ID - ClientId string `json:"client_id"` - ImgArr []string `json:"img_arr"` - Type TaskType `json:"type"` - UserId int `json:"user_id"` - Prompt string `json:"prompt,omitempty"` - NegPrompt string `json:"neg_prompt,omitempty"` - Params string `json:"full_prompt"` - Index int `json:"index,omitempty"` - MessageId string `json:"message_id,omitempty"` - MessageHash string `json:"message_hash,omitempty"` - RetryCount int `json:"retry_count"` - ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道 - Mode string `json:"mode"` // 绘画模式,relax, fast, turbo + Id uint `json:"id"` // 任务ID + TaskId string `json:"task_id"` // 中转任务ID + ClientId string `json:"client_id"` + ImgArr []string `json:"img_arr"` + Type TaskType `json:"type"` + UserId int `json:"user_id"` + Prompt string `json:"prompt,omitempty"` + NegPrompt string `json:"neg_prompt,omitempty"` + Params string `json:"full_prompt"` + Index int `json:"index,omitempty"` + MessageId string `json:"message_id,omitempty"` + MessageHash string `json:"message_hash,omitempty"` + ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道 + Mode string `json:"mode"` // 绘画模式,relax, fast, turbo + TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID } type SdTask struct { - Id int `json:"id"` // job 数据库ID - Type TaskType `json:"type"` - ClientId string `json:"client_id"` - UserId int `json:"user_id"` - Params SdTaskParams `json:"params"` - RetryCount int `json:"retry_count"` + Id int `json:"id"` // job 数据库ID + Type TaskType `json:"type"` + ClientId string `json:"client_id"` + UserId int `json:"user_id"` + Params SdTaskParams `json:"params"` + RetryCount int `json:"retry_count"` + TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID } type SdTaskParams struct { @@ -73,7 +74,7 @@ type SdTaskParams struct { // DallTask DALL-E task type DallTask struct { ClientId string `json:"client_id"` - JobId uint `json:"job_id"` + Id uint `json:"id"` UserId uint `json:"user_id"` Prompt string `json:"prompt"` N int `json:"n"` @@ -81,7 +82,8 @@ type DallTask struct { Size string `json:"size"` Style string `json:"style"` - Power int `json:"power"` + Power int `json:"power"` + TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID } type SunoTask struct { @@ -109,14 +111,15 @@ const ( ) type VideoTask struct { - ClientId string `json:"client_id"` - Id uint `json:"id"` - Channel string `json:"channel"` - UserId int `json:"user_id"` - Type string `json:"type"` - TaskId string `json:"task_id"` - Prompt string `json:"prompt"` // 提示词 - Params VideoParams `json:"params"` + ClientId string `json:"client_id"` + Id uint `json:"id"` + Channel string `json:"channel"` + UserId int `json:"user_id"` + Type string `json:"type"` + TaskId string `json:"task_id"` + Prompt string `json:"prompt"` // 提示词 + Params VideoParams `json:"params"` + TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID } type VideoParams struct { diff --git a/api/handler/admin/redeem_handler.go b/api/handler/admin/redeem_handler.go index 3bce461c..b2559a9c 100644 --- a/api/handler/admin/redeem_handler.go +++ b/api/handler/admin/redeem_handler.go @@ -8,6 +8,8 @@ package admin // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "encoding/csv" + "fmt" "geekai/core" "geekai/core/types" "geekai/handler" @@ -35,12 +37,10 @@ func (h *RedeemHandler) List(c *gin.Context) { session := h.DB.Session(&gorm.Session{}) if code != "" { - session.Where("code LIKE ?", "%"+code+"%") + session = session.Where("code LIKE ?", "%"+code+"%") } - if status == 0 { - session.Where("redeem_at = ?", 0) - } else if status == 1 { - session.Where("redeem_at > ?", 0) + if status >= 0 { + session = session.Where("redeemed_at", status) } var total int64 @@ -80,6 +80,65 @@ func (h *RedeemHandler) List(c *gin.Context) { resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items)) } +// Export 导出 CVS 文件 +func (h *RedeemHandler) Export(c *gin.Context) { + var data struct { + Status int `json:"status"` + Ids []int `json:"ids"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + } + + session := h.DB.Session(&gorm.Session{}) + if data.Status >= 0 { + session = session.Where("redeemed_at", data.Status) + } + if len(data.Ids) > 0 { + session = session.Where("id IN ?", data.Ids) + } + + var items []model.Redeem + err := session.Order("id DESC").Find(&items).Error + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + // 设置响应头,告诉浏览器这是一个附件,需要下载 + c.Header("Content-Disposition", "attachment; filename=output.csv") + c.Header("Content-Type", "text/csv") + + // 创建一个 CSV writer + writer := csv.NewWriter(c.Writer) + + // 写入 CSV 文件的标题行 + headers := []string{"名称", "兑换码", "算力", "创建时间"} + if err := writer.Write(headers); err != nil { + resp.ERROR(c, err.Error()) + return + } + + // 写入数据行 + records := make([][]string, 0) + for _, item := range items { + records = append(records, []string{item.Name, item.Code, fmt.Sprintf("%d", item.Power), item.CreatedAt.Format("2006-01-02 15:04:05")}) + } + for _, record := range records { + if err := writer.Write(record); err != nil { + resp.ERROR(c, err.Error()) + return + } + } + + // 确保所有数据都已写入响应 + writer.Flush() + if err := writer.Error(); err != nil { + resp.ERROR(c, err.Error()) + return + } +} + func (h *RedeemHandler) Create(c *gin.Context) { var data struct { Name string `json:"name"` diff --git a/api/handler/chat_handler.go b/api/handler/chat_handler.go index 30948f1d..b71e86b9 100644 --- a/api/handler/chat_handler.go +++ b/api/handler/chat_handler.go @@ -40,7 +40,7 @@ type ChatHandler struct { uploadManager *oss.UploaderManager licenseService *service.LicenseService ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function - ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message + ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message userService *service.UserService } @@ -51,7 +51,7 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag uploadManager: manager, licenseService: licenseService, ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), - ChatContexts: types.NewLMap[string, []types.Message](), + ChatContexts: types.NewLMap[string, []interface{}](), userService: userService, } } @@ -143,8 +143,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio } // 加载聊天上下文 - chatCtx := make([]types.Message, 0) - messages := make([]types.Message, 0) + chatCtx := make([]interface{}, 0) + messages := make([]interface{}, 0) if h.App.SysConfig.EnableContext { if h.ChatContexts.Has(session.ChatId) { messages = h.ChatContexts.Get(session.ChatId) @@ -174,7 +174,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio for i := len(messages) - 1; i >= 0; i-- { v := messages[i] - tks, _ = utils.CalcTokens(v.Content, req.Model) + tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model) // 上下文 token 超出了模型的最大上下文长度 if tokens+tks >= session.Model.MaxContext { break @@ -192,8 +192,9 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio logger.Debugf("聊天上下文:%+v", chatCtx) } reqMgs := make([]interface{}, 0) - for _, m := range chatCtx { - reqMgs = append(reqMgs, m) + + for i := len(chatCtx) - 1; i >= 0; i-- { + reqMgs = append(reqMgs, chatCtx[i]) } fullPrompt := prompt @@ -258,7 +259,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio logger.Debugf("%+v", req.Messages) - return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) + return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws) } // Tokens 统计 token 数量 @@ -371,7 +372,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi } else { client = http.DefaultClient } - logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model) + logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value)) // 更新API KEY 最后使用时间 h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix()) @@ -399,17 +400,15 @@ func (h *ChatHandler) saveChatHistory( req types.ApiRequest, usage Usage, message types.Message, - chatCtx []types.Message, session *types.ChatSession, role model.ChatRole, userVo vo.User, promptCreatedAt time.Time, replyCreatedAt time.Time) { - useMsg := types.Message{Role: "user", Content: usage.Prompt} - // 更新上下文消息,如果是调用函数则不需要更新上下文 + // 更新上下文消息 if h.App.SysConfig.EnableContext { - chatCtx = append(chatCtx, useMsg) // 提问消息 + chatCtx := req.Messages // 提问消息 chatCtx = append(chatCtx, message) // 回复消息 h.ChatContexts.Put(session.ChatId, chatCtx) } diff --git a/api/handler/chat_model_handler.go b/api/handler/chat_model_handler.go index 1b74f348..d054ef3c 100644 --- a/api/handler/chat_model_handler.go +++ b/api/handler/chat_model_handler.go @@ -30,29 +30,25 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler { func (h *ChatModelHandler) List(c *gin.Context) { var items []model.ChatModel var chatModels = make([]vo.ChatModel, 0) - var res *gorm.DB session := h.DB.Session(&gorm.Session{}).Where("enabled", true) t := c.Query("type") if t != "" { session = session.Where("type", t) } - // 如果用户没有登录,则加载所有开放模型 - if !h.IsLogin(c) { - res = session.Where("open", true).Order("sort_num ASC").Find(&items) - } else { + + session = session.Where("open", true) + if h.IsLogin(c) { user, _ := h.GetLoginUser(c) var models []int err := utils.JsonDecode(user.ChatModels, &models) - if err != nil { - resp.ERROR(c, "当前用户没有订阅任何模型") - return - } // 查询用户有权限访问的模型以及所有开放的模型 - res = h.DB.Where("enabled = ?", true).Where( - h.DB.Where("id IN ?", models).Or("open", true), - ).Order("sort_num ASC").Find(&items) + if err == nil { + session = session.Or("id IN ?", models) + } + } + res := session.Order("sort_num ASC").Find(&items) if res.Error == nil { for _, item := range items { var cm vo.ChatModel diff --git a/api/handler/dalle_handler.go b/api/handler/dalle_handler.go index 404c9704..94c49d4f 100644 --- a/api/handler/dalle_handler.go +++ b/api/handler/dalle_handler.go @@ -8,7 +8,6 @@ package handler // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( - "fmt" "geekai/core" "geekai/core/types" "geekai/service" @@ -72,10 +71,21 @@ func (h *DallJobHandler) Image(c *gin.Context) { idValue, _ := c.Get(types.LoginUserID) userId := utils.IntValue(utils.InterfaceToString(idValue), 0) + task := types.DallTask{ + ClientId: data.ClientId, + UserId: uint(userId), + Prompt: data.Prompt, + Quality: data.Quality, + Size: data.Size, + Style: data.Style, + Power: h.App.SysConfig.DallPower, + TranslateModelId: h.App.SysConfig.TranslateModelId, + } job := model.DallJob{ - UserId: uint(userId), - Prompt: data.Prompt, - Power: h.App.SysConfig.DallPower, + UserId: uint(userId), + Prompt: data.Prompt, + Power: task.Power, + TaskInfo: utils.JsonEncode(task), } res := h.DB.Create(&job) if res.Error != nil { @@ -83,16 +93,8 @@ func (h *DallJobHandler) Image(c *gin.Context) { return } - h.dallService.PushTask(types.DallTask{ - ClientId: data.ClientId, - JobId: job.Id, - UserId: uint(userId), - Prompt: data.Prompt, - Quality: data.Quality, - Size: data.Size, - Style: data.Style, - Power: job.Power, - }) + task.Id = job.Id + h.dallService.PushTask(task) resp.SUCCESS(c) } @@ -179,25 +181,14 @@ func (h *DallJobHandler) Remove(c *gin.Context) { } // 删除任务 - tx := h.DB.Begin() - tx.Delete(&job) - // 如果任务未完成,或者任务失败,则恢复用户算力 - if job.Progress != 100 { - err := h.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: "dall-e-3", - Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg), - }) - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } + err := h.DB.Delete(&job).Error + if err != nil { + resp.ERROR(c, err.Error()) + return } - tx.Commit() // remove image - err := h.uploader.GetUploadHandler().Delete(job.ImgURL) + err = h.uploader.GetUploadHandler().Delete(job.ImgURL) if err != nil { logger.Error("remove image failed: ", err) } diff --git a/api/handler/function_handler.go b/api/handler/function_handler.go index f1838d4d..a4dcd005 100644 --- a/api/handler/function_handler.go +++ b/api/handler/function_handler.go @@ -113,10 +113,13 @@ func (h *FunctionHandler) WeiBo(c *gin.Context) { SetHeader("AppId", h.config.AppId). SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)). SetSuccessResult(&res).Get(url) - if err != nil || r.IsErrorState() { - resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err)) + if err != nil { + resp.ERROR(c, fmt.Sprintf("%v", err)) return } + if r.IsErrorState() { + resp.ERROR(c, fmt.Sprintf("error http code status: %v", r.Status)) + } if res.Code != types.Success { resp.ERROR(c, res.Message) @@ -209,7 +212,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) { } content, err := h.dallService.Image(types.DallTask{ - JobId: job.Id, + Id: job.Id, UserId: user.Id, Prompt: job.Prompt, N: 1, diff --git a/api/handler/markmap_handler.go b/api/handler/markmap_handler.go index e57f6f4d..3d7a7b14 100644 --- a/api/handler/markmap_handler.go +++ b/api/handler/markmap_handler.go @@ -87,7 +87,7 @@ func (h *MarkMapHandler) Generate(c *gin.Context) { 请直接生成结果,不要任何解释性语句。 `}) messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图,要求结构清晰,有条理", data.Prompt)}) - content, err := utils.SendOpenAIMessage(h.DB, messages, chatModel.Value, chatModel.KeyId) + content, err := utils.SendOpenAIMessage(h.DB, messages, data.ModelId) if err != nil { resp.ERROR(c, fmt.Sprintf("请求 OpenAI API 失败: %s", err)) return diff --git a/api/handler/mj_handler.go b/api/handler/mj_handler.go index 858a0d89..a845d740 100644 --- a/api/handler/mj_handler.go +++ b/api/handler/mj_handler.go @@ -152,10 +152,23 @@ func (h *MidJourneyHandler) Image(c *gin.Context) { resp.ERROR(c, "error with generate task id: "+err.Error()) return } + task := types.MjTask{ + ClientId: data.ClientId, + TaskId: taskId, + Type: types.TaskType(data.TaskType), + Prompt: data.Prompt, + NegPrompt: data.NegPrompt, + Params: params, + UserId: userId, + ImgArr: data.ImgArr, + Mode: h.App.SysConfig.MjMode, + TranslateModelId: h.App.SysConfig.TranslateModelId, + } job := model.MidJourneyJob{ Type: data.TaskType, UserId: userId, TaskId: taskId, + TaskInfo: utils.JsonEncode(task), Progress: 0, Prompt: fmt.Sprintf("%s %s", data.Prompt, params), Power: h.App.SysConfig.MjPower, @@ -175,18 +188,8 @@ func (h *MidJourneyHandler) Image(c *gin.Context) { return } - h.mjService.PushTask(types.MjTask{ - Id: job.Id, - ClientId: data.ClientId, - TaskId: taskId, - Type: types.TaskType(data.TaskType), - Prompt: data.Prompt, - NegPrompt: data.NegPrompt, - Params: params, - UserId: userId, - ImgArr: data.ImgArr, - Mode: h.App.SysConfig.MjMode, - }) + task.Id = job.Id + h.mjService.PushTask(task) // update user's power err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ @@ -225,22 +228,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) { idValue, _ := c.Get(types.LoginUserID) userId := utils.IntValue(utils.InterfaceToString(idValue), 0) taskId, _ := h.snowflake.Next(true) - job := model.MidJourneyJob{ - Type: types.TaskUpscale.String(), - ReferenceId: data.MessageId, - UserId: userId, - TaskId: taskId, - Progress: 0, - Power: h.App.SysConfig.MjActionPower, - CreatedAt: time.Now(), - } - if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { - resp.ERROR(c, "添加任务失败:"+res.Error.Error()) - return - } - - h.mjService.PushTask(types.MjTask{ - Id: job.Id, + task := types.MjTask{ ClientId: data.ClientId, Type: types.TaskUpscale, UserId: userId, @@ -249,7 +237,23 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) { MessageId: data.MessageId, MessageHash: data.MessageHash, Mode: h.App.SysConfig.MjMode, - }) + } + job := model.MidJourneyJob{ + Type: types.TaskUpscale.String(), + UserId: userId, + TaskId: taskId, + TaskInfo: utils.JsonEncode(task), + Progress: 0, + Power: h.App.SysConfig.MjActionPower, + CreatedAt: time.Now(), + } + if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { + resp.ERROR(c, "添加任务失败:"+res.Error.Error()) + return + } + + task.Id = job.Id + h.mjService.PushTask(task) // update user's power err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ @@ -280,23 +284,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { idValue, _ := c.Get(types.LoginUserID) userId := utils.IntValue(utils.InterfaceToString(idValue), 0) taskId, _ := h.snowflake.Next(true) - job := model.MidJourneyJob{ - Type: types.TaskVariation.String(), - ChannelId: data.ChannelId, - ReferenceId: data.MessageId, - UserId: userId, - TaskId: taskId, - Progress: 0, - Power: h.App.SysConfig.MjActionPower, - CreatedAt: time.Now(), - } - if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { - resp.ERROR(c, "添加任务失败:"+res.Error.Error()) - return - } - - h.mjService.PushTask(types.MjTask{ - Id: job.Id, + task := types.MjTask{ Type: types.TaskVariation, ClientId: data.ClientId, UserId: userId, @@ -305,7 +293,24 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) { MessageId: data.MessageId, MessageHash: data.MessageHash, Mode: h.App.SysConfig.MjMode, - }) + } + job := model.MidJourneyJob{ + Type: types.TaskVariation.String(), + ChannelId: data.ChannelId, + UserId: userId, + TaskId: taskId, + TaskInfo: utils.JsonEncode(task), + Progress: 0, + Power: h.App.SysConfig.MjActionPower, + CreatedAt: time.Now(), + } + if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { + resp.ERROR(c, "添加任务失败:"+res.Error.Error()) + return + } + + task.Id = job.Id + h.mjService.PushTask(task) err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ Type: types.PowerConsume, @@ -401,26 +406,15 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) { return } - // remove job recode - tx := h.DB.Begin() - tx.Delete(&job) - // 如果任务未完成,或者任务失败,则恢复用户算力 - if job.Progress != 100 { - err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: "mid-journey", - Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg), - }) - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } + // remove job + err := h.DB.Delete(&job).Error + if err != nil { + resp.ERROR(c, err.Error()) + return } - tx.Commit() // remove image - err := h.uploader.GetUploadHandler().Delete(job.ImgURL) + err = h.uploader.GetUploadHandler().Delete(job.ImgURL) if err != nil { logger.Error("remove image failed: ", err) } diff --git a/api/handler/openai_handler.go b/api/handler/openai_handler.go index 302b3138..6e8ff960 100644 --- a/api/handler/openai_handler.go +++ b/api/handler/openai_handler.go @@ -51,7 +51,6 @@ type OpenAIResVo struct { // OPenAI 消息发送实现 func (h *ChatHandler) sendOpenAiMessage( - chatCtx []types.Message, req types.ApiRequest, userVo vo.User, ctx context.Context, @@ -201,7 +200,7 @@ func (h *ChatHandler) sendOpenAiMessage( TotalTokens: 0, } message.Content = usage.Content - h.saveChatHistory(req, usage, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt) + h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) } } else { // 非流式输出 var respVo OpenAIResVo @@ -220,7 +219,7 @@ func (h *ChatHandler) sendOpenAiMessage( utils.SendChunkMsg(ws, content) respVo.Usage.Prompt = prompt respVo.Usage.Content = content - h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, chatCtx, session, role, userVo, promptCreatedAt, time.Now()) + h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) } return nil diff --git a/api/handler/prompt_handler.go b/api/handler/prompt_handler.go new file mode 100644 index 00000000..596791b0 --- /dev/null +++ b/api/handler/prompt_handler.go @@ -0,0 +1,123 @@ +package handler + +// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +// * 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 ( + "fmt" + "geekai/core" + "geekai/core/types" + "geekai/service" + "geekai/service/oss" + "geekai/service/suno" + "geekai/utils" + "geekai/utils/resp" + "github.com/gin-gonic/gin" + "gorm.io/gorm" + "strings" +) + +// 提示词生成 handler +// 使用 AI 生成绘画指令,歌词,视频生成指令等 + +type PromptHandler struct { + BaseHandler + sunoService *suno.Service + uploader *oss.UploaderManager + userService *service.UserService +} + +func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *PromptHandler { + return &PromptHandler{ + BaseHandler: BaseHandler{ + App: app, + DB: db, + }, + userService: userService, + } +} + +// Lyric 生成歌词 +func (h *PromptHandler) Lyric(c *gin.Context) { + var data struct { + Prompt string `json:"prompt"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, content) +} + +// Image 生成 AI 绘画提示词 +func (h *PromptHandler) Image(c *gin.Context) { + var data struct { + Prompt string `json:"prompt"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, strings.Trim(content, `"`)) +} + +// Video 生成视频提示词 +func (h *PromptHandler) Video(c *gin.Context) { + var data struct { + Prompt string `json:"prompt"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, strings.Trim(content, `"`)) +} + +// MetaPrompt 生成元提示词 +func (h *PromptHandler) MetaPrompt(c *gin.Context) { + var data struct { + Prompt string `json:"prompt"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + messages := make([]interface{}, 0) + messages = append(messages, types.Message{ + Role: "system", + Content: service.MetaPromptTemplate, + }) + messages = append(messages, types.Message{ + Role: "user", + Content: "Task, Goal, or the Role to actor is:\n" + data.Prompt, + }) + content, err := utils.SendOpenAIMessage(h.DB, messages, 0) + if err != nil { + resp.ERROR(c, err.Error()) + return + } + + resp.SUCCESS(c, strings.Trim(content, `"`)) +} diff --git a/api/handler/sd_handler.go b/api/handler/sd_handler.go index 437dceac..93ca386e 100644 --- a/api/handler/sd_handler.go +++ b/api/handler/sd_handler.go @@ -109,29 +109,37 @@ func (h *SdJobHandler) Image(c *gin.Context) { resp.ERROR(c, "error with generate task id: "+err.Error()) return } - params := types.SdTaskParams{ - TaskId: taskId, - Prompt: data.Prompt, - NegPrompt: data.NegPrompt, - Steps: data.Steps, - Sampler: data.Sampler, - FaceFix: data.FaceFix, - CfgScale: data.CfgScale, - Seed: data.Seed, - Height: data.Height, - Width: data.Width, - HdFix: data.HdFix, - HdRedrawRate: data.HdRedrawRate, - HdScale: data.HdScale, - HdScaleAlg: data.HdScaleAlg, - HdSteps: data.HdSteps, + + task := types.SdTask{ + ClientId: data.ClientId, + Type: types.TaskImage, + Params: types.SdTaskParams{ + TaskId: taskId, + Prompt: data.Prompt, + NegPrompt: data.NegPrompt, + Steps: data.Steps, + Sampler: data.Sampler, + FaceFix: data.FaceFix, + CfgScale: data.CfgScale, + Seed: data.Seed, + Height: data.Height, + Width: data.Width, + HdFix: data.HdFix, + HdRedrawRate: data.HdRedrawRate, + HdScale: data.HdScale, + HdScaleAlg: data.HdScaleAlg, + HdSteps: data.HdSteps, + }, + UserId: userId, + TranslateModelId: h.App.SysConfig.TranslateModelId, } job := model.SdJob{ UserId: userId, Type: types.TaskImage.String(), - TaskId: params.TaskId, - Params: utils.JsonEncode(params), + TaskId: taskId, + Params: utils.JsonEncode(task.Params), + TaskInfo: utils.JsonEncode(task), Prompt: data.Prompt, Progress: 0, Power: h.App.SysConfig.SdPower, @@ -143,13 +151,8 @@ func (h *SdJobHandler) Image(c *gin.Context) { return } - h.sdService.PushTask(types.SdTask{ - Id: int(job.Id), - ClientId: data.ClientId, - Type: types.TaskImage, - Params: params, - UserId: userId, - }) + task.Id = int(job.Id) + h.sdService.PushTask(task) // update user's power err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ @@ -249,25 +252,14 @@ func (h *SdJobHandler) Remove(c *gin.Context) { } // 删除任务 - tx := h.DB.Begin() - tx.Delete(&job) - // 如果任务未完成,或者任务失败,则恢复用户算力 - if job.Progress != 100 { - err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: "stable-diffusion", - Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d, Err: %s", job.Id, job.ErrMsg), - }) - if err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } + err := h.DB.Delete(&job).Error + if err != nil { + resp.ERROR(c, err.Error()) + return } - tx.Commit() // remove image - err := h.uploader.GetUploadHandler().Delete(job.ImgURL) + err = h.uploader.GetUploadHandler().Delete(job.ImgURL) if err != nil { logger.Error("remove image failed: ", err) } diff --git a/api/handler/suno_handler.go b/api/handler/suno_handler.go index d284c66f..aa6b7c11 100644 --- a/api/handler/suno_handler.go +++ b/api/handler/suno_handler.go @@ -89,13 +89,29 @@ func (h *SunoHandler) Create(c *gin.Context) { data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt) } } + task := types.SunoTask{ + ClientId: data.ClientId, + UserId: int(h.GetLoginUserId(c)), + Type: data.Type, + Title: data.Title, + RefTaskId: data.RefTaskId, + RefSongId: data.RefSongId, + ExtendSecs: data.ExtendSecs, + Prompt: data.Prompt, + Tags: data.Tags, + Model: data.Model, + Instrumental: data.Instrumental, + SongId: data.SongId, + AudioURL: data.AudioURL, + } // 插入数据库 job := model.SunoJob{ - UserId: int(h.GetLoginUserId(c)), + UserId: task.UserId, Prompt: data.Prompt, Instrumental: data.Instrumental, ModelName: data.Model, + TaskInfo: utils.JsonEncode(task), Tags: data.Tags, Title: data.Title, Type: data.Type, @@ -115,26 +131,13 @@ func (h *SunoHandler) Create(c *gin.Context) { } // 创建任务 - h.sunoService.PushTask(types.SunoTask{ - ClientId: data.ClientId, - Id: job.Id, - UserId: job.UserId, - Type: job.Type, - Title: job.Title, - RefTaskId: data.RefTaskId, - RefSongId: data.RefSongId, - ExtendSecs: data.ExtendSecs, - Prompt: job.Prompt, - Tags: data.Tags, - Model: data.Model, - Instrumental: data.Instrumental, - SongId: data.SongId, - AudioURL: data.AudioURL, - }) + task.Id = job.Id + h.sunoService.PushTask(task) // update user's power err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ Type: types.PowerConsume, + Model: job.ModelName, Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName), CreatedAt: time.Now(), }) @@ -219,25 +222,11 @@ func (h *SunoHandler) Remove(c *gin.Context) { } // 删除任务 - tx := h.DB.Begin() - if err := tx.Delete(&job).Error; err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - - // 恢复用户算力 - err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: job.ModelName, - Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg), - }) + err = h.DB.Delete(&job).Error if err != nil { - tx.Rollback() resp.ERROR(c, err.Error()) return } - tx.Commit() // 删除文件 _ = h.uploader.GetUploadHandler().Delete(job.CoverURL) @@ -334,40 +323,3 @@ func (h *SunoHandler) Play(c *gin.Context) { } h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1)) } - -const genLyricTemplate = ` -你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。 -请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。 -输出格式如下: -歌曲名称 -第一节: -{{歌词内容}} -副歌: -{{歌词内容}} - -第二节: -{{歌词内容}} -副歌: -{{歌词内容}} - -尾声: -{{歌词内容}} -` - -// Lyric 生成歌词 -func (h *SunoHandler) Lyric(c *gin.Context) { - var data struct { - Prompt string `json:"prompt"` - } - if err := c.ShouldBindJSON(&data); err != nil { - resp.ERROR(c, types.InvalidArgs) - return - } - content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(genLyricTemplate, data.Prompt), "gpt-4o-mini", 0) - if err != nil { - resp.ERROR(c, err.Error()) - return - } - - resp.SUCCESS(c, content) -} diff --git a/api/handler/user_handler.go b/api/handler/user_handler.go index 3b93310b..3bc42ec1 100644 --- a/api/handler/user_handler.go +++ b/api/handler/user_handler.go @@ -130,15 +130,28 @@ func (h *UserHandler) Register(c *gin.Context) { } } + salt := utils.RandString(8) + user := model.User{ + Username: data.Username, + Password: utils.GenPassword(data.Password, salt), + Avatar: "/images/avatar/user.png", + Salt: salt, + Status: true, + ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 + Power: h.App.SysConfig.InitPower, + } + // check if the username is existing var item model.User session := h.DB.Session(&gorm.Session{}) if data.Mobile != "" { session = session.Where("mobile = ?", data.Mobile) - data.Username = data.Mobile + user.Username = data.Mobile + user.Mobile = data.Mobile } else if data.Email != "" { session = session.Where("email = ?", data.Email) - data.Username = data.Email + user.Username = data.Email + user.Email = data.Email } else if data.Username != "" { session = session.Where("username = ?", data.Username) } @@ -148,20 +161,6 @@ func (h *UserHandler) Register(c *gin.Context) { return } - salt := utils.RandString(8) - user := model.User{ - Username: data.Username, - Mobile: data.Mobile, - Email: data.Email, - Password: utils.GenPassword(data.Password, salt), - Avatar: "/images/avatar/user.png", - Salt: salt, - Status: true, - ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 - ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型 - Power: h.App.SysConfig.InitPower, - } - // 被邀请人也获得赠送算力 if data.InviteCode != "" { user.Power += h.App.SysConfig.InvitePower @@ -417,16 +416,15 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) { salt := utils.RandString(8) password := fmt.Sprintf("%d", utils.RandomNumber(8)) user = model.User{ - Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)), - Password: utils.GenPassword(password, salt), - Avatar: fmt.Sprintf("%s", data["avatar"]), - Salt: salt, - Status: true, - ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 - ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型 - Power: h.App.SysConfig.InitPower, - OpenId: fmt.Sprintf("%s", data["openid"]), - Nickname: fmt.Sprintf("%s", data["nickname"]), + Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)), + Password: utils.GenPassword(password, salt), + Avatar: fmt.Sprintf("%s", data["avatar"]), + Salt: salt, + Status: true, + ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色 + Power: h.App.SysConfig.InitPower, + OpenId: fmt.Sprintf("%s", data["openid"]), + Nickname: fmt.Sprintf("%s", data["nickname"]), } tx = h.DB.Create(&user) diff --git a/api/handler/video_handler.go b/api/handler/video_handler.go index aaa0bd86..e42cd9ca 100644 --- a/api/handler/video_handler.go +++ b/api/handler/video_handler.go @@ -80,13 +80,21 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) { StartImgURL: data.FirstFrameImg, EndImgURL: data.EndFrameImg, } + task := types.VideoTask{ + ClientId: data.ClientId, + UserId: userId, + Type: types.VideoLuma, + Prompt: data.Prompt, + Params: params, + TranslateModelId: h.App.SysConfig.TranslateModelId, + } // 插入数据库 job := model.VideoJob{ - UserId: userId, - Type: types.VideoLuma, - Prompt: data.Prompt, - Power: h.App.SysConfig.LumaPower, - Params: utils.JsonEncode(params), + UserId: userId, + Type: types.VideoLuma, + Prompt: data.Prompt, + Power: h.App.SysConfig.LumaPower, + TaskInfo: utils.JsonEncode(task), } tx := h.DB.Create(&job) if tx.Error != nil { @@ -95,14 +103,8 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) { } // 创建任务 - h.videoService.PushTask(types.VideoTask{ - ClientId: data.ClientId, - Id: job.Id, - UserId: userId, - Type: types.VideoLuma, - Prompt: data.Prompt, - Params: params, - }) + task.Id = job.Id + h.videoService.PushTask(task) // update user's power err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ @@ -181,25 +183,11 @@ func (h *VideoHandler) Remove(c *gin.Context) { } // 删除任务 - tx := h.DB.Begin() - if err := tx.Delete(&job).Error; err != nil { - tx.Rollback() - resp.ERROR(c, err.Error()) - return - } - - // 恢复算力 - err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ - Type: types.PowerRefund, - Model: "luma", - Remark: fmt.Sprintf("Luma 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg), - }) + err = h.DB.Delete(&job).Error if err != nil { - tx.Rollback() resp.ERROR(c, err.Error()) return } - tx.Commit() // 删除文件 _ = h.uploader.GetUploadHandler().Delete(job.CoverURL) diff --git a/api/main.go b/api/main.go index 8fd36c82..843f17ca 100644 --- a/api/main.go +++ b/api/main.go @@ -350,6 +350,7 @@ func main() { group.POST("create", h.Create) group.POST("set", h.Set) group.GET("remove", h.Remove) + group.POST("export", h.Export) }), fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { group := s.Engine.Group("/api/admin/dashboard/") @@ -484,7 +485,6 @@ func main() { group.POST("update", h.Update) group.GET("detail", h.Detail) group.GET("play", h.Play) - group.POST("lyric", h.Lyric) }), fx.Provide(handler.NewVideoHandler), fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) { @@ -518,6 +518,14 @@ func main() { fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) { s.Engine.Any("/api/ws", h.Client) }), + fx.Provide(handler.NewPromptHandler), + fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) { + group := s.Engine.Group("/api/prompt") + group.POST("/lyric", h.Lyric) + group.POST("/image", h.Image) + group.POST("/video", h.Video) + group.POST("/meta", h.MetaPrompt) + }), fx.Invoke(func(s *core.AppServer, db *gorm.DB) { go func() { err := s.Run(db) diff --git a/api/service/dalle/service.go b/api/service/dalle/service.go index 12bef395..24206a40 100644 --- a/api/service/dalle/service.go +++ b/api/service/dalle/service.go @@ -59,6 +59,20 @@ func (s *Service) PushTask(task types.DallTask) { } func (s *Service) Run() { + // 将数据库中未提交的人物加载到队列 + var jobs []model.DallJob + s.db.Where("progress", 0).Find(&jobs) + for _, v := range jobs { + var task types.DallTask + err := utils.JsonDecode(v.TaskInfo, &task) + if err != nil { + logger.Errorf("decode task info with error: %v", err) + continue + } + task.Id = v.Id + s.PushTask(task) + } + logger.Info("Starting DALL-E job consumer...") go func() { for { @@ -69,15 +83,15 @@ func (s *Service) Run() { continue } logger.Infof("handle a new DALL-E task: %+v", task) - s.clientIds[task.JobId] = task.ClientId + s.clientIds[task.Id] = task.ClientId _, err = s.Image(task, false) if err != nil { logger.Errorf("error with image task: %v", err) - s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{ + s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ "progress": service.FailTaskProgress, "err_msg": err.Error(), }) - s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed}) + s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed}) } } }() @@ -114,7 +128,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { prompt := task.Prompt // translate prompt if utils.HasChinese(prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini", 0) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, prompt), task.TranslateModelId) if err == nil { prompt = content logger.Debugf("重写后提示词:%s", prompt) @@ -177,7 +191,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { // update the api key last use time s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix()) // update task progress - err = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{ + err = s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ "progress": 100, "org_url": res.Data[0].Url, "prompt": prompt, @@ -186,10 +200,10 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { return "", fmt.Errorf("err with update database: %v", err) } - s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed}) + s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed}) var content string if sync { - imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url) + imgURL, err := s.downloadImage(task.Id, int(task.UserId), res.Data[0].Url) if err != nil { return "", fmt.Errorf("error with download image: %v", err) } @@ -223,13 +237,9 @@ func (s *Service) CheckTaskStatus() { go func() { logger.Info("Running DALL-E task status checking ...") for { + // 检查未完成任务进度 var jobs []model.DallJob - res := s.db.Where("progress < ?", 100).Find(&jobs) - if res.Error != nil { - time.Sleep(5 * time.Second) - continue - } - + s.db.Where("progress < ?", 100).Find(&jobs) for _, job := range jobs { // 超时的任务标记为失败 if time.Now().Sub(job.CreatedAt) > time.Minute*10 { @@ -238,6 +248,21 @@ func (s *Service) CheckTaskStatus() { s.db.Updates(&job) } } + + // 找出失败的任务,并恢复其扣减算力 + s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) + for _, job := range jobs { + err := s.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: "dall-e-3", + Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg), + }) + if err != nil { + continue + } + // 更新任务状态 + s.db.Model(&job).UpdateColumn("power", 0) + } time.Sleep(time.Second * 10) } }() diff --git a/api/service/mj/service.go b/api/service/mj/service.go index 717b553b..e56a1879 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -30,10 +30,11 @@ type Service struct { db *gorm.DB wsService *service.WebsocketService uploaderManager *oss.UploaderManager + userService *service.UserService clientIds map[uint]string } -func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, wsService *service.WebsocketService) *Service { +func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, wsService *service.WebsocketService, userService *service.UserService) *Service { return &Service{ db: db, taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli), @@ -42,10 +43,26 @@ func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *os wsService: wsService, uploaderManager: manager, clientIds: map[uint]string{}, + userService: userService, } } func (s *Service) Run() { + // 将数据库中未提交的人物加载到队列 + var jobs []model.MidJourneyJob + s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) + for _, v := range jobs { + var task types.MjTask + err := utils.JsonDecode(v.TaskInfo, &task) + if err != nil { + logger.Errorf("decode task info with error: %v", err) + continue + } + task.Id = v.Id + s.clientIds[task.Id] = task.ClientId + s.PushTask(task) + } + logger.Info("Starting MidJourney job consumer for service") go func() { for { @@ -58,7 +75,7 @@ func (s *Service) Run() { // translate prompt if utils.HasChinese(task.Prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), "gpt-4o-mini", 0) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId) if err == nil { task.Prompt = content } else { @@ -67,7 +84,7 @@ func (s *Service) Run() { } // translate negative prompt if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), "gpt-4o-mini", 0) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt), task.TranslateModelId) if err == nil { task.NegPrompt = content } else { @@ -279,7 +296,6 @@ func (s *Service) SyncTaskProgress() { } oldProgress := job.Progress job.Progress = utils.IntValue(strings.Replace(task.Progress, "%", "", 1), 0) - job.Prompt = task.PromptEn if task.ImageUrl != "" { job.OrgURL = task.ImageUrl } @@ -303,6 +319,21 @@ func (s *Service) SyncTaskProgress() { } } + // 找出失败的任务,并恢复其扣减算力 + s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) + for _, job := range jobs { + err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: "mid-journey", + Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d,Err: %s", job.Id, job.ErrMsg), + }) + if err != nil { + continue + } + // 更新任务状态 + s.db.Model(&job).UpdateColumn("power", 0) + } + time.Sleep(time.Second * 5) } }() diff --git a/api/service/sd/service.go b/api/service/sd/service.go index 9bfd1ecd..427b6cf8 100644 --- a/api/service/sd/service.go +++ b/api/service/sd/service.go @@ -34,9 +34,10 @@ type Service struct { db *gorm.DB uploadManager *oss.UploaderManager wsService *service.WebsocketService + userService *service.UserService } -func NewService(db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB, redisCli *redis.Client, wsService *service.WebsocketService) *Service { +func NewService(db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service { return &Service{ httpClient: req.C(), taskQueue: store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli), @@ -44,10 +45,24 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelD db: db, wsService: wsService, uploadManager: manager, + userService: userService, } } func (s *Service) Run() { + // 将数据库中未提交的人物加载到队列 + var jobs []model.SdJob + s.db.Where("progress", 0).Find(&jobs) + for _, v := range jobs { + var task types.SdTask + err := utils.JsonDecode(v.TaskInfo, &task) + if err != nil { + logger.Errorf("decode task info with error: %v", err) + continue + } + task.Id = int(v.Id) + s.PushTask(task) + } logger.Infof("Starting Stable-Diffusion job consumer") go func() { for { @@ -60,7 +75,7 @@ func (s *Service) Run() { // translate prompt if utils.HasChinese(task.Params.Prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt), "gpt-4o-mini", 0) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.Prompt), task.TranslateModelId) if err == nil { task.Params.Prompt = content } else { @@ -70,7 +85,7 @@ func (s *Service) Run() { // translate negative prompt if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), "gpt-4o-mini", 0) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), task.TranslateModelId) if err == nil { task.Params.NegPrompt = content } else { @@ -161,7 +176,7 @@ func (s *Service) Txt2Img(task types.SdTask) error { } apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", apiKey.ApiURL) - logger.Debugf("send image request to %s", apiURL) + logger.Infof("send image request to %s", apiURL) // send a request to sd api endpoint go func() { response, err := s.httpClient.R(). @@ -288,6 +303,21 @@ func (s *Service) CheckTaskStatus() { s.db.Updates(&job) } } + + // 找出失败的任务,并恢复其扣减算力 + s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) + for _, job := range jobs { + err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: "stable-diffusion", + Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%d, Err: %s", job.Id, job.ErrMsg), + }) + if err != nil { + continue + } + // 更新任务状态 + s.db.Model(&job).UpdateColumn("power", 0) + } time.Sleep(time.Second * 5) } }() diff --git a/api/service/suno/service.go b/api/service/suno/service.go index 9e293b3e..f724f5ad 100644 --- a/api/service/suno/service.go +++ b/api/service/suno/service.go @@ -36,9 +36,10 @@ type Service struct { notifyQueue *store.RedisQueue wsService *service.WebsocketService clientIds map[string]string + userService *service.UserService } -func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService) *Service { +func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service { return &Service{ httpClient: req.C().SetTimeout(time.Minute * 3), db: db, @@ -47,6 +48,7 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien uploadManager: manager, wsService: wsService, clientIds: map[string]string{}, + userService: userService, } } @@ -58,22 +60,17 @@ func (s *Service) PushTask(task types.SunoTask) { func (s *Service) Run() { // 将数据库中未提交的人物加载到队列 var jobs []model.SunoJob - s.db.Where("task_id", "").Find(&jobs) + s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) for _, v := range jobs { - s.PushTask(types.SunoTask{ - Id: v.Id, - Channel: v.Channel, - UserId: v.UserId, - Type: v.Type, - Title: v.Title, - RefTaskId: v.RefTaskId, - RefSongId: v.RefSongId, - Prompt: v.Prompt, - Tags: v.Tags, - Model: v.ModelName, - Instrumental: v.Instrumental, - ExtendSecs: v.ExtendSecs, - }) + var task types.SunoTask + err := utils.JsonDecode(v.TaskInfo, &task) + if err != nil { + logger.Errorf("decode task info with error: %v", err) + continue + } + task.Id = v.Id + s.PushTask(task) + s.clientIds[v.TaskId] = task.ClientId } logger.Info("Starting Suno job consumer...") go func() { @@ -389,6 +386,20 @@ func (s *Service) SyncTaskProgress() { } } + // 找出失败的任务,并恢复其扣减算力 + s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) + for _, job := range jobs { + err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: job.ModelName, + Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg), + }) + if err != nil { + continue + } + // 更新任务状态 + s.db.Model(&job).UpdateColumn("power", 0) + } time.Sleep(time.Second * 10) } }() diff --git a/api/service/types.go b/api/service/types.go index 1c5c601e..a2b69e36 100644 --- a/api/service/types.go +++ b/api/service/types.go @@ -14,5 +14,153 @@ type NotifyMessage struct { Message string `json:"message"` } -const RewritePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other creative elements. Just output the final prompt word directly. Do not output any explanation lines. The text to be rewritten is: [%s]" const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]" + +const ImagePromptOptimizeTemplate = ` +Create a highly effective prompt to provide to an AI image generation tool in order to create an artwork based on a desired concept. + +Please specify details about the artwork, such as the style, subject, mood, and other important characteristics you want the resulting image to have. + +Remember, prompts should always be output in English. + +# Steps + +1. **Subject Description**: Describe the main subject of the image clearly. Include as much detail as possible about what should be in the scene. For example, "a majestic lion roaring at sunrise" or "a futuristic city with flying cars." + +2. **Art Style**: Specify the art style you envision. Possible options include 'realistic', 'impressionist', a specific artist name, or imaginative styles like "cyberpunk." This helps the AI achieve your visual expectations. + +3. **Mood or Atmosphere**: Convey the feeling you want the image to evoke. For instance, peaceful, chaotic, epic, etc. + +4. **Color Palette and Lighting**: Mention color preferences or lighting. For example, "vibrant with shades of blue and purple" or "dim and dramatic lighting." + +5. **Optional Features**: You can add any additional attributes, such as background details, attention to textures, or any specific kind of framing. + +# Output Format + +- **Prompt Format**: A descriptive phrase that includes key aspects of the artwork (subject, style, mood, colors, lighting, any optional features). + +Here is an example of how the final prompt should look: + +"An ethereal landscape featuring towering ice mountains, in an impressionist style reminiscent of Claude Monet, with a serene mood. The sky is glistening with soft purples and whites, with a gentle morning sun illuminating the scene." + +**Please input the prompt words directly in English, and do not input any other explanatory statements** + +# Examples + +1. **Input**: + - Subject: A white tiger in a dense jungle + - Art Style: Realistic + - Mood: Intense, mysterious + - Lighting: Dramatic contrast with light filtering through leaves + + **Output Prompt**: "A realistic rendering of a white tiger stealthily moving through a dense jungle, with an intense, mysterious mood. The lighting creates strong contrasts as beams of sunlight filter through a thick canopy of leaves." + +2. **Input**: + - Subject: An enchanted castle on a floating island + - Art Style: Fantasy + - Mood: Majestic, magical + - Colors: Bright blues, greens, and gold + + **Output Prompt**: "A majestic fantasy castle on a floating island above the clouds, with bright blues, greens, and golds to create a magical, dreamy atmosphere. Textured cobblestone details and glistening waters surround the scene." + +# Notes + +- Ensure that you mix different aspects to get a comprehensive and visually compelling prompt. +- Be as descriptive as possible as it often helps generate richer, more detailed images. +- If you want the image to resemble a particular artist's work, be sure to mention the artist explicitly. e.g., "in the style of Van Gogh." + +The theme of the creation is:【%s】 +` + +const LyricPromptTemplate = ` +你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。 +请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。 +输出格式如下: +歌曲名称 +第一节: +{{歌词内容}} +副歌: +{{歌词内容}} + +第二节: +{{歌词内容}} +副歌: +{{歌词内容}} + +尾声: +{{歌词内容}} +` + +const VideoPromptTemplate = ` +As an expert in video generation prompts, please create a detailed descriptive prompt for the following video concept. The description should include the setting, character appearance, actions, overall atmosphere, and camera angles. Please make it as detailed and vivid as possible to help ensure that every aspect of the video is accurately captured. + +Please remember that regardless of the user’s input, the final output must be in English. + +# Details to Include + +- Describe the overall visual style of the video (e.g., animated, realistic, retro tone, etc.) +- Identify key characters or objects in the video and describe their appearance, attire, and expressions +- Describe the environment of the scene, including weather, lighting, colors, and important details +- Explain the behavior and interactions of the characters +- Include any unique camera angles, movements, or special effects + +# Output Format +Provide the prompt in paragraph form, ensuring that the description is detailed enough for a video generation system to recreate the envisioned scene. Include the beginning, middle, and end of the scene to convey a complete storyline. + +# Example +**User Input:** +“A small cat basking in the sun on a balcony.” + +**Generated Prompt:** +On a bright spring afternoon, an orange-striped kitten lies lazily on a balcony, basking in the warm sunlight. The iron railings around the balcony cast soft shadows that dance gently with the light. The cat’s eyes are half-closed, exuding a sense of contentment and tranquility in its surroundings. In the distance, a few fluffy white clouds drift slowly across the blue sky. The camera initially focuses on the cat’s face, capturing the delicate details of its fur, and then gradually zooms out to reveal the full balcony scene, immersing viewers in a moment of calm and relaxation. + +The theme of the creation is:【%s】 +` + +const MetaPromptTemplate = ` +Given a task description or existing prompt, produce a detailed system prompt to guide a language model in completing the task effectively. + +Please remember, the final output must be the same language with user’s input. + +# Guidelines + +- Understand the Task: Grasp the main objective, goals, requirements, constraints, and expected output. +- Minimal Changes: If an existing prompt is provided, improve it only if it's simple. For complex prompts, enhance clarity and add missing elements without altering the original structure. +- Reasoning Before Conclusions**: Encourage reasoning steps before any conclusions are reached. ATTENTION! If the user provides examples where the reasoning happens afterward, REVERSE the order! NEVER START EXAMPLES WITH CONCLUSIONS! + - Reasoning Order: Call out reasoning portions of the prompt and conclusion parts (specific fields by name). For each, determine the ORDER in which this is done, and whether it needs to be reversed. + - Conclusion, classifications, or results should ALWAYS appear last. +- Examples: Include high-quality examples if helpful, using placeholders [in brackets] for complex elements. + - What kinds of examples may need to be included, how many, and whether they are complex enough to benefit from placeholders. +- Clarity and Conciseness: Use clear, specific language. Avoid unnecessary instructions or bland statements. +- Formatting: Use markdown features for readability. DO NOT USE CODE BLOCKS UNLESS SPECIFICALLY REQUESTED. +- Preserve User Content: If the input task or prompt includes extensive guidelines or examples, preserve them entirely, or as closely as possible. If they are vague, consider breaking down into sub-steps. Keep any details, guidelines, examples, variables, or placeholders provided by the user. +- Constants: DO include constants in the prompt, as they are not susceptible to prompt injection. Such as guides, rubrics, and examples. +- Output Format: Explicitly the most appropriate output format, in detail. This should include length and syntax (e.g. short sentence, paragraph, JSON, etc.) +- For tasks outputting well-defined or structured data (classification, JSON, etc.) bias toward outputting a JSON. +- JSON should never be wrapped in code blocks unless explicitly requested. + +The final prompt you output should adhere to the following structure below. Do not include any additional commentary, only output the completed system prompt. SPECIFICALLY, do not include any additional messages at the start or end of the prompt. (e.g. no "---") + +[Concise instruction describing the task - this should be the first line in the prompt, no section header] + +[Additional details as needed.] + +[Optional sections with headings or bullet points for detailed steps.] + +# Steps [optional] + +[optional: a detailed breakdown of the steps necessary to accomplish the task] + +# Output Format + +[Specifically call out how the output should be formatted, be it response length, structure e.g. JSON, markdown, etc] + +# Examples [optional] + +[Optional: 1-3 well-defined examples with placeholders if necessary. Clearly mark where examples start and end, and what the input and output are. User placeholders as necessary.] +[If the examples are shorter than what a realistic example is expected to be, make a reference with () explaining how real examples should be longer / shorter / different. AND USE PLACEHOLDERS! ] + +# Notes [optional] + +[optional: edge cases, details, and an area to call or repeat out specific important considerations] +` diff --git a/api/service/video/luma.go b/api/service/video/luma.go index 144269f7..08e133c4 100644 --- a/api/service/video/luma.go +++ b/api/service/video/luma.go @@ -36,9 +36,10 @@ type Service struct { notifyQueue *store.RedisQueue wsService *service.WebsocketService clientIds map[uint]string + userService *service.UserService } -func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService) *Service { +func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, wsService *service.WebsocketService, userService *service.UserService) *Service { return &Service{ httpClient: req.C().SetTimeout(time.Minute * 3), db: db, @@ -47,6 +48,7 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien wsService: wsService, uploadManager: manager, clientIds: map[uint]string{}, + userService: userService, } } @@ -60,20 +62,15 @@ func (s *Service) Run() { var jobs []model.VideoJob s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) for _, v := range jobs { - var params types.VideoParams - if err := utils.JsonDecode(v.Params, ¶ms); err != nil { - logger.Errorf("unmarshal params failed: %v", err) + var task types.VideoTask + err := utils.JsonDecode(v.TaskInfo, &task) + if err != nil { + logger.Errorf("decode task info with error: %v", err) continue } - s.PushTask(types.VideoTask{ - Id: v.Id, - Channel: v.Channel, - UserId: v.UserId, - Type: v.Type, - TaskId: v.TaskId, - Prompt: v.Prompt, - Params: params, - }) + task.Id = v.Id + s.PushTask(task) + s.clientIds[v.Id] = task.ClientId } logger.Info("Starting Video job consumer...") go func() { @@ -87,7 +84,7 @@ func (s *Service) Run() { // translate prompt if utils.HasChinese(task.Prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), "gpt-4o-mini", 0) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt), task.TranslateModelId) if err == nil { task.Prompt = content } else { @@ -291,6 +288,20 @@ func (s *Service) SyncTaskProgress() { } + // 找出失败的任务,并恢复其扣减算力 + s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) + for _, job := range jobs { + err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ + Type: types.PowerRefund, + Model: "luma", + Remark: fmt.Sprintf("Luma 任务失败,退回算力。任务ID:%s,Err:%s", job.TaskId, job.ErrMsg), + }) + if err != nil { + continue + } + // 更新任务状态 + s.db.Model(&job).UpdateColumn("power", 0) + } time.Sleep(time.Second * 10) } }() diff --git a/api/store/model/dalle_job.go b/api/store/model/dalle_job.go index de7a13a0..f22f7426 100644 --- a/api/store/model/dalle_job.go +++ b/api/store/model/dalle_job.go @@ -6,6 +6,7 @@ type DallJob struct { Id uint `gorm:"primarykey;column:id"` UserId uint Prompt string + TaskInfo string // 原始任务信息 ImgURL string OrgURL string Publish bool diff --git a/api/store/model/mj_job.go b/api/store/model/mj_job.go index b4e03a6c..feb0471c 100644 --- a/api/store/model/mj_job.go +++ b/api/store/model/mj_job.go @@ -7,6 +7,7 @@ type MidJourneyJob struct { Type string UserId int TaskId string + TaskInfo string // 原始任务信息 ChannelId string MessageId string ReferenceId string diff --git a/api/store/model/sd_job.go b/api/store/model/sd_job.go index 8542c307..c2ff59a8 100644 --- a/api/store/model/sd_job.go +++ b/api/store/model/sd_job.go @@ -7,6 +7,7 @@ type SdJob struct { Type string UserId int TaskId string + TaskInfo string // 原始任务信息 ImgURL string Progress int Prompt string diff --git a/api/store/model/suno_job.go b/api/store/model/suno_job.go index abbbd631..ecc0de09 100644 --- a/api/store/model/suno_job.go +++ b/api/store/model/suno_job.go @@ -9,6 +9,7 @@ type SunoJob struct { Title string Type int TaskId string + TaskInfo string // 原始任务信息 RefTaskId string // 续写的任务id Tags string // 歌曲风格和标签 Instrumental bool // 是否生成纯音乐 diff --git a/api/store/model/video_job.go b/api/store/model/video_job.go index 5dc7cb3e..b3bbb08e 100644 --- a/api/store/model/video_job.go +++ b/api/store/model/video_job.go @@ -8,6 +8,7 @@ type VideoJob struct { Channel string // 频道 Type string // luma,runway,cog TaskId string + TaskInfo string // 原始任务信息 Prompt string // 提示词 PromptExt string // 优化后提示词 CoverURL string // 封面图 URL @@ -18,7 +19,6 @@ type VideoJob struct { ErrMsg string // 错误信息 RawData string // 原始数据 json Power int // 消耗算力 - Params string // 任务参数 CreatedAt time.Time } diff --git a/api/store/vo/mj_job.go b/api/store/vo/mj_job.go index 458e9a2d..ab6d99d9 100644 --- a/api/store/vo/mj_job.go +++ b/api/store/vo/mj_job.go @@ -1,21 +1,20 @@ package vo type MidJourneyJob struct { - Id uint `json:"id"` - Type string `json:"type"` - UserId int `json:"user_id"` - ChannelId string `json:"channel_id"` - TaskId string `json:"task_id"` - MessageId string `json:"message_id"` - ReferenceId string `json:"reference_id"` - ImgURL string `json:"img_url"` - OrgURL string `json:"org_url"` - Hash string `json:"hash"` - Progress int `json:"progress"` - Prompt string `json:"prompt"` - UseProxy bool `json:"use_proxy"` - Publish bool `json:"publish"` - ErrMsg string `json:"err_msg"` - Power int `json:"power"` - CreatedAt int64 `json:"created_at"` + Id uint `json:"id"` + Type string `json:"type"` + UserId int `json:"user_id"` + ChannelId string `json:"channel_id"` + TaskId string `json:"task_id"` + MessageId string `json:"message_id"` + ImgURL string `json:"img_url"` + OrgURL string `json:"org_url"` + Hash string `json:"hash"` + Progress int `json:"progress"` + Prompt string `json:"prompt"` + UseProxy bool `json:"use_proxy"` + Publish bool `json:"publish"` + ErrMsg string `json:"err_msg"` + Power int `json:"power"` + CreatedAt int64 `json:"created_at"` } diff --git a/api/store/vo/video_job.go b/api/store/vo/video_job.go index 3582c667..b7530132 100644 --- a/api/store/vo/video_job.go +++ b/api/store/vo/video_job.go @@ -1,7 +1,5 @@ package vo -import "geekai/core/types" - type VideoJob struct { Id uint `json:"id"` UserId int `json:"user_id"` @@ -18,6 +16,5 @@ type VideoJob struct { ErrMsg string `json:"err_msg"` // 错误信息 RawData map[string]interface{} `json:"raw_data"` // 原始数据 json Power int `json:"power"` // 消耗算力 - Params types.VideoParams `json:"params"` // 任务参数 CreatedAt int64 `json:"created_at"` } diff --git a/api/utils/openai.go b/api/utils/openai.go index 3c1e4f15..eae2e902 100644 --- a/api/utils/openai.go +++ b/api/utils/openai.go @@ -45,20 +45,25 @@ type apiRes struct { } `json:"choices"` } -func OpenAIRequest(db *gorm.DB, prompt string, modelName string, keyId int) (string, error) { +func OpenAIRequest(db *gorm.DB, prompt string, modelId int) (string, error) { messages := make([]interface{}, 1) messages[0] = types.Message{ Role: "user", Content: prompt, } - return SendOpenAIMessage(db, messages, modelName, keyId) + return SendOpenAIMessage(db, messages, modelId) } -func SendOpenAIMessage(db *gorm.DB, messages []interface{}, modelName string, keyId int) (string, error) { +func SendOpenAIMessage(db *gorm.DB, messages []interface{}, modelId int) (string, error) { + var chatModel model.ChatModel + db.Where("id", modelId).First(&chatModel) + if chatModel.Value == "" { + chatModel.Value = "gpt-4o" // 默认使用 gpt-4o + } var apiKey model.ApiKey session := db.Session(&gorm.Session{}).Where("type", "chat").Where("enabled", true) - if keyId > 0 { - session = session.Where("id", keyId) + if chatModel.KeyId > 0 { + session = session.Where("id", chatModel.KeyId) } err := session.First(&apiKey).Error if err != nil { @@ -71,11 +76,11 @@ func SendOpenAIMessage(db *gorm.DB, messages []interface{}, modelName string, ke client.SetProxyURL(apiKey.ApiURL) } apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) - logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, modelName) + logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, chatModel.Name) r, err := client.R().SetHeader("Body-Type", "application/json"). SetHeader("Authorization", "Bearer "+apiKey.Value). SetBody(types.ApiRequest{ - Model: modelName, + Model: chatModel.Value, Temperature: 0.9, MaxTokens: 1024, Stream: false, diff --git a/database/update-v4.1.7.sql b/database/update-v4.1.7.sql new file mode 100644 index 00000000..b33b7f8c --- /dev/null +++ b/database/update-v4.1.7.sql @@ -0,0 +1,5 @@ +ALTER TABLE `chatgpt_sd_jobs` ADD `task_info` TEXT NOT NULL COMMENT '任务详情' AFTER `task_id`; +ALTER TABLE `chatgpt_mj_jobs` ADD `task_info` TEXT NOT NULL COMMENT '任务详情' AFTER `task_id`; +ALTER TABLE `chatgpt_dall_jobs` ADD `task_info` TEXT NOT NULL COMMENT '任务详情' AFTER `prompt`; +ALTER TABLE `chatgpt_suno_jobs` ADD `task_info` TEXT NOT NULL COMMENT '任务详情' AFTER `task_id`; +ALTER TABLE `chatgpt_video_jobs` CHANGE `params` `task_info` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '原始任务信息' AFTER `task_id`; \ No newline at end of file diff --git a/web/src/assets/css/admin/form.styl b/web/src/assets/css/admin/form.styl index dd3e4638..cdc4787d 100644 --- a/web/src/assets/css/admin/form.styl +++ b/web/src/assets/css/admin/form.styl @@ -3,7 +3,7 @@ display flex width 100% - .el-input,.el-select,.el-switch { + .el-input, .el-select, .el-switch { margin-right 10px } diff --git a/web/src/assets/css/image-mj.styl b/web/src/assets/css/image-mj.styl index 61845709..7e5f13c1 100644 --- a/web/src/assets/css/image-mj.styl +++ b/web/src/assets/css/image-mj.styl @@ -473,8 +473,13 @@ padding 30px } } - } + .generate-btn { + .iconfont { + margin-right 5px + } + } + } } .mj-list-item-prompt { diff --git a/web/src/assets/css/main.styl b/web/src/assets/css/main.styl index b275eb11..4a80af43 100644 --- a/web/src/assets/css/main.styl +++ b/web/src/assets/css/main.styl @@ -172,6 +172,22 @@ body { } } +.mr-1 { + margin-right 5px +} + +.mr-2 { + margin-right 10px +} + +.ml-1 { + margin-left 5px +} + +.ml-2 { + margin-left 10px +} + diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css index 9871d1bb..57f915d2 100644 --- a/web/src/assets/iconfont/iconfont.css +++ b/web/src/assets/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4125778 */ - src: url('iconfont.woff2?t=1728891448746') format('woff2'), - url('iconfont.woff?t=1728891448746') format('woff'), - url('iconfont.ttf?t=1728891448746') format('truetype'); + src: url('iconfont.woff2?t=1731289567907') format('woff2'), + url('iconfont.woff?t=1731289567907') format('woff'), + url('iconfont.ttf?t=1731289567907') format('truetype'); } .iconfont { @@ -13,6 +13,14 @@ -moz-osx-font-smoothing: grayscale; } +.icon-linggan:before { + content: "\e641"; +} + +.icon-chuangzuo:before { + content: "\e6cc"; +} + .icon-call:before { content: "\e769"; } diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js index 1cdb9982..ff1c4172 100644 --- a/web/src/assets/iconfont/iconfont.js +++ b/web/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,t,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(i=h,o=a.document,z=!1,s(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){z||(z=!0,i())}function s(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(s,50)}p()}})(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',(a=>{var l=(c=(c=document.getElementsByTagName("script"))[c.length-1]).getAttribute("data-injectcss"),c=c.getAttribute("data-disable-injectsvg");if(!c){var h,t,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(l&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}h=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(h,0):(t=function(){document.removeEventListener("DOMContentLoaded",t,!1),h()},document.addEventListener("DOMContentLoaded",t,!1)):document.attachEvent&&(i=h,o=a.document,z=!1,s(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,p())})}function p(){z||(z=!0,i())}function s(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(s,50)}p()}})(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index e32aad7f..fc973f80 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -5,6 +5,20 @@ "css_prefix_text": "icon-", "description": "", "glyphs": [ + { + "icon_id": "15330210", + "name": "创意灵感", + "font_class": "linggan", + "unicode": "e641", + "unicode_decimal": 58945 + }, + { + "icon_id": "39170417", + "name": "创作", + "font_class": "chuangzuo", + "unicode": "e6cc", + "unicode_decimal": 59084 + }, { "icon_id": "11231556", "name": "打电话", diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index 14ee893b..d2528705 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index ee2f117c..550b27bb 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index aba9800a..3dc3c3da 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/components/ui/BlackDialog.vue b/web/src/components/ui/BlackDialog.vue index 4e659ad2..1c3f4196 100644 --- a/web/src/components/ui/BlackDialog.vue +++ b/web/src/components/ui/BlackDialog.vue @@ -40,8 +40,8 @@ const props = defineProps({ default: 'Tips', }, width: { - type: Number, - default: 500, + type: String, + default: 'auto', }, hideFooter:{ type: Boolean, diff --git a/web/src/utils/http.js b/web/src/utils/http.js index 18123f2e..a06123ea 100644 --- a/web/src/utils/http.js +++ b/web/src/utils/http.js @@ -6,12 +6,12 @@ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import axios from 'axios' -import {getAdminToken, getSessionId, getUserToken, removeAdminToken, removeUserToken} from "@/store/session"; +import {getAdminToken, getUserToken, removeAdminToken, removeUserToken} from "@/store/session"; axios.defaults.timeout = 180000 axios.defaults.baseURL = process.env.VUE_APP_API_HOST axios.defaults.withCredentials = true; -axios.defaults.headers.post['Content-Type'] = 'application/json' +//axios.defaults.headers.post['Content-Type'] = 'application/json' // HTTP拦截器 axios.interceptors.request.use( @@ -82,4 +82,19 @@ export function httpDownload(url) { reject(err) }) }) +} + +export function httpPostDownload(url, data) { + return new Promise((resolve, reject) => { + axios({ + method: 'POST', + url: url, + data: data, + responseType: 'blob' // 将响应类型设置为 `blob` + }).then(response => { + resolve(response) + }).catch(err => { + reject(err) + }) + }) } \ No newline at end of file diff --git a/web/src/utils/libs.js b/web/src/utils/libs.js index cbb4b098..22d6fa10 100644 --- a/web/src/utils/libs.js +++ b/web/src/utils/libs.js @@ -225,6 +225,9 @@ export function showLoginDialog(router) { } export const replaceImg =(img) => { + if (!img.startsWith("http")) { + img = `${location.protocol}//${location.host}/${img}` + } const devHost = process.env.VUE_APP_API_HOST const localhost = "http://localhost:5678" if (img.includes(localhost)) { diff --git a/web/src/views/Dalle.vue b/web/src/views/Dalle.vue index f1c039e3..41f405c3 100644 --- a/web/src/views/Dalle.vue +++ b/web/src/views/Dalle.vue @@ -59,10 +59,19 @@ :autosize="{ minRows: 4, maxRows: 6 }" type="textarea" ref="promptRef" - placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词" + placeholder="请在此输入绘画提示词,您也可以点击下面的提示词助手生成绘画提示词" + v-loading="isGenerating" + style="--el-mask-color:rgba(100, 100, 100, 0.8)" /> + + + + 生成专业绘画指令 + + +
@@ -212,6 +221,7 @@ import {checkSession, getClientId, getSystemInfo} from "@/store/cache"; import {useSharedStore} from "@/store/sharedata"; import TaskList from "@/components/TaskList.vue"; import BackTop from "@/components/BackTop.vue"; +import {showMessageError} from "@/utils/dialog"; const listBoxHeight = ref(0) // const paramBoxHeight = ref(0) @@ -410,6 +420,21 @@ const publishImage = (item, action) => { }) } +const isGenerating = ref(false) +const generatePrompt = () => { + if (params.value.prompt === "") { + return showMessageError("请输入原始提示词") + } + isGenerating.value = true + httpPost("/api/prompt/image", {prompt: params.value.prompt}).then(res => { + params.value.prompt = res.data + isGenerating.value = false + }).catch(e => { + showMessageError("生成提示词失败:"+e.message) + isGenerating.value = false + }) +} + \ No newline at end of file diff --git a/web/src/views/admin/Home.vue b/web/src/views/admin/Home.vue index 346c4131..86fb9124 100644 --- a/web/src/views/admin/Home.vue +++ b/web/src/views/admin/Home.vue @@ -49,7 +49,7 @@ watch(() => store.adminTheme, (val) => { -