fix conflicts

This commit is contained in:
RockYang 2024-03-12 18:03:24 +08:00
commit 72f80a96bc
239 changed files with 602 additions and 452 deletions

View File

@ -2,6 +2,9 @@
## v3.2.8 ## v3.2.8
* 功能优化SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽 * 功能优化SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
* 功能优化:移动端聊天页面图片支持预览和放大功能 * 功能优化:移动端聊天页面图片支持预览和放大功能
* 功能优化MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题
* 功能优化:手机端 MJ 增加提示词翻译功能
* 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能。
* 功能新增:移动端支持充值 * 功能新增:移动端支持充值
## v3.2.7 ## v3.2.7

View File

@ -28,7 +28,7 @@ type AppServer struct {
Debug bool Debug bool
Config *types.AppConfig Config *types.AppConfig
Engine *gin.Engine Engine *gin.Engine
ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
ChatConfig *types.ChatConfig // chat config cache ChatConfig *types.ChatConfig // chat config cache
SysConfig *types.SystemConfig // system config cache SysConfig *types.SystemConfig // system config cache
@ -47,7 +47,7 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
Debug: false, Debug: false,
Config: appConfig, Config: appConfig,
Engine: gin.Default(), Engine: gin.Default(),
ChatContexts: types.NewLMap[string, []interface{}](), ChatContexts: types.NewLMap[string, []types.Message](),
ChatSession: types.NewLMap[string, *types.ChatSession](), ChatSession: types.NewLMap[string, *types.ChatSession](),
ChatClients: types.NewLMap[string, *types.WsClient](), ChatClients: types.NewLMap[string, *types.WsClient](),
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),

View File

@ -57,7 +57,7 @@ type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform Platform `json:"platform"` Platform Platform `json:"platform"`
Value string `json:"value"` Value string `json:"value"`
Weight int `json:"weight"` Power int `json:"power"`
} }
type ApiError struct { type ApiError struct {
@ -92,3 +92,21 @@ func GetModelMaxToken(model string) int {
} }
return 4096 return 4096
} }
// PowerType 算力日志类型
type PowerType int
const (
PowerRecharge = PowerType(1) // 充值
PowerConsume = PowerType(2) // 消费
PowerRefund = PowerType(3) // 任务SD,MJ执行失败退款
PowerInvite = PowerType(4) // 邀请奖励
PowerReward = PowerType(5) // 众筹
)
type PowerMark int
const (
PowerSub = PowerMark(0)
PowerAdd = PowerMark(1)
)

View File

@ -158,37 +158,32 @@ type UserChatConfig struct {
ApiKeys map[Platform]string `json:"api_keys"` ApiKeys map[Platform]string `json:"api_keys"`
} }
type InviteReward struct {
ChatCalls int `json:"chat_calls"`
ImgCalls int `json:"img_calls"`
}
type ModelAPIConfig struct { type ModelAPIConfig struct {
Temperature float32 `json:"temperature"` Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"` MaxTokens int `json:"max_tokens"`
} }
type SystemConfig struct { type SystemConfig struct {
Title string `json:"title"` Title string `json:"title"`
AdminTitle string `json:"admin_title"` AdminTitle string `json:"admin_title"`
InitChatCalls int `json:"init_chat_calls"` // 新用户注册赠送对话次数 InitPower int `json:"init_power"` // 新用户注册赠送算力值
InitImgCalls int `json:"init_img_calls"` // 新用户注册赠送绘图次数
VipMonthCalls int `json:"vip_month_calls"` // VIP 会员每月赠送的对话次数
VipMonthImgCalls int `json:"vip_month_img_calls"` // VIP 会员每月赠送绘图次数
RegisterWays []string `json:"register_ways"` // 注册方式:支持手机,邮箱注册 RegisterWays []string `json:"register_ways"` // 注册方式:支持手机,邮箱注册
EnabledRegister bool `json:"enabled_register"` // 是否开放注册 EnabledRegister bool `json:"enabled_register"` // 是否开放注册
RewardImg string `json:"reward_img"` // 众筹收款二维码地址 RewardImg string `json:"reward_img"` // 众筹收款二维码地址
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能 EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
ChatCallPrice float64 `json:"chat_call_price"` // 对话单次调用费用 PowerPrice float64 `json:"power_price"` // 算力单价
ImgCallPrice float64 `json:"img_call_price"` // 绘图单次调用费用
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间 OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型 DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字 OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字
InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数 InvitePower int `json:"invite_power"` // 邀请新用户赠送算力值
InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数 VipMonthPower int `json:"vip_month_power"` // VIP 会员每月赠送的算力值
MjPower int `json:"mj_power"` // MJ 绘画消耗算力
SdPower int `json:"sd_power"` // SD 绘画消耗算力
DallPower int `json:"dall_power"` // DALLE3 绘图消耗算力
WechatCardURL string `json:"wechat_card_url"` // 微信客服地址 WechatCardURL string `json:"wechat_card_url"` // 微信客服地址
} }

View File

