Merge tag 'v4.1.7' of gitee.com:blackfox/geekai-plus

This commit is contained in:
RockYang
2025-03-24 11:27:11 +08:00
58 changed files with 1146 additions and 494 deletions

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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%dErr: %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)
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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%dErr: %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)
}

View File

@@ -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

View File

@@ -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, `"`))
}

View File

@@ -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)
}

View File

@@ -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%sErr:%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)
}

View File

@@ -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)

View File

@@ -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%sErr:%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)

View File

@@ -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)

View File

@@ -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%dErr: %s", job.Id, job.ErrMsg),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(time.Second * 10)
}
}()

View File

@@ -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%dErr: %s", job.Id, job.ErrMsg),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(time.Second * 5)
}
}()

View File

@@ -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)
}
}()

View File

@@ -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%sErr:%s", job.TaskId, job.ErrMsg),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(time.Second * 10)
}
}()

View File

@@ -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 users 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 cats 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 cats 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 users 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]
`

View File

@@ -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, &params); 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%sErr:%s", job.TaskId, job.ErrMsg),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(time.Second * 10)
}
}()

View File

@@ -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

View File

@@ -7,6 +7,7 @@ type MidJourneyJob struct {
Type string
UserId int
TaskId string
TaskInfo string // 原始任务信息
ChannelId string
MessageId string
ReferenceId string

View File

@@ -7,6 +7,7 @@ type SdJob struct {
Type string
UserId int
TaskId string
TaskInfo string // 原始任务信息
ImgURL string
Progress int
Prompt string

View File

@@ -9,6 +9,7 @@ type SunoJob struct {
Title string
Type int
TaskId string
TaskInfo string // 原始任务信息
RefTaskId string // 续写的任务id
Tags string // 歌曲风格和标签
Instrumental bool // 是否生成纯音乐

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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,