Merge branch 'main' into husm_2024-09-02

This commit is contained in:
胡双明 2024-09-10 14:29:08 +08:00
commit fe7fa46a0c
60 changed files with 1229 additions and 1263 deletions

View File

@ -6,6 +6,11 @@
* 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码
* 功能优化:管理后台给可以拖动排序的组件添加拖动图标
* 功能优化Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作
* Bug修复手机端角色和模型选择不生效
* Bug修复用户登录过期之后聊天页面出现大量报错需要刷新页面才能正常
* 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验
* 功能优化:给算力增减服务全部加上数据库事务和同步锁
* 功能优化:支持用户在前端对话界面选择插件
* 功能新增:支持 Luma 文生视频功能
## v4.1.2

View File

@ -201,7 +201,6 @@ func needLogin(c *gin.Context) bool {
c.Request.URL.Path == "/api/admin/logout" ||
c.Request.URL.Path == "/api/admin/login/captcha" ||
c.Request.URL.Path == "/api/user/register" ||
c.Request.URL.Path == "/api/user/session" ||
c.Request.URL.Path == "/api/chat/history" ||
c.Request.URL.Path == "/api/chat/detail" ||
c.Request.URL.Path == "/api/chat/list" ||

View File

@ -57,6 +57,7 @@ type ChatSession struct {
ClientIP string `json:"client_ip"` // 客户端 IP
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
Model ChatModel `json:"model"` // GPT 模型
Tools string `json:"tools"` // 函数
}
type ChatModel struct {

View File

@ -150,8 +150,9 @@ type SystemConfig struct {
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
DallPower int `json:"dall_power,omitempty"` // DALL-E-3 绘图消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址

View File

@ -85,7 +85,6 @@ type SunoTask struct {
Channel string `json:"channel"`
UserId int `json:"user_id"`
Type int `json:"type"`
TaskId string `json:"task_id"`
Title string `json:"title"`
RefTaskId string `json:"ref_task_id,omitempty"`
RefSongId string `json:"ref_song_id,omitempty"`
@ -97,3 +96,30 @@ type SunoTask struct {
SongId string `json:"song_id,omitempty"` // 合并歌曲ID
AudioURL string `json:"audio_url"` // 用户上传音频地址
}
const (
VideoLuma = "luma"
VideoRunway = "runway"
VideoCog = "cog"
)
type VideoTask struct {
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"`
}
type VideoParams struct {
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
Loop bool `json:"loop"` // 是否循环参考图
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
EndImgURL string `json:"end_img_url"` // 最后一帧参考图地址
Model string `json:"model"` // 使用哪个模型生成视频
Radio string `json:"radio"` // 视频尺寸
Style string `json:"style"` // 风格
Duration int `json:"duration"` // 视频时长(秒)
}

View File

@ -46,9 +46,10 @@ type ChatHandler struct {
licenseService *service.LicenseService
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
userService *service.UserService
}
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService) *ChatHandler {
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler {
return &ChatHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
redis: redis,
@ -56,6 +57,7 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag
licenseService: licenseService,
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
ChatContexts: types.NewLMap[string, []types.Message](),
userService: userService,
}
}
@ -71,6 +73,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
roleId := h.GetInt(c, "role_id", 0)
chatId := c.Query("chat_id")
modelId := h.GetInt(c, "model_id", 0)
tools := c.Query("tools")
client := types.NewWsClient(ws)
var chatRole model.ChatRole
@ -97,6 +100,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
SessionId: sessionId,
ClientIP: c.ClientIP(),
UserId: h.GetLoginUserId(c),
Tools: tools,
}
// use old chat data override the chat model and role ID
@ -209,34 +213,37 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
}
req.Temperature = session.Model.Temperature
req.MaxTokens = session.Model.MaxTokens
// OpenAI 支持函数功能
var items []model.Function
res = h.DB.Where("enabled", true).Find(&items)
if res.Error == nil {
var tools = make([]types.Tool, 0)
for _, v := range items {
var parameters map[string]interface{}
err = utils.JsonDecode(v.Parameters, &parameters)
if err != nil {
continue
}
tool := types.Tool{
Type: "function",
Function: types.Function{
Name: v.Name,
Description: v.Description,
Parameters: parameters,
},
}
if v, ok := parameters["required"]; v == nil || !ok {
tool.Function.Parameters["required"] = []string{}
}
tools = append(tools, tool)
}
if len(tools) > 0 {
req.Tools = tools
req.ToolChoice = "auto"
if session.Tools != "" {
toolIds := strings.Split(session.Tools, ",")
var items []model.Function
res = h.DB.Where("enabled", true).Where("id IN ?", toolIds).Find(&items)
if res.Error == nil {
var tools = make([]types.Tool, 0)
for _, v := range items {
var parameters map[string]interface{}
err = utils.JsonDecode(v.Parameters, &parameters)
if err != nil {
continue
}
tool := types.Tool{
Type: "function",
Function: types.Function{
Name: v.Name,
Description: v.Description,
Parameters: parameters,
},
}
if v, ok := parameters["required"]; v == nil || !ok {
tool.Function.Parameters["required"] = []string{}
}
tools = append(tools, tool)
}
if len(tools) > 0 {
req.Tools = tools
req.ToolChoice = "auto"
}
}
}
@ -270,7 +277,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
tokens += tks + promptTokens
for _, v := range messages {
for i := len(messages) - 1; i >= 0; i-- {
v := messages[i]
tks, _ := utils.CalcTokens(v.Content, req.Model)
// 上下文 token 超出了模型的最大上下文长度
if tokens+tks >= session.Model.MaxContext {
@ -481,24 +489,15 @@ func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, p
if session.Model.Power > 0 {
power = session.Model.Power
}
res := h.DB.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power))
if res.Error == nil {
// 记录算力消费日志
var u model.User
h.DB.Where("id", userVo.Id).First(&u)
h.DB.Create(&model.PowerLog{
UserId: userVo.Id,
Username: userVo.Username,
Type: types.PowerConsume,
Amount: power,
Mark: types.PowerSub,
Balance: u.Power,
Model: session.Model.Value,
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d回复长度%d", session.Model.Name, promptTokens, replyTokens),
CreatedAt: time.Now(),
})
}
err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{
Type: types.PowerConsume,
Model: session.Model.Value,
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d回复长度%d", session.Model.Name, promptTokens, replyTokens),
})
if err != nil {
logger.Error(err)
}
}
func (h *ChatHandler) saveChatHistory(

View File

@ -11,32 +11,33 @@ import (
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/service/dalle"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gorilla/websocket"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
)
type DallJobHandler struct {
BaseHandler
redis *redis.Client
service *dalle.Service
uploader *oss.UploaderManager
redis *redis.Client
dallService *dalle.Service
uploader *oss.UploaderManager
userService *service.UserService
}
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager) *DallJobHandler {
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler {
return &DallJobHandler{
service: service,
uploader: manager,
dallService: service,
uploader: manager,
userService: userService,
BaseHandler: BaseHandler{
App: app,
DB: db,
@ -61,14 +62,14 @@ func (h *DallJobHandler) Client(c *gin.Context) {
}
client := types.NewWsClient(ws)
h.service.Clients.Put(uint(userId), client)
h.dallService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
go func() {
for {
_, msg, err := client.Receive()
if err != nil {
client.Close()
h.service.Clients.Delete(uint(userId))
h.dallService.Clients.Delete(uint(userId))
return
}
@ -127,7 +128,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
return
}
h.service.PushTask(types.DallTask{
h.dallService.PushTask(types.DallTask{
JobId: job.Id,
UserId: uint(userId),
Prompt: data.Prompt,
@ -137,7 +138,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
Power: job.Power,
})
client := h.service.Clients.Get(job.UserId)
client := h.dallService.Clients.Get(job.UserId)
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
@ -175,7 +176,7 @@ func (h *DallJobHandler) JobList(c *gin.Context) {
}
// JobList 获取任务列表
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.DallJob) {
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
session := h.DB.Session(&gorm.Session{})
if finish {
@ -193,11 +194,14 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
offset := (page - 1) * pageSize
session = session.Offset(offset).Limit(pageSize)
}
// 统计总数
var total int64
session.Model(&model.DallJob{}).Count(&total)
var items []model.DallJob
res := session.Find(&items)
if res.Error != nil {
return res.Error, nil
return res.Error, vo.Page{}
}
var jobs = make([]vo.DallJob, 0)
@ -210,7 +214,7 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
jobs = append(jobs, job)
}
return nil, jobs
return nil, vo.NewPage(total, page, pageSize, jobs)
}
// Remove remove task image
@ -233,26 +237,11 @@ func (h *DallJobHandler) Remove(c *gin.Context) {
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "dall-e-3",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
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())

View File

@ -8,15 +8,16 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/service/dalle"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"errors"
"fmt"
"strings"
"time"
@ -224,3 +225,27 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
resp.SUCCESS(c, content)
}
// List 获取所有的工具函数列表
func (h *FunctionHandler) List(c *gin.Context) {
var items []model.Function
err := h.DB.Where("enabled", true).Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
tools := make([]vo.Function, 0)
for _, v := range items {
var f vo.Function
err = utils.CopyObject(v, &f)
if err != nil {
continue
}
f.Action = ""
f.Token = ""
tools = append(tools, f)
}
resp.SUCCESS(c, tools)
}

View File

@ -15,6 +15,7 @@ import (
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils"
"github.com/gin-gonic/gin"
@ -30,13 +31,15 @@ import (
// MarkMapHandler 生成思维导图
type MarkMapHandler struct {
BaseHandler
clients *types.LMap[int, *types.WsClient]
clients *types.LMap[int, *types.WsClient]
userService *service.UserService
}
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB) *MarkMapHandler {
func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *MarkMapHandler {
return &MarkMapHandler{
BaseHandler: BaseHandler{App: app, DB: db},
clients: types.NewLMap[int, *types.WsClient](),
userService: userService,
}
}
@ -185,22 +188,13 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
// 扣减算力
if chatModel.Power > 0 {
res = h.DB.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", chatModel.Power))
if res.Error == nil {
// 记录算力消费日志
var u model.User
h.DB.Where("id", userId).First(&u)
h.DB.Create(&model.PowerLog{
UserId: u.Id,
Username: u.Username,
Type: types.PowerConsume,
Amount: chatModel.Power,
Mark: types.PowerSub,
Balance: u.Power,
Model: chatModel.Value,
Remark: fmt.Sprintf("AI绘制思维导图模型名称%s, ", chatModel.Value),
CreatedAt: time.Now(),
})
err = h.userService.DecreasePower(userId, chatModel.Power, model.PowerLog{
Type: types.PowerConsume,
Model: chatModel.Value,
Remark: fmt.Sprintf("AI绘制思维导图模型名称%s, ", chatModel.Value),
})
if err != nil {
return err
}
}

View File

@ -30,16 +30,18 @@ import (
type MidJourneyHandler struct {
BaseHandler
service *mj.Service
snowflake *service.Snowflake
uploader *oss.UploaderManager
mjService *mj.Service
snowflake *service.Snowflake
uploader *oss.UploaderManager
userService *service.UserService
}
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager) *MidJourneyHandler {
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler {
return &MidJourneyHandler{
snowflake: snowflake,
service: service,
uploader: manager,
snowflake: snowflake,
mjService: service,
uploader: manager,
userService: userService,
BaseHandler: BaseHandler{
App: app,
DB: db,
@ -80,7 +82,7 @@ func (h *MidJourneyHandler) Client(c *gin.Context) {
}
client := types.NewWsClient(ws)
h.service.Clients.Put(uint(userId), client)
h.mjService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
@ -196,7 +198,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return
}
h.service.PushTask(types.MjTask{
h.mjService.PushTask(types.MjTask{
Id: job.Id,
TaskId: taskId,
Type: types.TaskType(data.TaskType),
@ -208,28 +210,22 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
Mode: h.App.SysConfig.MjMode,
})
client := h.service.Clients.Get(uint(job.UserId))
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
// update user's power
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := h.GetLoginUser(c)
h.DB.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "mid-journey",
Remark: fmt.Sprintf("%s操作任务ID%s", opt, job.TaskId),
CreatedAt: time.Now(),
})
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "mid-journey",
Remark: fmt.Sprintf("%s操作任务ID%s", opt, job.TaskId),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
@ -269,7 +265,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
return
}
h.service.PushTask(types.MjTask{
h.mjService.PushTask(types.MjTask{
Id: job.Id,
Type: types.TaskUpscale,
UserId: userId,
@ -280,27 +276,22 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
Mode: h.App.SysConfig.MjMode,
})
client := h.service.Clients.Get(uint(job.UserId))
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
// update user's power
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := h.GetLoginUser(c)
h.DB.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "mid-journey",
Remark: fmt.Sprintf("Upscale 操作任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "mid-journey",
Remark: fmt.Sprintf("Upscale 操作任务ID%s", job.TaskId),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
@ -334,7 +325,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
return
}
h.service.PushTask(types.MjTask{
h.mjService.PushTask(types.MjTask{
Id: job.Id,
Type: types.TaskVariation,
UserId: userId,
@ -345,28 +336,21 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
Mode: h.App.SysConfig.MjMode,
})
client := h.service.Clients.Get(uint(job.UserId))
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
// update user's power
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := h.GetLoginUser(c)
h.DB.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "mid-journey",
Remark: fmt.Sprintf("Variation 操作任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "mid-journey",
Remark: fmt.Sprintf("Variation 操作任务ID%s", job.TaskId),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
@ -401,7 +385,7 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
}
// JobList 获取 MJ 任务列表
func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.MidJourneyJob) {
func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
session := h.DB.Session(&gorm.Session{})
if finish {
session = session.Where("progress >= ?", 100).Order("id DESC")
@ -419,10 +403,14 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
session = session.Offset(offset).Limit(pageSize)
}
// 统计总数
var total int64
session.Model(&model.MidJourneyJob{}).Count(&total)
var items []model.MidJourneyJob
res := session.Find(&items)
if res.Error != nil {
return res.Error, nil
return res.Error, vo.Page{}
}
var jobs = make([]vo.MidJourneyJob, 0)
@ -442,7 +430,7 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
jobs = append(jobs, job)
}
return nil, jobs
return nil, vo.NewPage(total, page, pageSize, jobs)
}
// Remove remove task image
@ -465,25 +453,11 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "mid-journey",
Remark: fmt.Sprintf("绘画任务失败退回算力。任务ID%sErr: %s", job.TaskId, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
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())
@ -498,7 +472,7 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
logger.Error("remove image failed: ", err)
}
client := h.service.Clients.Get(uint(job.UserId))
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}

View File

@ -11,6 +11,7 @@ import (
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
@ -21,11 +22,12 @@ import (
type RedeemHandler struct {
BaseHandler
lock sync.Mutex
lock sync.Mutex
userService *service.UserService
}
func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}}
func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *RedeemHandler {
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}, userService: userService}
}
func (h *RedeemHandler) Verify(c *gin.Context) {
@ -59,7 +61,11 @@ func (h *RedeemHandler) Verify(c *gin.Context) {
}
tx := h.DB.Begin()
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power + ?", item.Power)).Error
err := h.userService.IncreasePower(int(userId), item.Power, model.PowerLog{
Type: types.PowerRedeem,
Model: "兑换码",
Remark: fmt.Sprintf("兑换码核销,算力:%d兑换码%s...", item.Power, item.Code[:10]),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
@ -76,26 +82,6 @@ func (h *RedeemHandler) Verify(c *gin.Context) {
return
}
// 记录算力充值日志
var user model.User
err = tx.Where("id", userId).First(&user).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
h.DB.Create(&model.PowerLog{
UserId: userId,
Username: user.Username,
Type: types.PowerRedeem,
Amount: item.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "兑换码",
Remark: fmt.Sprintf("兑换码核销,算力:%d兑换码%s...", item.Power, item.Code[:10]),
CreatedAt: time.Now(),
})
tx.Commit()
resp.SUCCESS(c)

View File

@ -31,19 +31,27 @@ import (
type SdJobHandler struct {
BaseHandler
redis *redis.Client
service *sd.Service
uploader *oss.UploaderManager
snowflake *service.Snowflake
leveldb *store.LevelDB
redis *redis.Client
sdService *sd.Service
uploader *oss.UploaderManager
snowflake *service.Snowflake
leveldb *store.LevelDB
userService *service.UserService
}
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, service *sd.Service, manager *oss.UploaderManager, snowflake *service.Snowflake, levelDB *store.LevelDB) *SdJobHandler {
func NewSdJobHandler(app *core.AppServer,
db *gorm.DB,
service *sd.Service,
manager *oss.UploaderManager,
snowflake *service.Snowflake,
userService *service.UserService,
levelDB *store.LevelDB) *SdJobHandler {
return &SdJobHandler{
service: service,
uploader: manager,
snowflake: snowflake,
leveldb: levelDB,
sdService: service,
uploader: manager,
snowflake: snowflake,
leveldb: levelDB,
userService: userService,
BaseHandler: BaseHandler{
App: app,
DB: db,
@ -68,7 +76,7 @@ func (h *SdJobHandler) Client(c *gin.Context) {
}
client := types.NewWsClient(ws)
h.service.Clients.Put(uint(userId), client)
h.sdService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
@ -159,34 +167,27 @@ func (h *SdJobHandler) Image(c *gin.Context) {
return
}
h.service.PushTask(types.SdTask{
h.sdService.PushTask(types.SdTask{
Id: int(job.Id),
Type: types.TaskImage,
Params: params,
UserId: userId,
})
client := h.service.Clients.Get(uint(job.UserId))
client := h.sdService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
// update user's power
tx := h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := h.GetLoginUser(c)
h.DB.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: "stable-diffusion",
Remark: fmt.Sprintf("绘图操作任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "stable-diffusion",
Remark: fmt.Sprintf("绘图操作任务ID%s", job.TaskId),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
@ -223,7 +224,7 @@ func (h *SdJobHandler) JobList(c *gin.Context) {
}
// JobList 获取 MJ 任务列表
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.SdJob) {
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
session := h.DB.Session(&gorm.Session{})
if finish {
@ -242,10 +243,14 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
session = session.Offset(offset).Limit(pageSize)
}
// 统计总数
var total int64
session.Model(&model.SdJob{}).Count(&total)
var items []model.SdJob
res := session.Find(&items)
if res.Error != nil {
return res.Error, nil
return res.Error, vo.Page{}
}
var jobs = make([]vo.SdJob, 0)
@ -267,7 +272,7 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
jobs = append(jobs, job)
}
return nil, jobs
return nil, vo.NewPage(total, page, pageSize, jobs)
}
// Remove remove task image
@ -290,25 +295,11 @@ func (h *SdJobHandler) Remove(c *gin.Context) {
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%s Err: %s", job.TaskId, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%s Err: %s", job.TaskId, job.ErrMsg),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())

View File

@ -11,6 +11,7 @@ import (
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/service/oss"
"geekai/service/suno"
"geekai/store/model"
@ -26,18 +27,20 @@ import (
type SunoHandler struct {
BaseHandler
service *suno.Service
uploader *oss.UploaderManager
sunoService *suno.Service
uploader *oss.UploaderManager
userService *service.UserService
}
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager) *SunoHandler {
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
return &SunoHandler{
BaseHandler: BaseHandler{
App: app,
DB: db,
},
service: service,
uploader: uploader,
sunoService: service,
uploader: uploader,
userService: userService,
}
}
@ -58,7 +61,7 @@ func (h *SunoHandler) Client(c *gin.Context) {
}
client := types.NewWsClient(ws)
h.service.Clients.Put(uint(userId), client)
h.sunoService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
@ -123,7 +126,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
}
// 创建任务
h.service.PushTask(types.SunoTask{
h.sunoService.PushTask(types.SunoTask{
Id: job.Id,
UserId: job.UserId,
Type: job.Type,
@ -140,24 +143,17 @@ func (h *SunoHandler) Create(c *gin.Context) {
})
// update user's power
tx = h.DB.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power - ?", job.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
user, _ := h.GetLoginUser(c)
h.DB.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power - job.Power,
Mark: types.PowerSub,
Model: job.ModelName,
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
CreatedAt: time.Now(),
})
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
CreatedAt: time.Now(),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
client := h.service.Clients.Get(uint(job.UserId))
client := h.sunoService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
@ -166,8 +162,8 @@ func (h *SunoHandler) Create(c *gin.Context) {
func (h *SunoHandler) List(c *gin.Context) {
userId := h.GetLoginUserId(c)
page := h.GetInt(c, "page", 0)
pageSize := h.GetInt(c, "page_size", 0)
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
// 统计总数
@ -239,25 +235,11 @@ func (h *SunoHandler) Remove(c *gin.Context) {
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := tx.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power)).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
var user model.User
tx.Where("id = ?", job.UserId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRefund,
Amount: job.Power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: job.ModelName,
Remark: fmt.Sprintf("Suno 任务失败退回算力。任务ID%sErr:%s", job.TaskId, job.ErrMsg),
CreatedAt: time.Now(),
}).Error
err := h.userService.DecreasePower(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 {
tx.Rollback()
resp.ERROR(c, err.Error())

View File

@ -34,6 +34,7 @@ type UserHandler struct {
redis *redis.Client
licenseService *service.LicenseService
captcha *service.CaptchaService
userService *service.UserService
}
func NewUserHandler(
@ -42,6 +43,7 @@ func NewUserHandler(
searcher *xdb.Searcher,
client *redis.Client,
captcha *service.CaptchaService,
userService *service.UserService,
licenseService *service.LicenseService) *UserHandler {
return &UserHandler{
BaseHandler: BaseHandler{DB: db, App: app},
@ -49,6 +51,7 @@ func NewUserHandler(
redis: client,
captcha: captcha,
licenseService: licenseService,
userService: userService,
}
}
@ -155,10 +158,9 @@ func (h *UserHandler) Register(c *gin.Context) {
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
}
res := h.DB.Create(&user)
if res.Error != nil {
resp.ERROR(c, "保存数据失败")
logger.Error(res.Error)
tx := h.DB.Begin()
if err := tx.Create(&user).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
@ -167,35 +169,35 @@ func (h *UserHandler) Register(c *gin.Context) {
// 增加邀请数量
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.InvitePower > 0 {
h.DB.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower))
// 记录邀请算力充值日志
var inviter model.User
h.DB.Where("id", inviteCode.UserId).First(&inviter)
h.DB.Create(&model.PowerLog{
UserId: inviter.Id,
Username: inviter.Username,
Type: types.PowerInvite,
Amount: h.App.SysConfig.InvitePower,
Balance: inviter.Power,
Mark: types.PowerAdd,
Model: "",
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d邀请码%s新用户%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
CreatedAt: time.Now(),
err := h.userService.IncreasePower(int(inviteCode.UserId), h.App.SysConfig.InvitePower, model.PowerLog{
Type: types.PowerInvite,
Model: "",
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d邀请码%s新用户%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
// 添加邀请记录
h.DB.Create(&model.InviteLog{
err := tx.Create(&model.InviteLog{
InviterId: inviteCode.UserId,
UserId: user.Id,
Username: user.Username,
InviteCode: inviteCode.Code,
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
})
}).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
// 自动登录创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id,

View File

@ -0,0 +1,233 @@
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/video"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
)
type VideoHandler struct {
BaseHandler
videoService *video.Service
uploader *oss.UploaderManager
userService *service.UserService
}
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
return &VideoHandler{
BaseHandler: BaseHandler{
App: app,
DB: db,
},
videoService: service,
uploader: uploader,
userService: userService,
}
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *VideoHandler) Client(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
c.Abort()
return
}
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
logger.Info("Invalid user ID")
c.Abort()
return
}
client := types.NewWsClient(ws)
h.videoService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
func (h *VideoHandler) LumaCreate(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
FirstFrameImg string `json:"first_frame_img,omitempty"`
EndFrameImg string `json:"end_frame_img,omitempty"`
ExpandPrompt bool `json:"expand_prompt,omitempty"`
Loop bool `json:"loop,omitempty"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Prompt == "" {
resp.ERROR(c, "prompt is needed")
return
}
userId := int(h.GetLoginUserId(c))
params := types.VideoParams{
PromptOptimize: data.ExpandPrompt,
Loop: data.Loop,
StartImgURL: data.FirstFrameImg,
EndImgURL: data.EndFrameImg,
}
// 插入数据库
job := model.VideoJob{
UserId: userId,
Type: types.VideoLuma,
Prompt: data.Prompt,
Power: h.App.SysConfig.LumaPower,
Params: utils.JsonEncode(params),
}
tx := h.DB.Create(&job)
if tx.Error != nil {
resp.ERROR(c, tx.Error.Error())
return
}
// 创建任务
h.videoService.PushTask(types.VideoTask{
Id: job.Id,
UserId: userId,
Type: types.VideoLuma,
Prompt: data.Prompt,
Params: params,
})
// update user's power
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "luma",
Remark: fmt.Sprintf("Luma 文生视频任务ID%d", job.Id),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
client := h.videoService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
resp.SUCCESS(c)
}
func (h *VideoHandler) List(c *gin.Context) {
userId := h.GetLoginUserId(c)
t := c.Query("type")
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
all := h.GetBool(c, "all")
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
if t != "" {
session = session.Where("type", t)
}
if all {
session = session.Where("publish", 0).Where("progress", 100)
} else {
session = session.Where("user_id", h.GetLoginUserId(c))
}
// 统计总数
var total int64
session.Model(&model.VideoJob{}).Count(&total)
if page > 0 && pageSize > 0 {
offset := (page - 1) * pageSize
session = session.Offset(offset).Limit(pageSize)
}
var list []model.VideoJob
err := session.Order("id desc").Find(&list).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 转换为 VO
items := make([]vo.VideoJob, 0)
for _, v := range list {
var item vo.VideoJob
err = utils.CopyObject(v, &item)
if err != nil {
continue
}
item.CreatedAt = v.CreatedAt.Unix()
items = append(items, item)
}
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
}
func (h *VideoHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
var job model.VideoJob
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 删除任务
tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err = h.userService.DecreasePower(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 {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
}
func (h *VideoHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
publish := h.GetBool(c, "publish")
var job model.VideoJob
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
err = h.DB.Model(&job).UpdateColumn("publish", publish).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}

View File

@ -24,6 +24,7 @@ import (
"geekai/service/sd"
"geekai/service/sms"
"geekai/service/suno"
"geekai/service/video"
"geekai/store"
"io"
"log"
@ -199,9 +200,16 @@ func main() {
s.Run()
s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadImages()
s.DownloadFiles()
}),
fx.Provide(video.NewService),
fx.Invoke(func(s *video.Service) {
s.Run()
s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadFiles()
}),
fx.Provide(service.NewUserService),
fx.Provide(payment.NewAlipayService),
fx.Provide(payment.NewHuPiPay),
fx.Provide(payment.NewJPayService),
@ -425,6 +433,7 @@ func main() {
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
group := s.Engine.Group("/api/admin/chat/")
@ -484,6 +493,15 @@ func main() {
group.GET("play", h.Play)
group.POST("lyric", h.Lyric)
}),
fx.Provide(handler.NewVideoHandler),
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
group := s.Engine.Group("/api/video")
group.Any("client", h.Client)
group.POST("luma/create", h.LumaCreate)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
}),
fx.Provide(handler.NewTestHandler),
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
group := s.Engine.Group("/api/test")

View File

@ -35,9 +35,10 @@ type Service struct {
taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
userService *service.UserService
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client) *Service {
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{
httpClient: req.C().SetTimeout(time.Minute * 3),
db: db,
@ -45,6 +46,7 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
Clients: types.NewLMap[uint, *types.WsClient](),
uploadManager: manager,
userService: userService,
}
}
@ -122,32 +124,23 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
return "", errors.New("insufficient of power")
}
// 更新用户算力
tx := s.db.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", task.Power))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
var u model.User
s.db.Where("id", user.Id).First(&u)
s.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: task.Power,
Balance: u.Power,
Mark: types.PowerSub,
Model: "dall-e-3",
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
CreatedAt: time.Now(),
})
// 扣减算力
err := s.userService.DecreasePower(int(user.Id), task.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "dall-e-3",
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
})
if err != nil {
return "", fmt.Errorf("error with decrease power: %v", err)
}
// get image generation API KEY
var apiKey model.ApiKey
tx = s.db.Where("type", "dalle").
err = s.db.Where("type", "dalle").
Where("enabled", true).
Order("last_used_at ASC").First(&apiKey)
if tx.Error != nil {
return "", fmt.Errorf("no available DALL-E api key: %v", tx.Error)
Order("last_used_at ASC").First(&apiKey).Error
if err != nil {
return "", fmt.Errorf("no available DALL-E api key: %v", err)
}
var res imgRes
@ -181,13 +174,13 @@ 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
tx = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
err = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
"progress": 100,
"org_url": res.Data[0].Url,
"prompt": prompt,
})
if tx.Error != nil {
return "", fmt.Errorf("err with update database: %v", tx.Error)
}).Error
if err != nil {
return "", fmt.Errorf("err with update database: %v", err)
}
s.notifyQueue.RPush(service.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})

View File

@ -242,6 +242,10 @@ func (s *Service) Upload(task types.SunoTask) (RespVo, error) {
return RespVo{}, fmt.Errorf("请求 API 出错:%v", err)
}
if r.StatusCode != 200 {
return RespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String())
}
body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &res)
if err != nil {
@ -279,7 +283,7 @@ func (s *Service) CheckTaskNotify() {
}()
}
func (s *Service) DownloadImages() {
func (s *Service) DownloadFiles() {
go func() {
var items []model.SunoJob
for {
@ -425,11 +429,11 @@ type QueryRespVo struct {
func (s *Service) QueryTask(taskId string, channel string) (QueryRespVo, error) {
// 读取 API KEY
var apiKey model.ApiKey
tx := s.db.Session(&gorm.Session{}).Where("type", "suno").
err := s.db.Session(&gorm.Session{}).Where("type", "suno").
Where("api_url", channel).
Where("enabled", true).
Order("last_used_at DESC").First(&apiKey)
if tx.Error != nil {
Order("last_used_at DESC").First(&apiKey).Error
if err != nil {
return QueryRespVo{}, errors.New("no available API KEY for Suno")
}

View File

@ -0,0 +1,83 @@
package service
import (
"fmt"
"geekai/core/types"
"geekai/store/model"
"gorm.io/gorm"
"sync"
"time"
)
type UserService struct {
db *gorm.DB
lock sync.Mutex
}
func NewUserService(db *gorm.DB) *UserService {
return &UserService{db: db, lock: sync.Mutex{}}
}
// IncreasePower 增加用户算力
func (s *UserService) IncreasePower(userId int, power int, log model.PowerLog) error {
s.lock.Lock()
defer s.lock.Unlock()
tx := s.db.Begin()
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power + ?", power)).Error
if err != nil {
tx.Rollback()
return err
}
var user model.User
tx.Where("id", userId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: log.Type,
Amount: power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: log.Model,
Remark: log.Remark,
CreatedAt: time.Now(),
}).Error
if err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}
// DecreasePower 减少用户算力
func (s *UserService) DecreasePower(userId int, power int, log model.PowerLog) error {
s.lock.Lock()
defer s.lock.Unlock()
tx := s.db.Begin()
err := tx.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", power)).Error
if err != nil {
tx.Rollback()
return fmt.Errorf("扣减算力失败:%v", err)
}
var user model.User
tx.Where("id", userId).First(&user)
err = tx.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: log.Type,
Amount: power,
Balance: user.Power,
Mark: types.PowerSub,
Model: log.Model,
Remark: log.Remark,
CreatedAt: time.Now(),
}).Error
if err != nil {
tx.Rollback()
return fmt.Errorf("记录算力日志失败:%v", err)
}
tx.Commit()
return nil
}

326
api/service/video/luma.go Normal file
View File

@ -0,0 +1,326 @@
package video
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
logger2 "geekai/logger"
"geekai/service"
"geekai/service/oss"
"geekai/store"
"geekai/store/model"
"geekai/utils"
"github.com/go-redis/redis/v8"
"io"
"time"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
var logger = logger2.GetLogger()
type Service struct {
httpClient *req.Client
db *gorm.DB
uploadManager *oss.UploaderManager
taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client) *Service {
return &Service{
httpClient: req.C().SetTimeout(time.Minute * 3),
db: db,
taskQueue: store.NewRedisQueue("Video_Task_Queue", redisCli),
notifyQueue: store.NewRedisQueue("Video_Notify_Queue", redisCli),
Clients: types.NewLMap[uint, *types.WsClient](),
uploadManager: manager,
}
}
func (s *Service) PushTask(task types.VideoTask) {
logger.Infof("add a new Video task to the task list: %+v", task)
s.taskQueue.RPush(task)
}
func (s *Service) Run() {
// 将数据库中未提交的人物加载到队列
var jobs []model.VideoJob
s.db.Where("task_id", "").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)
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,
})
}
logger.Info("Starting Video job consumer...")
go func() {
for {
var task types.VideoTask
err := s.taskQueue.LPop(&task)
if err != nil {
logger.Errorf("taking task with error: %v", err)
continue
}
var r LumaRespVo
r, err = s.LumaCreate(task)
if err != nil {
logger.Errorf("create task with error: %v", err)
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"err_msg": err.Error(),
"progress": service.FailTaskProgress,
})
s.notifyQueue.RPush(service.NotifyMessage{UserId: task.UserId, JobId: int(task.Id), Message: service.TaskStatusFailed})
continue
}
// 更新任务信息
err = s.db.Model(&model.VideoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"task_id": r.Id,
"channel": r.Channel,
"prompt_ext": r.Prompt,
}).Error
if err != nil {
logger.Errorf("update task with error: %v", err)
s.PushTask(task)
}
}
}()
}
type LumaRespVo struct {
Id string `json:"id"`
Prompt string `json:"prompt"`
State string `json:"state"`
CreatedAt time.Time `json:"created_at"`
Video interface{} `json:"video"`
Liked interface{} `json:"liked"`
EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"`
Channel string `json:"channel,omitempty"`
}
func (s *Service) LumaCreate(task types.VideoTask) (LumaRespVo, error) {
// 读取 API KEY
var apiKey model.ApiKey
session := s.db.Session(&gorm.Session{}).Where("type", "luma").Where("enabled", true)
if task.Channel != "" {
session = session.Where("api_url", task.Channel)
}
tx := session.Order("last_used_at DESC").First(&apiKey)
if tx.Error != nil {
return LumaRespVo{}, errors.New("no available API KEY for Luma")
}
reqBody := map[string]interface{}{
"user_prompt": task.Prompt,
"expand_prompt": task.Params.PromptOptimize,
"loop": task.Params.Loop,
"image_url": task.Params.StartImgURL,
"image_end_url": task.Params.EndImgURL,
}
var res LumaRespVo
apiURL := fmt.Sprintf("%s/luma/generations", apiKey.ApiURL)
logger.Debugf("API URL: %s, request body: %+v", apiURL, reqBody)
r, err := req.C().R().
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(reqBody).
Post(apiURL)
if err != nil {
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%v", err)
}
if r.StatusCode != 200 && r.StatusCode != 201 {
return LumaRespVo{}, fmt.Errorf("请求 API 出错:%d, %s", r.StatusCode, r.String())
}
body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &res)
if err != nil {
return LumaRespVo{}, fmt.Errorf("解析API数据失败%v, %s", err, string(body))
}
// update the last_use_at for api key
apiKey.LastUsedAt = time.Now().Unix()
session.Updates(&apiKey)
res.Channel = apiKey.ApiURL
return res, nil
}
func (s *Service) CheckTaskNotify() {
go func() {
logger.Info("Running Suno task notify checking ...")
for {
var message service.NotifyMessage
err := s.notifyQueue.LPop(&message)
if err != nil {
continue
}
client := s.Clients.Get(uint(message.UserId))
if client == nil {
continue
}
err = client.Send([]byte(message.Message))
if err != nil {
continue
}
}
}()
}
func (s *Service) DownloadFiles() {
go func() {
var items []model.VideoJob
for {
res := s.db.Where("progress", 102).Find(&items)
if res.Error != nil {
continue
}
for _, v := range items {
if v.WaterURL == "" {
continue
}
logger.Infof("try download video: %s", v.WaterURL)
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.WaterURL, true)
if err != nil {
logger.Errorf("download video with error: %v", err)
continue
}
logger.Infof("download video success: %s", videoURL)
v.WaterURL = videoURL
if v.VideoURL != "" {
logger.Infof("try download no water video: %s", v.VideoURL)
videoURL, err = s.uploadManager.GetUploadHandler().PutUrlFile(v.VideoURL, true)
if err != nil {
logger.Errorf("download video with error: %v", err)
continue
}
}
logger.Info("download no water video success: %s", videoURL)
v.VideoURL = videoURL
v.Progress = 100
s.db.Updates(&v)
s.notifyQueue.RPush(service.NotifyMessage{UserId: v.UserId, JobId: int(v.Id), Message: service.TaskStatusFinished})
}
time.Sleep(time.Second * 10)
}
}()
}
// SyncTaskProgress 异步拉取任务
func (s *Service) SyncTaskProgress() {
go func() {
var jobs []model.VideoJob
for {
res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
if res.Error != nil {
continue
}
for _, job := range jobs {
task, err := s.QueryLumaTask(job.TaskId, job.Channel)
if err != nil {
logger.Errorf("query task with error: %v", err)
// 更新任务信息
s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
"progress": service.FailTaskProgress, // 102 表示资源未下载完成,
"err_msg": err.Error(),
})
continue
}
logger.Debugf("task: %+v", task)
if task.State == "completed" { // 更新任务信息
data := map[string]interface{}{
"progress": 102, // 102 表示资源未下载完成,
"water_url": task.Video.Url,
"raw_data": utils.JsonEncode(task),
"prompt_ext": task.Prompt,
}
if task.Video.DownloadUrl != "" {
data["video_url"] = task.Video.DownloadUrl
}
err = s.db.Model(&model.VideoJob{Id: job.Id}).UpdateColumns(data).Error
if err != nil {
logger.Errorf("更新数据库失败:%v", err)
continue
}
}
}
time.Sleep(time.Second * 10)
}
}()
}
type LumaTaskVo struct {
Id string `json:"id"`
Liked interface{} `json:"liked"`
State string `json:"state"`
Video struct {
Url string `json:"url"`
Width int `json:"width"`
Height int `json:"height"`
DownloadUrl string `json:"download_url"`
} `json:"video"`
Prompt string `json:"prompt"`
CreatedAt time.Time `json:"created_at"`
EstimateWaitSeconds interface{} `json:"estimate_wait_seconds"`
}
func (s *Service) QueryLumaTask(taskId string, channel string) (LumaTaskVo, error) {
// 读取 API KEY
var apiKey model.ApiKey
err := s.db.Session(&gorm.Session{}).Where("type", "luma").
Where("api_url", channel).
Where("enabled", true).
Order("last_used_at DESC").First(&apiKey).Error
if err != nil {
return LumaTaskVo{}, errors.New("no available API KEY for Luma")
}
apiURL := fmt.Sprintf("%s/luma/generations/%s", apiKey.ApiURL, taskId)
var res LumaTaskVo
r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
if err != nil {
return LumaTaskVo{}, fmt.Errorf("请求 API 失败:%v", err)
}
defer r.Body.Close()
if r.StatusCode != 200 {
return LumaTaskVo{}, fmt.Errorf("API 返回失败:%v", r.String())
}
body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &res)
if err != nil {
return LumaTaskVo{}, fmt.Errorf("解析API数据失败%v, %s", err, string(body))
}
return res, nil
}

View File

@ -81,54 +81,6 @@ func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (ms
// 自动将 VIP 会员的算力补充到每月赠送的最大值
func (e *XXLJobExecutor) ResetVipPower(cxt context.Context, param *xxl.RunReq) (msg string) {
logger.Info("开始进行月底账号盘点...")
var users []model.User
res := e.db.Where("vip", 1).Where("status", 1).Find(&users)
if res.Error != nil {
return "No vip users found"
}
var sysConfig model.Config
res = e.db.Where("marker", "system").First(&sysConfig)
if res.Error != nil {
return "error with get system config: " + res.Error.Error()
}
var config types.SystemConfig
err := utils.JsonDecode(sysConfig.Config, &config)
if err != nil {
return "error with decode system config: " + err.Error()
}
for _, u := range users {
// 处理过期的 VIP
if u.ExpiredTime > 0 && u.ExpiredTime <= time.Now().Unix() {
u.Vip = false
e.db.Model(&model.User{}).Where("id", u.Id).UpdateColumn("vip", false)
continue
}
if u.Power < config.VipMonthPower {
power := config.VipMonthPower - u.Power
// update user
tx := e.db.Model(&model.User{}).Where("id", u.Id).UpdateColumn("power", gorm.Expr("power + ?", power))
// 记录算力变动日志
if tx.Error == nil {
var user model.User
e.db.Where("id", u.Id).First(&user)
e.db.Create(&model.PowerLog{
UserId: u.Id,
Username: u.Username,
Type: types.PowerRecharge,
Amount: power,
Mark: types.PowerAdd,
Balance: user.Power,
Model: "系统盘点",
Remark: fmt.Sprintf("VIP会员每月算力派发%d", config.VipMonthPower),
CreatedAt: time.Now(),
})
}
}
}
logger.Info("月底盘点完成!")
return "success"
}

View File

@ -0,0 +1,27 @@
package model
import "time"
type VideoJob struct {
Id uint `gorm:"primarykey;column:id"`
UserId int
Channel string // 频道
Type string // luma,runway,cog
TaskId string
Prompt string // 提示词
PromptExt string // 优化后提示词
CoverURL string // 封面图 URL
VideoURL string // 无水印视频 URL
WaterURL string // 有水印视频 URL
Progress int // 任务进度
Publish bool // 是否发布
ErrMsg string // 错误信息
RawData string // 原始数据 json
Power int // 消耗算力
Params string // 任务参数
CreatedAt time.Time
}
func (VideoJob) TableName() string {
return "chatgpt_video_jobs"
}

View File

@ -28,7 +28,3 @@ type SunoJob struct {
PlayTimes int `json:"play_times"` // 播放次数
CreatedAt int64 `json:"created_at"`
}
func (SunoJob) TableName() string {
return "chatgpt_suno_jobs"
}

23
api/store/vo/video_job.go Normal file
View File

@ -0,0 +1,23 @@
package vo
import "geekai/core/types"
type VideoJob struct {
Id uint `json:"id"`
UserId int `json:"user_id"`
Channel string `json:"channel"`
Type string `json:"type"`
TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词
PromptExt string `json:"prompt_ext"` // 提示词
CoverURL string `json:"cover_url"` // 封面图 URL
VideoURL string `json:"video_url"` // 无水印视频 URL
WaterURL string `json:"water_url"` // 有水印视频 URL
Progress int `json:"progress"` // 任务进度
Publish bool `json:"publish"` // 是否发布
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

@ -1,2 +1,27 @@
ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NULL COMMENT '手机号' AFTER `username`;
ALTER TABLE `chatgpt_users` ADD `email` VARCHAR(50) NULL COMMENT '邮箱地址' AFTER `mobile`;
ALTER TABLE `chatgpt_users` ADD `email` VARCHAR(50) NULL COMMENT '邮箱地址' AFTER `mobile`;
CREATE TABLE `chatgpt_video_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`channel` varchar(100) NOT NULL COMMENT '渠道',
`task_id` varchar(100) NOT NULL COMMENT '任务 ID',
`type` varchar(20) DEFAULT NULL COMMENT '任务类型,luma,runway,cogvideo',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`prompt_ext` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '优化后提示词',
`cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
`video_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '视频地址',
`water_url` varchar(512) DEFAULT NULL COMMENT '带水印的视频地址',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
`raw_data` text COMMENT '原始数据',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
ALTER TABLE `chatgpt_video_jobs`ADD PRIMARY KEY (`id`);
ALTER TABLE `chatgpt_video_jobs` MODIFY `id` int NOT NULL AUTO_INCREMENT;
ALTER TABLE `chatgpt_video_jobs` ADD `params` VARCHAR(512) NULL COMMENT '参数JSON' AFTER `raw_data`;

View File

@ -27,17 +27,17 @@ services:
ports:
- "6380:6379"
xxl-job-admin:
container_name: geekai-xxl-job-admin
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/xxl-job-admin:2.4.0
restart: always
ports:
- "8081:8080"
environment:
- PARAMS=--spring.config.location=/application.properties
volumes:
- ./logs/xxl-job:/data/applogs
- ./conf/xxl-job/application.properties:/application.properties
# xxl-job-admin:
# container_name: geekai-xxl-job-admin
# image: registry.cn-shenzhen.aliyuncs.com/geekmaster/xxl-job-admin:2.4.0
# restart: always
# ports:
# - "8081:8080"
# environment:
# - PARAMS=--spring.config.location=/application.properties
# volumes:
# - ./logs/xxl-job:/data/applogs
# - ./conf/xxl-job/application.properties:/application.properties
tika:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/tika:latest
@ -46,14 +46,14 @@ services:
ports:
- "9998:9998"
midjourney-proxy:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/midjourney-proxy:2.6.2
container_name: geekai-midjourney-proxy
restart: always
ports:
- "8082:8080"
volumes:
- ./conf/mj-proxy:/home/spring/config
# midjourney-proxy:
# image: registry.cn-shenzhen.aliyuncs.com/geekmaster/midjourney-proxy:2.6.2
# container_name: geekai-midjourney-proxy
# restart: always
# ports:
# - "8082:8080"
# volumes:
# - ./conf/mj-proxy:/home/spring/config
# 后端 API 程序

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 337 KiB

View File

@ -1,334 +0,0 @@
#app {
height: 100%;
}
#app .chat-page {
height: 100%;
}
#app .chat-page .el-aside {
padding: 10px;
width: var(--el-aside-width, 320px);
}
#app .chat-page .el-aside .chat-list {
display: flex;
flex-flow: column;
border-radius: 10px;
padding: 10px 0;
}
#app .chat-page .el-aside .chat-list .search-box {
flex-wrap: wrap;
padding: 10px 0;
}
#app .chat-page .el-aside .chat-list .search-box .search-input {
--el-input-bg-color: #363535;
--el-input-border-color: #464545;
--el-input-focus-border-color: #47fff1;
--el-input-hover-border-color: #2da39a;
box-shadow: none;
}
#app .chat-page .el-aside .chat-list ::-webkit-scrollbar {
width: 0;
height: 0;
background-color: transparent;
}
#app .chat-page .el-aside .chat-list .content {
width: 100%;
overflow-y: scroll;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item {
display: flex;
width: 100%;
justify-content: flex-start;
padding: 8px 12px;
cursor: pointer;
border: 1px solid #3c3c3c;
margin-bottom: 6px;
border-radius: 5px;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item:hover {
background-color: #343540;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item .avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-title-input {
font-size: 14px;
margin-top: 4px;
margin-left: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 190px;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-title {
color: #c1c1c1;
padding: 5px 10px;
max-width: 220px;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-opt {
position: absolute;
right: 2px;
top: 16px;
color: #fff;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-opt .el-dropdown-link {
color: #fff;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item .chat-opt .el-icon {
margin-right: 8px;
}
#app .chat-page .el-aside .chat-list .content .chat-list-item.active {
background-color: #343540;
border-color: #21aa93;
}
#app .chat-page .el-aside .tool-box {
display: flex;
justify-content: center;
padding-top: 12px;
border-top: 1px solid #3c3c3c;
}
#app .chat-page .el-aside .tool-box .iconfont {
margin-right: 5px;
}
#app .chat-page .el-main {
overflow: hidden;
--el-main-padding: 0;
margin: 0;
}
#app .chat-page .el-main .chat-container {
min-width: 0;
flex: 1;
background-color: var(--el-bg-color);
color: var(--el-text-color-primary);
}
#app .chat-page .el-main .chat-container .chat-config {
height: 30px;
padding: 10px 30px;
display: flex;
justify-content: center;
justify-items: center;
border-bottom: 1px solid #d9d9e3;
}
#app .chat-page .el-main .chat-container .chat-config .role-select-label {
color: #fff;
}
#app .chat-page .el-main .chat-container .chat-config .el-select {
max-width: 150px;
margin-right: 10px;
}
#app .chat-page .el-main .chat-container .chat-config .role-select {
max-width: 130px;
}
#app .chat-page .el-main .chat-container .chat-config .setting {
padding: 5px;
border-radius: 5px;
cursor: pointer;
}
#app .chat-page .el-main .chat-container .chat-config .setting .iconfont {
font-size: 18px;
color: #19c37d;
}
#app .chat-page .el-main .chat-container .chat-config .setting:hover {
background: #d5fad3;
}
#app .chat-page .el-main .chat-container .chat-config .el-button .el-icon {
margin-right: 5px;
}
#app .chat-page .el-main .chat-container #container {
overflow: hidden;
width: 100%;
position: relative;
}
#app .chat-page .el-main .chat-container #container ::-webkit-scrollbar {
width: 12px /* 滚动条宽度 */;
background: #f1f1f1;
}
#app .chat-page .el-main .chat-container #container ::-webkit-scrollbar-track {
background-color: #e1e1e1;
}
#app .chat-page .el-main .chat-container #container ::-webkit-scrollbar-thumb {
background-color: #c1c1c1;
border-radius: 12px;
}
#app .chat-page .el-main .chat-container #container ::-webkit-scrollbar-thumb:hover {
background-color: #a8a8a8;
}
#app .chat-page .el-main .chat-container #container .chat-box {
overflow-y: auto;
--content-font-size: 16px;
--content-color: #c1c1c1;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 0 0 50px 0;
}
#app .chat-page .el-main .chat-container #container .chat-box .chat-line {
font-size: 14px;
display: flex;
align-items: flex-start;
}
#app .chat-page .el-main .chat-container #container .input-box {
position: absolute;
bottom: 0;
width: 100%;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner {
display: flex;
background-color: #fff;
justify-content: center;
align-items: center;
box-shadow: 0 2px 15px rgba(0,0,0,0.1);
padding: 0 15px;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .tool-item {
margin-right: 15px;
border-radius: 6px;
color: #19c37d;
display: flex;
justify-content: center;
justify-items: center;
padding: 6px;
cursor: pointer;
background: #f2f2f2;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .tool-item:hover {
background: #d5fad3;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .tool-item .iconfont {
font-size: 24px;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body {
width: 100%;
margin: 0;
border: none;
padding: 10px 0;
display: flex;
justify-content: center;
position: relative;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .hide-div {
white-space: pre-wrap; /* 保持文本换行 */
visibility: hidden; /* 隐藏 div */
position: absolute; /* 脱离文档流 */
line-height: 24px;
font-size: 14px;
word-wrap: break-word; /* 允许单词换行 */
overflow-wrap: break-word; /* 允许长单词换行,适用于现代浏览器 */
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border {
display: flex;
width: 100%;
overflow: hidden;
border: 2px solid #21aa93;
border-radius: 10px;
padding: 10px;
background-color: #f4f4f4;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border .input-inner {
display: flex;
flex-flow: column;
width: 100%;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border .input-inner .file-list {
padding-bottom: 10px;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border .input-inner .prompt-input::-webkit-scrollbar {
width: 0;
height: 0;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border .input-inner .prompt-input {
width: 100%;
line-height: 24px;
border: none;
font-size: 14px;
background: none;
resize: none;
white-space: pre-wrap; /* 保持文本换行 */
word-wrap: break-word; /* 允许单词换行 */
overflow-wrap: break-word; /* 允许长单词换行,适用于现代浏览器 */
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border .send-btn {
width: 32px;
margin-left: 10px;
}
#app .chat-page .el-main .chat-container #container .input-box .input-box-inner .input-body .input-border .send-btn .el-button {
padding: 8px 5px;
border-radius: 6px;
font-size: 20px;
}
#app .chat-page .el-main .chat-container #container::-webkit-scrollbar {
width: 0;
height: 0;
}
#app .el-message-box {
width: 90%;
max-width: 420px;
}
#app .el-message {
min-width: 100px;
max-width: 600px;
}
.el-select-dropdown__wrap .el-select-dropdown__item .role-option {
display: flex;
flex-flow: row;
margin-top: 8px;
}
.el-select-dropdown__wrap .el-select-dropdown__item .role-option .el-image {
width: 20px;
height: 20px;
border-radius: 50%;
}
.el-select-dropdown__wrap .el-select-dropdown__item .role-option span {
margin-left: 5px;
height: 20px;
line-height: 20px;
}
.account {
display: flex;
background-color: #90ffc2;
color: #000;
width: 100%;
border-radius: 10px;
padding: 10px;
}
.account .vip-logo .el-image {
width: 40px;
height: 40px;
border-radius: 100%;
background-color: #fff;
}
.account .vip-info {
padding: 0 10px 0 10px;
}
.account .vip-info h4,
.account .vip-info p {
margin: 0;
}
.account .vip-info h4 {
font-weight: bold;
font-size: 16px;
}
.account .vip-info p {
color: #333;
}
.account .pay-btn {
width: 100%;
display: flex;
justify-content: right;
align-items: center;
}
.el-overlay-dialog .el-dialog .el-dialog__body .notice {
line-height: 1.8;
font-size: 16px;
overflow: auto;
height: 100%;
}
.dialog-service {
text-align: center;
}
.dialog-service .el-image {
width: 360px;
}

View File

@ -160,13 +160,15 @@ $borderColor = #4676d0;
padding 5px
border-radius 5px
cursor pointer
background-color #f2f2f2
margin-right 10px
.iconfont {
font-size 18px
color #19c37d
}
&:hover {
background #D5FAD3
background-color #D5FAD3
}
}
@ -427,4 +429,11 @@ $borderColor = #4676d0;
.el-image {
width 360px;
}
}
.tools-dropdown {
width auto
.el-icon {
margin-left 5px;
}
}

View File

@ -1,154 +0,0 @@
.home {
display: flex;
height: 100vh;
width: 100%;
flex-flow: column;
}
.home .header {
display: flex;
justify-content: space-between;
height: 50px;
line-height: 50px;
background-color: #1e1f22;
padding-right: 20px;
}
.home .header .banner {
display: flex;
}
.home .header .banner .logo {
display: flex;
padding: 5px;
cursor: pointer;
}
.home .header .banner .logo .el-image {
width: 48px;
height: 48px;
background-color: #fff;
border-radius: 50%;
}
.home .header .banner .title {
display: flex;
color: #fff;
font-size: 20px;
padding: 0 10px;
}
.home .header .navbar {
display: flex;
flex-flow: row;
}
.home .header .navbar .link-button {
margin-right: 15px;
color: #e1e1e1;
padding: 0 10px;
}
.home .header .navbar .link-button:hover {
background-color: #414141;
}
.home .header .navbar .link-button .iconfont {
font-size: 24px;
}
.home .header .navbar .user-info {
width: 100%;
padding: 5px 0;
}
.home .header .navbar .user-info .el-dropdown-link {
width: 100%;
cursor: pointer;
display: flex;
}
.home .header .navbar .user-info .el-dropdown-link .el-image {
width: 36px;
height: 36px;
border-radius: 50%;
}
.home .header .navbar .user-info .el-dropdown-link .el-icon {
color: #ccc;
line-height: 24px;
}
.home .main {
width: 100%;
display: flex;
flex-flow: row;
}
.home .main .navigator {
display: flex;
flex-flow: column;
width: 60px;
padding: 10px 1px;
border-right: 1px solid #3c3c3c;
background-color: #1e1f22;
}
.home .main .navigator .nav-items {
margin-top: 10px;
padding: 0 5px;
}
.home .main .navigator .nav-items li {
margin-bottom: 15px;
display: flex;
flex-flow: column;
}
.home .main .navigator .nav-items li a {
color: #dadbdc;
border-radius: 10px;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background-color: #414348;
}
.home .main .navigator .nav-items li a .el-image {
border-radius: 10px;
}
.home .main .navigator .nav-items li a .iconfont {
font-size: 20px;
}
.home .main .navigator .nav-items li a:hover,
.home .main .navigator .nav-items li a.active {
color: #47fff1;
background-color: #0f7a71;
}
.home .main .navigator .nav-items li .title {
font-size: 12px;
padding-top: 6px;
color: #e5e7eb;
text-align: center;
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: unset; /* 使用省略号表示溢出内容 */
}
.home .main .navigator .nav-items li .active {
color: #47fff1;
}
.home .main .content {
width: 100%;
overflow: auto;
box-sizing: border-box;
background-color: #282c34;
}
.el-popper .more-menus li {
padding: 10px 15px;
cursor: pointer;
border-radius: 5px;
margin: 5px 0;
}
.el-popper .more-menus li .el-image {
position: relative;
top: 5px;
right: 5px;
}
.el-popper .more-menus li:hover {
background-color: #f1f1f1;
}
.el-popper .more-menus li.active {
background-color: #f1f1f1;
}
.el-popper .user-info-menu li a {
width: 100%;
justify-content: left;
}
.el-popper .user-info-menu li a:hover {
text-decoration: none !important;
color: var(--el-primary-text-color);
}

View File

@ -23,7 +23,6 @@
.el-image {
width 48px
height 48px
background-color #ffffff
border-radius 50%
}
}

View File

@ -1,101 +0,0 @@
.index-page {
margin: 0;
overflow: hidden;
color: #fff;
display: flex;
justify-content: center;
align-items: baseline;
padding-top: 150px;
}
.index-page .color-bg {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
}
.index-page .image-bg {
filter: blur(8px);
background-size: cover;
background-position: center;
}
.index-page .shadow {
box-shadow: rgba(0,0,0,0.3) 0px 0px 3px;
}
.index-page .shadow:hover {
box-shadow: rgba(0,0,0,0.3) 0px 0px 8px;
}
.index-page .menu-box {
position: absolute;
top: 0;
width: 100%;
display: flex;
}
.index-page .menu-box .el-menu {
padding: 0 30px;
width: 100%;
display: flex;
justify-content: space-between;
background: none;
border: none;
}
.index-page .menu-box .el-menu .menu-item {
display: flex;
padding: 20px 0;
color: #fff;
}
.index-page .menu-box .el-menu .menu-item .title {
font-size: 24px;
padding: 10px 10px 0 10px;
}
.index-page .menu-box .el-menu .menu-item .el-image {
height: 50px;
background-color: #fff;
}
.index-page .menu-box .el-menu .menu-item .el-button {
margin-left: 10px;
}
.index-page .menu-box .el-menu .menu-item .el-button span {
margin-left: 5px;
}
.index-page .content {
text-align: center;
position: relative;
display: flex;
flex-flow: column;
align-items: center;
}
.index-page .content h1 {
font-size: 5rem;
margin-bottom: 1rem;
}
.index-page .content p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
.index-page .content .navs {
display: flex;
max-width: 900px;
padding: 20px;
}
.index-page .content .navs .el-space--horizontal {
justify-content: center;
}
.index-page .content .navs .nav-item {
width: 200px;
}
.index-page .content .navs .nav-item .el-button {
width: 100%;
padding: 25px 20px;
font-size: 1.3rem;
transition: all 0.3s ease;
}
.index-page .content .navs .nav-item .el-button .iconfont {
font-size: 24px;
margin-right: 10px;
position: relative;
top: -2px;
}
.index-page .footer .el-link__inner {
color: #fff;
}

View File

@ -54,9 +54,9 @@
padding 10px 10px 0 10px
}
.el-image {
.logo {
height 50px
background-color #ffffff
border-radius 50%
}
.el-button {

View File

@ -1,99 +0,0 @@
.bg {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: #313237;
background-image: url("~@/assets/img/login-bg.jpg");
background-size: cover;
background-position: center;
background-repeat: repeat-y;
}
.main .contain {
position: fixed;
left: 50%;
top: 40%;
width: 90%;
max-width: 400px;
transform: translate(-50%, -50%);
padding: 20px 10px;
color: #fff;
border-radius: 10px;
}
.main .contain .logo {
text-align: center;
}
.main .contain .logo .el-image {
width: 120px;
cursor: pointer;
background-color: #fff;
border-radius: 50%;
}
.main .contain .header {
width: 100%;
margin-bottom: 24px;
font-size: 24px;
color: $white_v1;
letter-space: 2px;
text-align: center;
padding-top: 10px;
}
.main .contain .content {
width: 100%;
height: auto;
border-radius: 3px;
}
.main .contain .content .block {
margin-bottom: 16px;
}
.main .contain .content .block .el-input__inner {
border: 1px solid $gray-v6 !important;
}
.main .contain .content .block .el-input__inner .el-icon-user,
.main .contain .content .block .el-input__inner .el-icon-lock {
font-size: 20px;
}
.main .contain .content .btn-row {
padding-top: 10px;
}
.main .contain .content .btn-row .login-btn {
width: 100%;
font-size: 16px;
letter-spacing: 2px;
}
.main .contain .content .text-line {
justify-content: center;
padding-top: 10px;
font-size: 14px;
}
.main .contain .content .opt {
padding: 15px;
}
.main .contain .content .opt .el-col {
text-align: center;
}
.main .contain .content .divider {
border-top: 2px solid #c1c1c1;
}
.main .contain .content .clogin {
padding: 15px;
display: flex;
justify-content: center;
}
.main .contain .content .clogin .iconfont {
font-size: 20px;
background: #e9f1f6;
padding: 8px;
border-radius: 50%;
cursor: pointer;
}
.main .contain .content .clogin .iconfont.icon-wechat {
color: #0bc15f;
}
.main .footer {
color: #fff;
}
.main .footer .container {
padding: 20px;
}

View File

@ -30,7 +30,6 @@
.el-image {
width 120px;
cursor pointer
background-color #ffffff
border-radius 50%
}
}

View File

@ -41,7 +41,7 @@ import {computed, ref, watch} from "vue";
import SendMsg from "@/components/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {checkSession, removeUserInfo} from "@/store/cache";
import {checkSession} from "@/store/cache";
const props = defineProps({
show: Boolean,
@ -76,7 +76,6 @@ const save = () => {
}
httpPost('/api/user/bind/email', form.value).then(() => {
removeUserInfo()
ElMessage.success("绑定成功")
emits('hide')
}).catch(e => {

View File

@ -41,7 +41,7 @@ import {computed, ref, watch} from "vue";
import SendMsg from "@/components/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {checkSession, removeUserInfo} from "@/store/cache";
import {checkSession} from "@/store/cache";
const props = defineProps({
show: Boolean,
@ -79,7 +79,6 @@ const save = () => {
}
httpPost('/api/user/bind/mobile', form.value).then(() => {
removeUserInfo()
ElMessage.success("绑定成功")
emits('hide')
}).catch(e => {

View File

@ -26,6 +26,12 @@ const routes = [
meta: {title: '创作中心'},
component: () => import('@/views/ChatPlus.vue'),
},
{
name: 'chat-id',
path: '/chat/:id',
meta: {title: '创作中心'},
component: () => import('@/views/ChatPlus.vue'),
},
{
name: 'image-mj',
path: '/mj',

View File

@ -6,29 +6,15 @@ const adminDataKey = "ADMIN_INFO_CACHE_KEY"
const systemInfoKey = "SYSTEM_INFO_CACHE_KEY"
const licenseInfoKey = "LICENSE_INFO_CACHE_KEY"
export function checkSession() {
const item = Storage.get(userDataKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
return Promise.resolve(item.data)
}
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
item.data = res.data
// cache expires after 10 secs
item.expire = Date.now() + 1000 * 30
Storage.set(userDataKey, item)
resolve(item.data)
resolve(res.data)
}).catch(e => {
Storage.remove(userDataKey)
reject(e)
})
})
}
export function removeUserInfo() {
Storage.remove(userDataKey)
}
export function checkAdminSession() {
const item = Storage.get(adminDataKey) ?? {expire:0, data:null}
if (item.expire > Date.now()) {
@ -63,7 +49,7 @@ export function getSystemInfo() {
Storage.set(systemInfoKey, item)
resolve(item.data)
}).catch(err => {
resolve(err)
reject(err)
})
})
}

View File

@ -1,6 +1,6 @@
import {randString} from "@/utils/libs";
import Storage from "good-storage";
import {checkAdminSession, checkSession, removeAdminInfo, removeUserInfo} from "@/store/cache";
import {removeAdminInfo} from "@/store/cache";
/**
* storage handler
@ -24,7 +24,6 @@ export function setUserToken(token) {
export function removeUserToken() {
Storage.remove(UserTokenKey)
removeUserInfo()
}
export function getAdminToken() {

View File

@ -3,7 +3,7 @@
<el-container>
<el-aside>
<div class="chat-list">
<el-button @click="newChat" color="#21aa93">
<el-button @click="_newChat" color="#21aa93">
<el-icon style="margin-right: 5px">
<Plus/>
</el-icon>
@ -23,7 +23,7 @@
<div class="content" :style="{height: leftBoxHeight+'px'}">
<el-row v-for="chat in chatList" :key="chat.chat_id">
<div :class="chat.chat_id === activeChat.chat_id?'chat-list-item active':'chat-list-item'"
<div :class="chat.chat_id === chatId?'chat-list-item active':'chat-list-item'"
@click="loadChat(chat)">
<el-image :src="chat.icon" class="avatar"/>
<span class="chat-title-input" v-if="chat.edit">
@ -100,11 +100,25 @@
</el-option>
</el-select>
<el-dropdown :hide-on-click="false" trigger="click">
<span class="setting"><i class="iconfont icon-plugin"></i></span>
<template #dropdown>
<el-dropdown-menu class="tools-dropdown">
<el-checkbox-group v-model="toolSelected">
<el-dropdown-item v-for="item in tools" :key="item.id">
<el-checkbox :value="item.id" :label="item.label" @change="changeTool" />
<el-tooltip :content="item.description" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</el-dropdown-item>
</el-checkbox-group>
</el-dropdown-menu>
</template>
</el-dropdown>
<span class="setting" @click="showChatSetting = true">
<el-tooltip class="box-item" effect="dark" content="对话设置">
<i class="iconfont icon-config"></i>
</el-tooltip>
</span>
<i class="iconfont icon-config"></i>
</span>
</div>
<div>
@ -202,7 +216,7 @@
import {nextTick, onMounted, onUnmounted, ref, watch} from 'vue'
import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue";
import {Delete, Edit, More, Plus, Promotion, Search, Share, VideoPause} from '@element-plus/icons-vue'
import {Delete, Edit, InfoFilled, More, Plus, Promotion, Search, Share, VideoPause} from '@element-plus/icons-vue'
import 'highlight.js/styles/a11y-dark.css'
import {
isMobile,
@ -211,7 +225,7 @@ import {
UUID
} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus";
import {getSessionId, getUserToken, removeUserToken} from "@/store/session";
import {getSessionId, getUserToken} from "@/store/session";
import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router";
import Clipboard from "clipboard";
@ -230,15 +244,15 @@ const modelID = ref(0)
const chatData = ref([]);
const allChats = ref([]); //
const chatList = ref(allChats.value);
const activeChat = ref({});
const mainWinHeight = ref(0); //
const chatBoxHeight = ref(0); //
const leftBoxHeight = ref(0);
const loading = ref(true);
const loading = ref(false);
const loginUser = ref(null);
const roles = ref([]);
const router = useRouter();
const roleId = ref(0)
const chatId = ref();
const newChatItem = ref(null);
const isLogin = ref(false)
const showHello = ref(true)
@ -254,7 +268,15 @@ const listStyle = ref(store.chatListStyle)
watch(() => store.chatListStyle, (newValue) => {
listStyle.value = newValue
});
const tools = ref([])
const toolSelected = ref([])
const loadHistory = ref(false)
// ChatID
chatId.value = router.currentRoute.value.params.id
if (!chatId.value) {
chatId.value = UUID()
}
if (isMobile()) {
router.replace("/mobile/chat")
@ -290,6 +312,13 @@ httpGet("/api/config/get?key=notice").then(res => {
ElMessage.error("获取系统配置失败:" + e.message)
})
//
httpGet("/api/function/list").then(res => {
tools.value = res.data
}).catch(e => {
showMessageError("获取工具函数失败:" + e.message)
})
onMounted(() => {
resizeElement();
initData()
@ -351,7 +380,6 @@ const initData = () => {
ElMessage.error("加载会话列表失败!")
})
}).catch(() => {
loading.value = false
//
httpGet('/api/model/list',{id:roleId.value}).then(res => {
models.value = res.data
@ -418,6 +446,7 @@ const resizeElement = function () {
const _newChat = () => {
if (isLogin.value) {
chatId.value = UUID()
newChat()
}
}
@ -428,6 +457,7 @@ const newChat = () => {
store.setShowLoginDialog(true)
return;
}
const role = getRoleById(roleId.value)
showHello.value = role.key === 'gpt';
// if the role bind a model, disable model change
@ -457,9 +487,19 @@ const newChat = () => {
edit: false,
removing: false,
};
activeChat.value = {} //
showStopGenerate.value = false;
connect(null, roleId.value)
router.push(`/chat/${chatId.value}`)
loadHistory.value = true
connect()
}
//
const changeTool = () => {
if (!isLogin.value) {
return;
}
loadHistory.value = false
socket.value.close()
}
@ -470,16 +510,18 @@ const loadChat = function (chat) {
return;
}
if (activeChat.value['chat_id'] === chat.chat_id) {
if (chatId.value === chat.chat_id) {
return;
}
activeChat.value = chat
newChatItem.value = null;
roleId.value = chat.role_id;
modelID.value = chat.model_id;
chatId.value = chat.chat_id;
showStopGenerate.value = false;
connect(chat.chat_id, chat.role_id)
router.push(`/chat/${chatId.value}`)
loadHistory.value = true
socket.value.close()
}
//
@ -487,7 +529,6 @@ const tmpChatTitle = ref('');
const editChatTitle = (chat) => {
chat.edit = true;
tmpChatTitle.value = chat.title;
console.log(chat.chat_id)
nextTick(() => {
document.getElementById('chat-' + chat.chat_id).focus()
})
@ -542,7 +583,7 @@ const removeChat = function (chat) {
return e1.id === e2.id
})
//
newChat();
_newChat();
}).catch(e => {
ElMessage.error("操作失败:" + e.message);
})
@ -557,23 +598,10 @@ const prompt = ref('');
const showStopGenerate = ref(false); //
const lineBuffer = ref(''); //
const socket = ref(null);
const activelyClose = ref(false); //
const canSend = ref(true);
const heartbeatHandle = ref(null)
const sessionId = ref("")
const connect = function (chat_id, role_id) {
let isNewChat = false;
if (!chat_id) {
isNewChat = true;
chat_id = UUID();
}
//
if (socket.value !== null) {
activelyClose.value = true;
socket.value.close();
}
const _role = getRoleById(role_id);
const connect = function () {
const chatRole = getRoleById(roleId.value);
// WebSocket
sessionId.value = getSessionId();
let host = process.env.VUE_APP_WS_HOST
@ -585,26 +613,15 @@ const connect = function (chat_id, role_id) {
}
}
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
loading.value = true
const toolIds = toolSelected.value.join(',')
const _socket = new WebSocket(host + `/api/chat/new?session_id=${sessionId.value}&role_id=${roleId.value}&chat_id=${chatId.value}&model_id=${modelID.value}&token=${getUserToken()}&tools=${toolIds}`);
_socket.addEventListener('open', () => {
chatData.value = []; //
enableInput()
activelyClose.value = false;
if (isNewChat) { //
loading.value = false;
chatData.value.push({
chat_id: chat_id,
role_id: role_id,
type: "reply",
id: randString(32),
icon: _role['icon'],
content: _role['hello_msg'],
})
ElMessage.success({message: "对话连接成功!", duration: 1000})
} else { //
loadChatHistory(chat_id);
if (loadHistory.value) {
loadChatHistory(chatId.value)
}
loading.value = false
});
_socket.addEventListener('message', event => {
@ -619,17 +636,16 @@ const connect = function (chat_id, role_id) {
chatData.value.push({
type: "reply",
id: randString(32),
icon: _role['icon'],
icon: chatRole['icon'],
prompt:prePrompt,
content: "",
});
} else if (data.type === 'end') { //
//
if (isNewChat && newChatItem.value !== null) {
if (newChatItem.value !== null) {
newChatItem.value['title'] = tmpChatTitle.value;
newChatItem.value['chat_id'] = chat_id;
newChatItem.value['chat_id'] = chatId.value;
chatList.value.unshift(newChatItem.value);
activeChat.value = newChatItem.value;
newChatItem.value = null; //
}
@ -641,7 +657,7 @@ const connect = function (chat_id, role_id) {
httpPost("/api/chat/tokens", {
text: "",
model: getModelValue(modelID.value),
chat_id: chat_id
chat_id: chatId.value,
}).then(res => {
reply['created_at'] = new Date().getTime();
reply['tokens'] = res.data;
@ -662,7 +678,7 @@ const connect = function (chat_id, role_id) {
//
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
localStorage.setItem("chat_id", chat_id)
localStorage.setItem("chat_id", chatId.value)
})
};
}
@ -673,18 +689,8 @@ const connect = function (chat_id, role_id) {
});
_socket.addEventListener('close', () => {
if (activelyClose.value || socket.value === null) { //
return;
}
//
disableInput(true)
loading.value = true;
checkSession().then(() => {
connect(chat_id, role_id)
}).catch(() => {
loading.value = true
showMessageError("会话已断开,刷新页面...")
});
connect()
});
socket.value = _socket;
@ -801,21 +807,20 @@ const clearAllChats = function () {
})
}
const logout = function () {
activelyClose.value = true;
httpGet('/api/user/logout').then(() => {
removeUserToken()
router.push("/login")
}).catch(() => {
ElMessage.error('注销失败!');
})
}
const loadChatHistory = function (chatId) {
chatData.value = []
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
const data = res.data
if (!data) {
loading.value = false
if (!data || data.length === 0) { //
const _role = getRoleById(roleId.value)
chatData.value.push({
chat_id: chatId,
role_id: roleId.value,
type: "reply",
id: randString(32),
icon: _role['icon'],
content: _role['hello_msg'],
})
return
}
showHello.value = false
@ -829,7 +834,6 @@ const loadChatHistory = function (chatId) {
nextTick(() => {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
})
loading.value = false
}).catch(e => {
// TODO:
ElMessage.error('加载聊天记录失败:' + e.message);
@ -882,7 +886,6 @@ const shareChat = (chat) => {
}
const url = location.protocol + '//' + location.host + '/chat/export?chat_id=' + chat.chat_id
// console.log(url)
window.open(url, '_blank');
}

View File

@ -206,7 +206,7 @@
import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {Delete, InfoFilled, Picture} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import {ElMessage, ElMessageBox} from "element-plus";
import Clipboard from "clipboard";
import {checkSession, getSystemInfo} from "@/store/cache";
import {useSharedStore} from "@/store/sharedata";
@ -338,7 +338,7 @@ const fetchRunningJobs = () => {
}
//
httpGet(`/api/dall/jobs?finish=false`).then(res => {
runningJobs.value = res.data
runningJobs.value = res.data.items
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
@ -356,10 +356,10 @@ const fetchFinishJobs = () => {
page.value = page.value + 1
httpGet(`/api/dall/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
if (res.data.items.length < pageSize.value) {
isOver.value = true
}
const imageList = res.data
const imageList = res.data.items
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"
}

View File

@ -816,7 +816,7 @@ const fetchRunningJobs = () => {
}
httpGet(`/api/mj/jobs?finish=false`).then(res => {
const jobs = res.data
const jobs = res.data.items
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === 101) {
@ -853,7 +853,7 @@ const fetchFinishJobs = () => {
page.value = page.value + 1
//
httpGet(`/api/mj/jobs?finish=true&page=${page.value}&page_size=${pageSize.value}`).then(res => {
const jobs = res.data
const jobs = res.data.items
for (let i = 0; i < jobs.length; i++) {
if (jobs[i]['img_url'] !== "") {
if (jobs[i].type === 'upscale' || jobs[i].type === 'swapFace') {

View File

@ -549,7 +549,6 @@ const sdPower = ref(0) // 画一张 SD 图片消耗算力
const socket = ref(null)
const userId = ref(0)
const heartbeatHandle = ref(null)
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
@ -637,7 +636,7 @@ const fetchRunningJobs = () => {
//
httpGet(`/api/sd/jobs?finish=0`).then(res => {
runningJobs.value = res.data
runningJobs.value = res.data.items
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
})
@ -655,10 +654,10 @@ const fetchFinishJobs = () => {
page.value = page.value + 1
httpGet(`/api/sd/jobs?finish=1&page=${page.value}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
if (res.data.items.length < pageSize.value) {
isOver.value = true
}
const imageList = res.data
const imageList = res.data.items
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"
}

View File

@ -355,13 +355,13 @@ const getNext = () => {
}
httpGet(`${url}?page=${page.value}&page_size=${pageSize.value}`).then(res => {
loading.value = false
if (!res.data || res.data.length === 0) {
if (!res.data.items || res.data.items.length === 0) {
isOver.value = true
return
}
//
const imageList = res.data
const imageList = res.data.items
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"
}

View File

@ -7,7 +7,7 @@
:ellipsis="false"
>
<div class="menu-item">
<el-image :src="logo" alt="Geek-AI"/>
<el-image :src="logo" class="logo" alt="Geek-AI"/>
<div class="title" :style="{color:theme.textColor}">{{ title }}</div>
</div>
<div class="menu-item">

View File

@ -42,7 +42,7 @@ import {setUserToken} from "@/store/session";
import Clipboard from "clipboard";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import {getRoute} from "@/store/system";
import {checkSession, removeUserInfo} from "@/store/cache";
import {checkSession} from "@/store/cache";
const winHeight = ref(window.innerHeight)
const loading = ref(true)
@ -68,7 +68,6 @@ if (code === "") {
const doLogin = (userId) => {
//
httpGet("/api/user/clogin/callback",{login_type: "wx",code: code, action:action, user_id: userId}).then(res => {
removeUserInfo()
if (res.data.token) {
setUserToken(res.data.token)
}

View File

@ -340,7 +340,6 @@ const doSubmitRegister = (verifyData) => {
.el-image {
width 120px;
cursor pointer
background-color #ffffff
border-radius 50%
}
}

View File

@ -145,7 +145,6 @@ const doLogin = function (verifyData) {
.el-image {
width 120px;
cursor pointer
background-color #ffffff
border-radius 50%
}
}

View File

@ -302,6 +302,9 @@
<el-form-item label="Suno 算力" prop="suno_power">
<el-input v-model.number="system['suno_power']" placeholder="使用 Suno 生成一首音乐消耗算力"/>
</el-form-item>
<el-form-item label="Luma 算力" prop="luma_power">
<el-input v-model.number="system['luma_power']" placeholder="使用 Luma 生成一段视频消耗算力"/>
</el-form-item>
</el-tab-pane>
</el-tabs>

View File

@ -11,7 +11,8 @@
<el-table :data="users.items" border class="table" :row-key="row => row.id"
@selection-change="handleSelectionChange" table-layout="auto">
<el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="mobile" label="账号">
<el-table-column prop="id" label="ID"/>
<el-table-column label="账号">
<template #default="scope">
<span>{{ scope.row.username }}</span>
<el-image v-if="scope.row.vip" :src="vipImg" style="height: 20px;position: relative; top:5px; left: 5px"/>

View File

@ -225,10 +225,7 @@ const newChat = (item) => {
}
showPicker.value = false
const options = item.selectedOptions
router.push({
name: "mobile-chat-session",
params: {role_id: options[0].value, model_id: options[1].value, title: '新建会话', chat_id: 0}
})
router.push(`/mobile/chat/session?title=新对话&role_id=${options[0].value}&model_id=${options[1].value}&chat_id=0}`)
}
const changeChat = (chat) => {

View File

@ -102,6 +102,7 @@
<van-popup v-model:show="showPicker" position="bottom" class="popup">
<van-picker
:columns="columns"
v-model="selectedValues"
title="选择模型和角色"
@cancel="showPicker = false"
@confirm="newChat"
@ -153,6 +154,7 @@ const loginUser = ref(null)
// const showMic = ref(false)
const showPicker = ref(false)
const columns = ref([roles.value, models.value])
const selectedValues = ref([roleId.value, modelId.value])
checkSession().then(user => {
loginUser.value = user

View File

@ -1,7 +1,7 @@
<template>
<div class="mobile-user-profile container">
<div class="content">
<van-form>
<van-form v-if="isLogin">
<div class="avatar">
<van-image :src="fileList[0].url" size="80" width="80" fit="cover" round />
<!-- <van-uploader v-model="fileList"-->

View File

@ -317,7 +317,7 @@ const initData = () => {
const fetchRunningJobs = () => {
//
httpGet(`/api/dall/jobs?finish=0`).then(res => {
const jobs = res.data
const jobs = res.data.items
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
@ -345,13 +345,14 @@ const pageSize = ref(10)
const fetchFinishJobs = (page) => {
loading.value = true
httpGet(`/api/dall/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
const jobs = res.data.items
if (jobs.length < pageSize.value) {
finished.value = true
}
if (page === 1) {
finishedJobs.value = res.data
finishedJobs.value = jobs
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
finishedJobs.value = finishedJobs.value.concat(jobs)
}
loading.value = false
}).catch(e => {

View File

@ -430,7 +430,7 @@ const connect = () => {
//
const fetchRunningJobs = (userId) => {
httpGet(`/api/mj/jobs?finish=0&user_id=${userId}`).then(res => {
const jobs = res.data
const jobs = res.data.items
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
@ -462,7 +462,7 @@ const fetchFinishJobs = (page) => {
loading.value = true
//
httpGet(`/api/mj/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
const jobs = res.data
const jobs = res.data.items
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === 101) {
showNotify({

View File

@ -382,7 +382,7 @@ const initData = () => {
const fetchRunningJobs = () => {
//
httpGet(`/api/sd/jobs?finish=0`).then(res => {
const jobs = res.data
const jobs = res.data.items
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
@ -410,13 +410,14 @@ const pageSize = ref(10)
const fetchFinishJobs = (page) => {
loading.value = true
httpGet(`/api/sd/jobs?finish=1&page=${page}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
const jobs = res.data.items
if (jobs.length < pageSize.value) {
finished.value = true
}
if (page === 1) {
finishedJobs.value = res.data
finishedJobs.value = jobs
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
finishedJobs.value = finishedJobs.value.concat(jobs)
}
loading.value = false
}).catch(e => {

View File

@ -134,13 +134,13 @@ const onLoad = () => {
const d = data.value[activeName.value]
httpGet(`${d.url}?status=1&page=${d.page}&page_size=${d.pageSize}&publish=true`).then(res => {
d.loading = false
if (res.data.length === 0) {
if (res.data.items.length === 0) {
d.finished = true
return
}
//
const imageList = res.data
const imageList = res.data.items
for (let i = 0; i < imageList.length; i++) {
imageList[i]["img_thumb"] = imageList[i]["img_url"] + "?imageView2/4/w/300/h/0/q/75"
}