@ -9,7 +9,7 @@ type MKey interface {
string | int | uint string | int | uint
} }
type MValue interface { type MValue interface {
*WsClient | *ChatSession | context.CancelFunc | []interface{} *WsClient | *ChatSession | context.CancelFunc | []Message
} }
type LMap[K MKey, T MValue] struct { type LMap[K MKey, T MValue] struct {
lock sync.RWMutex lock sync.RWMutex

View File

@ -9,10 +9,9 @@ const (
) )
type OrderRemark struct { type OrderRemark struct {
Days int `json:"days"` // 有效期 Days int `json:"days"` // 有效期
Calls int `json:"calls"` // 增加对话次数 Power int `json:"power"` // 增加算力点数
ImgCalls int `json:"img_calls"` // 增加绘图次数 Name string `json:"name"` // 产品名称
Name string `json:"name"` // 产品名称
Price float64 `json:"price"` Price float64 `json:"price"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
} }

View File

@ -48,7 +48,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
Enabled: data.Enabled, Enabled: data.Enabled,
SortNum: data.SortNum, SortNum: data.SortNum,
Open: data.Open, Open: data.Open,
Weight: data.Weight} Power: data.Weight}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0) item.CreatedAt = time.Unix(data.CreatedAt, 0)

View File

@ -32,8 +32,7 @@ func (h *ProductHandler) Save(c *gin.Context) {
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Days int `json:"days"` Days int `json:"days"`
Calls int `json:"calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@ -46,8 +45,7 @@ func (h *ProductHandler) Save(c *gin.Context) {
Price: data.Price, Price: data.Price,
Discount: data.Discount, Discount: data.Discount,
Days: data.Days, Days: data.Days,
Calls: data.Calls, Power: data.Power,
ImgCalls: data.ImgCalls,
Enabled: data.Enabled} Enabled: data.Enabled}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {

View File

@ -66,8 +66,6 @@ func (h *UserHandler) Save(c *gin.Context) {
Id uint `json:"id"` Id uint `json:"id"`
Password string `json:"password"` Password string `json:"password"`
Username string `json:"username"` Username string `json:"username"`
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
ChatRoles []string `json:"chat_roles"` ChatRoles []string `json:"chat_roles"`
ChatModels []string `json:"chat_models"` ChatModels []string `json:"chat_models"`
ExpiredTime string `json:"expired_time"` ExpiredTime string `json:"expired_time"`
@ -86,8 +84,6 @@ func (h *UserHandler) Save(c *gin.Context) {
// 此处需要用 map 更新,用结构体无法更新 0 值 // 此处需要用 map 更新,用结构体无法更新 0 值
res = h.db.Model(&user).Updates(map[string]interface{}{ res = h.db.Model(&user).Updates(map[string]interface{}{
"username": data.Username, "username": data.Username,
"calls": data.Calls,
"img_calls": data.ImgCalls,
"status": data.Status, "status": data.Status,
"vip": data.Vip, "vip": data.Vip,
"chat_roles_json": utils.JsonEncode(data.ChatRoles), "chat_roles_json": utils.JsonEncode(data.ChatRoles),
@ -113,8 +109,6 @@ func (h *UserHandler) Save(c *gin.Context) {
types.ChatGLM: "", types.ChatGLM: "",
}, },
}), }),
Calls: data.Calls,
ImgCalls: data.ImgCalls,
} }
res = h.db.Create(&u) res = h.db.Create(&u)
_ = utils.CopyObject(u, &userVo) _ = utils.CopyObject(u, &userVo)

View File

@ -19,7 +19,7 @@ import (
// 微软 Azure 模型消息发送实现 // 微软 Azure 模型消息发送实现
func (h *ChatHandler) sendAzureMessage( func (h *ChatHandler) sendAzureMessage(
chatCtx []interface{}, chatCtx []types.Message,
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
@ -103,8 +103,6 @@ func (h *ChatHandler) sendAzureMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
@ -145,8 +143,8 @@ func (h *ChatHandler) sendAzureMessage(
} }
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
totalTokens, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens += getTotalTokens(req) replyTokens += getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
@ -155,7 +153,7 @@ func (h *ChatHandler) sendAzureMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: message.Content, Content: message.Content,
Tokens: totalTokens, Tokens: replyTokens,
UseContext: true, UseContext: true,
Model: req.Model, Model: req.Model,
} }
@ -166,8 +164,8 @@ func (h *ChatHandler) sendAzureMessage(
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息 // 更新用户算力
h.incUserTokenFee(userVo.Id, totalTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -36,7 +36,7 @@ type baiduResp struct {
// 百度文心一言消息发送实现 // 百度文心一言消息发送实现
func (h *ChatHandler) sendBaiduMessage( func (h *ChatHandler) sendBaiduMessage(
chatCtx []interface{}, chatCtx []types.Message,
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
@ -128,9 +128,6 @@ func (h *ChatHandler) sendBaiduMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -171,8 +168,8 @@ func (h *ChatHandler) sendBaiduMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -190,8 +187,8 @@ func (h *ChatHandler) sendBaiduMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息 // 更新用户算力
h.incUserTokenFee(userVo.Id, totalTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -111,7 +111,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session.Model = types.ChatModel{ session.Model = types.ChatModel{
Id: chatModel.Id, Id: chatModel.Id,
Value: chatModel.Value, Value: chatModel.Value,
Weight: chatModel.Weight, Power: chatModel.Power,
Platform: types.Platform(chatModel.Platform)} Platform: types.Platform(chatModel.Platform)}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username) logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
var chatRole model.ChatRole var chatRole model.ChatRole
@ -207,13 +207,13 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
return nil return nil
} }
if userVo.Calls < session.Model.Weight { if userVo.Power < session.Model.Power {
utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d已不足以支付当前模型的单次对话需要消耗的对话额度%d", userVo.Calls, session.Model.Weight)) utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d已不足以支付当前模型的单次对话需要消耗的对话额度%d", userVo.Power, session.Model.Power))
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
return nil return nil
} }
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { if userVo.Power <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!") utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
return nil return nil
@ -224,6 +224,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
return nil return nil
} }
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
if promptTokens > types.GetModelMaxToken(session.Model.Value) {
utils.ReplyMessage(ws, "对话内容超出了当前模型允许的最大上下文长度!")
return nil
}
var req = types.ApiRequest{ var req = types.ApiRequest{
Model: session.Model.Value, Model: session.Model.Value,
Stream: true, Stream: true,
@ -252,7 +260,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
} }
var tools = make([]interface{}, 0) var tools = make([]interface{}, 0)
var functions = make([]interface{}, 0)
for _, v := range items { for _, v := range items {
var parameters map[string]interface{} var parameters map[string]interface{}
err = utils.JsonDecode(v.Parameters, &parameters) err = utils.JsonDecode(v.Parameters, &parameters)
@ -270,20 +277,11 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
"required": required, "required": required,
}, },
}) })
functions = append(functions, gin.H{
"name": v.Name,
"description": v.Description,
"parameters": parameters,
"required": required,
})
} }
//if len(tools) > 0 { if len(tools) > 0 {
// req.Tools = tools req.Tools = tools
// req.ToolChoice = "auto" req.ToolChoice = "auto"
//}
if len(functions) > 0 {
req.Functions = functions
} }
case types.XunFei: case types.XunFei:
@ -301,40 +299,19 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
} }
// 加载聊天上下文 // 加载聊天上下文
var chatCtx []interface{} chatCtx := make([]types.Message, 0)
messages := make([]types.Message, 0)
if h.App.ChatConfig.EnableContext { if h.App.ChatConfig.EnableContext {
if h.App.ChatContexts.Has(session.ChatId) { if h.App.ChatContexts.Has(session.ChatId) {
chatCtx = h.App.ChatContexts.Get(session.ChatId) messages = h.App.ChatContexts.Get(session.ChatId)
} else { } else {
// calculate the tokens of current request, to prevent to exceeding the max tokens num _ = utils.JsonDecode(role.Context, &messages)
tokens := req.MaxTokens
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
tokens += tks
// loading the role context
var messages []types.Message
err := utils.JsonDecode(role.Context, &messages)
if err == nil {
for _, v := range messages {
tks, _ := utils.CalcTokens(v.Content, req.Model)
if tokens+tks >= types.GetModelMaxToken(req.Model) {
break
}
tokens += tks
chatCtx = append(chatCtx, v)
}
}
// loading recent chat history as chat context
if chatConfig.ContextDeep > 0 { if chatConfig.ContextDeep > 0 {
var historyMessages []model.ChatMessage var historyMessages []model.ChatMessage
res := h.db.Debug().Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("id desc").Find(&historyMessages) res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
if res.Error == nil { if res.Error == nil {
for i := len(historyMessages) - 1; i >= 0; i-- { for i := len(historyMessages) - 1; i >= 0; i-- {
msg := historyMessages[i] msg := historyMessages[i]
if tokens+msg.Tokens >= types.GetModelMaxToken(session.Model.Value) {
break
}
tokens += msg.Tokens
ms := types.Message{Role: "user", Content: msg.Content} ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.ReplyMsg { if msg.Type == types.ReplyMsg {
ms.Role = "assistant" ms.Role = "assistant"
@ -344,6 +321,29 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
} }
} }
} }
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
// MaxContextLength = Response + Tool + Prompt + Context
tokens := req.MaxTokens // 最大响应长度
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
tokens += tks + promptTokens
for _, v := range messages {
tks, _ := utils.CalcTokens(v.Content, req.Model)
// 上下文 token 超出了模型的最大上下文长度
if tokens+tks >= types.GetModelMaxToken(req.Model) {
break
}
// 上下文的深度超出了模型的最大上下文深度
if len(chatCtx) >= h.App.ChatConfig.ContextDeep {
break
}
tokens += tks
chatCtx = append(chatCtx, v)
}
logger.Debugf("聊天上下文:%+v", chatCtx) logger.Debugf("聊天上下文:%+v", chatCtx)
} }
reqMgs := make([]interface{}, 0) reqMgs := make([]interface{}, 0)
@ -533,23 +533,28 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
return client.Do(request) return client.Do(request)
} }
// 扣减用户的对话次数 // 扣减用户算力
func (h *ChatHandler) subUserCalls(userVo vo.User, session *types.ChatSession) { func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
// 仅当用户没有导入自己的 API KEY 时才进行扣减 power := 1
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { if session.Model.Power > 0 {
num := 1 power = session.Model.Power
if session.Model.Weight > 0 { }
num = session.Model.Weight res := h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("power", gorm.Expr("power - ?", power))
} if res.Error == nil {
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", num)) // 记录算力消费日志
h.db.Debug().Create(&model.PowerLog{
UserId: userVo.Id,
Username: userVo.Username,
Type: types.PowerConsume,
Amount: power,
Mark: types.PowerSub,
Balance: userVo.Power - power,
Model: session.Model.Value,
Remark: fmt.Sprintf("提问长度:%d回复长度%d", promptTokens, replyTokens),
CreatedAt: time.Now(),
})
} }
}
func (h *ChatHandler) incUserTokenFee(userId uint, tokens int) {
h.db.Model(&model.User{}).Where("id = ?", userId).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", tokens))
h.db.Model(&model.User{}).Where("id = ?", userId).
UpdateColumn("tokens", gorm.Expr("tokens + ?", tokens))
} }
// 将AI回复消息中生成的图片链接下载到本地 // 将AI回复消息中生成的图片链接下载到本地

View File

@ -20,7 +20,7 @@ import (
// 清华大学 ChatGML 消息发送实现 // 清华大学 ChatGML 消息发送实现
func (h *ChatHandler) sendChatGLMMessage( func (h *ChatHandler) sendChatGLMMessage(
chatCtx []interface{}, chatCtx []types.Message,
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
@ -107,9 +107,6 @@ func (h *ChatHandler) sendChatGLMMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -150,8 +147,8 @@ func (h *ChatHandler) sendChatGLMMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -169,8 +166,9 @@ func (h *ChatHandler) sendChatGLMMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
h.incUserTokenFee(userVo.Id, totalTokens) // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -20,7 +20,7 @@ import (
// OPenAI 消息发送实现 // OPenAI 消息发送实现
func (h *ChatHandler) sendOpenAiMessage( func (h *ChatHandler) sendOpenAiMessage(
chatCtx []interface{}, chatCtx []types.Message,
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
@ -46,8 +46,10 @@ func (h *ChatHandler) sendOpenAiMessage(
utils.ReplyMessage(ws, ErrorMsg) utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, ErrImg) utils.ReplyMessage(ws, ErrImg)
all, _ := io.ReadAll(response.Body) if response.Body != nil {
logger.Error(string(all)) all, _ := io.ReadAll(response.Body)
logger.Error(string(all))
}
return err return err
} else { } else {
defer response.Body.Close() defer response.Body.Close()
@ -171,9 +173,6 @@ func (h *ChatHandler) sendOpenAiMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -218,16 +217,16 @@ func (h *ChatHandler) sendOpenAiMessage(
} }
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
var totalTokens = 0 var replyTokens = 0
if toolCall { // prompt + 函数名 + 参数 token if toolCall { // prompt + 函数名 + 参数 token
tokens, _ := utils.CalcTokens(function.Name, req.Model) tokens, _ := utils.CalcTokens(function.Name, req.Model)
totalTokens += tokens replyTokens += tokens
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
totalTokens += tokens replyTokens += tokens
} else { } else {
totalTokens, _ = utils.CalcTokens(message.Content, req.Model) replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
} }
totalTokens += getTotalTokens(req) replyTokens += getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
@ -236,7 +235,7 @@ func (h *ChatHandler) sendOpenAiMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: h.extractImgUrl(message.Content), Content: h.extractImgUrl(message.Content),
Tokens: totalTokens, Tokens: replyTokens,
UseContext: useContext, UseContext: useContext,
Model: req.Model, Model: req.Model,
} }
@ -247,8 +246,8 @@ func (h *ChatHandler) sendOpenAiMessage(
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息 // 更新用户算力
h.incUserTokenFee(userVo.Id, totalTokens) h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -31,7 +31,7 @@ type qWenResp struct {
// 通义千问消息发送实现 // 通义千问消息发送实现
func (h *ChatHandler) sendQWenMessage( func (h *ChatHandler) sendQWenMessage(
chatCtx []interface{}, chatCtx []types.Message,
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
@ -128,9 +128,6 @@ func (h *ChatHandler) sendQWenMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -171,8 +168,8 @@ func (h *ChatHandler) sendQWenMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -190,8 +187,9 @@ func (h *ChatHandler) sendQWenMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
h.incUserTokenFee(userVo.Id, totalTokens) // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -58,7 +58,7 @@ var Model2URL = map[string]string{
// 科大讯飞消息发送实现 // 科大讯飞消息发送实现
func (h *ChatHandler) sendXunFeiMessage( func (h *ChatHandler) sendXunFeiMessage(
chatCtx []interface{}, chatCtx []types.Message,
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
@ -166,9 +166,6 @@ func (h *ChatHandler) sendXunFeiMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数
h.subUserCalls(userVo, session)
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
} }
@ -209,8 +206,8 @@ func (h *ChatHandler) sendXunFeiMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
replyToken, _ := utils.CalcTokens(message.Content, req.Model) replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyToken + getTotalTokens(req) totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@ -228,8 +225,9 @@ func (h *ChatHandler) sendXunFeiMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
h.incUserTokenFee(userVo.Id, totalTokens) // 更新用户算力
h.subUserPower(userVo, session, promptToken, replyTokens)
} }
// 保存当前会话 // 保存当前会话

View File

@ -192,7 +192,6 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
} }
logger.Debugf("绘画参数:%+v", params) logger.Debugf("绘画参数:%+v", params)
// check img calls
var user model.User var user model.User
tx := h.db.Where("id = ?", params["user_id"]).First(&user) tx := h.db.Where("id = ?", params["user_id"]).First(&user)
if tx.Error != nil { if tx.Error != nil {
@ -200,8 +199,8 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
return return
} }
if user.ImgCalls <= 0 { if user.Power < h.App.SysConfig.DallPower {
resp.ERROR(c, "当前用户的绘图次数额度不足") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画")
return return
} }
@ -274,8 +273,22 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
} }
content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n![](%s)\n", prompt, imgURL) content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n![](%s)\n", prompt, imgURL)
// update user's img_calls // 更新用户算力
h.db.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) tx = h.db.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("power", gorm.Expr("power - ?", h.App.SysConfig.DallPower))
// 记录算力变化日志
if tx.Error == nil && tx.RowsAffected > 0 {
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: h.App.SysConfig.DallPower,
Balance: user.Power - h.App.SysConfig.DallPower,
Mark: types.PowerSub,
Model: "dall-e-3",
Remark: "",
CreatedAt: time.Now(),
})
}
resp.SUCCESS(c, content) resp.SUCCESS(c, content)
} }

View File

@ -48,8 +48,8 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
return false return false
} }
if user.ImgCalls <= 0 { if user.Power < h.App.SysConfig.MjPower {
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画")
return false return false
} }
@ -160,13 +160,18 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
TaskId: taskId, TaskId: taskId,
Progress: 0, Progress: 0,
Prompt: prompt, Prompt: prompt,
Power: h.App.SysConfig.MjPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
opt := "绘图"
if data.TaskType == types.TaskBlend.String() { if data.TaskType == types.TaskBlend.String() {
data.Prompt = "融图:" + strings.Join(data.ImgArr, ",") job.Prompt = "融图:" + strings.Join(data.ImgArr, ",")
opt = "融图"
} else if data.TaskType == types.TaskSwapFace.String() { } else if data.TaskType == types.TaskSwapFace.String() {
data.Prompt = "换脸:" + strings.Join(data.ImgArr, ",") job.Prompt = "换脸:" + strings.Join(data.ImgArr, ",")
opt = "换脸"
} }
if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 { if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error()) resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return return
@ -187,8 +192,23 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
// update user's img calls // update user's power
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) 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, _ := utils.GetLoginUser(c, h.db)
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(),
})
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -276,6 +296,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
TaskId: taskId, TaskId: taskId,
Progress: 0, Progress: 0,
Prompt: data.Prompt, Prompt: data.Prompt,
Power: h.App.SysConfig.MjPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 { if res := h.db.Create(&job); res.Error != nil || res.RowsAffected == 0 {
@ -300,8 +321,23 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
// update user's img calls // update user's power
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) 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, _ := utils.GetLoginUser(c, h.db)
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(),
})
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -368,13 +404,6 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
continue continue
} }
// 失败的任务直接删除
if job.Progress == -1 {
h.db.Delete(&model.MidJourneyJob{Id: job.Id})
jobs = append(jobs, job)
continue
}
if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" { if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" {
// discord 服务器图片需要使用代理转发图片数据流 // discord 服务器图片需要使用代理转发图片数据流
if strings.HasPrefix(item.OrgURL, "https://cdn.discordapp.com") { if strings.HasPrefix(item.OrgURL, "https://cdn.discordapp.com") {

View File

@ -202,8 +202,7 @@ func (h *PaymentHandler) PayQrcode(c *gin.Context) {
// 创建订单 // 创建订单
remark := types.OrderRemark{ remark := types.OrderRemark{
Days: product.Days, Days: product.Days,
Calls: product.Calls, Power: product.Power,
ImgCalls: product.ImgCalls,
Name: product.Name, Name: product.Name,
Price: product.Price, Price: product.Price,
Discount: product.Discount, Discount: product.Discount,
@ -313,20 +312,16 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
if remark.Days > 0 { // 只延期 VIP不增加调用次数 if remark.Days > 0 { // 只延期 VIP不增加调用次数
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix() user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
} else { // 充值点卡,直接增加次数即可 } else { // 充值点卡,直接增加次数即可
user.Calls += remark.Calls user.Power += remark.Power
user.ImgCalls += remark.ImgCalls
} }
} else { // 非 VIP 用户 } else { // 非 VIP 用户
if remark.Days > 0 { // vip 套餐days > 0, calls == 0 if remark.Days > 0 { // vip 套餐days > 0, power == 0
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix() user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
user.Calls += h.App.SysConfig.VipMonthCalls user.Power += h.App.SysConfig.VipMonthPower
user.ImgCalls += h.App.SysConfig.VipMonthImgCalls
user.Vip = true user.Vip = true
} else { //点卡days == 0, calls > 0 } else { //点卡days == 0, calls > 0
user.Calls += remark.Calls user.Power += remark.Power
user.ImgCalls += remark.ImgCalls
} }
} }

View File

@ -7,11 +7,13 @@ import (
"chatplus/store/vo" "chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"math" "math"
"strings" "strings"
"sync" "sync"
"time"
) )
type RewardHandler struct { type RewardHandler struct {
@ -30,7 +32,6 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
func (h *RewardHandler) Verify(c *gin.Context) { func (h *RewardHandler) Verify(c *gin.Context) {
var data struct { var data struct {
TxId string `json:"tx_id"` TxId string `json:"tx_id"`
Type string `json:"type"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@ -63,16 +64,11 @@ func (h *RewardHandler) Verify(c *gin.Context) {
tx := h.db.Begin() tx := h.db.Begin()
exchange := vo.RewardExchange{} exchange := vo.RewardExchange{}
if data.Type == "chat" { power := math.Ceil(item.Amount / h.App.SysConfig.PowerPrice)
calls := math.Ceil(item.Amount / h.App.SysConfig.ChatCallPrice) exchange.Power = int(power)
exchange.Calls = int(calls) res = tx.Model(&user).UpdateColumn("power", gorm.Expr("power + ?", exchange.Power))
res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
} else if data.Type == "img" {
calls := math.Ceil(item.Amount / h.App.SysConfig.ImgCallPrice)
exchange.ImgCalls = int(calls)
res = h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", calls))
}
if res.Error != nil { if res.Error != nil {
tx.Rollback()
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return
} }
@ -81,13 +77,25 @@ func (h *RewardHandler) Verify(c *gin.Context) {
item.Status = true item.Status = true
item.UserId = user.Id item.UserId = user.Id
item.Exchange = utils.JsonEncode(exchange) item.Exchange = utils.JsonEncode(exchange)
res = h.db.Updates(&item) res = tx.Updates(&item)
if res.Error != nil { if res.Error != nil {
tx.Rollback() tx.Rollback()
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return
} }
// 记录算力充值日志
h.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerReward,
Amount: exchange.Power,
Balance: user.Power + exchange.Power,
Mark: types.PowerAdd,
Model: "",
Remark: fmt.Sprintf("众筹充值算力,金额:%f价格%f", item.Amount, h.App.SysConfig.PowerPrice),
CreatedAt: time.Now(),
})
tx.Commit() tx.Commit()
resp.SUCCESS(c) resp.SUCCESS(c)

View File

@ -72,8 +72,8 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
return false return false
} }
if user.ImgCalls <= 0 { if user.Power < h.App.SysConfig.SdPower {
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画")
return false return false
} }
@ -140,6 +140,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
Params: utils.JsonEncode(params), Params: utils.JsonEncode(params),
Prompt: data.Prompt, Prompt: data.Prompt,
Progress: 0, Progress: 0,
Power: h.App.SysConfig.SdPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
res := h.db.Create(&job) res := h.db.Create(&job)
@ -162,8 +163,23 @@ func (h *SdJobHandler) Image(c *gin.Context) {
_ = client.Send([]byte("Task Updated")) _ = client.Send([]byte("Task Updated"))
} }
// update user's img calls // update user's power
h.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) 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, _ := utils.GetLoginUser(c, h.db)
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(),
})
}
resp.SUCCESS(c) resp.SUCCESS(c)
} }
@ -232,18 +248,7 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
continue continue
} }
if job.Progress == -1 {
h.db.Delete(&model.SdJob{Id: job.Id})
}
if item.Progress < 100 { if item.Progress < 100 {
// 5 分钟还没完成的任务直接删除
if time.Now().Sub(item.CreatedAt) > time.Minute*5 {
h.db.Delete(&item)
// 退回绘图次数
h.db.Model(&model.User{}).Where("id = ?", item.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
continue
}
// 正在运行中任务使用代理访问图片 // 正在运行中任务使用代理访问图片
image, err := utils.DownloadImage(item.ImgURL, "") image, err := utils.DownloadImage(item.ImgURL, "")
if err == nil { if err == nil {

View File

@ -102,8 +102,7 @@ func (h *UserHandler) Register(c *gin.Context) {
types.ChatGLM: "", types.ChatGLM: "",
}, },
}), }),
Calls: h.App.SysConfig.InitChatCalls, Power: h.App.SysConfig.InitPower,
ImgCalls: h.App.SysConfig.InitImgCalls,
} }
res = h.db.Create(&user) res = h.db.Create(&user)
@ -117,11 +116,8 @@ func (h *UserHandler) Register(c *gin.Context) {
if data.InviteCode != "" { if data.InviteCode != "" {
// 增加邀请数量 // 增加邀请数量
h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1)) h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.InviteChatCalls > 0 { if h.App.SysConfig.InvitePower > 0 {
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("calls", gorm.Expr("calls + ?", h.App.SysConfig.InviteChatCalls)) h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower))
}
if h.App.SysConfig.InviteImgCalls > 0 {
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", h.App.SysConfig.InviteImgCalls))
} }
// 添加邀请记录 // 添加邀请记录
@ -130,7 +126,7 @@ func (h *UserHandler) Register(c *gin.Context) {
UserId: user.Id, UserId: user.Id,
Username: user.Username, Username: user.Username,
InviteCode: inviteCode.Code, InviteCode: inviteCode.Code,
Reward: utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}), Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
}) })
} }
@ -254,10 +250,7 @@ type userProfile struct {
Username string `json:"username"` Username string `json:"username"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
ChatConfig types.UserChatConfig `json:"chat_config"` ChatConfig types.UserChatConfig `json:"chat_config"`
Calls int `json:"calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
TotalTokens int64 `json:"total_tokens"`
Tokens int `json:"tokens"`
ExpiredTime int64 `json:"expired_time"` ExpiredTime int64 `json:"expired_time"`
Vip bool `json:"vip"` Vip bool `json:"vip"`
} }

View File

@ -96,12 +96,6 @@ func (s *Service) Run() {
s.db.Updates(&job) s.db.Updates(&job)
// 任务失败,通知前端 // 任务失败,通知前端
s.notifyQueue.RPush(task.UserId) s.notifyQueue.RPush(task.UserId)
// restore img_call quota
if task.Type.String() != types.TaskUpscale.String() {
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
}
// TODO: 任务提交失败,加入队列重试
continue continue
} }
logger.Infof("任务提交成功:%+v", res) logger.Infof("任务提交成功:%+v", res)

View File

@ -191,28 +191,43 @@ func (p *ServicePool) SyncTaskProgress() {
go func() { go func() {
var items []model.MidJourneyJob var items []model.MidJourneyJob
for { for {
res := p.db.Where("progress >= ? AND progress < ?", 0, 100).Find(&items) res := p.db.Where("progress < ?", 100).Find(&items)
if res.Error != nil { if res.Error != nil {
continue continue
} }
for _, v := range items { for _, job := range items {
// 30 分钟还没完成的任务直接删除 // 失败或者 30 分钟还没完成的任务删除并退回算力
if time.Now().Sub(v.CreatedAt) > time.Minute*30 { if time.Now().Sub(job.CreatedAt) > time.Minute*30 || job.Progress == -1 {
p.db.Delete(&v) p.db.Delete(&job)
// 非放大任务,退回绘图次数 // 略过 Upscale 任务
if v.Type != types.TaskUpscale.String() { if job.Type != types.TaskUpscale.String() {
p.db.Model(&model.User{}).Where("id = ?", v.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1)) continue
} }
tx := p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
if tx.Error == nil && tx.RowsAffected > 0 {
var user model.User
p.db.Where("id = ?", job.UserId).First(&user)
p.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power + job.Power,
Mark: types.PowerAdd,
Model: "mid-journey",
Remark: fmt.Sprintf("绘画任务失败退回算力。任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
}
}
if !strings.HasPrefix(job.ChannelId, "mj-service-plus") {
continue continue
} }
if !strings.HasPrefix(v.ChannelId, "mj-service-plus") { if servicePlus := p.getServicePlus(job.ChannelId); servicePlus != nil {
continue _ = servicePlus.Notify(job)
}
if servicePlus := p.getServicePlus(v.ChannelId); servicePlus != nil {
_ = servicePlus.Notify(v)
} }
} }

View File

@ -84,7 +84,7 @@ func (s *Service) Run() {
if err != nil { if err != nil {
logger.Error("绘画任务执行失败:", err.Error()) logger.Error("绘画任务执行失败:", err.Error())
// update the task progress // update the task progress
s.db.Model(&model.MidJourneyJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.MidJourneyJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"progress": -1, "progress": -1,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })

View File

@ -4,7 +4,9 @@ import (
"chatplus/core/types" "chatplus/core/types"
"chatplus/service/oss" "chatplus/service/oss"
"chatplus/store" "chatplus/store"
"chatplus/store/model"
"fmt" "fmt"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"gorm.io/gorm" "gorm.io/gorm"
@ -14,6 +16,7 @@ type ServicePool struct {
services []*Service services []*Service
taskQueue *store.RedisQueue taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue notifyQueue *store.RedisQueue
db *gorm.DB
Clients *types.LMap[uint, *types.WsClient] // UserId => Client Clients *types.LMap[uint, *types.WsClient] // UserId => Client
} }
@ -42,6 +45,7 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
taskQueue: taskQueue, taskQueue: taskQueue,
notifyQueue: notifyQueue, notifyQueue: notifyQueue,
services: services, services: services,
db: db,
Clients: types.NewLMap[uint, *types.WsClient](), Clients: types.NewLMap[uint, *types.WsClient](),
} }
} }
@ -72,6 +76,46 @@ func (p *ServicePool) CheckTaskNotify() {
}() }()
} }
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
func (p *ServicePool) CheckTaskStatus() {
go func() {
for {
var jobs []model.SdJob
res := p.db.Where("progress < ?", 100).Find(&jobs)
if res.Error != nil {
time.Sleep(5 * time.Second)
continue
}
for _, job := range jobs {
// 5 分钟还没完成的任务直接删除
if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
p.db.Delete(&job)
var user model.User
p.db.Where("id = ?", job.UserId).First(&user)
// 退回绘图次数
res = p.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
if res.Error == nil && res.RowsAffected > 0 {
p.db.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerConsume,
Amount: job.Power,
Balance: user.Power + job.Power,
Mark: types.PowerAdd,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败退回算力。任务ID%s", job.TaskId),
CreatedAt: time.Now(),
})
}
continue
}
}
}
}()
}
// HasAvailableService check if it has available mj service in pool // HasAvailableService check if it has available mj service in pool
func (p *ServicePool) HasAvailableService() bool { func (p *ServicePool) HasAvailableService() bool {
return len(p.services) > 0 return len(p.services) > 0

View File

@ -74,8 +74,6 @@ func (s *Service) Run() {
"progress": -1, "progress": -1,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })
// restore img_call quota
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
// release task num // release task num
atomic.AddInt32(&s.handledTaskNum, -1) atomic.AddInt32(&s.handledTaskNum, -1)
// 通知前端,任务失败 // 通知前端,任务失败
@ -307,8 +305,6 @@ func (s *Service) callback(data CBReq) {
"progress": -1, "progress": -1,
"err_msg": data.Message, "err_msg": data.Message,
}) })
// restore img_calls
s.db.Model(&model.User{}).Where("id = ? AND img_calls > 0", data.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", 1))
} }
// 发送更新状态信号 // 发送更新状态信号

View File

@ -39,7 +39,7 @@ func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
func (e *XXLJobExecutor) Run() error { func (e *XXLJobExecutor) Run() error {
e.executor.RegTask("ClearOrders", e.ClearOrders) e.executor.RegTask("ClearOrders", e.ClearOrders)
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls) e.executor.RegTask("ResetVipPower", e.ResetVipPower)
return e.executor.Run() return e.executor.Run()
} }
@ -68,8 +68,8 @@ func (e *XXLJobExecutor) ClearOrders(cxt context.Context, param *xxl.RunReq) (ms
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected) return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
} }
// ResetVipCalls 清理过期的 VIP 会员 // ResetVipPower 清理过期的 VIP 会员
func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (msg string) { func (e *XXLJobExecutor) ResetVipPower(cxt context.Context, param *xxl.RunReq) (msg string) {
logger.Info("开始进行月底账号盘点...") logger.Info("开始进行月底账号盘点...")
var users []model.User var users []model.User
res := e.db.Where("vip = ?", 1).Find(&users) res := e.db.Where("vip = ?", 1).Find(&users)
@ -89,55 +89,27 @@ func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (
return "error with decode system config: " + err.Error() return "error with decode system config: " + err.Error()
} }
// 获取本月月初时间
currentTime := time.Now()
year, month, _ := currentTime.Date()
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, currentTime.Location()).Unix()
for _, u := range users { for _, u := range users {
// 账号到期,直接清零 if u.Power <= 0 {
if u.ExpiredTime <= currentTime.Unix() { u.Power = 0
logger.Info("账号过期:", u.Username)
u.Calls = 0
u.Vip = false
} else {
if u.Calls <= 0 {
u.Calls = 0
}
if u.ImgCalls <= 0 {
u.ImgCalls = 0
}
// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
var orders []model.Order
e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
var calls = 0
var imgCalls = 0
for _, o := range orders {
var remark types.OrderRemark
err = utils.JsonDecode(o.Remark, &remark)
if err != nil {
continue
}
if remark.Days > 0 { // 会员续费
continue
}
calls += remark.Calls
imgCalls += remark.ImgCalls
}
if u.Calls > calls { // 本月套餐没有用完
u.Calls = calls + config.VipMonthCalls
} else {
u.Calls = u.Calls + config.VipMonthCalls
}
if u.ImgCalls > imgCalls { // 本月套餐没有用完
u.ImgCalls = imgCalls + config.VipMonthImgCalls
} else {
u.ImgCalls = u.ImgCalls + config.VipMonthImgCalls
}
logger.Infof("%s 点卡结余:%d", u.Username, calls)
} }
u.Tokens = 0 u.Power += config.VipMonthPower
// update user // update user
e.db.Updates(&u) tx := e.db.Updates(&u)
// 记录算力充值日志
if tx.Error == nil {
e.db.Create(&model.PowerLog{
UserId: u.Id,
Username: u.Username,
Type: types.PowerRecharge,
Amount: config.VipMonthPower,
Mark: types.PowerAdd,
Balance: u.Power + config.VipMonthPower,
Model: "",
Remark: fmt.Sprintf("月底盘点,会员每月赠送算力:%d", config.VipMonthPower),
CreatedAt: time.Now(),
})
}
} }
logger.Info("月底盘点完成!") logger.Info("月底盘点完成!")
return "success" return "success"

View File

@ -7,6 +7,6 @@ type ChatModel struct {
Value string // API Key 的值 Value string // API Key 的值
SortNum int SortNum int
Enabled bool Enabled bool
Weight int // 对话权重,每次对话扣减多少次对话额度 Power int // 每次对话消耗算力
Open bool // 是否开放模型给所有人使用 Open bool // 是否开放模型给所有人使用
} }

View File

@ -10,6 +10,6 @@ type InviteLog struct {
UserId uint UserId uint
Username string Username string
InviteCode string InviteCode string
Reward string `gorm:"column:reward_json"` // 邀请奖励 Remark string
CreatedAt time.Time CreatedAt time.Time
} }

View File

@ -18,6 +18,7 @@ type MidJourneyJob struct {
UseProxy bool // 是否使用反代加载图片 UseProxy bool // 是否使用反代加载图片
Publish bool //是否发布图片到画廊 Publish bool //是否发布图片到画廊
ErrMsg string // 报错信息 ErrMsg string // 报错信息
Power int // 消耗算力
CreatedAt time.Time CreatedAt time.Time
} }

View File

@ -0,0 +1,20 @@
package model
import (
"chatplus/core/types"
"time"
)
// PowerLog 算力消费日志
type PowerLog struct {
Id uint `gorm:"primarykey;column:id"`
UserId uint
Username string
Type types.PowerType
Amount int
Balance int
Model string // 模型
Remark string // 备注
Mark types.PowerMark // 资金类型
CreatedAt time.Time
}

View File

@ -7,8 +7,7 @@ type Product struct {
Price float64 Price float64
Discount float64 Discount float64
Days int Days int
Calls int Power int
ImgCalls int
Enabled bool Enabled bool
Sales int Sales int
SortNum int SortNum int

View File

@ -13,6 +13,7 @@ type SdJob struct {
Params string Params string
Publish bool //是否发布图片到画廊 Publish bool //是否发布图片到画廊
ErrMsg string // 报错信息 ErrMsg string // 报错信息
Power int // 消耗算力
CreatedAt time.Time CreatedAt time.Time
} }

View File

@ -7,9 +7,7 @@ type User struct {
Password string Password string
Avatar string Avatar string
Salt string // 密码盐 Salt string // 密码盐
TotalTokens int64 // 总消耗 tokens Power int // 剩余算力
Calls int // 剩余对话次数
ImgCalls int // 剩余绘图次数
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色 ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型 ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型
@ -18,5 +16,4 @@ type User struct {
LastLoginAt int64 // 最后登录时间 LastLoginAt int64 // 最后登录时间
LastLoginIp string // 最后登录 IP LastLoginIp string // 最后登录 IP
Vip bool // 是否 VIP 会员 Vip bool // 是否 VIP 会员
Tokens int
} }

View File

@ -1,15 +1,11 @@
package vo package vo
import (
"chatplus/core/types"
)
type InviteLog struct { type InviteLog struct {
Id uint `json:"id"` Id uint `json:"id"`
InviterId uint `json:"inviter_id"` InviterId uint `json:"inviter_id"`
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
InviteCode string `json:"invite_code"` InviteCode string `json:"invite_code"`
Reward types.InviteReward `json:"reward"` Remark string `json:"remark"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }

View File

@ -18,5 +18,6 @@ type MidJourneyJob struct {
UseProxy bool `json:"use_proxy"` UseProxy bool `json:"use_proxy"`
Publish bool `json:"publish"` Publish bool `json:"publish"`
ErrMsg string `json:"err_msg"` ErrMsg string `json:"err_msg"`
Power int `json:"power"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }

16
api/store/vo/power_log.go Normal file
View File

@ -0,0 +1,16 @@
package vo
import "chatplus/core/types"
type PowerLog struct {
Id uint `json:"id"`
UserId uint `json:"user_id"`
Username string `json:"username"`
Type types.PowerType `json:"name"`
Amount int `json:"amount"`
Mark types.PowerMark `json:"fund_type"`
Balance int `json:"balance"`
Model string `json:"model"`
Remark string `json:"remark"`
CreatedAt int64 `json:"created_at"`
}

View File

@ -6,8 +6,7 @@ type Product struct {
Price float64 `json:"price"` Price float64 `json:"price"`
Discount float64 `json:"discount"` Discount float64 `json:"discount"`
Days int `json:"days"` Days int `json:"days"`
Calls int `json:"calls"` Power int `json:"power"`
ImgCalls int `json:"img_calls"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Sales int `json:"sales"` Sales int `json:"sales"`
SortNum int `json:"sort_num"` SortNum int `json:"sort_num"`

View File

@ -12,6 +12,5 @@ type Reward struct {
} }
type RewardExchange struct { type RewardExchange struct {
Calls int `json:"calls"` Power int `json:"calls"`
ImgCalls int `json:"img_calls"`
} }

View File

@ -16,5 +16,6 @@ type SdJob struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Publish bool `json:"publish"` Publish bool `json:"publish"`
ErrMsg string `json:"err_msg"` ErrMsg string `json:"err_msg"`
Power int `json:"power"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
} }

View File

@ -7,10 +7,8 @@ type User struct {
Username string `json:"username"` Username string `json:"username"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐 Salt string `json:"salt"` // 密码盐
TotalTokens int64 `json:"total_tokens"` // 总消耗tokens Power int `json:"calls"` // 剩余算力
Calls int `json:"calls"` // 剩余对话次数
ImgCalls int `json:"img_calls"`
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置 ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
ChatRoles []string `json:"chat_roles"` // 聊天角色集合 ChatRoles []string `json:"chat_roles"` // 聊天角色集合
ChatModels []string `json:"chat_models"` // AI模型集合 ChatModels []string `json:"chat_models"` // AI模型集合
@ -19,5 +17,4 @@ type User struct {
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间 LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
Vip bool `json:"vip"` Vip bool `json:"vip"`
Tokens int `json:"token"` // 当月消耗的 fee
} }

View File

@ -1,23 +0,0 @@
-- 删除用户名重复的用户,只保留一条
DELETE FROM chatgpt_users
WHERE username IN (
SELECT username
FROM (
SELECT username
FROM chatgpt_users
GROUP BY username
HAVING COUNT(*) > 1
) AS temp
) AND id NOT IN (
SELECT MIN(id)
FROM (
SELECT id, username
FROM chatgpt_users
GROUP BY id, username
HAVING COUNT(*) > 1
) AS temp
GROUP BY username
);
-- 给 username 字段建立唯一索引
ALTER TABLE `chatgpt_users` ADD UNIQUE(`username`)

View File

@ -0,0 +1,46 @@
-- 删除用户名重复的用户,只保留一条
DELETE FROM chatgpt_users
WHERE username IN (
SELECT username
FROM (
SELECT username
FROM chatgpt_users
GROUP BY username
HAVING COUNT(*) > 1
) AS temp
) AND id NOT IN (
SELECT MIN(id)
FROM (
SELECT id, username
FROM chatgpt_users
GROUP BY id, username
HAVING COUNT(*) > 1
) AS temp
GROUP BY username
);
-- 给 username 字段建立唯一索引
ALTER TABLE `chatgpt_users` ADD UNIQUE(`username`)
-- 当前用户剩余算力
ALTER TABLE `chatgpt_users` CHANGE `calls` `power` INT NOT NULL DEFAULT '0' COMMENT '剩余算力';
ALTER TABLE `chatgpt_users`
DROP `total_tokens`,
DROP `tokens`,
DROP `img_calls`;
ALTER TABLE `chatgpt_chat_models` CHANGE `weight` `power` TINYINT NOT NULL COMMENT '消耗算力点数';
ALTER TABLE `chatgpt_chat_models` ADD `temperature` FLOAT(3,2) NOT NULL DEFAULT '1' COMMENT '模型创意度' AFTER `power`, ADD `max_tokens` INT(11) NOT NULL DEFAULT '1024' COMMENT '最大响应长度' AFTER `temperature`, ADD `max_context` INT(11) NOT NULL DEFAULT '4096' COMMENT '最大上下文长度' AFTER `max_tokens`;
CREATE TABLE `chatgpt_plus`.`chatgpt_power_logs` ( `id` INT(11) NOT NULL AUTO_INCREMENT , `user_id` INT(11) NOT NULL COMMENT '用户ID' , `username` VARCHAR(30) NOT NULL COMMENT '用户名' , `type` TINYINT(1) NOT NULL COMMENT '类型1充值2消费3退费' , `amount` SMALLINT(3) NOT NULL COMMENT '算力花费' , `balance` INT(11) NOT NULL COMMENT '余额' , `model` VARCHAR(30) NOT NULL COMMENT '模型' , `remark` VARCHAR(255) NOT NULL COMMENT '备注' , `created_at` DATETIME NOT NULL COMMENT '创建时间' , PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = '用户算力消费日志';
ALTER TABLE `chatgpt_products` CHANGE `calls` `power` INT(11) NOT NULL DEFAULT '0' COMMENT '增加算力值';
ALTER TABLE `chatgpt_products` DROP `img_calls`;
ALTER TABLE `chatgpt_power_logs` CHANGE `amount` `amount` SMALLINT NOT NULL COMMENT '算力数值';
ALTER TABLE `chatgpt_power_logs` ADD `mark` TINYINT(1) NOT NULL COMMENT '资金类型0支出1收入' AFTER `remark`;
ALTER TABLE `chatgpt_mj_jobs` ADD `power` SMALLINT(5) NOT NULL DEFAULT '0' COMMENT '消耗算力' AFTER `err_msg`;
ALTER TABLE `chatgpt_sd_jobs` ADD `power` SMALLINT(5) NOT NULL DEFAULT '0' COMMENT '消耗算力' AFTER `err_msg`;
ALTER TABLE `chatgpt_invite_logs` CHANGE `reward_json` `remark` VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注';

View File

@ -1,3 +0,0 @@
VITE_PROXY_BASE_URL="/api"
VITE_TARGET_URL="http://172.22.11.2:5678"
VITE_SOCKET_IO_URL="http://172.28.1.3:8899"

View File

@ -0,0 +1,3 @@
VITE_PROXY_BASE_URL="/api"
VITE_TARGET_URL="http://localhost:5678"
VITE_SOCKET_IO_URL="http://localhost:8899"

View File

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 66 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 41 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Some files were not shown because too many files have changed in this diff Show More