merge v4.1.1 and fixed conflicts

This commit is contained in:
RockYang 2024-11-13 18:40:04 +08:00
commit 44240f65d5
110 changed files with 4820 additions and 1977 deletions

View File

@ -1,4 +1,18 @@
# 更新日志 # 更新日志
## v4.1.1
* Bug修复修复 GPT 模型 function call 调用后没有输出的问题
* 功能新增:允许获取 License 授权用户可以自定义版权信息
* 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
* 功能新增MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
* 功能新增:允许在管理后台设置首页显示的导航菜单
* Bug修复修复注册页面先显示关闭注册组件然后再显示注册组件
* 功能新增:增加 Suno 文生歌曲功能
* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
* 功能优化:在所有列表页面增加返回顶部按钮
## v4.1.0 ## v4.1.0
* bug修复修复移动端修改聊天标题不生效的问题 * bug修复修复移动端修改聊天标题不生效的问题
* Bug修复修复用户注册不显示用户名的问题 * Bug修复修复用户注册不显示用户名的问题

View File

@ -18,7 +18,7 @@ TikaHost = "http://tika:9998"
DB = 0 DB = 0
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
ApiURL = "" ApiURL = "https://sapi.geekai.me"
AppId = "" AppId = ""
Token = "" Token = ""

View File

@ -83,7 +83,7 @@ func errorHandler(c *gin.Context) {
if r := recover(); r != nil { if r := recover(); r != nil {
logger.Errorf("Handler Panic: %v", r) logger.Errorf("Handler Panic: %v", r)
debug.PrintStack() debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) c.JSON(http.StatusBadRequest, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort() c.Abort()
} }
}() }()
@ -139,7 +139,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
if tokenString == "" { if tokenString == "" {
if needLogin(c) { if needLogin(c) {
resp.ERROR(c, "You should put Authorization in request headers") resp.NotAuth(c, "You should put Authorization in request headers")
c.Abort() c.Abort()
return return
} else { // 直接放行 } else { // 直接放行
@ -224,6 +224,9 @@ func needLogin(c *gin.Context) bool {
c.Request.URL.Path == "/api/payment/wechat/notify" || c.Request.URL.Path == "/api/payment/wechat/notify" ||
c.Request.URL.Path == "/api/payment/doPay" || c.Request.URL.Path == "/api/payment/doPay" ||
c.Request.URL.Path == "/api/payment/payWays" || c.Request.URL.Path == "/api/payment/payWays" ||
c.Request.URL.Path == "/api/suno/client" ||
c.Request.URL.Path == "/api/suno/Detail" ||
c.Request.URL.Path == "/api/suno/play" ||
strings.HasPrefix(c.Request.URL.Path, "/api/test") || strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") || strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") || strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||

View File

@ -155,45 +155,6 @@ func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port) return fmt.Sprintf("%s:%d", c.Host, c.Port)
} }
type Platform struct {
Name string `json:"name"`
Value string `json:"value"`
ChatURL string `json:"chat_url"`
ImgURL string `json:"img_url"`
}
var OpenAI = Platform{
Name: "OpenAI - GPT",
Value: "OpenAI",
ChatURL: "https://api.chat-plus.net/v1/chat/completions",
ImgURL: "https://api.chat-plus.net/v1/images/generations",
}
var Azure = Platform{
Name: "微软 - Azure",
Value: "Azure",
ChatURL: "https://chat-bot-api.openai.azure.com/openai/deployments/{model}/chat/completions?api-version=2023-05-15",
}
var ChatGLM = Platform{
Name: "智谱 - ChatGLM",
Value: "ChatGLM",
ChatURL: "https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke",
}
var Baidu = Platform{
Name: "百度 - 文心大模型",
Value: "Baidu",
ChatURL: "https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}",
}
var XunFei = Platform{
Name: "讯飞 - 星火大模型",
Value: "XunFei",
ChatURL: "wss://spark-api.xf-yun.com/{version}/chat",
}
var QWen = Platform{
Name: "阿里 - 通义千问",
Value: "QWen",
ChatURL: "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation",
}
type SystemConfig struct { type SystemConfig struct {
Title string `json:"title,omitempty"` // 网站标题 Title string `json:"title,omitempty"` // 网站标题
Slogan string `json:"slogan,omitempty"` // 网站 slogan Slogan string `json:"slogan,omitempty"` // 网站 slogan
@ -219,6 +180,7 @@ type SystemConfig struct {
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力 DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址 WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
@ -228,4 +190,6 @@ type SystemConfig struct {
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词 SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片 IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
Copyright string `json:"copyright"` // 版权信息
} }

View File

@ -78,3 +78,19 @@ type DallTask struct {
Power int `json:"power"` Power int `json:"power"`
} }
type SunoTask struct {
Id uint `json:"id"`
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"`
RefSongId string `json:"ref_song_id"`
Prompt string `json:"prompt"` // 提示词/歌词
Tags string `json:"tags"`
Model string `json:"model"`
Instrumental bool `json:"instrumental"` // 是否纯音乐
ExtendSecs int `json:"extend_secs"` // 延长秒杀
}

View File

@ -55,12 +55,6 @@ func (h *ManagerHandler) Login(c *gin.Context) {
return return
} }
//// add captcha
//if !base64Captcha.DefaultMemStore.Verify(data.CaptchaId, data.Captcha, true) {
// resp.ERROR(c, "验证码错误!")
// return
//}
var manager model.AdminUser var manager model.AdminUser
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager) res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
if res.Error != nil { if res.Error != nil {

View File

@ -31,7 +31,6 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
func (h *ApiKeyHandler) Save(c *gin.Context) { func (h *ApiKeyHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform string `json:"platform"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Value string `json:"value"` Value string `json:"value"`
@ -48,7 +47,6 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
if data.Id > 0 { if data.Id > 0 {
h.DB.Find(&apiKey, data.Id) h.DB.Find(&apiKey, data.Id)
} }
apiKey.Platform = data.Platform
apiKey.Value = data.Value apiKey.Value = data.Value
apiKey.Type = data.Type apiKey.Type = data.Type
apiKey.ApiURL = data.ApiURL apiKey.ApiURL = data.ApiURL

View File

@ -49,28 +49,32 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
return return
} }
item := model.ChatModel{ item := model.ChatModel{}
Platform: data.Platform, // 更新
Name: data.Name, if data.Id > 0 {
Value: data.Value, h.DB.Where("id", data.Id).First(&item)
Enabled: data.Enabled, }
Open: data.Open,
MaxTokens: data.MaxTokens, item.Name = data.Name
MaxContext: data.MaxContext, item.Value = data.Value
Temperature: data.Temperature, item.Enabled = data.Enabled
KeyId: data.KeyId, item.SortNum = data.SortNum
Power: data.Power} item.Open = data.Open
item.Power = data.Power
item.MaxTokens = data.MaxTokens
item.MaxContext = data.MaxContext
item.Temperature = data.Temperature
item.KeyId = data.KeyId
var res *gorm.DB var res *gorm.DB
if data.Id > 0 { if data.Id > 0 {
item.Id = data.Id res = h.DB.Save(&item)
item.SortNum = data.SortNum
res = h.DB.Select("*").Omit("created_at").Updates(&item)
} else { } else {
res = h.DB.Create(&item) res = h.DB.Create(&item)
} }
if res.Error != nil { if res.Error != nil {
logger.Error("error with update database", res.Error) logger.Error("error with update database", res.Error)
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, res.Error.Error())
return return
} }
@ -89,12 +93,12 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
func (h *ChatModelHandler) List(c *gin.Context) { func (h *ChatModelHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
enable := h.GetBool(c, "enable") enable := h.GetBool(c, "enable")
platform := h.GetTrim(c, "platform") name := h.GetTrim(c, "name")
if enable { if enable {
session = session.Where("enabled", enable) session = session.Where("enabled", enable)
} }
if platform != "" { if name != "" {
session = session.Where("platform", platform) session = session.Where("name LIKE ?", name+"%")
} }
var items []model.ChatModel var items []model.ChatModel
var cms = make([]vo.ChatModel, 0) var cms = make([]vo.ChatModel, 0)

View File

@ -50,6 +50,7 @@ func (h *ConfigHandler) Update(c *gin.Context) {
Content string `json:"content,omitempty"` Content string `json:"content,omitempty"`
Updated bool `json:"updated,omitempty"` Updated bool `json:"updated,omitempty"`
} `json:"config"` } `json:"config"`
ConfigBak types.SystemConfig `json:"config_bak,omitempty"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@ -57,6 +58,12 @@ func (h *ConfigHandler) Update(c *gin.Context) {
return return
} }
// ONLY authorized user can change the copyright
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy {
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权")
return
}
value := utils.JsonEncode(&data.Config) value := utils.JsonEncode(&data.Config)
config := model.Config{Key: data.Key, Config: value} config := model.Config{Key: data.Key, Config: value}
res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key}) res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key})
@ -143,10 +150,9 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) {
// GetAppConfig 获取内置配置 // GetAppConfig 获取内置配置
func (h *ConfigHandler) GetAppConfig(c *gin.Context) { func (h *ConfigHandler) GetAppConfig(c *gin.Context) {
resp.SUCCESS(c, gin.H{ resp.SUCCESS(c, gin.H{
"mj_plus": h.App.Config.MjPlusConfigs, "mj_plus": h.App.Config.MjPlusConfigs,
"mj_proxy": h.App.Config.MjProxyConfigs, "mj_proxy": h.App.Config.MjProxyConfigs,
"sd": h.App.Config.SdConfigs, "sd": h.App.Config.SdConfigs,
"platforms": Platforms,
}) })
} }

View File

@ -1,12 +0,0 @@
package admin
import "geekai/core/types"
var Platforms = []types.Platform{
types.OpenAI,
types.QWen,
types.XunFei,
types.ChatGLM,
types.Baidu,
types.Azure,
}

View File

@ -112,7 +112,7 @@ func (h *UserHandler) Save(c *gin.Context) {
res = h.DB.Select("username", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user) res = h.DB.Select("username", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
if res.Error != nil { if res.Error != nil {
logger.Error("error with update database", res.Error) logger.Error("error with update database", res.Error)
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, res.Error.Error())
return return
} }
// 记录算力日志 // 记录算力日志
@ -136,6 +136,13 @@ func (h *UserHandler) Save(c *gin.Context) {
}) })
} }
} else { } else {
// 检查用户是否已经存在
h.DB.Where("username", data.Username).First(&user)
if user.Id > 0 {
resp.ERROR(c, "用户名已存在")
return
}
salt := utils.RandString(8) salt := utils.RandString(8)
u := model.User{ u := model.User{
Username: data.Username, Username: data.Username,

View File

@ -31,9 +31,14 @@ func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel var items []model.ChatModel
var chatModels = make([]vo.ChatModel, 0) var chatModels = make([]vo.ChatModel, 0)
var res *gorm.DB var res *gorm.DB
session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
t := c.Query("type")
if t != "" {
session = session.Where("type", t)
}
// 如果用户没有登录,则加载所有开放模型 // 如果用户没有登录,则加载所有开放模型
if !h.IsLogin(c) { if !h.IsLogin(c) {
res = h.DB.Where("enabled", true).Where("open", true).Order("sort_num ASC").Find(&items) res = session.Where("open", true).Order("sort_num ASC").Find(&items)
} else { } else {
user, _ := h.GetLoginUser(c) user, _ := h.GetLoginUser(c)
var models []int var models []int

View File

@ -29,45 +29,32 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
// List 获取用户聊天应用列表 // List 获取用户聊天应用列表
func (h *ChatRoleHandler) List(c *gin.Context) { func (h *ChatRoleHandler) List(c *gin.Context) {
all := h.GetBool(c, "all") id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
var roles []model.ChatRole var roles []model.ChatRole
var roleVos = make([]vo.ChatRole, 0) query := h.DB.Where("enable", true)
if userId > 0 {
var user model.User
h.DB.First(&user, userId)
var roleKeys []string
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
if err != nil {
resp.ERROR(c, "角色解析失败!")
return
}
query = query.Where("marker IN ?", roleKeys)
}
if id > 0 {
query = query.Or("id", id)
}
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles) res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
if res.Error != nil { if res.Error != nil {
resp.SUCCESS(c, roleVos) resp.ERROR(c, res.Error.Error())
return
}
// 获取所有角色
if userId == 0 || all {
// 转成 vo
var roleVos = make([]vo.ChatRole, 0)
for _, r := range roles {
var v vo.ChatRole
err := utils.CopyObject(r, &v)
if err == nil {
v.Id = r.Id
roleVos = append(roleVos, v)
}
}
resp.SUCCESS(c, roleVos)
return
}
var user model.User
h.DB.First(&user, userId)
var roleKeys []string
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
if err != nil {
resp.ERROR(c, "角色解析失败!")
return return
} }
var roleVos = make([]vo.ChatRole, 0)
for _, r := range roles { for _, r := range roles {
if !utils.Contains(roleKeys, r.Key) {
continue
}
var v vo.ChatRole var v vo.ChatRole
err := utils.CopyObject(r, &v) err := utils.CopyObject(r, &v)
if err == nil { if err == nil {

View File

@ -1,111 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"io"
"strings"
"time"
)
// 微软 Azure 模型消息发送实现
func (h *ChatHandler) sendAzureMessage(
chatCtx []types.Message,
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt)
} else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
}
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "data:") || len(line) < 30 {
continue
}
var responseBody = types.ApiResponse{}
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错
return errors.New(line)
}
if len(responseBody.Choices) == 0 {
continue
}
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, utils.InterfaceToString(content))
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
})
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
// 消息发送成功
if len(contents) > 0 {
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
}
} else {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求大模型 API 失败:%s", body)
}
return nil
}

View File

@ -1,185 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"io"
"net/http"
"strings"
"time"
)
type baiduResp struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
SentenceId int `json:"sentence_id"`
IsEnd bool `json:"is_end"`
IsTruncated bool `json:"is_truncated"`
Result string `json:"result"`
NeedClearHistory bool `json:"need_clear_history"`
Usage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"`
}
// 百度文心一言消息发送实现
func (h *ChatHandler) sendBaiduMessage(
chatCtx []types.Message,
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
logger.Error(err)
if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt)
} else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
}
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var content string
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
line := scanner.Text()
if len(line) < 5 || strings.HasPrefix(line, "id:") {
continue
}
if strings.HasPrefix(line, "data:") {
content = line[5:]
}
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
var resp baiduResp
err := utils.JsonDecode(content, &resp)
if err != nil {
logger.Error("error with parse data line: ", err)
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
break
}
if len(contents) == 0 {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(resp.Result),
})
contents = append(contents, resp.Result)
if resp.IsTruncated {
utils.ReplyMessage(ws, "AI 输出异常中断")
break
}
if resp.IsEnd {
break
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
// 消息发送成功
if len(contents) > 0 {
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
}
} else {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求大模型 API 失败:%s", body)
}
return nil
}
func (h *ChatHandler) getBaiduToken(apiKey string) (string, error) {
ctx := context.Background()
tokenString, err := h.redis.Get(ctx, apiKey).Result()
if err == nil {
return tokenString, nil
}
expr := time.Hour * 24 * 20 // access_token 有效期
key := strings.Split(apiKey, "|")
if len(key) != 2 {
return "", fmt.Errorf("invalid api key: %s", apiKey)
}
url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials", key[0], key[1])
client := &http.Client{}
req, err := http.NewRequest("POST", url, nil)
if err != nil {
return "", err
}
req.Header.Add("Content-Type", "application/json")
req.Header.Add("Accept", "application/json")
res, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("error with send request: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("error with read response: %w", err)
}
var r map[string]interface{}
err = json.Unmarshal(body, &r)
if err != nil {
return "", fmt.Errorf("error with parse response: %w", err)
}
if r["error"] != nil {
return "", fmt.Errorf("error with api response: %s", r["error_description"])
}
tokenString = fmt.Sprintf("%s", r["access_token"])
h.redis.Set(ctx, apiKey, tokenString, expr)
return tokenString, nil
}

View File

@ -116,8 +116,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
MaxTokens: chatModel.MaxTokens, MaxTokens: chatModel.MaxTokens,
MaxContext: chatModel.MaxContext, MaxContext: chatModel.MaxContext,
Temperature: chatModel.Temperature, Temperature: chatModel.Temperature,
KeyId: chatModel.KeyId, KeyId: chatModel.KeyId}
Platform: chatModel.Platform}
logger.Infof("New websocket connected, IP: %s", c.ClientIP()) logger.Infof("New websocket connected, IP: %s", c.ClientIP())
go func() { go func() {
@ -208,21 +207,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
Model: session.Model.Value, Model: session.Model.Value,
Stream: true, Stream: true,
} }
switch session.Model.Platform { req.Temperature = session.Model.Temperature
case types.Azure.Value, types.ChatGLM.Value, types.Baidu.Value, types.XunFei.Value: req.MaxTokens = session.Model.MaxTokens
req.Temperature = session.Model.Temperature // OpenAI 支持函数功能
req.MaxTokens = session.Model.MaxTokens var items []model.Function
break res = h.DB.Where("enabled", true).Find(&items)
case types.OpenAI.Value: if res.Error == nil {
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 {
break
}
var tools = make([]types.Tool, 0) var tools = make([]types.Tool, 0)
for _, v := range items { for _, v := range items {
var parameters map[string]interface{} var parameters map[string]interface{}
@ -248,15 +238,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
req.Tools = tools req.Tools = tools
req.ToolChoice = "auto" req.ToolChoice = "auto"
} }
case types.QWen.Value:
req.Parameters = map[string]interface{}{
"max_tokens": session.Model.MaxTokens,
"temperature": session.Model.Temperature,
}
break
default:
return fmt.Errorf("不支持的平台:%s", session.Model.Platform)
} }
// 加载聊天上下文 // 加载聊天上下文
@ -344,65 +325,37 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
} }
logger.Debug("最终Prompt", fullPrompt) logger.Debug("最终Prompt", fullPrompt)
if session.Model.Platform == types.QWen.Value { // extract images from prompt
req.Input = make(map[string]interface{}) imgURLs := utils.ExtractImgURLs(prompt)
reqMgs = append(reqMgs, types.Message{ logger.Debugf("detected IMG: %+v", imgURLs)
Role: "user", var content interface{}
Content: fullPrompt, if len(imgURLs) > 0 {
}) data := make([]interface{}, 0)
req.Input["messages"] = reqMgs for _, v := range imgURLs {
} else if session.Model.Platform == types.OpenAI.Value || session.Model.Platform == types.Azure.Value { // extract image for gpt-vision model text = strings.Replace(text, v, "", 1)
imgURLs := utils.ExtractImgURLs(prompt)
logger.Debugf("detected IMG: %+v", imgURLs)
var content interface{}
if len(imgURLs) > 0 {
data := make([]interface{}, 0)
for _, v := range imgURLs {
text = strings.Replace(text, v, "", 1)
data = append(data, gin.H{
"type": "image_url",
"image_url": gin.H{
"url": v,
},
})
}
data = append(data, gin.H{ data = append(data, gin.H{
"type": "text", "type": "image_url",
"text": strings.TrimSpace(text), "image_url": gin.H{
"url": v,
},
}) })
content = data
} else {
content = fullPrompt
} }
req.Messages = append(reqMgs, map[string]interface{}{ data = append(data, gin.H{
"role": "user", "type": "text",
"content": content, "text": strings.TrimSpace(text),
}) })
content = data
} else { } else {
req.Messages = append(reqMgs, map[string]interface{}{ content = fullPrompt
"role": "user",
"content": fullPrompt,
})
} }
req.Messages = append(reqMgs, map[string]interface{}{
"role": "user",
"content": content,
})
logger.Debugf("%+v", req.Messages) logger.Debugf("%+v", req.Messages)
switch session.Model.Platform { return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.Azure.Value:
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.OpenAI.Value:
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.ChatGLM.Value:
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.Baidu.Value:
return h.sendBaiduMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.XunFei.Value:
return h.sendXunFeiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.QWen.Value:
return h.sendQWenMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
}
return nil
} }
// Tokens 统计 token 数量 // Tokens 统计 token 数量
@ -478,55 +431,20 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
} }
// use the last unused key // use the last unused key
if apiKey.Id == 0 { if apiKey.Id == 0 {
h.DB.Where("platform", session.Model.Platform).Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey) h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
} }
if apiKey.Id == 0 { if apiKey.Id == 0 {
return nil, errors.New("no available key, please import key") return nil, errors.New("no available key, please import key")
} }
// ONLY allow apiURL in blank list // ONLY allow apiURL in blank list
if session.Model.Platform == types.OpenAI.Value { err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
err := h.licenseService.IsValidApiURL(apiKey.ApiURL) if err != nil {
if err != nil { return nil, err
return nil, err
}
} }
var apiURL string
switch session.Model.Platform {
case types.Azure.Value:
md := strings.Replace(req.Model, ".", "", 1)
apiURL = strings.Replace(apiKey.ApiURL, "{model}", md, 1)
break
case types.ChatGLM.Value:
apiURL = strings.Replace(apiKey.ApiURL, "{model}", req.Model, 1)
req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
req.Messages = nil
break
case types.Baidu.Value:
apiURL = strings.Replace(apiKey.ApiURL, "{model}", req.Model, 1)
break
case types.QWen.Value:
apiURL = apiKey.ApiURL
req.Messages = nil
break
default:
apiURL = apiKey.ApiURL
}
// 更新 API KEY 的最后使用时间
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
// 百度文心,需要串接 access_token
if session.Model.Platform == types.Baidu.Value {
token, err := h.getBaiduToken(apiKey.Value)
if err != nil {
return nil, err
}
logger.Info("百度文心 Access_Token", token)
apiURL = fmt.Sprintf("%s?access_token=%s", apiURL, token)
}
logger.Debugf(utils.JsonEncode(req)) logger.Debugf(utils.JsonEncode(req))
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
// 创建 HttpClient 请求对象 // 创建 HttpClient 请求对象
var client *http.Client var client *http.Client
requestBody, err := json.Marshal(req) requestBody, err := json.Marshal(req)
@ -550,28 +468,10 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
} else { } else {
client = http.DefaultClient client = http.DefaultClient
} }
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiURL, apiKey.Value, apiKey.ProxyURL, req.Model) logger.Debugf("Sending %s request, Channel:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
switch session.Model.Platform { request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
case types.Azure.Value: // 更新API KEY 最后使用时间
request.Header.Set("api-key", apiKey.Value) h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
break
case types.ChatGLM.Value:
token, err := h.getChatGLMToken(apiKey.Value)
if err != nil {
return nil, err
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
break
case types.Baidu.Value:
request.RequestURI = ""
case types.OpenAI.Value:
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
break
case types.QWen.Value:
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
request.Header.Set("X-DashScope-SSE", "enable")
break
}
return client.Do(request) return client.Do(request)
} }
@ -708,7 +608,7 @@ func (h *ChatHandler) extractImgUrl(text string) string {
continue continue
} }
newImgURL, err := h.uploadManager.GetUploadHandler().PutImg(imageURL, false) newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
if err != nil { if err != nil {
logger.Error("error with download image: ", err) logger.Error("error with download image: ", err)
continue continue

View File

@ -1,142 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"bufio"
"context"
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"github.com/golang-jwt/jwt/v5"
"io"
"strings"
"time"
)
// 清华大学 ChatGML 消息发送实现
func (h *ChatHandler) sendChatGLMMessage(
chatCtx []types.Message,
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt)
} else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
}
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var event, content string
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
line := scanner.Text()
if len(line) < 5 || strings.HasPrefix(line, "id:") {
continue
}
if strings.HasPrefix(line, "event:") {
event = line[6:]
continue
}
if strings.HasPrefix(line, "data:") {
content = line[5:]
}
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
switch event {
case "add":
if len(contents) == 0 {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(content),
})
contents = append(contents, content)
case "finish":
break
case "error":
utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content))
break
case "interrupted":
utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**")
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
// 消息发送成功
if len(contents) > 0 {
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
}
} else {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求大模型 API 失败:%s", body)
}
return nil
}
func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) {
ctx := context.Background()
tokenString, err := h.redis.Get(ctx, apiKey).Result()
if err == nil {
return tokenString, nil
}
expr := time.Hour * 2
key := strings.Split(apiKey, ".")
if len(key) != 2 {
return "", fmt.Errorf("invalid api key: %s", apiKey)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"api_key": key[0],
"timestamp": time.Now().Unix(),
"exp": time.Now().Add(expr).Add(time.Second * 10).Unix(),
})
token.Header["alg"] = "HS256"
token.Header["sign_type"] = "SIGN"
delete(token.Header, "typ")
// Sign and get the complete encoded token as a string using the secret
tokenString, err = token.SignedString([]byte(key[1]))
h.redis.Set(ctx, apiKey, tokenString, expr)
return tokenString, err
}

View File

@ -65,7 +65,6 @@ func (h *ChatHandler) sendOpenAiMessage(
if !strings.Contains(line, "data:") || len(line) < 30 { if !strings.Contains(line, "data:") || len(line) < 30 {
continue continue
} }
var responseBody = types.ApiResponse{} var responseBody = types.ApiResponse{}
err = json.Unmarshal([]byte(line[6:]), &responseBody) err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错 if err != nil { // 数据解析出错
@ -74,7 +73,7 @@ func (h *ChatHandler) sendOpenAiMessage(
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue continue
} }
if responseBody.Choices[0].Delta.Content == nil { if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil {
continue continue
} }

View File

@ -1,150 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"bufio"
"context"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"github.com/syndtr/goleveldb/leveldb/errors"
"io"
"strings"
"time"
)
type qWenResp struct {
Output struct {
FinishReason string `json:"finish_reason"`
Text string `json:"text"`
} `json:"output,omitempty"`
Usage struct {
TotalTokens int `json:"total_tokens"`
InputTokens int `json:"input_tokens"`
OutputTokens int `json:"output_tokens"`
} `json:"usage,omitempty"`
RequestID string `json:"request_id"`
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
}
// 通义千问消息发送实现
func (h *ChatHandler) sendQWenMessage(
chatCtx []types.Message,
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt)
} else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
}
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
scanner := bufio.NewScanner(response.Body)
var content, lastText, newText string
var outPutStart = false
for scanner.Scan() {
line := scanner.Text()
if len(line) < 5 || strings.HasPrefix(line, "id:") ||
strings.HasPrefix(line, "event:") || strings.HasPrefix(line, ":HTTP_STATUS/200") {
continue
}
if !strings.HasPrefix(line, "data:") {
continue
}
content = line[5:]
var resp qWenResp
if len(contents) == 0 { // 发送消息头
if !outPutStart {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
outPutStart = true
continue
} else {
// 处理代码换行
content = "\n"
}
} else {
err := utils.JsonDecode(content, &resp)
if err != nil {
logger.Error("error with parse data line: ", content)
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
break
}
if resp.Message != "" {
utils.ReplyMessage(ws, fmt.Sprintf("**API 返回错误:%s**", resp.Message))
break
}
}
//通过比较 lastText上一次的文本和 currentText当前的文本
//提取出新添加的文本部分。然后只将这部分新文本发送到客户端。
//每次循环结束后lastText 会更新为当前的完整文本,以便于下一次循环进行比较。
currentText := resp.Output.Text
if currentText != lastText {
// 提取新增文本
newText = strings.Replace(currentText, lastText, "", 1)
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(newText),
})
lastText = currentText // 更新 lastText
}
contents = append(contents, newText)
if resp.Output.FinishReason == "stop" {
break
}
} //end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
// 消息发送成功
if len(contents) > 0 {
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
}
} else {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求大模型 API 失败:%s", body)
}
return nil
}

View File

@ -1,255 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"io"
"net/http"
"net/url"
"strings"
"time"
)
type xunFeiResp struct {
Header struct {
Code int `json:"code"`
Message string `json:"message"`
Sid string `json:"sid"`
Status int `json:"status"`
} `json:"header"`
Payload struct {
Choices struct {
Status int `json:"status"`
Seq int `json:"seq"`
Text []struct {
Content string `json:"content"`
Role string `json:"role"`
Index int `json:"index"`
} `json:"text"`
} `json:"choices"`
Usage struct {
Text struct {
QuestionTokens int `json:"question_tokens"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"text"`
} `json:"usage"`
} `json:"payload"`
}
var Model2URL = map[string]string{
"general": "v1.1",
"generalv2": "v2.1",
"generalv3": "v3.1",
"generalv3.5": "v3.5",
}
// 科大讯飞消息发送实现
func (h *ChatHandler) sendXunFeiMessage(
chatCtx []types.Message,
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
var apiKey model.ApiKey
var res *gorm.DB
// use the bind key
if session.Model.KeyId > 0 {
res = h.DB.Where("id", session.Model.KeyId).Find(&apiKey)
}
// use the last unused key
if apiKey.Id == 0 {
res = h.DB.Where("platform", session.Model.Platform).Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(&apiKey)
}
if res.Error != nil {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
}
// 更新 API KEY 的最后使用时间
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
d := websocket.Dialer{
HandshakeTimeout: 5 * time.Second,
}
key := strings.Split(apiKey.Value, "|")
if len(key) != 3 {
utils.ReplyMessage(ws, "非法的 API KEY")
return nil
}
apiURL := strings.Replace(apiKey.ApiURL, "{version}", Model2URL[req.Model], 1)
logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiURL, apiKey.Value, apiKey.ProxyURL, req.Model)
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
//握手并建立websocket 连接
conn, resp, err := d.Dial(wsURL, nil)
if err != nil {
logger.Error(readResp(resp) + err.Error())
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
return nil
} else if resp.StatusCode != 101 {
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
return nil
}
data := buildRequest(key[0], req)
fmt.Printf("%+v", data)
fmt.Println(apiURL)
err = conn.WriteJSON(data)
if err != nil {
utils.ReplyMessage(ws, "发送消息失败:"+err.Error())
return nil
}
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var content string
for {
_, msg, err := conn.ReadMessage()
if err != nil {
logger.Error("error with read message:", err)
utils.ReplyMessage(ws, fmt.Sprintf("**数据读取失败:%s**", err))
break
}
// 解析数据
var result xunFeiResp
err = json.Unmarshal(msg, &result)
if err != nil {
logger.Error("error with parsing JSON:", err)
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
return nil
}
if result.Header.Code != 0 {
utils.ReplyMessage(ws, fmt.Sprintf("**请求 API 返回错误:%s**", result.Header.Message))
return nil
}
content = result.Payload.Choices.Text[0].Content
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
contents = append(contents, content)
// 第一个结果
if result.Payload.Choices.Status == 0 {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(content),
})
if result.Payload.Choices.Status == 2 { // 最终结果
_ = conn.Close() // 关闭连接
break
}
select {
case <-ctx.Done():
utils.ReplyMessage(ws, "**用户取消了生成指令!**")
return nil
default:
continue
}
}
// 消息发送成功
if len(contents) > 0 {
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
}
return nil
}
// 构建 websocket 请求实体
func buildRequest(appid string, req types.ApiRequest) map[string]interface{} {
return map[string]interface{}{
"header": map[string]interface{}{
"app_id": appid,
},
"parameter": map[string]interface{}{
"chat": map[string]interface{}{
"domain": req.Model,
"temperature": req.Temperature,
"top_k": int64(6),
"max_tokens": int64(req.MaxTokens),
"auditing": "default",
},
},
"payload": map[string]interface{}{
"message": map[string]interface{}{
"text": req.Messages,
},
},
}
}
// 创建鉴权 URL
func assembleAuthUrl(hostURL string, apiKey, apiSecret string) (string, error) {
ul, err := url.Parse(hostURL)
if err != nil {
return "", err
}
date := time.Now().UTC().Format(time.RFC1123)
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
//拼接签名字符串
signStr := strings.Join(signString, "\n")
sha := hmacWithSha256(signStr, apiSecret)
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
"hmac-sha256", "host date request-line", sha)
//将请求参数使用base64编码
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
v := url.Values{}
v.Add("host", ul.Host)
v.Add("date", date)
v.Add("authorization", authorization)
//将编码后的字符串url encode后添加到url后面
return hostURL + "?" + v.Encode(), nil
}
// 使用 sha256 签名
func hmacWithSha256(data, key string) string {
mac := hmac.New(sha256.New, []byte(key))
mac.Write([]byte(data))
encodeData := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(encodeData)
}
// 读取响应
func readResp(resp *http.Response) string {
if resp == nil {
return ""
}
b, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
return fmt.Sprintf("code=%d,body=%s", resp.StatusCode, string(b))
}

View File

@ -212,21 +212,21 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
} }
func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) { func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) {
session := h.DB.Session(&gorm.Session{})
// if the chat model bind a KEY, use it directly // if the chat model bind a KEY, use it directly
var res *gorm.DB
if chatModel.KeyId > 0 { if chatModel.KeyId > 0 {
res = h.DB.Where("id", chatModel.KeyId).Find(apiKey) session = session.Where("id", chatModel.KeyId)
} } else { // use the last unused key
// use the last unused key session = session.Where("type", "chat").
if apiKey.Id == 0 { Where("enabled", true).Order("last_used_at ASC")
res = h.DB.Where("platform", types.OpenAI.Value).
Where("type", "chat").
Where("enabled", true).Order("last_used_at ASC").First(apiKey)
} }
res := session.First(apiKey)
if res.Error != nil { if res.Error != nil {
return nil, errors.New("no available key, please import key") return nil, errors.New("no available key, please import key")
} }
apiURL := apiKey.ApiURL apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
// 更新 API KEY 的最后使用时间 // 更新 API KEY 的最后使用时间
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix()) h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())

View File

@ -27,9 +27,15 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
// List 数据列表 // List 数据列表
func (h *MenuHandler) List(c *gin.Context) { func (h *MenuHandler) List(c *gin.Context) {
index := h.GetBool(c, "index")
var items []model.Menu var items []model.Menu
var list = make([]vo.Menu, 0) var list = make([]vo.Menu, 0)
res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items) session := h.DB.Session(&gorm.Session{})
session = session.Where("enabled", true)
if index {
session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
}
res := session.Order("sort_num ASC").Find(&items)
if res.Error == nil { if res.Error == nil {
for _, item := range items { for _, item := range items {
var product vo.Menu var product vo.Menu

View File

@ -406,7 +406,7 @@ func (h *MidJourneyHandler) JobList(c *gin.Context) {
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.MidJourneyJob) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if finish { if finish {
session = session.Where("progress = ?", 100).Order("id DESC") session = session.Where("progress >= ?", 100).Order("id DESC")
} else { } else {
session = session.Where("progress < ?", 100).Order("id ASC") session = session.Where("progress < ?", 100).Order("id ASC")
} }
@ -456,15 +456,44 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
resp.ERROR(c, "记录不存在") resp.ERROR(c, "记录不存在")
return return
} }
// remove job recode // remove job recode
res := h.DB.Delete(&job) tx := h.DB.Begin()
if res.Error != nil { if err := tx.Delete(&job).Error; err != nil {
resp.ERROR(c, res.Error.Error()) tx.Rollback()
resp.ERROR(c, err.Error())
return return
} }
// refund power
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
h.DB.Where("id = ?", job.UserId).First(&user)
err = tx.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(),
}).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
tx.Commit()
// remove image // remove image
err := h.uploader.GetUploadHandler().Delete(job.ImgURL) err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil { if err != nil {
logger.Error("remove image failed: ", err) logger.Error("remove image failed: ", err)
} }

345
api/handler/suno_handler.go Normal file
View File

@ -0,0 +1,345 @@
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/oss"
"geekai/service/suno"
"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"
"time"
)
type SunoHandler struct {
BaseHandler
service *suno.Service
uploader *oss.UploaderManager
}
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager) *SunoHandler {
return &SunoHandler{
BaseHandler: BaseHandler{
App: app,
DB: db,
},
service: service,
uploader: uploader,
}
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *SunoHandler) 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.service.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
func (h *SunoHandler) Create(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
Instrumental bool `json:"instrumental"`
Lyrics string `json:"lyrics"`
Model string `json:"model"`
Tags string `json:"tags"`
Title string `json:"title"`
Type int `json:"type"`
RefTaskId string `json:"ref_task_id"` // 续写的任务id
ExtendSecs int `json:"extend_secs"` // 续写秒数
RefSongId string `json:"ref_song_id"` // 续写的歌曲id
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 插入数据库
job := model.SunoJob{
UserId: int(h.GetLoginUserId(c)),
Prompt: data.Prompt,
Instrumental: data.Instrumental,
ModelName: data.Model,
Tags: data.Tags,
Title: data.Title,
Type: data.Type,
RefSongId: data.RefSongId,
RefTaskId: data.RefTaskId,
ExtendSecs: data.ExtendSecs,
Power: h.App.SysConfig.SunoPower,
}
if data.Lyrics != "" {
job.Prompt = data.Lyrics
}
tx := h.DB.Create(&job)
if tx.Error != nil {
resp.ERROR(c, tx.Error.Error())
return
}
// 创建任务
h.service.PushTask(types.SunoTask{
Id: job.Id,
UserId: job.UserId,
Type: job.Type,
Title: job.Title,
RefTaskId: data.RefTaskId,
RefSongId: data.RefSongId,
ExtendSecs: data.ExtendSecs,
Prompt: job.Prompt,
Tags: data.Tags,
Model: data.Model,
Instrumental: data.Instrumental,
})
// 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(),
})
}
client := h.service.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
resp.SUCCESS(c)
}
func (h *SunoHandler) List(c *gin.Context) {
userId := h.GetLoginUserId(c)
page := h.GetInt(c, "page", 0)
pageSize := h.GetInt(c, "page_size", 0)
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
// 统计总数
var total int64
session.Model(&model.SunoJob{}).Count(&total)
if page > 0 && pageSize > 0 {
offset := (page - 1) * pageSize
session = session.Offset(offset).Limit(pageSize)
}
var list []model.SunoJob
err := session.Order("id desc").Find(&list).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 初始化续写关系
songIds := make([]string, 0)
for _, v := range list {
if v.RefTaskId != "" {
songIds = append(songIds, v.RefSongId)
}
}
var tasks []model.SunoJob
h.DB.Where("song_id IN ?", songIds).Find(&tasks)
songMap := make(map[string]model.SunoJob)
for _, t := range tasks {
songMap[t.SongId] = t
}
// 转换为 VO
items := make([]vo.SunoJob, 0)
for _, v := range list {
var item vo.SunoJob
err = utils.CopyObject(v, &item)
if err != nil {
continue
}
item.CreatedAt = v.CreatedAt.Unix()
if s, ok := songMap[v.RefSongId]; ok {
item.RefSong = map[string]interface{}{
"id": s.Id,
"title": s.Title,
"cover": s.CoverURL,
"audio": s.AudioURL,
}
}
items = append(items, item)
}
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
}
func (h *SunoHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
var job model.SunoJob
err := h.DB.Where("id = ?", id).Where("user_id", userId).First(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 删除任务
h.DB.Delete(&job)
// 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
_ = h.uploader.GetUploadHandler().Delete(job.AudioURL)
}
func (h *SunoHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
publish := h.GetBool(c, "publish")
err := h.DB.Model(&model.SunoJob{}).Where("id", id).Where("user_id", userId).UpdateColumn("publish", publish).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
func (h *SunoHandler) Update(c *gin.Context) {
var data struct {
Id int `json:"id"`
Title string `json:"title"`
Cover string `json:"cover"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Id == 0 || data.Title == "" || data.Cover == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
userId := h.GetLoginUserId(c)
var item model.SunoJob
if err := h.DB.Where("id", data.Id).Where("user_id", userId).First(&item).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
item.Title = data.Title
item.CoverURL = data.Cover
if err := h.DB.Updates(&item).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// Detail 歌曲详情
func (h *SunoHandler) Detail(c *gin.Context) {
songId := c.Query("song_id")
if songId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
var item model.SunoJob
if err := h.DB.Where("song_id", songId).First(&item).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
// 读取用户信息
var user model.User
if err := h.DB.Where("id", item.UserId).First(&user).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
var itemVo vo.SunoJob
if err := utils.CopyObject(item, &itemVo); err != nil {
resp.ERROR(c, err.Error())
return
}
itemVo.CreatedAt = item.CreatedAt.Unix()
itemVo.User = map[string]interface{}{
"nickname": user.Nickname,
"avatar": user.Avatar,
}
resp.SUCCESS(c, itemVo)
}
// Play 增加歌曲播放次数
func (h *SunoHandler) Play(c *gin.Context) {
songId := c.Query("song_id")
if songId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
}
const genLyricTemplate = `
你是一位才华横溢的作曲家拥有丰富的情感和细腻的笔触你对文字有着独特的感悟力能将各种情感和意境巧妙地融入歌词中
请以%s为主题创作一首歌曲歌曲时间不要太短3分钟左右不要输出任何解释性的内容
输出格式如下
歌曲名称
第一节
{{歌词内容}}
副歌
{{歌词内容}}
第二节
{{歌词内容}}
副歌
{{歌词内容}}
尾声
{{歌词内容}}
`
// Lyric 生成歌词
func (h *SunoHandler) Lyric(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(genLyricTemplate, data.Prompt), "gpt-4o-mini")
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, content)
}

View File

@ -23,6 +23,7 @@ import (
"geekai/service/payment" "geekai/service/payment"
"geekai/service/sd" "geekai/service/sd"
"geekai/service/sms" "geekai/service/sms"
"geekai/service/suno"
"geekai/service/wx" "geekai/service/wx"
"geekai/store" "geekai/store"
"io" "io"
@ -209,6 +210,14 @@ func main() {
} }
}), }),
fx.Provide(suno.NewService),
fx.Invoke(func(s *suno.Service) {
s.Run()
s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadImages()
}),
fx.Provide(payment.NewAlipayService), fx.Provide(payment.NewAlipayService),
fx.Provide(payment.NewHuPiPay), fx.Provide(payment.NewHuPiPay),
fx.Provide(payment.NewJPayService), fx.Provide(payment.NewJPayService),
@ -475,6 +484,19 @@ func main() {
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("publish", h.Publish) group.GET("publish", h.Publish)
}), }),
fx.Provide(handler.NewSunoHandler),
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
group := s.Engine.Group("/api/suno")
group.Any("client", h.Client)
group.POST("create", h.Create)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
group.POST("update", h.Update)
group.GET("detail", h.Detail)
group.GET("play", h.Play)
group.POST("lyric", h.Lyric)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) { fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() { go func() {
err := s.Run(db) err := s.Run(db)

View File

@ -110,12 +110,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
prompt := task.Prompt prompt := task.Prompt
// translate prompt // translate prompt
if utils.HasChinese(prompt) { if utils.HasChinese(prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt)) content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini")
if err == nil { if err == nil {
prompt = content prompt = content
logger.Debugf("重写后提示词:%s", prompt) logger.Debugf("重写后提示词:%s", prompt)
} }
} }
var user model.User var user model.User
@ -145,7 +144,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
// get image generation API KEY // get image generation API KEY
var apiKey model.ApiKey var apiKey model.ApiKey
tx = s.db.Where("type", "img"). tx = s.db.Where("type", "dalle").
Where("enabled", true). Where("enabled", true).
Order("last_used_at ASC").First(&apiKey) Order("last_used_at ASC").First(&apiKey)
if tx.Error != nil { if tx.Error != nil {
@ -157,6 +156,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
if len(apiKey.ProxyURL) > 5 { if len(apiKey.ProxyURL) > 5 {
s.httpClient.SetProxyURL(apiKey.ProxyURL).R() s.httpClient.SetProxyURL(apiKey.ProxyURL).R()
} }
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
reqBody := imgReq{ reqBody := imgReq{
Model: "dall-e-3", Model: "dall-e-3",
Prompt: prompt, Prompt: prompt,
@ -165,14 +165,13 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
Style: task.Style, Style: task.Style,
Quality: task.Quality, Quality: task.Quality,
} }
logger.Infof("Sending %s request, ApiURL:%s, API KEY:%s, BODY: %+v", apiKey.Platform, apiKey.ApiURL, apiKey.Value, reqBody) logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
request := s.httpClient.R().SetHeader("Content-Type", "application/json") r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
if apiKey.Platform == types.Azure.Value { SetHeader("Authorization", "Bearer "+apiKey.Value).
request = request.SetHeader("api-key", apiKey.Value) SetBody(reqBody).
} else { SetErrorResult(&errRes).
request = request.SetHeader("Authorization", "Bearer "+apiKey.Value) SetSuccessResult(&res).
} Post(apiURL)
r, err := request.SetBody(reqBody).SetErrorResult(&errRes).SetSuccessResult(&res).Post(apiKey.ApiURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error with send request: %v", err) return "", fmt.Errorf("error with send request: %v", err)
} }
@ -259,7 +258,7 @@ func (s *Service) DownloadImages() {
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) { func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
// sava image // sava image
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false) imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -67,25 +67,7 @@ func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
} }
} }
logger.Info("API URL: ", apiURL) return c.doRequest(body, apiURL)
var res ImageRes
var errRes ErrRes
r, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
SetErrorResult(&errRes).
Post(apiURL)
if err != nil {
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
}
if r.IsErrorState() {
errStr, _ := io.ReadAll(r.Body)
return ImageRes{}, fmt.Errorf("API 返回错误:%s%v", errRes.Error.Message, string(errStr))
}
return res, nil
} }
// Blend 融图 // Blend 融图
@ -112,23 +94,7 @@ func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) {
} }
} }
} }
var res ImageRes return c.doRequest(body, apiURL)
var errRes ErrRes
r, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
SetErrorResult(&errRes).
Post(apiURL)
if err != nil {
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
}
if r.IsErrorState() {
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
}
return res, nil
} }
// SwapFace 换脸 // SwapFace 换脸
@ -165,23 +131,7 @@ func (c *PlusClient) SwapFace(task types.MjTask) (ImageRes, error) {
}, },
"state": "", "state": "",
} }
var res ImageRes return c.doRequest(body, apiURL)
var errRes ErrRes
r, err := c.client.SetTimeout(time.Minute).R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
SetErrorResult(&errRes).
Post(apiURL)
if err != nil {
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
}
if r.IsErrorState() {
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
}
return res, nil
} }
// Upscale 放大指定的图片 // Upscale 放大指定的图片
@ -195,24 +145,7 @@ func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) {
"taskId": task.MessageId, "taskId": task.MessageId,
} }
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode) apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
logger.Info("API URL: ", apiURL) return c.doRequest(body, apiURL)
var res ImageRes
var errRes ErrRes
r, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
SetErrorResult(&errRes).
Post(apiURL)
if err != nil {
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err)
}
if r.IsErrorState() {
return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message)
}
return res, nil
} }
// Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效 // Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
@ -226,9 +159,14 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
"taskId": task.MessageId, "taskId": task.MessageId,
} }
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode) apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode)
logger.Info("API URL: ", apiURL)
return c.doRequest(body, apiURL)
}
func (c *PlusClient) doRequest(body interface{}, apiURL string) (ImageRes, error) {
var res ImageRes var res ImageRes
var errRes ErrRes var errRes ErrRes
logger.Info("API URL: ", apiURL)
r, err := req.C().R(). r, err := req.C().R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey). SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body). SetBody(body).
@ -236,7 +174,13 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
SetErrorResult(&errRes). SetErrorResult(&errRes).
Post(apiURL) Post(apiURL)
if err != nil { if err != nil {
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", err) errMsg := err.Error()
if r != nil {
errStr, _ := io.ReadAll(r.Body)
logger.Error("请求 API 出错:", string(errStr))
errMsg = errMsg + " " + string(errStr)
}
return ImageRes{}, fmt.Errorf("请求 API 出错:%v", errMsg)
} }
if r.IsErrorState() { if r.IsErrorState() {

View File

@ -8,7 +8,6 @@ package mj
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" logger2 "geekai/logger"
"geekai/service" "geekai/service"
@ -139,7 +138,7 @@ func (p *ServicePool) DownloadImages() {
if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") { if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") {
proxy = true proxy = true
} }
imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, proxy) imgURL, err := p.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy)
if err != nil { if err != nil {
logger.Errorf("error with download image %s, %v", v.OrgURL, err) logger.Errorf("error with download image %s, %v", v.OrgURL, err)
@ -188,28 +187,6 @@ func (p *ServicePool) SyncTaskProgress() {
} }
for _, job := range jobs { for _, job := range jobs {
// 失败或者 30 分钟还没完成的任务删除并退回算力
if time.Now().Sub(job.CreatedAt) > time.Minute*30 || job.Progress == -1 {
p.db.Delete(&job)
// 退回算力
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(),
})
}
continue
}
if servicePlus := p.getService(job.ChannelId); servicePlus != nil { if servicePlus := p.getService(job.ChannelId); servicePlus != nil {
_ = servicePlus.Notify(job) _ = servicePlus.Notify(job)
} }

View File

@ -29,6 +29,7 @@ type Service struct {
notifyQueue *store.RedisQueue notifyQueue *store.RedisQueue
db *gorm.DB db *gorm.DB
running bool running bool
retryCount map[uint]int
} }
func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, cli Client) *Service { func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, cli Client) *Service {
@ -39,9 +40,12 @@ func NewService(name string, taskQueue *store.RedisQueue, notifyQueue *store.Red
notifyQueue: notifyQueue, notifyQueue: notifyQueue,
Client: cli, Client: cli,
running: true, running: true,
retryCount: make(map[uint]int),
} }
} }
const failedProgress = 101
func (s *Service) Run() { func (s *Service) Run() {
logger.Infof("Starting MidJourney job consumer for %s", s.Name) logger.Infof("Starting MidJourney job consumer for %s", s.Name)
for s.running { for s.running {
@ -55,15 +59,20 @@ func (s *Service) Run() {
// 如果配置了多个中转平台的 API KEY // 如果配置了多个中转平台的 API KEY
// U,V 操作必须和 Image 操作属于同一个平台,否则找不到关联任务,需重新放回任务列表 // U,V 操作必须和 Image 操作属于同一个平台,否则找不到关联任务,需重新放回任务列表
if task.ChannelId != "" && task.ChannelId != s.Name { if task.ChannelId != "" && task.ChannelId != s.Name {
if s.retryCount[task.Id] > 5 {
s.db.Model(model.MidJourneyJob{Id: task.Id}).Delete(&model.MidJourneyJob{})
continue
}
logger.Debugf("handle other service task, name: %s, channel_id: %s, drop it.", s.Name, task.ChannelId) logger.Debugf("handle other service task, name: %s, channel_id: %s, drop it.", s.Name, task.ChannelId)
s.taskQueue.RPush(task) s.taskQueue.RPush(task)
s.retryCount[task.Id]++
time.Sleep(time.Second) time.Sleep(time.Second)
continue continue
} }
// translate prompt // translate prompt
if utils.HasChinese(task.Prompt) { if utils.HasChinese(task.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt)) content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt), "gpt-4o-mini")
if err == nil { if err == nil {
task.Prompt = content task.Prompt = content
} else { } else {
@ -72,7 +81,7 @@ func (s *Service) Run() {
} }
// translate negative prompt // translate negative prompt
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) { if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt)) content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt), "gpt-4o-mini")
if err == nil { if err == nil {
task.NegPrompt = content task.NegPrompt = content
} else { } else {
@ -116,7 +125,7 @@ func (s *Service) Run() {
} }
logger.Error("绘画任务执行失败:", errMsg) logger.Error("绘画任务执行失败:", errMsg)
job.Progress = -1 job.Progress = failedProgress
job.ErrMsg = errMsg job.ErrMsg = errMsg
// update the task progress // update the task progress
s.db.Updates(&job) s.db.Updates(&job)
@ -164,7 +173,7 @@ func (s *Service) Notify(job model.MidJourneyJob) error {
// 任务执行失败了 // 任务执行失败了
if task.FailReason != "" { if task.FailReason != "" {
s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{
"progress": -1, "progress": failedProgress,
"err_msg": task.FailReason, "err_msg": task.FailReason,
}) })
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed}) s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed})

View File

@ -84,25 +84,25 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) {
}, nil }, nil
} }
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) { func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
var imageData []byte var fileData []byte
var err error var err error
if useProxy { if useProxy {
imageData, err = utils.DownloadImage(imageURL, s.proxyURL) fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else { } else {
imageData, err = utils.DownloadImage(imageURL, "") fileData, err = utils.DownloadImage(fileURL, "")
} }
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)
} }
parse, err := url.Parse(imageURL) parse, err := url.Parse(fileURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err) return "", fmt.Errorf("error with parse image URL: %v", err)
} }
fileExt := utils.GetImgExt(parse.Path) fileExt := utils.GetImgExt(parse.Path)
objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt) objectKey := fmt.Sprintf("%s/%d%s", s.config.SubDir, time.Now().UnixMicro(), fileExt)
// 上传文件字节数据 // 上传文件字节数据
err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData)) err = s.bucket.PutObject(objectKey, bytes.NewReader(fileData))
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -57,8 +57,8 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) {
}, nil }, nil
} }
func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) { func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) {
parse, err := url.Parse(imageURL) parse, err := url.Parse(fileURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err) return "", fmt.Errorf("error with parse image URL: %v", err)
} }
@ -69,9 +69,9 @@ func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
} }
if useProxy { if useProxy {
err = utils.DownloadFile(imageURL, filePath, s.proxyURL) err = utils.DownloadFile(fileURL, filePath, s.proxyURL)
} else { } else {
err = utils.DownloadFile(imageURL, filePath, "") err = utils.DownloadFile(fileURL, filePath, "")
} }
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)

View File

@ -44,18 +44,18 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
} }
func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) { func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
var imageData []byte var fileData []byte
var err error var err error
if useProxy { if useProxy {
imageData, err = utils.DownloadImage(imageURL, s.proxyURL) fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else { } else {
imageData, err = utils.DownloadImage(imageURL, "") fileData, err = utils.DownloadImage(fileURL, "")
} }
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)
} }
parse, err := url.Parse(imageURL) parse, err := url.Parse(fileURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err) return "", fmt.Errorf("error with parse image URL: %v", err)
} }
@ -65,8 +65,8 @@ func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
context.Background(), context.Background(),
s.config.Bucket, s.config.Bucket,
filename, filename,
strings.NewReader(string(imageData)), strings.NewReader(string(fileData)),
int64(len(imageData)), int64(len(fileData)),
minio.PutObjectOptions{ContentType: "image/png"}) minio.PutObjectOptions{ContentType: "image/png"})
if err != nil { if err != nil {
return "", err return "", err

View File

@ -93,18 +93,18 @@ func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (File, error) {
} }
func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) { func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) {
var imageData []byte var fileData []byte
var err error var err error
if useProxy { if useProxy {
imageData, err = utils.DownloadImage(imageURL, s.proxyURL) fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else { } else {
imageData, err = utils.DownloadImage(imageURL, "") fileData, err = utils.DownloadImage(fileURL, "")
} }
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)
} }
parse, err := url.Parse(imageURL) parse, err := url.Parse(fileURL)
if err != nil { if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err) return "", fmt.Errorf("error with parse image URL: %v", err)
} }
@ -113,7 +113,7 @@ func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
ret := storage.PutRet{} ret := storage.PutRet{}
extra := storage.PutExtra{} extra := storage.PutExtra{}
// 上传文件字节数据 // 上传文件字节数据
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra) err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(fileData), int64(len(fileData)), &extra)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -23,7 +23,7 @@ type File struct {
} }
type Uploader interface { type Uploader interface {
PutFile(ctx *gin.Context, name string) (File, error) PutFile(ctx *gin.Context, name string) (File, error)
PutImg(imageURL string, useProxy bool) (string, error) PutUrlFile(url string, useProxy bool) (string, error)
PutBase64(imageData string) (string, error) PutBase64(imageData string) (string, error)
Delete(fileURL string) error Delete(fileURL string) error
} }

View File

@ -63,7 +63,7 @@ func (s *Service) Run() {
// translate prompt // translate prompt
if utils.HasChinese(task.Params.Prompt) { if utils.HasChinese(task.Params.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt)) content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt), "gpt-4o-mini")
if err == nil { if err == nil {
task.Params.Prompt = content task.Params.Prompt = content
} else { } else {
@ -73,7 +73,7 @@ func (s *Service) Run() {
// translate negative prompt // translate negative prompt
if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) { if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt)) content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), "gpt-4o-mini")
if err == nil { if err == nil {
task.Params.NegPrompt = content task.Params.NegPrompt = content
} else { } else {

355
api/service/suno/service.go Normal file
View File

@ -0,0 +1,355 @@
package suno
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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/oss"
"geekai/service/sd"
"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("Suno_Task_Queue", redisCli),
notifyQueue: store.NewRedisQueue("Suno_Notify_Queue", redisCli),
Clients: types.NewLMap[uint, *types.WsClient](),
uploadManager: manager,
}
}
func (s *Service) PushTask(task types.SunoTask) {
logger.Infof("add a new Suno task to the task list: %+v", task)
s.taskQueue.RPush(task)
}
func (s *Service) Run() {
// 将数据库中未提交的人物加载到队列
var jobs []model.SunoJob
s.db.Where("task_id", "").Find(&jobs)
for _, v := range jobs {
s.PushTask(types.SunoTask{
Id: v.Id,
Channel: v.Channel,
UserId: v.UserId,
Type: v.Type,
Title: v.Title,
RefTaskId: v.RefTaskId,
RefSongId: v.RefSongId,
Prompt: v.Prompt,
Tags: v.Tags,
Model: v.ModelName,
Instrumental: v.Instrumental,
ExtendSecs: v.ExtendSecs,
})
}
logger.Info("Starting Suno job consumer...")
go func() {
for {
var task types.SunoTask
err := s.taskQueue.LPop(&task)
if err != nil {
logger.Errorf("taking task with error: %v", err)
continue
}
r, err := s.Create(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": 101,
})
continue
}
// 更新任务信息
s.db.Model(&model.SunoJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"task_id": r.Data,
"channel": r.Channel,
})
}
}()
}
type RespVo struct {
Code string `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
Channel string `json:"channel,omitempty"`
}
func (s *Service) Create(task types.SunoTask) (RespVo, error) {
// 读取 API KEY
var apiKey model.ApiKey
session := s.db.Session(&gorm.Session{}).Where("type", "suno").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 RespVo{}, errors.New("no available API KEY for Suno")
}
reqBody := map[string]interface{}{
"task_id": task.RefTaskId,
"continue_clip_id": task.RefSongId,
"continue_at": task.ExtendSecs,
"make_instrumental": task.Instrumental,
}
// 灵感模式
if task.Type == 1 {
reqBody["gpt_description_prompt"] = task.Prompt
} else { // 自定义模式
reqBody["prompt"] = task.Prompt
reqBody["tags"] = task.Tags
reqBody["mv"] = task.Model
reqBody["title"] = task.Title
}
var res RespVo
apiURL := fmt.Sprintf("%s/task/suno/v1/submit/music", 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 RespVo{}, fmt.Errorf("请求 API 出错:%v", err)
}
body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &res)
if err != nil {
return RespVo{}, fmt.Errorf("解析API数据失败%v, %s", err, string(body))
}
if res.Code != "success" {
return RespVo{}, fmt.Errorf("API 返回失败:%s", res.Message)
}
res.Channel = apiKey.ApiURL
return res, nil
}
func (s *Service) CheckTaskNotify() {
go func() {
logger.Info("Running Suno task notify checking ...")
for {
var message sd.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) DownloadImages() {
go func() {
var items []model.SunoJob
for {
res := s.db.Where("progress", 102).Find(&items)
if res.Error != nil {
continue
}
for _, v := range items {
// 下载图片和音频
logger.Infof("try download cover image: %s", v.CoverURL)
coverURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.CoverURL, true)
if err != nil {
logger.Errorf("download image with error: %v", err)
continue
}
logger.Infof("try download audio: %s", v.AudioURL)
audioURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v.AudioURL, true)
if err != nil {
logger.Errorf("download audio with error: %v", err)
continue
}
v.CoverURL = coverURL
v.AudioURL = audioURL
v.Progress = 100
s.db.Updates(&v)
s.notifyQueue.RPush(sd.NotifyMessage{UserId: v.UserId, JobId: int(v.Id), Message: sd.Finished})
}
time.Sleep(time.Second * 10)
}
}()
}
// SyncTaskProgress 异步拉取任务
func (s *Service) SyncTaskProgress() {
go func() {
var jobs []model.SunoJob
for {
res := s.db.Where("progress < ?", 100).Where("task_id <> ?", "").Find(&jobs)
if res.Error != nil {
continue
}
for _, job := range jobs {
task, err := s.QueryTask(job.TaskId, job.Channel)
if err != nil {
logger.Errorf("query task with error: %v", err)
continue
}
if task.Code != "success" {
logger.Errorf("query task with error: %v", task.Message)
continue
}
logger.Debugf("task: %+v", task.Data.Status)
// 任务完成,删除旧任务插入两条新任务
if task.Data.Status == "SUCCESS" {
var jobId = job.Id
var flag = false
tx := s.db.Begin()
for _, v := range task.Data.Data {
job.Id = 0
job.Progress = 102 // 102 表示资源未下载完成
job.Title = v.Title
job.SongId = v.Id
job.Duration = int(v.Metadata.Duration)
job.Prompt = v.Metadata.Prompt
job.Tags = v.Metadata.Tags
job.ModelName = v.ModelName
job.RawData = utils.JsonEncode(v)
job.CoverURL = v.ImageLargeUrl
job.AudioURL = v.AudioUrl
if err = tx.Create(&job).Error; err != nil {
logger.Error("create job with error: %v", err)
tx.Rollback()
break
}
flag = true
}
// 删除旧任务
if flag {
if err = tx.Delete(&model.SunoJob{}, "id = ?", jobId).Error; err != nil {
logger.Error("create job with error: %v", err)
tx.Rollback()
continue
}
}
tx.Commit()
} else if task.Data.FailReason != "" {
job.Progress = 101
job.ErrMsg = task.Data.FailReason
s.db.Updates(&job)
s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed})
}
}
time.Sleep(time.Second * 10)
}
}()
}
type QueryRespVo struct {
Code string `json:"code"`
Message string `json:"message"`
Data struct {
TaskId string `json:"task_id"`
Action string `json:"action"`
Status string `json:"status"`
FailReason string `json:"fail_reason"`
SubmitTime int `json:"submit_time"`
StartTime int `json:"start_time"`
FinishTime int `json:"finish_time"`
Progress string `json:"progress"`
Data []struct {
Id string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
Metadata struct {
Tags string `json:"tags"`
Type string `json:"type"`
Prompt string `json:"prompt"`
Stream bool `json:"stream"`
Duration float64 `json:"duration"`
ErrorMessage interface{} `json:"error_message"`
} `json:"metadata"`
AudioUrl string `json:"audio_url"`
ImageUrl string `json:"image_url"`
VideoUrl string `json:"video_url"`
ModelName string `json:"model_name"`
DisplayName string `json:"display_name"`
ImageLargeUrl string `json:"image_large_url"`
MajorModelVersion string `json:"major_model_version"`
} `json:"data"`
} `json:"data"`
}
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").
Where("api_url", channel).
Where("enabled", true).
Order("last_used_at DESC").First(&apiKey)
if tx.Error != nil {
return QueryRespVo{}, errors.New("no available API KEY for Suno")
}
apiURL := fmt.Sprintf("%s/task/suno/v1/fetch/%s", apiKey.ApiURL, taskId)
var res QueryRespVo
r, err := req.C().R().SetHeader("Authorization", "Bearer "+apiKey.Value).Get(apiURL)
if err != nil {
return QueryRespVo{}, fmt.Errorf("请求 API 失败:%v", err)
}
defer r.Body.Close()
body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &res)
if err != nil {
return QueryRespVo{}, fmt.Errorf("解析API数据失败%v, %s", err, string(body))
}
return res, nil
}

View File

@ -3,7 +3,6 @@ package model
// ApiKey OpenAI API 模型 // ApiKey OpenAI API 模型
type ApiKey struct { type ApiKey struct {
BaseModel BaseModel
Platform string
Name string Name string
Type string // 用途 chat => 聊天img => 绘图 Type string // 用途 chat => 聊天img => 绘图
Value string // API Key 的值 Value string // API Key 的值

View File

@ -2,7 +2,6 @@ package model
type ChatModel struct { type ChatModel struct {
BaseModel BaseModel
Platform string
Name string Name string
Value string // API Key 的值 Value string // API Key 的值
SortNum int SortNum int

View File

@ -0,0 +1,34 @@
package model
import "time"
type SunoJob struct {
Id uint `gorm:"primarykey;column:id"`
UserId int
Channel string // 频道
Title string
Type int
TaskId string
RefTaskId string // 续写的任务id
Tags string // 歌曲风格和标签
Instrumental bool // 是否生成纯音乐
ExtendSecs int // 续写秒数
SongId string // 续写的歌曲id
RefSongId string
Prompt string // 提示词
CoverURL string // 封面图 URL
AudioURL string // 音频 URL
ModelName string // 模型名称
Progress int // 任务进度
Duration int // 银屏时长,秒
Publish bool // 是否发布
ErrMsg string // 错误信息
RawData string // 原始数据 json
Power int // 消耗算力
PlayTimes int // 播放次数
CreatedAt time.Time
}
func (SunoJob) TableName() string {
return "chatgpt_suno_jobs"
}

View File

@ -3,7 +3,6 @@ package vo
// ApiKey OpenAI API 模型 // ApiKey OpenAI API 模型
type ApiKey struct { type ApiKey struct {
BaseVo BaseVo
Platform string `json:"platform"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Value string `json:"value"` // API Key 的值 Value string `json:"value"` // API Key 的值

View File

@ -2,7 +2,6 @@ package vo
type ChatModel struct { type ChatModel struct {
BaseVo BaseVo
Platform string `json:"platform"`
Name string `json:"name"` Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@ -12,6 +11,6 @@ type ChatModel struct {
MaxTokens int `json:"max_tokens"` // 最大响应长度 MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度 MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度 Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id"` KeyId int `json:"key_id,omitempty"`
KeyName string `json:"key_name"` KeyName string `json:"key_name"`
} }

34
api/store/vo/suno_job.go Normal file
View File

@ -0,0 +1,34 @@
package vo
type SunoJob struct {
Id uint `json:"id"`
UserId int `json:"user_id"`
Channel string `json:"channel"`
Title string `json:"title"`
Type string `json:"type"`
TaskId string `json:"task_id"`
RefTaskId string `json:"ref_task_id"` // 续写的任务id
Tags string `json:"tags"` // 歌曲风格和标签
Instrumental bool `json:"instrumental"` // 是否生成纯音乐
ExtendSecs int `json:"extend_secs"` // 续写秒数
SongId string `json:"song_id"` // 续写的歌曲id
RefSongId string `json:"ref_song_id"` // 续写的歌曲id
Prompt string `json:"prompt"` // 提示词
CoverURL string `json:"cover_url"` // 封面图 URL
AudioURL string `json:"audio_url"` // 音频 URL
ModelName string `json:"model_name"` // 模型名称
Progress int `json:"progress"` // 任务进度
Duration int `json:"duration"` // 银屏时长,秒
Publish bool `json:"publish"` // 是否发布
ErrMsg string `json:"err_msg"` // 错误信息
RawData map[string]interface{} `json:"raw_data"` // 原始数据 json
Power int `json:"power"` // 消耗算力
RefSong map[string]interface{} `json:"ref_song,omitempty"`
User map[string]interface{} `json:"user,omitempty"` //关联用户信息
PlayTimes int `json:"play_times"` // 播放次数
CreatedAt int64 `json:"created_at"`
}
func (SunoJob) TableName() string {
return "chatgpt_suno_jobs"
}

View File

@ -84,6 +84,8 @@ func CopyObject(src interface{}, dst interface{}) error {
case reflect.Bool: case reflect.Bool:
value.SetBool(v.Bool()) value.SetBool(v.Bool())
break break
default:
value.Set(v)
} }
} }

View File

@ -8,12 +8,14 @@ package utils
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"encoding/json"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"geekai/store/model" "geekai/store/model"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
"github.com/pkoukk/tiktoken-go" "github.com/pkoukk/tiktoken-go"
"gorm.io/gorm" "gorm.io/gorm"
"io"
"time" "time"
) )
@ -43,18 +45,9 @@ type apiRes struct {
} `json:"choices"` } `json:"choices"`
} }
type apiErrRes struct { func OpenAIRequest(db *gorm.DB, prompt string, modelName string) (string, error) {
Error struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Param interface{} `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
var apiKey model.ApiKey var apiKey model.ApiKey
res := db.Where("platform", types.OpenAI.Value).Where("type", "chat").Where("enabled", true).First(&apiKey) res := db.Where("type", "chat").Where("enabled", true).First(&apiKey)
if res.Error != nil { if res.Error != nil {
return "", fmt.Errorf("error with fetch OpenAI API KEY%v", res.Error) return "", fmt.Errorf("error with fetch OpenAI API KEY%v", res.Error)
} }
@ -66,24 +59,27 @@ func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
} }
var response apiRes var response apiRes
var errRes apiErrRes
client := req.C() client := req.C()
if len(apiKey.ProxyURL) > 5 { if len(apiKey.ProxyURL) > 5 {
client.SetProxyURL(apiKey.ApiURL) client.SetProxyURL(apiKey.ApiURL)
} }
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
r, err := client.R().SetHeader("Content-Type", "application/json"). r, err := client.R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value). SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{ SetBody(types.ApiRequest{
Model: "gpt-3.5-turbo", Model: modelName,
Temperature: 0.9, Temperature: 0.9,
MaxTokens: 1024, MaxTokens: 1024,
Stream: false, Stream: false,
Messages: messages, Messages: messages,
}). }).Post(apiURL)
SetErrorResult(&errRes). if err != nil {
SetSuccessResult(&response).Post(apiKey.ApiURL) return "", fmt.Errorf("请求 OpenAI API失败%v", err)
if err != nil || r.IsErrorState() { }
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message) body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &response)
if err != nil {
return "", fmt.Errorf("解析API数据失败%v, %s", err, string(body))
} }
// 更新 API KEY 的最后使用时间 // 更新 API KEY 的最后使用时间

View File

@ -0,0 +1,883 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- 主机: 127.0.0.1
-- 生成日期: 2024-07-30 16:14:56
-- 服务器版本: 8.0.33
-- PHP 版本: 8.1.2-1ubuntu2.18
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
--
-- 数据库: `chatgpt_plus`
--
CREATE DATABASE IF NOT EXISTS `chatgpt_plus` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE `chatgpt_plus`;
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_admin_users`
--
DROP TABLE IF EXISTS `chatgpt_admin_users`;
CREATE TABLE `chatgpt_admin_users` (
`id` int NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`salt` char(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码盐',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`last_login_ip` char(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '最后登录 IP',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='系统用户' ROW_FORMAT=DYNAMIC;
--
-- 转存表中的数据 `chatgpt_admin_users`
--
INSERT INTO `chatgpt_admin_users` (`id`, `username`, `password`, `salt`, `status`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1719818809, '172.22.11.200', '2024-03-11 16:30:20', '2024-07-01 15:26:49');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_api_keys`
--
DROP TABLE IF EXISTS `chatgpt_api_keys`;
CREATE TABLE `chatgpt_api_keys` (
`id` int NOT NULL,
`name` varchar(30) DEFAULT NULL COMMENT '名称',
`value` varchar(100) NOT NULL COMMENT 'API KEY value',
`type` varchar(10) NOT NULL DEFAULT 'chat' COMMENT '用途chat=>聊天img=>图片)',
`last_used_at` int NOT NULL COMMENT '最后使用时间',
`api_url` varchar(255) DEFAULT NULL COMMENT 'API 地址',
`enabled` tinyint(1) DEFAULT NULL COMMENT '是否启用',
`proxy_url` varchar(100) DEFAULT NULL COMMENT '代理地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_history`
--
DROP TABLE IF EXISTS `chatgpt_chat_history`;
CREATE TABLE `chatgpt_chat_history` (
`id` bigint NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`type` varchar(10) NOT NULL COMMENT '类型prompt|reply',
`icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色图标',
`role_id` int NOT NULL COMMENT '角色 ID',
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
`content` text NOT NULL COMMENT '聊天内容',
`tokens` smallint NOT NULL COMMENT '耗费 token 数量',
`use_context` tinyint(1) NOT NULL COMMENT '是否允许作为上下文语料',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_items`
--
DROP TABLE IF EXISTS `chatgpt_chat_items`;
CREATE TABLE `chatgpt_chat_items` (
`id` int NOT NULL,
`chat_id` char(40) NOT NULL COMMENT '会话 ID',
`user_id` int NOT NULL COMMENT '用户 ID',
`role_id` int NOT NULL COMMENT '角色 ID',
`title` varchar(100) NOT NULL COMMENT '会话标题',
`model_id` int NOT NULL DEFAULT '0' COMMENT '模型 ID',
`model` varchar(30) DEFAULT NULL COMMENT '模型名称',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_models`
--
DROP TABLE IF EXISTS `chatgpt_chat_models`;
CREATE TABLE `chatgpt_chat_models` (
`id` int NOT NULL,
`name` varchar(50) NOT NULL COMMENT '模型名称',
`value` varchar(50) NOT NULL COMMENT '模型值',
`sort_num` tinyint(1) NOT NULL COMMENT '排序数字',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用模型',
`power` smallint NOT NULL COMMENT '消耗算力点数',
`temperature` float(3,1) NOT NULL DEFAULT '1.0' COMMENT '模型创意度',
`max_tokens` int NOT NULL DEFAULT '1024' COMMENT '最大响应长度',
`max_context` int NOT NULL DEFAULT '4096' COMMENT '最大上下文长度',
`open` tinyint(1) NOT NULL COMMENT '是否开放模型',
`key_id` int NOT NULL COMMENT '绑定API KEY ID',
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='AI 模型表';
--
-- 转存表中的数据 `chatgpt_chat_models`
--
INSERT INTO `chatgpt_chat_models` (`id`, `name`, `value`, `sort_num`, `enabled`, `power`, `temperature`, `max_tokens`, `max_context`, `open`, `key_id`, `created_at`, `updated_at`) VALUES
(1, 'gpt-4o-mini', 'gpt-4o-mini', 1, 1, 1, 1.0, 1024, 16384, 1, 0, '2023-08-23 12:06:36', '2024-07-30 15:55:35'),
(15, 'GPT-超级模型', 'gpt-4-all', 6, 1, 30, 1.0, 4096, 32768, 1, 0, '2024-01-15 11:32:52', '2024-07-22 14:27:04'),
(36, 'GPT-4O', 'gpt-4o', 3, 1, 15, 1.0, 4096, 16384, 1, 0, '2024-05-14 09:25:15', '2024-07-22 14:27:04'),
(39, 'Claude35-snonet', 'claude-3-5-sonnet-20240620', 5, 1, 2, 1.0, 4000, 200000, 1, 0, '2024-05-29 15:04:19', '2024-07-22 14:27:04'),
(41, 'GLM-3-Turbo', 'glm-3-turbo', 7, 1, 2, 1.0, 1024, 8192, 1, 0, '2024-06-06 11:40:46', '2024-07-30 15:55:45'),
(42, 'DeekSeek', 'deepseek-chat', 8, 1, 1, 1.0, 4096, 32768, 1, 0, '2024-06-27 16:13:01', '2024-07-30 15:55:49'),
(44, 'Claude3-opus', 'claude-3-opus-20240229', 4, 1, 5, 1.0, 4000, 128000, 1, 0, '2024-07-22 11:24:30', '2024-07-22 14:27:04'),
(46, 'gpt-3.5-turbo', 'gpt-3.5-turbo', 2, 1, 1, 1.0, 1024, 4096, 1, 0, '2024-07-22 13:53:41', '2024-07-22 14:27:04');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_chat_roles`
--
DROP TABLE IF EXISTS `chatgpt_chat_roles`;
CREATE TABLE `chatgpt_chat_roles` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '角色名称',
`marker` varchar(30) NOT NULL COMMENT '角色标识',
`context_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色语料 json',
`hello_msg` varchar(255) NOT NULL COMMENT '打招呼信息',
`icon` varchar(255) NOT NULL COMMENT '角色图标',
`enable` tinyint(1) NOT NULL COMMENT '是否被启用',
`sort_num` smallint NOT NULL DEFAULT '0' COMMENT '角色排序',
`model_id` int NOT NULL DEFAULT '0' COMMENT '绑定模型ID',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天角色表';
--
-- 转存表中的数据 `chatgpt_chat_roles`
--
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort_num`, `model_id`, `created_at`, `updated_at`) VALUES
(1, '通用AI助手', 'gpt', '', '您好我是您的AI智能助手我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, 0, '2023-05-30 07:02:06', '2024-06-26 15:20:27'),
(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 4, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 3, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 5, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '作为一名心理学家和心理治疗师,我的主要职责是帮助您解决心理健康问题,提升您的生活质量和幸福感。', '/images/avatar/psychiatrist.jpg', 1, 2, 1, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 6, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 9, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(30, '英语陪练员', 'english_trainer', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的英语练习教练,你非常有耐心,接下来你将全程使用英文跟我对话,并及时指出我的语法错误,要求在你的每次回复后面附上本次回复的中文解释。\"},{\"role\":\"assistant\",\"content\":\"Okay, let\'s start our conversation practice! What\'s your name?(Translation: 好的,让我们开始对话练习吧!请问你的名字是什么?)\"}]', 'Okay, let\'s start our conversation practice! What\'s your name?', '/images/avatar/english_trainer.jpg', 1, 7, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 8, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 10, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 11, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 12, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 13, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 14, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 15, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 16, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27'),
(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 17, 0, '2023-05-30 14:10:24', '2024-06-26 15:20:27');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_configs`
--
DROP TABLE IF EXISTS `chatgpt_configs`;
CREATE TABLE `chatgpt_configs` (
`id` int NOT NULL,
`marker` varchar(20) NOT NULL COMMENT '标识',
`config_json` text NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
--
-- 转存表中的数据 `chatgpt_configs`
--
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"title\":\"GeekAI 创作系统\",\"slogan\":\"你有多少想象力AI 就有多大创造力。我辈之人,先干为敬,陪您先把 AI 用起来。\",\"admin_title\":\"GeekAI 控制台\",\"logo\":\"http://localhost:5678/static/upload/2024/4/1714382860986912.png\",\"init_power\":100,\"daily_power\":99,\"invite_power\":1024,\"vip_month_power\":1000,\"register_ways\":[\"username\",\"mobile\",\"email\"],\"enabled_register\":true,\"reward_img\":\"http://localhost:5678/static/upload/2024/3/1710753716309668.jpg\",\"enabled_reward\":true,\"power_price\":0.1,\"order_pay_timeout\":600,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"default_models\":[11,7,1,10,12,19,18,17,3],\"mj_power\":30,\"mj_action_power\":10,\"sd_power\":10,\"dall_power\":15,\"suno_power\":20,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":4,\"sd_neg_prompt\":\"nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet\",\"index_bg_url\":\"color\",\"index_navs\":[1,5,6,13,19,9,12,8],\"copyright\":\"极客学长 © 2022- 2024 All rights reserved\"}'),
(3, 'notice', '{\"sd_neg_prompt\":\"\",\"index_bg_url\":\"\",\"index_navs\":null,\"copyright\":\"\",\"content\":\"## v4.1.1 更新日志\\n\\n* Bug修复修复 GPT 模型 function call 调用后没有输出的问题\\n* 功能新增:允许获取 License 授权用户可以自定义版权信息\\n* 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件\\n* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求\\n* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用\\n* 功能新增MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息\\n* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式\\n* 功能新增:允许在管理后台设置首页显示的导航菜单\\n* Bug修复修复注册页面先显示关闭注册组件然后再显示注册组件\\n* 功能新增:增加 Suno 文生歌曲功能\\n* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加\\n* 功能优化:在所有列表页面增加返回顶部按钮\\n\\n注意当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003eChatPlus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n\\u003cstrong style=\\\"color: #F56C6C\\\"\\u003e体验额度用完之后请不要在当前站点进行任何充值操作\\u003c/strong\\u003e\\n 如果觉得好用你就花几分钟自己部署一套没有API KEY 的同学可以去下面几个推荐的中转站购买:\\n1、\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e\\n2、\\u003ca href=\\\"https://api.geekai.me\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.geekai.me\\u003c/a\\u003e\\n3、 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e\\n支持MidJourneyGPTClaudeGoogle Gemmi以及国内各个厂家的大模型现在有超级优惠价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://ai.r9it.com/docs/install/errors-handle.html#%E8%B0%83%E7%94%A8%E4%B8%AD%E8%BD%AC-api-%E6%8A%A5%E9%94%99%E6%97%A0%E5%8F%AF%E7%94%A8%E6%B8%A0%E9%81%93)。GPT-3.5GPT-4DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程 \\u003ca href=\\\"https://ai.r9it.com/docs/install/\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://ai.r9it.com/docs/install/\\u003c/a\\u003e\\n本项目源码地址\\u003ca href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/chatgpt-plus\\u003c/a\\u003e\",\"updated\":true}');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_dall_jobs`
--
DROP TABLE IF EXISTS `chatgpt_dall_jobs`;
CREATE TABLE `chatgpt_dall_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`img_url` varchar(255) NOT NULL COMMENT '图片地址',
`org_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原图地址',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`power` smallint NOT NULL COMMENT '消耗算力',
`progress` smallint NOT NULL COMMENT '任务进度',
`err_msg` varchar(255) NOT NULL COMMENT '错误信息',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='DALLE 绘图任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_files`
--
DROP TABLE IF EXISTS `chatgpt_files`;
CREATE TABLE `chatgpt_files` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`name` varchar(100) NOT NULL COMMENT '文件名',
`obj_key` varchar(100) DEFAULT NULL COMMENT '文件标识',
`url` varchar(255) NOT NULL COMMENT '文件地址',
`ext` varchar(10) NOT NULL COMMENT '文件后缀',
`size` bigint NOT NULL DEFAULT '0' COMMENT '文件大小',
`created_at` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户文件表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_functions`
--
DROP TABLE IF EXISTS `chatgpt_functions`;
CREATE TABLE `chatgpt_functions` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '函数名称',
`label` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '函数标签',
`description` varchar(255) DEFAULT NULL COMMENT '函数描述',
`parameters` text COMMENT '函数参数JSON',
`token` varchar(255) DEFAULT NULL COMMENT 'API授权token',
`action` varchar(255) DEFAULT NULL COMMENT '函数处理 API',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='函数插件表';
--
-- 转存表中的数据 `chatgpt_functions`
--
INSERT INTO `chatgpt_functions` (`id`, `name`, `label`, `description`, `parameters`, `token`, `action`, `enabled`) VALUES
(1, 'weibo', '微博热搜', '新浪微博热搜榜,微博当日热搜榜单', '{\"type\":\"object\",\"properties\":{}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/weibo', 0),
(2, 'zaobao', '今日早报', '每日早报,获取当天新闻事件列表', '{\"type\":\"object\",\"properties\":{}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/zaobao', 0),
(3, 'dalle3', 'DALLE3', 'AI 绘画工具,根据输入的绘图描述用 AI 工具进行绘画', '{\"type\":\"object\",\"required\":[\"prompt\"],\"properties\":{\"prompt\":{\"type\":\"string\",\"description\":\"绘画提示词\"}}}', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVkIjowLCJ1c2VyX2lkIjowfQ.tLAGkF8XWh_G-oQzevpIodsswtPByBLoAZDz_eWuBgw', 'http://localhost:5678/api/function/dalle3', 0);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_invite_codes`
--
DROP TABLE IF EXISTS `chatgpt_invite_codes`;
CREATE TABLE `chatgpt_invite_codes` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`code` char(8) NOT NULL COMMENT '邀请码',
`hits` int NOT NULL COMMENT '点击次数',
`reg_num` smallint NOT NULL COMMENT '注册数量',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户邀请码';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_invite_logs`
--
DROP TABLE IF EXISTS `chatgpt_invite_logs`;
CREATE TABLE `chatgpt_invite_logs` (
`id` int NOT NULL,
`inviter_id` int NOT NULL COMMENT '邀请人ID',
`user_id` int NOT NULL COMMENT '注册用户ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`invite_code` char(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '邀请码',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='邀请注册日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_menus`
--
DROP TABLE IF EXISTS `chatgpt_menus`;
CREATE TABLE `chatgpt_menus` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '菜单名称',
`icon` varchar(150) NOT NULL COMMENT '菜单图标',
`url` varchar(100) NOT NULL COMMENT '地址',
`sort_num` smallint NOT NULL COMMENT '排序',
`enabled` tinyint(1) NOT NULL COMMENT '是否启用'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='前端菜单表';
--
-- 转存表中的数据 `chatgpt_menus`
--
INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`) VALUES
(1, 'AI 对话', '/images/menu/chat.png', '/chat', 1, 1),
(5, 'MJ 绘画', '/images/menu/mj.png', '/mj', 2, 1),
(6, 'SD 绘画', '/images/menu/sd.png', '/sd', 3, 1),
(7, '算力日志', '/images/menu/log.png', '/powerLog', 9, 1),
(8, '应用中心', '/images/menu/app.png', '/apps', 8, 1),
(9, '画廊', '/images/menu/img-wall.png', '/images-wall', 5, 1),
(10, '会员计划', '/images/menu/member.png', '/member', 10, 1),
(11, '分享计划', '/images/menu/share.png', '/invite', 11, 1),
(12, '思维导图', '/images/menu/xmind.png', '/xmind', 7, 1),
(13, 'DALLE', '/images/menu/dalle.png', '/dalle', 4, 1),
(14, '项目文档', '/images/menu/docs.png', 'https://docs.geekai.me', 12, 1),
(16, '极客论坛', '/images/menu/bbs.png', 'https://bbs.geekai.cn', 13, 1),
(19, 'Suno', '/images/menu/suno.png', '/suno', 5, 1);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_mj_jobs`
--
DROP TABLE IF EXISTS `chatgpt_mj_jobs`;
CREATE TABLE `chatgpt_mj_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`task_id` varchar(20) DEFAULT NULL COMMENT '任务 ID',
`type` varchar(20) DEFAULT 'image' COMMENT '任务类别',
`message_id` char(40) NOT NULL COMMENT '消息 ID',
`channel_id` char(40) DEFAULT NULL COMMENT '频道ID',
`reference_id` char(40) DEFAULT NULL COMMENT '引用消息 ID',
`prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
`img_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '图片URL',
`org_url` varchar(400) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '原始图片地址',
`hash` varchar(100) DEFAULT NULL COMMENT 'message hash',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`use_proxy` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否使用反代',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(255) DEFAULT NULL 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 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_orders`
--
DROP TABLE IF EXISTS `chatgpt_orders`;
CREATE TABLE `chatgpt_orders` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`product_id` int NOT NULL COMMENT '产品ID',
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户明',
`order_no` varchar(30) NOT NULL COMMENT '订单ID',
`trade_no` varchar(60) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '支付平台交易流水号',
`subject` varchar(100) NOT NULL COMMENT '订单产品',
`amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '订单状态0待支付1已扫码2支付成功',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '备注',
`pay_time` int DEFAULT NULL COMMENT '支付时间',
`pay_way` varchar(20) NOT NULL COMMENT '支付方式',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`deleted_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='充值订单表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_power_logs`
--
DROP TABLE IF EXISTS `chatgpt_power_logs`;
CREATE TABLE `chatgpt_power_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`type` tinyint(1) NOT NULL COMMENT '类型1充值2消费3退费',
`amount` smallint NOT NULL COMMENT '算力数值',
`balance` int NOT NULL COMMENT '余额',
`model` varchar(30) NOT NULL COMMENT '模型',
`remark` varchar(255) NOT NULL COMMENT '备注',
`mark` tinyint(1) NOT NULL COMMENT '资金类型0支出1收入',
`created_at` datetime NOT NULL COMMENT '创建时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户算力消费日志';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_products`
--
DROP TABLE IF EXISTS `chatgpt_products`;
CREATE TABLE `chatgpt_products` (
`id` int NOT NULL,
`name` varchar(30) NOT NULL COMMENT '名称',
`price` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '价格',
`discount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '优惠金额',
`days` smallint NOT NULL DEFAULT '0' COMMENT '延长天数',
`power` int NOT NULL DEFAULT '0' COMMENT '增加算力值',
`enabled` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否启动',
`sales` int NOT NULL DEFAULT '0' COMMENT '销量',
`sort_num` tinyint NOT NULL DEFAULT '0' COMMENT '排序',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL,
`app_url` varchar(255) DEFAULT NULL COMMENT 'App跳转地址',
`url` varchar(255) DEFAULT NULL COMMENT '跳转地址'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表';
--
-- 转存表中的数据 `chatgpt_products`
--
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `power`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`, `app_url`, `url`) VALUES
(5, '100次点卡', 9.99, 9.98, 0, 100, 1, 7, 0, '2023-08-28 10:55:08', '2024-06-11 16:48:44', NULL, NULL),
(6, '200次点卡', 19.90, 15.00, 0, 200, 1, 1, 0, '1970-01-01 08:00:00', '2024-06-11 11:41:52', NULL, NULL);
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_rewards`
--
DROP TABLE IF EXISTS `chatgpt_rewards`;
CREATE TABLE `chatgpt_rewards` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`tx_id` char(36) NOT NULL COMMENT '交易 ID',
`amount` decimal(10,2) NOT NULL COMMENT '打赏金额',
`remark` varchar(80) NOT NULL COMMENT '备注',
`status` tinyint(1) NOT NULL COMMENT '核销状态0未核销1已核销',
`exchange` varchar(255) NOT NULL COMMENT '兑换详情json',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_sd_jobs`
--
DROP TABLE IF EXISTS `chatgpt_sd_jobs`;
CREATE TABLE `chatgpt_sd_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT 'txt2img' COMMENT '任务类别',
`task_id` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务 ID',
`prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
`img_url` varchar(255) DEFAULT NULL COMMENT '图片URL',
`params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci COMMENT '绘画参数json',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`publish` tinyint(1) NOT NULL COMMENT '是否发布',
`err_msg` varchar(255) DEFAULT NULL COMMENT '错误信息',
`power` smallint NOT NULL DEFAULT '0' COMMENT '消耗算力',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Stable Diffusion 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_suno_jobs`
--
DROP TABLE IF EXISTS `chatgpt_suno_jobs`;
CREATE TABLE `chatgpt_suno_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`channel` varchar(100) NOT NULL COMMENT '渠道',
`title` varchar(100) DEFAULT NULL COMMENT '歌曲标题',
`type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作',
`task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID',
`ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID',
`tags` varchar(100) DEFAULT NULL COMMENT '歌曲风格',
`instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐',
`extend_secs` smallint DEFAULT '0' COMMENT '延长秒数',
`song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID',
`ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
`audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址',
`model_name` varchar(30) DEFAULT NULL COMMENT '模型地址',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`duration` smallint NOT NULL 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 '消耗算力',
`play_times` int DEFAULT NULL COMMENT '播放次数',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_users`
--
DROP TABLE IF EXISTS `chatgpt_users`;
CREATE TABLE `chatgpt_users` (
`id` int NOT NULL,
`username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户名',
`nickname` varchar(30) NOT NULL COMMENT '昵称',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '头像',
`salt` char(12) NOT NULL COMMENT '密码盐',
`power` int NOT NULL DEFAULT '0' COMMENT '剩余算力',
`expired_time` int NOT NULL COMMENT '用户过期时间',
`status` tinyint(1) NOT NULL COMMENT '当前状态',
`chat_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天配置json',
`chat_roles_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '聊天角色 json',
`chat_models_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'AI模型 json',
`last_login_at` int NOT NULL COMMENT '最后登录时间',
`vip` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否会员',
`last_login_ip` char(16) NOT NULL COMMENT '最后登录 IP',
`openid` varchar(100) DEFAULT NULL COMMENT '第三方登录账号ID',
`platform` varchar(30) DEFAULT NULL COMMENT '登录平台',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
--
-- 转存表中的数据 `chatgpt_users`
--
INSERT INTO `chatgpt_users` (`id`, `username`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `openid`, `platform`, `created_at`, `updated_at`) VALUES
(4, '18575670125', '极客学长', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/5/1715651569509929.png', 'ueedue5l', 5853, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"seller\",\"artist\",\"lu_xun\",\"girl_friend\",\"psychiatrist\",\"teacher\",\"programmer\",\"test\",\"qing_gan_da_shi\",\"english_trainer\",\"elon_musk\"]', '[1,11]', 1722319280, 1, '172.22.18.211', NULL, NULL, '2023-06-12 16:47:17', '2024-07-30 14:01:21'),
(5, 'yangjian102621@gmail.com', '极客学长@486041', '75d1a22f33e1ffffb7943946b6b8d5177d5ecd685d3cef1b468654038b0a8c22', '/images/avatar/user.png', '2q8ugxzk', 100, 0, 1, '', '[\"gpt\",\"programmer\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-04-23 09:17:26', '2024-04-23 09:17:26'),
(8, 'yangjian102623@gmail.com', '极客学长@714931', 'f8f0e0abf146569217273ea0712a0f9b6cbbe7d943a1d9bd5f91c55e6d8c05d1', '/images/avatar/user.png', 'geuddq7f', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-04-26 15:19:28', '2024-04-26 15:19:28'),
(9, '1234567', '极客学长@604526', '858e2afec79e1d6364f4567f945f2310024896d9aa45dd944efa95a0c31e4d08', '/images/avatar/user.png', '00qawlos', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-04-26 15:21:06', '2024-04-26 15:21:06'),
(11, 'abc123', '极客学长@965562', '7a15c53afdb1da7093d80f9940e716eb396e682cfb1f2d107d0b81b183a3ba13', '/images/avatar/user.png', '6433mfbk', 1124, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', NULL, NULL, '2024-06-06 09:37:44', '2024-06-06 09:37:44'),
(14, 'wx@3567548322', '极客学长', '5a349ba89582a4074938b5a3ce84e87c937681ad47e8b87aab03a987e22b6077', 'https://thirdwx.qlogo.cn/mmopen/vi_32/uyxRMqZcEkb7fHouKXbNzxrnrvAttBKkwNlZ7yFibibRGiahdmsrZ3A1NKf8Fw5qJNJn4TXRmygersgEbibaSGd9Sg/132', 'abhzbmij', 83, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', 'oCs0t62472W19z2LOEKI1rWyCTTA', '', '2024-07-04 14:52:08', '2024-07-04 14:52:08'),
(15, 'user123', '极客学长@191303', '4a4c0a14d5fc8787357517f14f6e442281b42c8ec4395016b77483997476011e', '/images/avatar/user.png', 'cyzwkbrx', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-09 10:49:27', '2024-07-09 10:49:27'),
(17, 'user1234', '极客学长@836764', 'bfe03c9c8c9fff5b77e36e40e8298ad3a6073d43c6a936b008eebb21113bf550', '/images/avatar/user.png', '1d2alwnj', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-09 10:53:17', '2024-07-09 10:53:17'),
(18, 'liaoyq', '极客学长@405564', 'ad1726089022db4c661235a8aab7307af1a7f8483eee08bac3f79b5a6a9bd26b', '/images/avatar/user.png', 'yq862l01', 100, 0, 1, '', '[\"string\"]', '[11,7,1,10,12,19,18,17,3]', 1720574265, 0, '172.22.11.29', '', '', '2024-07-10 09:15:33', '2024-07-10 09:17:45'),
(19, 'humm', '极客学长@483216', '420970ace96921c8b3ac7668d097182eab1b6436c730a484e82ae4661bd4f7d9', '/images/avatar/user.png', '1gokrcl2', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 1721381395, 0, '172.22.11.36', '', '', '2024-07-10 11:08:31', '2024-07-19 17:29:56'),
(20, 'abc', '极客学长@369852', '6cad48fb2cc0f54600d66a829e9be69ffd9340a49d5a5b1abda5d4082d946833', '/images/avatar/user.png', 'gop65zei', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-11 16:44:14', '2024-07-11 16:44:14'),
(21, 'husm@pvc123.com', '极客学长@721654', 'e030537dc43fea1bf1fa55a24f99e44f29311bebea96e88ea186995c77db083b', '/images/avatar/user.png', 'p1etg3oi', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-11 16:50:33', '2024-07-11 16:50:33'),
(22, '15818323616', 'ted0000', '3ca6b2ff585d03be8ca4de33ad00148497a09372914ee8aa4cfde343266cbcdd', 'http://localhost:5678/static/upload/2024/7/1720775695548167.jpg', 'sq4s1brf', 100, 0, 1, '', '[\"gpt\"]', '[11,7,1,10,12,19,18,17,3]', 1721785366, 0, '172.22.11.36', '', '', '2024-07-12 15:12:16', '2024-07-24 09:42:46'),
(23, 'aaaaaaaa', '极客学长@488661', 'a7f05323a6ec9dfc1e9bc126f15ccc17c38d0df47957a0bec51f4cc5c2a2b906', '/images/avatar/user.png', 'dsz5d6td', 19, 0, 1, '', '[\"gpt\",\"psychiatrist\",\"red_book\"]', '[11,7,1,10,12,19,18,17,3]', 0, 0, '', '', '', '2024-07-22 13:49:55', '2024-07-22 13:49:55'),
(24, 'test', '极客学长@822932', 'a54d3c38a4a20106ade96de0e9d4547cc691abc5dc39697b44c1a82850374775', '/images/avatar/user.png', '4aa7pijd', 10, 0, 1, '', '[\"gpt\"]', '[1,46]', 0, 0, '', '', '', '2024-07-22 14:40:42', '2024-07-22 14:40:42');
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_user_login_logs`
--
DROP TABLE IF EXISTS `chatgpt_user_login_logs`;
CREATE TABLE `chatgpt_user_login_logs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户ID',
`username` varchar(30) NOT NULL COMMENT '用户名',
`login_ip` char(16) NOT NULL COMMENT '登录IP',
`login_address` varchar(30) NOT NULL COMMENT '登录地址',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户登录日志';
--
-- 转储表的索引
--
--
-- 表的索引 `chatgpt_admin_users`
--
ALTER TABLE `chatgpt_admin_users`
ADD PRIMARY KEY (`id`) USING BTREE,
ADD UNIQUE KEY `username` (`username`) USING BTREE;
--
-- 表的索引 `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
ADD PRIMARY KEY (`id`),
ADD KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `chat_id` (`chat_id`);
--
-- 表的索引 `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `marker` (`marker`);
--
-- 表的索引 `chatgpt_dall_jobs`
--
ALTER TABLE `chatgpt_dall_jobs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_files`
--
ALTER TABLE `chatgpt_files`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_functions`
--
ALTER TABLE `chatgpt_functions`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `name` (`name`);
--
-- 表的索引 `chatgpt_invite_codes`
--
ALTER TABLE `chatgpt_invite_codes`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `code` (`code`);
--
-- 表的索引 `chatgpt_invite_logs`
--
ALTER TABLE `chatgpt_invite_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_menus`
--
ALTER TABLE `chatgpt_menus`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `task_id` (`task_id`),
ADD KEY `message_id` (`message_id`);
--
-- 表的索引 `chatgpt_orders`
--
ALTER TABLE `chatgpt_orders`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `order_no` (`order_no`);
--
-- 表的索引 `chatgpt_power_logs`
--
ALTER TABLE `chatgpt_power_logs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_products`
--
ALTER TABLE `chatgpt_products`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_rewards`
--
ALTER TABLE `chatgpt_rewards`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `tx_id` (`tx_id`);
--
-- 表的索引 `chatgpt_sd_jobs`
--
ALTER TABLE `chatgpt_sd_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `task_id` (`task_id`);
--
-- 表的索引 `chatgpt_suno_jobs`
--
ALTER TABLE `chatgpt_suno_jobs`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `username` (`username`);
--
-- 表的索引 `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
ADD PRIMARY KEY (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `chatgpt_admin_users`
--
ALTER TABLE `chatgpt_admin_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=113;
--
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
--
ALTER TABLE `chatgpt_chat_history`
MODIFY `id` bigint NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_items`
--
ALTER TABLE `chatgpt_chat_items`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_models`
--
ALTER TABLE `chatgpt_chat_models`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=48;
--
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
--
ALTER TABLE `chatgpt_chat_roles`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=132;
--
-- 使用表AUTO_INCREMENT `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- 使用表AUTO_INCREMENT `chatgpt_dall_jobs`
--
ALTER TABLE `chatgpt_dall_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_files`
--
ALTER TABLE `chatgpt_files`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_functions`
--
ALTER TABLE `chatgpt_functions`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=4;
--
-- 使用表AUTO_INCREMENT `chatgpt_invite_codes`
--
ALTER TABLE `chatgpt_invite_codes`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_invite_logs`
--
ALTER TABLE `chatgpt_invite_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_menus`
--
ALTER TABLE `chatgpt_menus`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=20;
--
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_orders`
--
ALTER TABLE `chatgpt_orders`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_power_logs`
--
ALTER TABLE `chatgpt_power_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_products`
--
ALTER TABLE `chatgpt_products`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- 使用表AUTO_INCREMENT `chatgpt_rewards`
--
ALTER TABLE `chatgpt_rewards`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_sd_jobs`
--
ALTER TABLE `chatgpt_sd_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_suno_jobs`
--
ALTER TABLE `chatgpt_suno_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=25;
--
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@ -0,0 +1,33 @@
CREATE TABLE `chatgpt_suno_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`channel` varchar(100) NOT NULL COMMENT '渠道',
`title` varchar(100) DEFAULT NULL COMMENT '歌曲标题',
`type` tinyint(1) DEFAULT '0' COMMENT '任务类型,1:灵感创作,2:自定义创作',
`task_id` varchar(50) DEFAULT NULL COMMENT '任务 ID',
`ref_task_id` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '引用任务 ID',
`tags` varchar(100) DEFAULT NULL COMMENT '歌曲风格',
`instrumental` tinyint(1) DEFAULT '0' COMMENT '是否为纯音乐',
`extend_secs` smallint DEFAULT '0' COMMENT '延长秒数',
`song_id` varchar(50) DEFAULT NULL COMMENT '要续写的歌曲 ID',
`ref_song_id` varchar(50) NOT NULL COMMENT '引用的歌曲ID',
`prompt` varchar(2000) NOT NULL COMMENT '提示词',
`cover_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '封面图地址',
`audio_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '音频地址',
`model_name` varchar(30) DEFAULT NULL COMMENT '模型地址',
`progress` smallint DEFAULT '0' COMMENT '任务进度',
`duration` smallint NOT NULL 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 '消耗算力',
`play_times` int DEFAULT NULL COMMENT '播放次数',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
ALTER TABLE `chatgpt_suno_jobs` ADD PRIMARY KEY (`id`);
ALTER TABLE `chatgpt_suno_jobs` ADD UNIQUE(`song_id`);
-- 删除字段
ALTER TABLE `chatgpt_api_keys` DROP `platform`;
ALTER TABLE `chatgpt_chat_models` DROP `platform`;

View File

@ -21,7 +21,7 @@ TikaHost = "http://tika:9998"
DB = 0 DB = 0
[ApiConfig] [ApiConfig]
ApiURL = "http://service.r9it.com:9001" ApiURL = "http://sapi.geekai.me"
AppId = "" AppId = ""
Token = "" Token = ""

View File

@ -45,7 +45,7 @@ services:
restart: always restart: always
ports: ports:
- "9998:9998" - "9998:9998"
midjourney-proxy: midjourney-proxy:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/midjourney-proxy:2.6.2 image: registry.cn-shenzhen.aliyuncs.com/geekmaster/midjourney-proxy:2.6.2
container_name: geekai-midjourney-proxy container_name: geekai-midjourney-proxy
@ -56,9 +56,9 @@ services:
- ./conf/mj-proxy:/home/spring/config - ./conf/mj-proxy:/home/spring/config
# 后端 API 程序 # 后端 API 程序
geekai-api: geekai-api:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.1.0-amd64 image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-api:v4.1.1-amd64
container_name: geekai-api container_name: geekai-api
restart: always restart: always
depends_on: depends_on:
@ -80,7 +80,7 @@ services:
# 前端应用 # 前端应用
geekai-web: geekai-web:
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.1.0-amd64 image: registry.cn-shenzhen.aliyuncs.com/geekmaster/geekai-web:v4.1.1-amd64
container_name: geekai-web container_name: geekai-web
restart: always restart: always
depends_on: depends_on:
@ -91,5 +91,4 @@ services:
- ./logs/nginx:/var/log/nginx - ./logs/nginx:/var/log/nginx
- ./conf/nginx/conf.d:/etc/nginx/conf.d - ./conf/nginx/conf.d:/etc/nginx/conf.d
- ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf - ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./conf/nginx/ssl:/etc/nginx/ssl - ./conf/nginx/ssl:/etc/nginx/ssl

View File

@ -6,4 +6,6 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123 VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_ VUE_APP_KEY_PREFIX=ChatPLUS_DEV_
VUE_APP_TITLE="Geek-AI 创作系统" VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.0 VUE_APP_VERSION=v4.1.1
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

View File

@ -2,4 +2,7 @@ VUE_APP_API_HOST=
VUE_APP_WS_HOST= VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=ChatPLUS_ VUE_APP_KEY_PREFIX=ChatPLUS_
VUE_APP_TITLE="Geek-AI 创作系统" VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.0 VUE_APP_VERSION=v4.1.1
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

187
web/package-lock.json generated
View File

@ -26,6 +26,7 @@
"markmap-toolbar": "^0.17.0", "markmap-toolbar": "^0.17.0",
"markmap-view": "^0.16.0", "markmap-view": "^0.16.0",
"md-editor-v3": "^2.2.1", "md-editor-v3": "^2.2.1",
"memfs": "^4.9.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
@ -1917,6 +1918,57 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@jsonjoy.com/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/json-pack": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz",
"integrity": "sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==",
"dependencies": {
"@jsonjoy.com/base64": "^1.1.1",
"@jsonjoy.com/util": "^1.1.2",
"hyperdyperid": "^1.2.0",
"thingies": "^1.20.0"
},
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@jsonjoy.com/util": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/@jsonjoy.com/util/-/util-1.2.0.tgz",
"integrity": "sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg==",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/@leichtgewicht/ip-codec": { "node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz", "resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz",
@ -6965,9 +7017,9 @@
} }
}, },
"node_modules/fs-monkey": { "node_modules/fs-monkey": {
"version": "1.0.3", "version": "1.0.6",
"resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.6.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==",
"dev": true "dev": true
}, },
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
@ -7381,6 +7433,14 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==",
"engines": {
"node": ">=10.18"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -8480,15 +8540,21 @@
} }
}, },
"node_modules/memfs": { "node_modules/memfs": {
"version": "3.4.1", "version": "4.9.3",
"resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.4.1.tgz", "resolved": "https://registry.npmmirror.com/memfs/-/memfs-4.9.3.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", "integrity": "sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==",
"dev": true,
"dependencies": { "dependencies": {
"fs-monkey": "1.0.3" "@jsonjoy.com/json-pack": "^1.0.3",
"@jsonjoy.com/util": "^1.1.2",
"tree-dump": "^1.0.1",
"tslib": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">= 4.0.0" "node": ">= 4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
} }
}, },
"node_modules/memoize-one": { "node_modules/memoize-one": {
@ -11394,6 +11460,17 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/thingies": {
"version": "1.21.0",
"resolved": "https://registry.npmmirror.com/thingies/-/thingies-1.21.0.tgz",
"integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
"engines": {
"node": ">=10.18"
},
"peerDependencies": {
"tslib": "^2"
}
},
"node_modules/thread-loader": { "node_modules/thread-loader": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmmirror.com/thread-loader/-/thread-loader-3.0.4.tgz", "resolved": "https://registry.npmmirror.com/thread-loader/-/thread-loader-3.0.4.tgz",
@ -11501,6 +11578,21 @@
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"node_modules/tree-dump": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/tree-dump/-/tree-dump-1.0.2.tgz",
"integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==",
"engines": {
"node": ">=10.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/streamich"
},
"peerDependencies": {
"tslib": "2"
}
},
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz",
@ -12225,6 +12317,18 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true "dev": true
}, },
"node_modules/webpack-dev-middleware/node_modules/memfs": {
"version": "3.5.3",
"resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.5.3.tgz",
"integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
"dev": true,
"dependencies": {
"fs-monkey": "^1.0.4"
},
"engines": {
"node": ">= 4.0.0"
}
},
"node_modules/webpack-dev-middleware/node_modules/schema-utils": { "node_modules/webpack-dev-middleware/node_modules/schema-utils": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz",
@ -14028,6 +14132,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"@jsonjoy.com/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@jsonjoy.com/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==",
"requires": {}
},
"@jsonjoy.com/json-pack": {
"version": "1.0.4",
"resolved": "https://registry.npmmirror.com/@jsonjoy.com/json-pack/-/json-pack-1.0.4.tgz",
"integrity": "sha512-aOcSN4MeAtFROysrbqG137b7gaDDSmVrl5mpo6sT/w+kcXpWnzhMjmY/Fh/sDx26NBxyIE7MB1seqLeCAzy9Sg==",
"requires": {
"@jsonjoy.com/base64": "^1.1.1",
"@jsonjoy.com/util": "^1.1.2",
"hyperdyperid": "^1.2.0",
"thingies": "^1.20.0"
}
},
"@jsonjoy.com/util": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/@jsonjoy.com/util/-/util-1.2.0.tgz",
"integrity": "sha512-4B8B+3vFsY4eo33DMKyJPlQ3sBMpPFUZK2dr3O3rXrOGKKbYG44J0XSFkDo1VOQiri5HFEhIeVvItjR2xcazmg==",
"requires": {}
},
"@leichtgewicht/ip-codec": { "@leichtgewicht/ip-codec": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz", "resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz",
@ -18175,9 +18302,9 @@
} }
}, },
"fs-monkey": { "fs-monkey": {
"version": "1.0.3", "version": "1.0.6",
"resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.6.tgz",
"integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==",
"dev": true "dev": true
}, },
"fs.realpath": { "fs.realpath": {
@ -18512,6 +18639,11 @@
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
"dev": true "dev": true
}, },
"hyperdyperid": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz",
"integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A=="
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -19387,12 +19519,14 @@
"dev": true "dev": true
}, },
"memfs": { "memfs": {
"version": "3.4.1", "version": "4.9.3",
"resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.4.1.tgz", "resolved": "https://registry.npmmirror.com/memfs/-/memfs-4.9.3.tgz",
"integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", "integrity": "sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==",
"dev": true,
"requires": { "requires": {
"fs-monkey": "1.0.3" "@jsonjoy.com/json-pack": "^1.0.3",
"@jsonjoy.com/util": "^1.1.2",
"tree-dump": "^1.0.1",
"tslib": "^2.0.0"
} }
}, },
"memoize-one": { "memoize-one": {
@ -21633,6 +21767,12 @@
"thenify": ">= 3.1.0 < 4" "thenify": ">= 3.1.0 < 4"
} }
}, },
"thingies": {
"version": "1.21.0",
"resolved": "https://registry.npmmirror.com/thingies/-/thingies-1.21.0.tgz",
"integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==",
"requires": {}
},
"thread-loader": { "thread-loader": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmmirror.com/thread-loader/-/thread-loader-3.0.4.tgz", "resolved": "https://registry.npmmirror.com/thread-loader/-/thread-loader-3.0.4.tgz",
@ -21718,6 +21858,12 @@
"resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
}, },
"tree-dump": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/tree-dump/-/tree-dump-1.0.2.tgz",
"integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==",
"requires": {}
},
"tslib": { "tslib": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz",
@ -22301,6 +22447,15 @@
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true "dev": true
}, },
"memfs": {
"version": "3.5.3",
"resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.5.3.tgz",
"integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==",
"dev": true,
"requires": {
"fs-monkey": "^1.0.4"
}
},
"schema-utils": { "schema-utils": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz",

View File

@ -26,6 +26,7 @@
"markmap-toolbar": "^0.17.0", "markmap-toolbar": "^0.17.0",
"markmap-view": "^0.16.0", "markmap-view": "^0.16.0",
"md-editor-v3": "^2.2.1", "md-editor-v3": "^2.2.1",
"memfs": "^4.9.3",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",

BIN
web/public/files/suno.mp3 Normal file

Binary file not shown.

BIN
web/public/files/test.mp3 Normal file

Binary file not shown.

View File

@ -0,0 +1,9 @@
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Frame 38">
<g id="Union">
<path d="M14.6368 0.5L15.7493 0.501512L16.0214 0.5C16.0346 0.554416 16.0476 0.608661 16.0605 0.662705C16.2469 1.44179 16.4236 2.18 17.0437 2.79343C17.7009 3.44502 18.6579 3.76096 19.5 3.96203V5.34684C18.6472 5.5464 17.8202 5.85629 17.1554 6.51393C16.4921 7.16854 16.2231 7.96829 16.0214 8.80887L14.9089 8.80735L14.6368 8.80887C14.6235 8.75426 14.6105 8.69983 14.5975 8.64561C14.4111 7.86652 14.2346 7.12887 13.6145 6.51544C12.9573 5.86384 12.0173 5.54791 11.1752 5.34684V3.96203C12.028 3.76247 12.838 3.45258 13.5028 2.79494C14.166 2.14032 14.435 1.34057 14.6368 0.5Z" fill="#FAF7F5"/>
<path d="M5.6368 6.73166L9.79064 6.03924L10.3222 6.13571C11.2831 6.36516 12.2179 6.69917 12.8413 7.31731L12.8419 7.31787C13.2892 7.7604 13.4912 8.27113 13.6667 8.92773L7.02141 10.1937V15.0405C7.02139 15.3867 6.99557 16.0106 6.94422 16.1683C6.77918 16.9615 6.03702 17.7656 4.97255 18.1954C3.43283 18.8171 2.00551 18.4478 1.60063 17.4448C1.19576 16.4417 2.04898 15.1516 3.50636 14.5631C4.25499 14.2609 5.01741 14.2026 5.6368 14.3533V6.73166Z" fill="#FAF7F5"/>
<path d="M16.0215 14.0019C16.0215 13.6557 16.0215 10.1937 16.0215 10.1937H14.6369V13.1465C14.0043 12.9713 13.2094 13.0215 12.4295 13.3364C10.9721 13.9248 10.1189 15.215 10.5237 16.218C10.9286 17.221 12.4383 17.5571 13.8956 16.9687C14.9829 16.5296 15.6752 15.7329 15.8771 14.8905C15.8802 14.8779 15.8842 14.8619 15.8889 14.843C15.929 14.6822 16.0214 14.3118 16.0215 14.0019Z" fill="#FAF7F5"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

View File

@ -0,0 +1,3 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 6L0 0V12L10 6Z" fill="#FAF7F5"/>
</svg>

After

Width:  |  Height:  |  Size: 149 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -79,7 +79,7 @@
background-color #383838 background-color #383838
border 1px solid #454545 border 1px solid #454545
border-radius 5px border-radius 5px
padding 10px padding 5px
margin-bottom 10px margin-bottom 10px
display flex display flex
flex-flow column flex-flow column
@ -91,12 +91,13 @@
} }
.el-image { .el-image {
height 60px height 30px
width 100% width 100%
} }
.text { .text {
margin-top 6px margin-top 4px
font-size 12px
} }
} }
@ -420,9 +421,27 @@
flex-flow column flex-flow column
justify-content center justify-content center
align-items center align-items center
min-height 200px min-height 220px
color #ffffff color #ffffff
overflow hidden
.err-msg-container {
overflow hidden
word-break break-all
padding 15px
.title {
font-size 20px
text-align center
font-weight bold
color #f56c6c
margin-bottom 30px
}
.opt {
display flex
justify-content center
}
}
.iconfont { .iconfont {
font-size 50px font-size 50px
margin-bottom 10px margin-bottom 10px

View File

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

View File

@ -0,0 +1,88 @@
.page-song {
display: flex;
justify-content: center;
background-color: #0E0808;
.inner {
text-align left
color rgb(250 247 245)
padding 20px
max-width 600px
width 100%
font-family "Neue Montreal,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji"
.title {
font-size 40px
font-weight: 500
line-height 1rem
white-space nowrap
text-overflow ellipsis
}
.row {
padding 8px 0
}
.author {
display flex
align-items center
.nickname {
margin 0 10px
}
.btn {
margin-right 10px
background-color #363030
border none
border-radius 5px
padding 5px 10px
cursor pointer
&:hover {
background-color #5F5958
}
}
}
.date {
color #999999
display flex
align-items center
.version {
background-color #1C1616
border 1px solid #8f8f8f
font-weight normal
font-size 14px
padding 1px 3px
border-radius 5px
margin-left 10px
}
}
.prompt {
width 100%
height 500px
background-color transparent
white-space pre-wrap
overflow-y auto
resize none
position relative
outline 2px solid transparent
outline-offset 2px
border none
font-size 100%
line-height 2rem
}
}
.music-player {
width 100%
position: fixed;
bottom: 0;
left: 50px;
padding 20px 0
}
}

View File

@ -0,0 +1,369 @@
.page-suno {
display flex
height 100%
background-color #0E0808
overflow auto
.left-bar {
max-width 340px
min-width 340px
padding 20px 30px
.bar-top {
display flex
flex-flow row
justify-content: space-between;
}
.params {
padding 20px 0
color rgb(250 247 245)
position relative
.pure-music {
position absolute
right 0
top 24px
display flex
.text {
margin-top 5px
margin-left 5px
}
}
.label {
padding 10px 0
.text {
margin-right 10px
}
.el-icon {
top 2px
}
}
.item {
margin-bottom: 20px
position relative
.create-btn {
margin 20px 0
background-image url("~@/assets/img/suno-create-bg.svg")
background-size: cover;
background-position: 50% 50%;
transition: background 1s ease-in-out;
overflow: hidden;
font-size 16px
width 100%
padding 16px
border-radius 25px
border none
cursor pointer
img {
position relative
top 3px
margin-right 5px
}
&:hover {
opacity: 0.9;
}
}
.song {
display flex
padding 10px
background-color #252020
border-radius 10px
margin-bottom 10px
font-size 14px
position relative
.el-image {
width 50px
height 50px
border-radius 10px
}
.title {
display flex
margin-left 10px
align-items center
}
.el-button--info {
position absolute
right 20px
top 20px
}
}
.extend-secs {
padding 10px 0
font-size 14px
input {
width 50px
text-align center
padding 8px 10px
font-size 14px
background none
border 1px solid #8f8f8f
margin 0 10px
border-radius 10px
outline: none;
transition: border-color 0.5s ease, box-shadow 0.5s ease;
&:focus {
border-color: #0F7A71;
box-shadow: 0 0 5px #0F7A71;
}
}
}
.btn-lyric {
position absolute
left 10px
bottom 10px
font-size 12px
padding 2px 5px
}
}
.tag-select {
position relative
overflow-x auto
overflow-y hidden
width 100%
.inner {
display flex
flex-flow row
padding-bottom 10px
.tag {
margin-right 10px
word-break keep-all
background-color #312C2C
color #e1e1e1
border-radius 5px
padding 3px 6px
cursor pointer
font-size 13px
}
}
}
}
}
.right-box {
width 100%
color rgb(250 247 245)
overflow auto
.list-box {
padding 0 0 0 20px
.item {
display flex
flex-flow row
padding 5px 0
cursor pointer
margin-bottom 10px
&:hover {
background-color #2A2525
}
.left {
.container {
width 60px
height 90px
position relative
.el-image {
height 90px
border-radius 5px
}
.duration {
position absolute
bottom 0
right 0
background-color rgba(14,8,8,.7)
padding 0 3px
font-family 'Input Sans'
font-size 14px
font-weight 700
border-radius .125rem
}
.play {
position absolute
width: 56px;
height 100%
top: 0;
left: 50%;
border none
border-radius 5px
background rgba(100, 100, 100, 0.3)
cursor pointer
color #ffffff
opacity 0
transform: translate(-50%, 0px);
transition opacity 0.3s ease 0s
}
&:hover {
.play {
opacity 1
//display block
}
}
}
}
.center {
width 100%
//border 1px solid saddlebrown
display flex
justify-content center
align-items flex-start
flex-flow column
height 90px
padding 0 20px
.title {
padding 6px 0
font-size 16px
font-weight 700
a {
color rgb(250 247 245)
&:hover {
text-decoration underline
}
}
.model {
color #E2E8F0
background-color #1C1616
border 1px solid #8f8f8f
font-weight normal
font-size 14px
padding 1px 3px
border-radius 5px
margin-left 10px
.iconfont {
font-size 12px
}
}
}
.tags {
font-size 14px
color #d1d1d1
padding 3px 0
}
}
.right {
min-width 320px;
font-size 14px
padding 0 15px
.tools {
display flex
justify-content left
align-items center
flex-flow row
height 90px
.btn-publish {
padding 2px 10px
.text {
margin-right 10px
}
}
.btn-icon {
background none
padding 6px
transition background 0.6s ease 0s
color #726E6C
&:hover {
background #3C3737
}
}
}
}
}
.task {
height 100px
background-color #2A2525
display flex
margin-bottom 10px
.left {
display flex
justify-content left
align-items center
padding 20px
width 320px
.title {
font-size 14px
color #e1e1e1
white-space: nowrap; /* */
overflow: hidden; /* */
text-overflow: ellipsis; /* */
}
}
.center {
display flex
width 100%
justify-content center
.failed {
display flex
align-items center
color #E4696B
font-size 14px
}
}
.right {
display flex
width 100px
justify-content center
align-items center
}
}
}
.pagination {
padding 10px 20px
display flex
justify-content center
}
.music-player {
width 100%
position: fixed;
bottom: 0;
left: 50px;
padding 20px 0
}
}
.btn {
margin-right 10px
background-color #363030
border none
border-radius 5px
padding 5px 10px
cursor pointer
&:hover {
background-color #5F5958
}
}
}

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4125778 */ font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1715987806624') format('woff2'), src: url('iconfont.woff2?t=1721896403264') format('woff2'),
url('iconfont.woff?t=1715987806624') format('woff'), url('iconfont.woff?t=1721896403264') format('woff'),
url('iconfont.ttf?t=1715987806624') format('truetype'); url('iconfont.ttf?t=1721896403264') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,62 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-link:before {
content: "\e6b4";
}
.icon-app:before {
content: "\e64f";
}
.icon-pause:before {
content: "\e693";
}
.icon-prev:before {
content: "\e6a5";
}
.icon-next:before {
content: "\e6a7";
}
.icon-play:before {
content: "\e6a8";
}
.icon-remove:before {
content: "\e82b";
}
.icon-edit:before {
content: "\e61d";
}
.icon-download:before {
content: "\e83a";
}
.icon-more-vertical:before {
content: "\e8cb";
}
.icon-share1:before {
content: "\e661";
}
.icon-suno:before {
content: "\e608";
}
.icon-mp:before {
content: "\e6c4";
}
.icon-mp1:before {
content: "\e647";
}
.icon-control-simple:before { .icon-control-simple:before {
content: "\e624"; content: "\e624";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,104 @@
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "880330",
"name": "link",
"font_class": "link",
"unicode": "e6b4",
"unicode_decimal": 59060
},
{
"icon_id": "1503777",
"name": "应用",
"font_class": "app",
"unicode": "e64f",
"unicode_decimal": 58959
},
{
"icon_id": "7156146",
"name": "暂停",
"font_class": "pause",
"unicode": "e693",
"unicode_decimal": 59027
},
{
"icon_id": "14929909",
"name": "多媒体控件Multimedia Controls (12)",
"font_class": "prev",
"unicode": "e6a5",
"unicode_decimal": 59045
},
{
"icon_id": "14929910",
"name": "多媒体控件Multimedia Controls (11)",
"font_class": "next",
"unicode": "e6a7",
"unicode_decimal": 59047
},
{
"icon_id": "14929913",
"name": "多媒体控件Multimedia Controls (13)",
"font_class": "play",
"unicode": "e6a8",
"unicode_decimal": 59048
},
{
"icon_id": "401063",
"name": "remove",
"font_class": "remove",
"unicode": "e82b",
"unicode_decimal": 59435
},
{
"icon_id": "968465",
"name": "编辑",
"font_class": "edit",
"unicode": "e61d",
"unicode_decimal": 58909
},
{
"icon_id": "6151351",
"name": "download",
"font_class": "download",
"unicode": "e83a",
"unicode_decimal": 59450
},
{
"icon_id": "18986714",
"name": "more",
"font_class": "more-vertical",
"unicode": "e8cb",
"unicode_decimal": 59595
},
{
"icon_id": "11903724",
"name": "share",
"font_class": "share1",
"unicode": "e661",
"unicode_decimal": 58977
},
{
"icon_id": "40001359",
"name": "suno",
"font_class": "suno",
"unicode": "e608",
"unicode_decimal": 58888
},
{
"icon_id": "4318807",
"name": "mp3",
"font_class": "mp",
"unicode": "e6c4",
"unicode_decimal": 59076
},
{
"icon_id": "12600802",
"name": "mp4",
"font_class": "mp1",
"unicode": "e647",
"unicode_decimal": 58951
},
{ {
"icon_id": "12243734", "icon_id": "12243734",
"name": "control", "name": "control",

Binary file not shown.

View File

@ -0,0 +1,47 @@
<svg width="385" height="88" viewBox="0 0 385 88" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_137_12468)">
<rect width="385" height="88" fill="#F24018"/>
<rect width="385" height="88" fill="url(#paint0_radial_137_12468)" fill-opacity="0.25" style="mix-blend-mode:plus-lighter"/>
<g opacity="0.65" filter="url(#filter0_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M111.347 35.6229C151.009 57.6671 209.015 49.1739 230.965 88.9126C255.253 132.884 248.592 191.777 220.732 233.655C193.951 273.912 142.319 284.869 94.8085 293.578C52.1601 301.396 10.1351 297.096 -29.4684 279.522C-74.979 259.325 -135.081 238.424 -141.005 188.932C-146.904 139.646 -81.2632 116.551 -53.9401 75.0726C-29.8623 38.5207 -35.6231 -25.4633 6.91152 -35.6152C49.5563 -45.7935 73.0569 14.3417 111.347 35.6229Z" fill="#1000C0" style="mix-blend-mode:plus-lighter"/>
</g>
<g opacity="0.75" filter="url(#filter1_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M332.217 262.301C293.589 260.201 253.893 255.317 227.782 226.721C199.552 195.803 185.551 152.726 194.871 111.877C204.187 71.0419 233.315 31.0675 274.306 22.8078C310.373 15.5401 331.935 59.2082 363.663 77.865C389.371 92.9816 423.695 92.907 438.039 119.086C458.358 156.168 477.027 204.906 451.904 238.906C426.46 273.342 374.92 264.623 332.217 262.301Z" fill="#1000C0" style="mix-blend-mode:plus-lighter"/>
</g>
<g opacity="0.5" filter="url(#filter2_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.4648 -39.9253C-7.15838 -71.5039 -65.3436 -78.3935 -76.3165 -122.436C-88.4582 -171.17 -66.8828 -226.25 -29.217 -259.404C6.9909 -291.275 59.6564 -288.42 107.765 -284.468C150.95 -280.92 190.417 -265.845 224.132 -238.593C262.877 -207.277 315.527 -171.486 308.519 -122.202C301.54 -73.1229 232.229 -67.9118 195.184 -35.0033C162.539 -6.00341 151.647 57.2182 107.972 55.9516C64.1833 54.6817 56.9587 -9.43976 25.4648 -39.9253Z" fill="#E1B1F8" style="mix-blend-mode:color-dodge"/>
</g>
<g filter="url(#filter3_f_137_12468)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M188.082 0.67854C155.276 -1.12779 121.563 -5.32785 99.3885 -29.9233C75.4135 -56.5155 63.5228 -93.5648 71.4379 -128.698C79.3502 -163.82 104.088 -198.201 138.899 -205.305C169.53 -211.556 187.842 -173.998 214.787 -157.952C236.62 -144.95 265.77 -145.014 277.953 -122.498C295.209 -90.6045 311.063 -48.6858 289.727 -19.4429C268.118 10.1745 224.347 2.6754 188.082 0.67854Z" fill="#F24018" style="mix-blend-mode:plus-lighter"/>
</g>
</g>
<defs>
<filter id="filter0_f_137_12468" x="-181.377" y="-76.7656" width="467.29" height="414.089" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="20" result="effect1_foregroundBlur_137_12468"/>
</filter>
<filter id="filter1_f_137_12468" x="136" y="-34" width="384" height="355" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="28" result="effect1_foregroundBlur_137_12468"/>
</filter>
<filter id="filter2_f_137_12468" x="-135.707" y="-342.825" width="500.858" height="454.795" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="28" result="effect1_foregroundBlur_137_12468"/>
</filter>
<filter id="filter3_f_137_12468" x="-3" y="-278" width="375" height="353" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="36" result="effect1_foregroundBlur_137_12468"/>
</filter>
<radialGradient id="paint0_radial_137_12468" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(210.237 44) rotate(172.274) scale(178.996 234.083)">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</radialGradient>
<clipPath id="clip0_137_12468">
<rect width="385" height="88" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,77 @@
<template>
<button v-if="showButton" @click="scrollToTop" class="scroll-to-top" :style="{bottom: bottom + 'px', right: right + 'px', backgroundColor: bgColor}">
<el-icon><ArrowUpBold /></el-icon>
</button>
</template>
<script>
import {ArrowUpBold} from "@element-plus/icons-vue";
export default {
name: 'BackTop',
components: {ArrowUpBold},
props: {
bottom: {
type: Number,
default: 30
},
right: {
type: Number,
default: 30
},
bgColor: {
type: String,
default: '#007bff'
}
},
data() {
return {
showButton: false
};
},
mounted() {
this.checkScroll();
window.addEventListener('resize', this.checkScroll);
this.$el.parentElement.addEventListener('scroll', this.checkScroll);
},
beforeUnmount() {
window.removeEventListener('resize', this.checkScroll);
this.$el.parentElement.removeEventListener('scroll', this.checkScroll);
},
methods: {
scrollToTop() {
const container = this.$el.parentElement;
container.scrollTo({
top: 0,
behavior: 'smooth'
});
},
checkScroll() {
const container = this.$el.parentElement;
this.showButton = container.scrollTop > 50;
}
}
}
</script>
<style scoped lang="stylus">
.scroll-to-top {
position: fixed;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
outline: none;
transition: opacity 0.3s;
width 40px
height 40px
display flex
justify-content center
align-items center
font-size 20px
&:hover {
opacity: 0.6;
}
}
</style>

View File

@ -1,244 +0,0 @@
<template>
<div class="chat-line chat-line-mj" v-loading="loading">
<div class="chat-line-inner">
<div class="chat-icon">
<img :src="icon" alt="User"/>
</div>
<div class="chat-item">
<div class="content">
<div class="text" v-html="data.html"></div>
<div class="images" v-if="data.image?.url !== ''">
<el-image :src="data.image?.url"
:zoom-rate="1.2"
:preview-src-list="[data.image?.url]"
fit="cover"
:initial-index="0" loading="lazy">
<template #placeholder>
<div class="image-slot"
:style="{height: height+'px', lineHeight:height+'px'}">
正在加载图片<span class="dot">...</span></div>
</template>
<template #error>
<div class="image-slot">
<el-icon>
<Picture/>
</el-icon>
</div>
</template>
</el-image>
</div>
</div>
<div class="opt" v-if="data.showOpt &&data.image?.hash !== ''">
<div class="opt-line">
<ul>
<li><a @click="upscale(1)">U1</a></li>
<li><a @click="upscale(2)">U2</a></li>
<li><a @click="upscale(3)">U3</a></li>
<li><a @click="upscale(4)">U4</a></li>
</ul>
</div>
<div class="opt-line">
<ul>
<li><a @click="variation(1)">V1</a></li>
<li><a @click="variation(2)">V2</a></li>
<li><a @click="variation(3)">V3</a></li>
<li><a @click="variation(4)">V4</a></li>
</ul>
</div>
</div>
<div class="bar" v-if="createdAt !== ''">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">tokens: {{ tokens }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import {ref, watch} from "vue";
import {Clock, Picture} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {getSessionId} from "@/store/session";
const props = defineProps({
content: Object,
icon: String,
chatId: String,
roleId: Number,
createdAt: String
});
const data = ref(props.content)
const tokens = ref(0)
const cacheKey = "img_placeholder_height"
const item = localStorage.getItem(cacheKey);
const loading = ref(false)
const height = ref(0)
if (item) {
height.value = parseInt(item)
}
if (data.value["image"]?.width > 0) {
height.value = 350 * data.value["image"]?.height / data.value["image"]?.width
localStorage.setItem(cacheKey, height.value)
}
data.value["showOpt"] = data.value["content"]?.indexOf("- Image #") === -1;
// console.log(data.value)
watch(() => props.content, (newVal) => {
data.value = newVal;
});
const emits = defineEmits(['disable-input', 'disable-input']);
const upscale = (index) => {
send('/api/mj/upscale', index)
}
const variation = (index) => {
send('/api/mj/variation', index)
}
const send = (url, index) => {
loading.value = true
emits('disable-input')
httpPost(url, {
index: index,
src: "chat",
message_id: data.value?.["message_id"],
message_hash: data.value?.["image"]?.hash,
session_id: getSessionId(),
prompt: data.value?.["prompt"],
chat_id: props.chatId,
role_id: props.roleId,
icon: props.icon,
}).then(() => {
ElMessage.success("任务推送成功,请耐心等待任务执行...")
loading.value = false
}).catch(e => {
ElMessage.error("任务推送失败:" + e.message)
emits('disable-input')
})
}
</script>
<style lang="stylus">
.chat-line-mj {
background-color #ffffff;
justify-content: center;
width 100%
padding-bottom: 1.5rem;
padding-top: 1.5rem;
border-bottom: 1px solid #d9d9e3;
.chat-line-inner {
display flex;
width 100%;
max-width 900px;
padding-left 10px;
.chat-icon {
margin-right 20px;
img {
width: 36px;
height: 36px;
border-radius: 10px;
padding: 1px;
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.content {
word-break break-word;
padding: 6px 10px;
color #374151;
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
.text {
p:first-child {
margin-top 0
}
}
.images {
max-width 350px;
.el-image {
border-radius 10px;
.image-slot {
color #c1c1c1
width 350px
text-align center
border-radius 10px;
border 1px solid #e1e1e1
}
}
}
}
.opt {
.opt-line {
margin 6px 0
ul {
display flex
flex-flow row
padding-left 10px
li {
margin-right 10px
a {
padding 6px 0
width 64px
text-align center
border-radius 5px
display block
cursor pointer
background-color #4E5058
color #ffffff
&:hover {
background-color #6D6F78
}
}
}
}
}
}
.bar {
padding 10px;
.bar-item {
background-color #f7f7f8;
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
.el-icon {
position relative
top 2px;
}
}
}
}
}
}
</style>

View File

@ -70,7 +70,7 @@
</div> </div>
<div class="bar" v-if="data.created_at > 0"> <div class="bar" v-if="data.created_at > 0">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">tokens: {{ finalTokens }}</span> <!-- <span class="bar-item">tokens: {{ finalTokens }}</span>-->
</div> </div>
</div> </div>
</div> </div>
@ -132,27 +132,38 @@ const content =ref(processPrompt(props.data.content))
const files = ref([]) const files = ref([])
onMounted(() => { onMounted(() => {
if (!finalTokens.value) { // if (!finalTokens.value) {
httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => { // httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => {
finalTokens.value = res.data; // finalTokens.value = res.data;
}).catch(() => { // }).catch(() => {
}) // })
} // }
const linkRegex = /(https?:\/\/\S+)/g; const linkRegex = /(https?:\/\/\S+)/g;
const links = props.data.content.match(linkRegex); const links = props.data.content.match(linkRegex);
if (links) { if (links) {
httpPost("/api/upload/list", {urls: links}).then(res => { httpPost("/api/upload/list", {urls: links}).then(res => {
files.value = res.data files.value = res.data
for (let link of links) {
if (isExternalImg(link, files.value)) {
files.value.push({url:link, ext: ".png"})
}
}
}).catch(() => { }).catch(() => {
}) })
for (let link of links) { for (let link of links) {
content.value = content.value.replace(link,"") content.value = content.value.replace(link,"")
} }
} }
content.value = md.render(content.value.trim()) content.value = md.render(content.value.trim())
}) })
const isExternalImg = (link, files) => {
return isImage(link) && !files.find(file => file.url === link)
}
</script> </script>
<style lang="stylus"> <style lang="stylus">
@ -297,9 +308,10 @@ onMounted(() => {
display flex; display flex;
width 100%; width 100%;
padding 0 25px; padding 0 25px;
flex-flow row-reverse
.chat-icon { .chat-icon {
margin-right 20px; margin-left 20px;
img { img {
width: 36px; width: 36px;
@ -366,6 +378,7 @@ onMounted(() => {
.content-wrapper { .content-wrapper {
display flex display flex
flex-flow row-reverse
.content { .content {
word-break break-word; word-break break-word;
padding: 1rem padding: 1rem
@ -373,7 +386,7 @@ onMounted(() => {
font-size: var(--content-font-size); font-size: var(--content-font-size);
overflow: auto; overflow: auto;
background-color #98e165 background-color #98e165
border-radius: 0 10px 10px 10px; border-radius: 10px 0 10px 10px;
img { img {
max-width: 600px; max-width: 600px;

View File

@ -67,14 +67,13 @@
<div class="chat-icon"> <div class="chat-icon">
<img :src="data.icon" alt="ChatGPT"> <img :src="data.icon" alt="ChatGPT">
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="content-wrapper"> <div class="content-wrapper">
<div class="content" v-html="data.content"></div> <div class="content" v-html="data.content"></div>
</div> </div>
<div class="bar" v-if="data.created_at"> <div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">tokens: {{ data.tokens }}</span> <!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg"> <span class="bar-item bg">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
@ -340,18 +339,15 @@ const reGenerate = (prompt) => {
.chat-line-reply-chat { .chat-line-reply-chat {
justify-content: center; justify-content: center;
width 100% padding 1.5rem;
padding-bottom: 1.5rem;
padding-top: 1.5rem;
.chat-line-inner { .chat-line-inner {
display flex; display flex;
padding 0 25px;
width 100% width 100%
flex-flow row-reverse flex-flow row
.chat-icon { .chat-icon {
margin-left 20px; margin-right 20px;
img { img {
width: 36px; width: 36px;
@ -365,11 +361,10 @@ const reGenerate = (prompt) => {
position: relative; position: relative;
padding: 0; padding: 0;
overflow: hidden; overflow: hidden;
max-width 60% max-width 70%
.content-wrapper { .content-wrapper {
display flex display flex
flex-flow row-reverse
.content { .content {
min-height 20px; min-height 20px;
word-break break-word; word-break break-word;
@ -378,7 +373,7 @@ const reGenerate = (prompt) => {
font-size: var(--content-font-size); font-size: var(--content-font-size);
overflow auto; overflow auto;
background-color #F5F5F5 background-color #F5F5F5
border-radius: 10px 0 10px 10px; border-radius: 0 10px 10px 10px;
img { img {
max-width: 600px; max-width: 600px;

View File

@ -1,22 +1,47 @@
<template> <template>
<div class="foot-container"> <div class="foot-container">
<div class="footer"> <div class="footer">
Powered by {{ author }} @ <div><span :style="{color:textColor}">{{copyRight}}</span></div>
<el-link type="primary" :href="gitURL" target="_blank" style="--el-link-text-color:#ffffff"> <div v-if="!license.de_copy">
{{ title }} - <a :href="gitURL" target="_blank" :style="{color:textColor}">
{{ version }} {{ title }} -
</el-link> {{ version }}
</a>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref} from "vue"; import {ref} from "vue";
import {httpGet} from "@/utils/http";
import {showMessageError} from "@/utils/dialog";
const title = ref(process.env.VUE_APP_TITLE) const title = ref("")
const version = ref(process.env.VUE_APP_VERSION) const version = ref(process.env.VUE_APP_VERSION)
const gitURL = ref(process.env.VUE_APP_GIT_URL) const gitURL = ref(process.env.VUE_APP_GIT_URL)
const author = ref('极客学长') const copyRight = ref('')
const license = ref({})
const props = defineProps({
textColor: {
type: String,
default: '#ffffff'
},
});
//
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE
copyRight.value = res.data.copyright.length>1?res.data.copyright:'极客学长 © 2023 - '+new Date().getFullYear()+' All rights reserved.'
}).catch(e => {
showMessageError("获取系统配置失败:" + e.message)
})
httpGet("/api/config/license").then(res => {
license.value = res.data
}).catch(e => {
showMessageError("获取 License 失败:" + e.message)
})
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">
@ -35,8 +60,10 @@ const author = ref('极客学长')
padding 20px; padding 20px;
width 100% width 100%
.el-link { a {
color #409eff &:hover {
text-decoration underline
}
} }
} }
} }

View File

@ -241,8 +241,8 @@ watch(() => props.show, (newValue) => {
const login = ref(true) const login = ref(true)
const data = ref({ const data = ref({
username: "", username: process.env.VUE_APP_USER,
password: "", password: process.env.VUE_APP_PASS,
repass: "", repass: "",
code: "", code: "",
invite_code: "" invite_code: ""
@ -251,7 +251,7 @@ const enableMobile = ref(false)
const enableEmail = ref(false) const enableEmail = ref(false)
const enableUser = ref(false) const enableUser = ref(false)
const enableRegister = ref(false) const enableRegister = ref(false)
const activeName = ref("mobile") const activeName = ref("")
const wxImg = ref("/images/wx.png") const wxImg = ref("/images/wx.png")
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
const emits = defineEmits(['hide', 'success']); const emits = defineEmits(['hide', 'success']);
@ -261,12 +261,15 @@ httpGet("/api/config/get?key=system").then(res => {
const registerWays = res.data['register_ways'] const registerWays = res.data['register_ways']
if (arrayContains(registerWays, "mobile")) { if (arrayContains(registerWays, "mobile")) {
enableMobile.value = true enableMobile.value = true
activeName.value = activeName.value === "" ? "mobile" : activeName.value
} }
if (arrayContains(registerWays, "email")) { if (arrayContains(registerWays, "email")) {
enableEmail.value = true enableEmail.value = true
activeName.value = activeName.value === "" ? "email" : activeName.value
} }
if (arrayContains(registerWays, "username")) { if (arrayContains(registerWays, "username")) {
enableUser.value = true enableUser.value = true
activeName.value = activeName.value === "" ? "username" : activeName.value
} }
// //
enableRegister.value = res.data['enabled_register'] enableRegister.value = res.data['enabled_register']

View File

@ -0,0 +1,282 @@
<template>
<div class="player">
<div class="container">
<div class="cover">
<el-image :src="cover" fit="cover" />
</div>
<div class="info">
<div class="title">{{title}}</div>
<div class="style">
<span class="tags">{{ tags }}</span>
<span class="text-lightGray"> | </span>
<span class="time">{{ formatTime(currentTime) }}<span class="split">/</span>{{ formatTime(duration) }}</span>
</div>
</div>
<div class="controls-container">
<div class="controls">
<button @click="prevSong" class="control-btn">
<i class="iconfont icon-prev"></i>
</button>
<button @click="togglePlay" class="control-btn">
<i class="iconfont icon-play" v-if="!isPlaying"></i>
<i class="iconfont icon-pause" v-else></i>
</button>
<button @click="nextSong" class="control-btn">
<i class="iconfont icon-next"></i>
</button>
</div>
</div>
<div class="progress-bar" @click="setProgress" ref="progressBarRef">
<div class="progress" :style="{ width: `${progressPercent}%` }"></div>
</div>
<audio ref="audio" @timeupdate="updateProgress" @ended="nextSong"></audio>
<el-button v-if="showClose" class="close" type="info" :icon="Close" circle size="small" @click="emits('close')" />
</div>
</div>
</template>
<script setup>
import {ref, onMounted, watch} from 'vue';
import {showMessageError} from "@/utils/dialog";
import {Close} from "@element-plus/icons-vue";
import {formatTime} from "@/utils/libs";
import {httpGet} from "@/utils/http"
const audio = ref(null);
const isPlaying = ref(false);
const songIndex = ref(0);
const currentTime = ref(0);
const duration = ref(100);
const progressPercent = ref(0);
const progressBarRef = ref(null)
const title = ref("")
const tags = ref("")
const cover = ref("")
// eslint-disable-next-line no-undef
const props = defineProps({
songs: {
type: Array,
required: true,
default: () => []
},
showClose: {
type: Boolean,
default: false
}
});
// eslint-disable-next-line no-undef
const emits = defineEmits(['close','play']);
watch(() => props.songs, (newVal) => {
loadSong(newVal[songIndex.value]);
});
const loadSong = (song) => {
if (!song) {
showMessageError("歌曲加载失败")
return
}
title.value = song.title
tags.value = song.tags
cover.value = song.cover_url
audio.value.src = song.audio_url;
audio.value.load();
isPlaying.value = false
audio.value.onloadedmetadata = () => {
duration.value = audio.value.duration;
};
};
const togglePlay = () => {
if (isPlaying.value) {
audio.value.pause();
isPlaying.value = false
} else {
play()
}
};
const play = () => {
if (isPlaying.value) {
return
}
audio.value.play();
isPlaying.value = true
if (audio.value.currentTime === 0) {
emits("play")
//
httpGet("/api/suno/play",{song_id:props.songs[songIndex.value].song_id}).then().catch()
}
}
const prevSong = () => {
songIndex.value = (songIndex.value - 1 + props.songs.length) % props.songs.length;
loadSong(props.songs[songIndex.value]);
audio.value.play();
isPlaying.value = true;
};
const nextSong = () => {
songIndex.value = (songIndex.value + 1) % props.songs.length;
loadSong(props.songs[songIndex.value]);
audio.value.play();
isPlaying.value = true;
};
const updateProgress = () => {
try {
currentTime.value = audio.value.currentTime;
progressPercent.value = (currentTime.value / duration.value) * 100;
} catch (e) {
console.error(e.message)
}
};
const setProgress = (event) => {
const totalWidth = progressBarRef.value.offsetWidth;
const clickX = event.offsetX;
const audioDuration = audio.value.duration;
audio.value.currentTime = (clickX / totalWidth) * audioDuration;
};
// eslint-disable-next-line no-undef
defineExpose({
play
});
onMounted(() => {
loadSong(props.songs[songIndex.value]);
});
</script>
<style lang="stylus" scoped>
.player {
display flex
justify-content center
width 100%
.container {
display flex
background-color: #363030;
border-radius: 10px;
border 1px solid #544F4F;
padding: 5px;
width: 80%
text-align: center;
position relative
overflow hidden
.cover {
.el-image {
border-radius: 50%;
width 50px
}
}
.info {
padding 0 10px
min-width 300px
display flex
justify-content center
align-items flex-start
flex-flow column
line-height 1.5
.title {
font-weight 700
font-size 16px
color #ffffff
}
.style {
font-size 14px
display flex
color #e1e1e1
.tags {
font-weight 600
white-space: nowrap; /* 防止文本换行 */
overflow: hidden; /* 隐藏溢出的文本 */
text-overflow: ellipsis; /* 使用省略号表示溢出的文本 */
max-width 200px
}
.text-lightGray {
color: rgb(114 110 108);
padding 0 3px
}
.time {
font-family 'Input Sans'
font-weight 700
.split {
font-size 12px
position relative
top -2px
margin 0 1px 0 3px
}
}
}
}
.controls-container {
width 100%
display flex
flex-flow column
justify-content center
.controls {
display: flex;
justify-content: space-around;
margin-bottom 10px
.control-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
background-color #363030
border-radius 5px
padding 6px
.iconfont {
font-size 20px
}
&:hover {
background-color #5F5958
}
}
}
}
.progress-bar {
position absolute
width 100%
left 0
bottom 0
height: 8px;
background-color: #555;
cursor: pointer;
.progress {
height: 100%;
background-color: #f50;
border-radius: 5px;
width: 0;
}
}
.close {
position absolute
right 10px
top 15px
}
}
}
</style>

View File

@ -58,8 +58,8 @@ import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
const title = ref('Chat-Plus-Admin') const title = ref('')
const logo = ref('/images/logo.png') const logo = ref('')
// //
httpGet('/api/admin/config/get?key=system').then(res => { httpGet('/api/admin/config/get?key=system').then(res => {

View File

@ -0,0 +1,106 @@
<template>
<div class="black-dialog">
<el-dialog
v-model="showDialog"
style="--el-dialog-bg-color:#414141;
--el-text-color-primary:#f1f1f1;
--el-border-color:#414141;
--el-color-primary:#21aa93;
--el-color-primary-dark-2:#41555d;
--el-color-white: #e1e1e1;
--el-color-primary-light-3:#549688;
--el-fill-color-blank:#616161;
--el-color-primary-light-7:#717171;
--el-color-primary-light-9:#717171;
--el-text-color-regular:#e1e1e1"
:title="title"
:width="width"
:before-close="cancel"
>
<div class="dialog-body">
<slot></slot>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">{{cancelText}}</el-button>
<el-button type="primary" @click="$emit('confirm')">{{confirmText}}</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref, watch} from "vue";
const props = defineProps({
show : Boolean,
title: {
type: String,
default: 'Tips',
},
width: {
type: Number,
default: 500,
},
confirmText: {
type: String,
default: '确定',
},
cancelText: {
type: String,
default: '取消',
},
});
const emits = defineEmits(['confirm','cancal']);
const showDialog = ref(props.show)
watch(() => props.show, (newValue) => {
showDialog.value = newValue
})
const cancel = () => {
showDialog.value = false
emits('cancal')
}
</script>
<style lang="stylus">
.black-dialog {
.dialog-body {
.form {
.form-item {
display flex
flex-flow column
font-family: "Neue Montreal";
padding 10px 0
.label {
margin-bottom 0.6rem
margin-inline-end 0.75rem
color #ffffff
font-size 1rem
font-weight 500
}
.input {
display flex
padding 10px
text-align left
font-size 1rem
background none
border-radius 0.375rem
border 1px solid #8f8f8f
outline: none;
transition: border-color 0.5s ease, box-shadow 0.5s ease;
&:focus {
border-color: #0F7A71;
box-shadow: 0 0 5px #0F7A71;
}
}
}
}
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div class="black-input-wrapper">
<el-input v-model="model" :type="type" :rows="rows"
@input="onInput"
style="--el-input-bg-color:#252020;
--el-input-border-color:#414141;
--el-input-focus-border-color:#414141;
--el-text-color-regular: #f1f1f1;
--el-input-border-radius: 10px;
--el-border-color-hover:#616161"
resize="none"
:placeholder="placeholder" :maxlength="maxlength"/>
<div class="word-stat" v-if="rows > 1">
<span>{{value.length}}</span>/<span>{{maxlength}}</span>
</div>
</div>
</template>
<script setup>
import {ref, watch} from "vue";
const props = defineProps({
value : {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
type: {
type: String,
default: 'input',
},
rows: {
type: Number,
default: 5,
},
maxlength: {
type: Number,
default: 1024
}
});
watch(() => props.value, (newValue) => {
model.value = newValue
})
const model = ref(props.value)
const emits = defineEmits(['update:value']);
const onInput = (value) => {
emits('update:value',value)
}
</script>
<style lang="stylus">
.black-input-wrapper {
position relative
.el-textarea__inner {
padding: 20px;
font-size: 16px;
}
.word-stat {
position: absolute;
bottom 10px
right 10px
color rgb(209 203 199)
font-family: Neue Montreal, ui-sans-serif, system-ui, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
font-size .875rem
line-height 1.25rem
span {
margin 0 1px
}
}
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<el-select v-model="model" :placeholder="placeholder"
:value="value" @change="$emit('update:value', $event)"
style="--el-fill-color-blank:#252020;
--el-text-color-regular: #a1a1a1;
--el-select-disabled-color:#0E0808;
--el-color-primary-light-9:#0E0808;
--el-border-radius-base:20px;
--el-border-color:#0E0808;">
<el-option v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
</template>
<script>
export default {
name: 'BlackSelect',
props: {
value : {
type: String,
default: '',
},
placeholder: {
type: String,
default: '请选择',
},
options: {
type: Array,
default: []
}
},
data() {
return {
model: this.value
}
}
}
</script>

View File

@ -0,0 +1,24 @@
<template>
<el-switch v-model="model" :size="size"
@change="$emit('update:value', $event)"
style="--el-switch-on-color:#555555;--el-color-white:#0E0808"/>
</template>
<script setup>
import {ref, watch} from "vue";
const props = defineProps({
value : Boolean,
size: {
type: String,
default: 'default',
}
});
const model = ref(props.value)
watch(() => props.value, (newValue) => {
model.value = newValue
})
</script>

View File

@ -0,0 +1,97 @@
<template>
<div class="container">
<div class="wave">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</div>
<div class="text">正在生成歌曲</div>
</div>
</template>
<style scoped lang="stylus">
.container {
display: flex;
flex-flow column
justify-content: center;
align-items: center;
margin: 0;
font-family: 'Arial', sans-serif;
color: #e1e1e1;
.wave {
display: flex;
justify-content: center;
align-items: flex-end;
height: 30px;
margin-bottom: 5px
.bar {
width: 8px;
margin: 0 2px;
background-color: #919191;
animation: wave 1.5s infinite;
}
.bar:nth-child(1) {
animation-delay: 0s;
}
.bar:nth-child(2) {
animation-delay: 0.1s;
}
.bar:nth-child(3) {
animation-delay: 0.2s;
}
.bar:nth-child(4) {
animation-delay: 0.3s;
}
.bar:nth-child(5) {
animation-delay: 0.4s;
}
.bar:nth-child(6) {
animation-delay: 0.5s;
}
.bar:nth-child(7) {
animation-delay: 0.6s;
}
.bar:nth-child(8) {
animation-delay: 0.7s;
}
.bar:nth-child(9) {
animation-delay: 0.8s;
}
.bar:nth-child(10) {
animation-delay: 0.9s;
}
}
}
@keyframes wave {
0%, 100% {
height: 10px;
}
50% {
height: 30px;
}
}
.text {
font-size: 14px;
}
</style>

View File

@ -82,11 +82,23 @@ const routes = [
meta: {title: 'DALLE-3'}, meta: {title: 'DALLE-3'},
component: () => import('@/views/Dalle.vue'), component: () => import('@/views/Dalle.vue'),
}, },
{
name: 'suno',
path: '/suno',
meta: {title: 'Suno音乐创作'},
component: () => import('@/views/Suno.vue'),
},
{ {
name: 'ExternalLink', name: 'ExternalLink',
path: '/external', path: '/external',
component: () => import('@/views/ExternalPage.vue'), component: () => import('@/views/ExternalPage.vue'),
}, },
{
name: 'song',
path: '/song/:id',
meta: {title: 'Suno音乐播放'},
component: () => import('@/views/Song.vue'),
},
] ]
}, },
{ {
@ -280,23 +292,11 @@ const router = createRouter({
routes: routes, routes: routes,
}) })
const active = ref(false)
const title = ref('')
httpGet("/api/config/license").then(res => {
active.value = res.data.de_copy
}).catch(() => {})
httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title
}).catch(()=>{})
let prevRoute = null let prevRoute = null
// dynamic change the title when router change // dynamic change the title when router change
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
if (!active.value) { document.title = to.meta.title
document.title = `${to.meta.title} | ${process.env.VUE_APP_TITLE}`
} else {
document.title = `${to.meta.title} | ${title.value}`
}
prevRoute = from prevRoute = from
next() next()
}) })

View File

@ -37,7 +37,11 @@ export function GetFileIcon(ext) {
".pptx": "ppt.png", ".pptx": "ppt.png",
".md": "md.png", ".md": "md.png",
".pdf": "pdf.png", ".pdf": "pdf.png",
".sql": "sql.png" ".sql": "sql.png",
".mp3": "mp3.png",
".wav": "mp3.png",
".mp4": "mp4.png",
".avi": "mp4.png",
} }
if (files[ext]) { if (files[ext]) {
return '/images/ext/' + files[ext] return '/images/ext/' + files[ext]

View File

@ -34,9 +34,15 @@ axios.interceptors.response.use(
} else { } else {
removeUserToken() removeUserToken()
} }
console.log(error.response.data)
error.response.data.message = "请先登录"
return Promise.reject(error.response.data) return Promise.reject(error.response.data)
} }
return Promise.reject(error) if (error.response.status === 400) {
return Promise.reject(new Error(error.response.data.message))
} else {
return Promise.reject(error)
}
}) })

View File

@ -90,6 +90,12 @@ export function dateFormat(timestamp, format) {
return timeDate; return timeDate;
} }
export function formatTime(time) {
const minutes = Math.floor(time / 60);
const seconds = Math.floor(time % 60);
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
}
// 判断数组中是否包含某个元素 // 判断数组中是否包含某个元素
export function arrayContains(array, value, compare) { export function arrayContains(array, value, compare) {
if (!array) { if (!array) {

View File

@ -15,18 +15,13 @@
<div class="info-text">{{ scope.item.hello_msg }}</div> <div class="info-text">{{ scope.item.hello_msg }}</div>
</div> </div>
<div class="btn"> <div class="btn">
<div v-if="hasRole(scope.item.key)"> <el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button>
<el-button size="small" color="#21aa93" @click="useRole(scope.item)">使用</el-button> <el-tooltip effect="light" content="从工作区移除" placement="top" v-if="hasRole(scope.item.key)">
<el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button> <el-button size="small" type="danger" @click="updateRole(scope.item,'remove')">移除</el-button>
</div> </el-tooltip>
<el-button v-else size="small" <el-tooltip effect="light" content="添加到工作区" placement="top" v-else>
style="--el-color-primary:#009999" <el-button size="small" style="--el-color-primary:#009999" @click="updateRole(scope.item, 'add')">添加</el-button>
@click="updateRole(scope.item, 'add')"> </el-tooltip>
<el-icon>
<Plus/>
</el-icon>
<span>添加应用</span>
</el-button>
</div> </div>
</div> </div>
@ -77,7 +72,7 @@ const roles = ref([])
const store = useSharedStore(); const store = useSharedStore();
onMounted(() => { onMounted(() => {
httpGet("/api/role/list?all=true").then((res) => { httpGet("/api/role/list").then((res) => {
const items = res.data const items = res.data
// hello message // hello message
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {

View File

@ -118,6 +118,8 @@
v-if="item.type==='prompt'" :data="item" :list-style="listStyle"/> v-if="item.type==='prompt'" :data="item" :list-style="listStyle"/>
<chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle"/> <chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle"/>
</div> </div>
<back-top :right="30" :bottom="100" bg-color="#19C27D"/>
</div><!-- end chat box --> </div><!-- end chat box -->
<div class="input-box"> <div class="input-box">
@ -219,6 +221,9 @@ import {useSharedStore} from "@/store/sharedata";
import FileSelect from "@/components/FileSelect.vue"; import FileSelect from "@/components/FileSelect.vue";
import FileList from "@/components/FileList.vue"; import FileList from "@/components/FileList.vue";
import ChatSetting from "@/components/ChatSetting.vue"; import ChatSetting from "@/components/ChatSetting.vue";
import axios from "axios";
import BackTop from "@/components/BackTop.vue";
import {showMessageError} from "@/utils/dialog";
const title = ref('ChatGPT-智能助手'); const title = ref('ChatGPT-智能助手');
const models = ref([]) const models = ref([])
@ -316,18 +321,17 @@ const initData = () => {
chatList.value = res.data; chatList.value = res.data;
allChats.value = res.data; allChats.value = res.data;
} }
if (router.currentRoute.value.query.role_id) {
roleId.value = parseInt(router.currentRoute.value.query.role_id)
}
// //
httpGet('/api/model/list').then(res => { httpGet('/api/model/list').then(res => {
models.value = res.data models.value = res.data
modelID.value = models.value[0].id modelID.value = models.value[0].id
// //
httpGet(`/api/role/list`).then((res) => { httpGet(`/api/role/list`,{id:roleId.value}).then((res) => {
roles.value = res.data; roles.value = res.data;
if (router.currentRoute.value.query.role_id) { if (!roleId.value) {
roleId.value = parseInt(router.currentRoute.value.query.role_id)
} else {
roleId.value = roles.value[0]['id'] roleId.value = roles.value[0]['id']
} }
@ -343,18 +347,8 @@ const initData = () => {
}) })
}).catch(() => { }).catch(() => {
loading.value = false loading.value = false
//
httpGet("/api/chat/list").then((res) => {
if (res.data) {
chatList.value = res.data;
allChats.value = res.data;
}
}).catch(() => {
ElMessage.error("加载会话列表失败!")
})
// //
httpGet('/api/model/list').then(res => { httpGet('/api/model/list',{id:roleId.value}).then(res => {
models.value = res.data models.value = res.data
modelID.value = models.value[0].id modelID.value = models.value[0].id
}).catch(e => { }).catch(e => {
@ -369,6 +363,37 @@ const initData = () => {
ElMessage.error('获取聊天角色失败: ' + e.messages) ElMessage.error('获取聊天角色失败: ' + e.messages)
}) })
}) })
inputRef.value.addEventListener('paste', (event) => {
const items = (event.clipboardData || window.clipboardData).items;
let fileFound = false;
loading.value = true
for (let item of items) {
if (item.kind === 'file') {
const file = item.getAsFile();
fileFound = true;
const formData = new FormData();
formData.append('file', file);
//
httpPost('/api/upload', formData).then((res) => {
files.value.push(res.data)
ElMessage.success({message: "上传成功", duration: 500})
loading.value = false
}).catch((e) => {
ElMessage.error('文件上传失败:' + e.message)
loading.value = false
})
break;
}
}
if (!fileFound) {
document.getElementById('status').innerText = 'No file found in paste data.';
}
});
} }
const getRoleById = function (rid) { const getRoleById = function (rid) {
@ -582,18 +607,6 @@ const connect = function (chat_id, role_id) {
} }
} }
//
const sendHeartbeat = () => {
clearTimeout(heartbeatHandle.value)
new Promise((resolve, reject) => {
if (socket.value !== null) {
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
}
resolve("success")
}).then(() => {
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
});
}
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()}`); 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()}`);
_socket.addEventListener('open', () => { _socket.addEventListener('open', () => {
chatData.value = []; // chatData.value = []; //
@ -615,8 +628,6 @@ const connect = function (chat_id, role_id) {
} else { // } else { //
loadChatHistory(chat_id); loadChatHistory(chat_id);
} }
//
sendHeartbeat()
}); });
_socket.addEventListener('message', event => { _socket.addEventListener('message', event => {
@ -627,13 +638,14 @@ const connect = function (chat_id, role_id) {
reader.onload = () => { reader.onload = () => {
const data = JSON.parse(String(reader.result)); const data = JSON.parse(String(reader.result));
if (data.type === 'start') { if (data.type === 'start') {
const prePrompt = chatData.value[chatData.value.length-1].content const prePrompt = chatData.value[chatData.value.length-1]?.content
chatData.value.push({ chatData.value.push({
type: "reply", type: "reply",
id: randString(32), id: randString(32),
icon: _role['icon'], icon: _role['icon'],
prompt:prePrompt, prompt:prePrompt,
content: "" content: "",
orgContent: "",
}); });
} else if (data.type === 'end') { // } else if (data.type === 'end') { //
// //
@ -667,8 +679,10 @@ const connect = function (chat_id, role_id) {
} else { } else {
lineBuffer.value += data.content; lineBuffer.value += data.content;
const reply = chatData.value[chatData.value.length - 1] const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value; if (reply) {
reply['content'] = md.render(processContent(lineBuffer.value)); reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(processContent(lineBuffer.value));
}
} }
// //
nextTick(() => { nextTick(() => {
@ -678,7 +692,7 @@ const connect = function (chat_id, role_id) {
}; };
} }
} catch (e) { } catch (e) {
console.error(e) console.warn(e)
} }
}); });
@ -694,7 +708,7 @@ const connect = function (chat_id, role_id) {
connect(chat_id, role_id) connect(chat_id, role_id)
}).catch(() => { }).catch(() => {
loading.value = true loading.value = true
setTimeout(() => connect(chat_id, role_id), 3000) showMessageError("会话已断开,刷新页面...")
}); });
}); });
@ -740,7 +754,7 @@ const onInput = (e) => {
const autofillPrompt = (text) => { const autofillPrompt = (text) => {
prompt.value = text prompt.value = text
inputRef.value.focus() inputRef.value.focus()
// sendMessage() sendMessage()
} }
// //
const sendMessage = function () { const sendMessage = function () {

View File

@ -174,7 +174,7 @@
</div> <!-- end finish job list--> </div> <!-- end finish job list-->
</div> </div>
</div> </div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box --> </div><!-- end task list box -->
</div> </div>
@ -193,6 +193,7 @@ import Clipboard from "clipboard";
import {checkSession} from "@/action/session"; import {checkSession} from "@/action/session";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
const listBoxHeight = ref(0) const listBoxHeight = ref(0)
// const paramBoxHeight = ref(0) // const paramBoxHeight = ref(0)

View File

@ -12,18 +12,18 @@
<div class="navbar"> <div class="navbar">
<el-tooltip <el-tooltip
v-if="!licenseConfig.de_copy" v-if="!license.de_copy"
class="box-item" class="box-item"
effect="light" effect="light"
content="部署文档" content="部署文档"
placement="bottom"> placement="bottom">
<a href="https://ai.r9it.com/docs/install/" class="link-button" target="_blank"> <a href="https://docs.geekai.me/install/" class="link-button" target="_blank">
<i class="iconfont icon-book"></i> <i class="iconfont icon-book"></i>
</a> </a>
</el-tooltip> </el-tooltip>
<el-tooltip <el-tooltip
v-if="!licenseConfig.de_copy" v-if="!license.de_copy"
class="box-item" class="box-item"
effect="light" effect="light"
content="项目源码" content="项目源码"
@ -46,7 +46,7 @@
<span class="username">{{ loginUser.nickname }}</span> <span class="username">{{ loginUser.nickname }}</span>
</el-dropdown-item> </el-dropdown-item>
<div v-if="!licenseConfig.de_copy"> <div v-if="!license.de_copy">
<el-dropdown-item> <el-dropdown-item>
<i class="iconfont icon-book"></i> <i class="iconfont icon-book"></i>
<a :href="docsURL" target="_blank"> <a :href="docsURL" target="_blank">
@ -146,7 +146,7 @@ import ConfigDialog from "@/components/UserInfoDialog.vue";
import {showMessageError} from "@/utils/dialog"; import {showMessageError} from "@/utils/dialog";
const router = useRouter(); const router = useRouter();
const logo = ref('/images/logo.png'); const logo = ref('');
const mainNavs = ref([]) const mainNavs = ref([])
const moreNavs = ref([]) const moreNavs = ref([])
const curPath = ref(router.currentRoute.value.path) const curPath = ref(router.currentRoute.value.path)
@ -156,7 +156,7 @@ const loginUser = ref({})
const version = ref(process.env.VUE_APP_VERSION) const version = ref(process.env.VUE_APP_VERSION)
const routerViewKey = ref(0) const routerViewKey = ref(0)
const showConfigDialog = ref(false) const showConfigDialog = ref(false)
const licenseConfig = ref({}) const license = ref({de_copy: true})
const docsURL = ref(process.env.VUE_APP_DOCS_URL) const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL) const gitURL = ref(process.env.VUE_APP_GIT_URL)
@ -166,6 +166,12 @@ watch(() => store.showLoginDialog, (newValue) => {
show.value = newValue show.value = newValue
}); });
//
router.beforeEach((to, from, next) => {
curPath.value = to.path
next();
});
if (curPath.value === "/external") { if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url curPath.value = router.currentRoute.value.query.url
} }
@ -199,8 +205,9 @@ onMounted(() => {
}) })
httpGet("/api/config/license").then(res => { httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data license.value = res.data
}).catch(e => { }).catch(e => {
license.value = {de_copy: false}
showMessageError("获取 License 配置:" + e.message) showMessageError("获取 License 配置:" + e.message)
}) })

View File

@ -487,6 +487,23 @@
</div> </div>
</template> </template>
</el-image> </el-image>
<el-image v-else-if="slotProp.item['err_msg'] !== ''">
<template #error>
<div class="image-slot">
<div class="err-msg-container">
<div class="title">任务失败</div>
<div class="opt">
<el-popover title="错误详情" trigger="click" :width="250" :content="slotProp.item['err_msg']" placement="top">
<template #reference>
<el-button type="info">详情</el-button>
</template>
</el-popover>
<el-button type="danger" @click="removeImage(slotProp.item)">删除</el-button>
</div>
</div>
</div>
</template>
</el-image>
<el-image v-else> <el-image v-else>
<template #error> <template #error>
<div class="image-slot"> <div class="image-slot">
@ -536,16 +553,30 @@
</div> </div>
</div> </div>
<div class="remove"> <div class="remove" v-if="slotProp.item.progress === 100">
<el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/> <el-tooltip effect="light" content="删除任务" placement="top">
<el-button type="warning" v-if="slotProp.item.publish" <el-button type="danger" :icon="Delete" @click="removeImage(slotProp.item)" circle/>
@click="publishImage(slotProp.item, false)" </el-tooltip>
circle> <el-tooltip effect="light" content="取消发布" placement="top" v-if="slotProp.item.publish">
<i class="iconfont icon-cancel-share"></i> <el-button type="warning"
</el-button> @click="publishImage(slotProp.item, false)"
<el-button type="success" v-else @click="publishImage(slotProp.item, true)" circle> circle>
<i class="iconfont icon-share-bold"></i> <i class="iconfont icon-cancel-share"></i>
</el-button> </el-button>
</el-tooltip>
<el-tooltip effect="light" content="发布图片" placement="top" v-else>
<el-button type="success" @click="publishImage(slotProp.item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
</el-tooltip>
<el-tooltip effect="light" content="复制提示词" placement="top">
<el-button type="success" class="copy-prompt-mj"
:data-clipboard-text="slotProp.item.prompt" circle>
<el-icon><DocumentCopy/></el-icon>
</el-button>
</el-tooltip>
</div> </div>
</div> </div>
</template> </template>
@ -562,6 +593,7 @@
</div> <!-- end finish job list--> </div> <!-- end finish job list-->
</div> </div>
</div> </div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box --> </div><!-- end task list box -->
</div> </div>
@ -582,6 +614,7 @@ import {getSessionId} from "@/store/session";
import {copyObj, removeArrayItem} from "@/utils/libs"; import {copyObj, removeArrayItem} from "@/utils/libs";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
const listBoxHeight = ref(0) const listBoxHeight = ref(0)
const paramBoxHeight = ref(0) const paramBoxHeight = ref(0)
@ -714,7 +747,7 @@ const connect = () => {
reader.readAsText(event.data, "UTF-8") reader.readAsText(event.data, "UTF-8")
reader.onload = () => { reader.onload = () => {
const message = String(reader.result) const message = String(reader.result)
if (message === "FINISH") { if (message === "FINISH" || message === "FAIL") {
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
fetchFinishJobs(page.value) fetchFinishJobs(page.value)
@ -786,7 +819,7 @@ const fetchRunningJobs = () => {
const jobs = res.data const jobs = res.data
const _jobs = [] const _jobs = []
for (let i = 0; i < jobs.length; i++) { for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) { if (jobs[i].progress === 101) {
ElNotification({ ElNotification({
title: '任务执行失败', title: '任务执行失败',
dangerouslyUseHTMLString: true, dangerouslyUseHTMLString: true,
@ -799,7 +832,6 @@ const fetchRunningJobs = () => {
} else { } else {
power.value += mjActionPower.value power.value += mjActionPower.value
} }
continue
} }
_jobs.push(jobs[i]) _jobs.push(jobs[i])
} }
@ -833,7 +865,7 @@ const fetchFinishJobs = () => {
jobs[i]['thumb_url'] = '/images/img-placeholder.jpg' jobs[i]['thumb_url'] = '/images/img-placeholder.jpg'
} }
if (jobs[i].type === 'image' || jobs[i].type === 'variation') { if ((jobs[i].type === 'image' || jobs[i].type === 'variation') && jobs[i].progress === 100) {
jobs[i]['can_opt'] = true jobs[i]['can_opt'] = true
} }
} }
@ -984,7 +1016,7 @@ const publishImage = (item, action) => {
item.publish = action item.publish = action
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
fetchFinishJobs() item.publish = action
}).catch(e => { }).catch(e => {
ElMessage.error(text + "失败:" + e.message) ElMessage.error(text + "失败:" + e.message)
}) })

View File

@ -345,7 +345,7 @@
</div> <!-- end finish job list--> </div> <!-- end finish job list-->
</div> </div>
</div> </div>
<back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end task list box --> </div><!-- end task list box -->
</div> </div>
@ -476,6 +476,7 @@ import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session"; import {getSessionId} from "@/store/session";
import {useSharedStore} from "@/store/sharedata"; import {useSharedStore} from "@/store/sharedata";
import TaskList from "@/components/TaskList.vue"; import TaskList from "@/components/TaskList.vue";
import BackTop from "@/components/BackTop.vue";
const listBoxHeight = ref(0) const listBoxHeight = ref(0)
// const paramBoxHeight = ref(0) // const paramBoxHeight = ref(0)
@ -755,7 +756,7 @@ const publishImage = (event, item, action) => {
item.publish = action item.publish = action
page.value = 0 page.value = 0
isOver.value = false isOver.value = false
fetchFinishJobs() item.publish = action
}).catch(e => { }).catch(e => {
ElMessage.error(text + "失败:" + e.message) ElMessage.error(text + "失败:" + e.message)
}) })

View File

@ -163,7 +163,10 @@
<i class="iconfont icon-face"></i> <i class="iconfont icon-face"></i>
</div> </div>
</div> <back-top :right="30" :bottom="30" bg-color="#0f7a71"/>
</div><!-- end of waterfall -->
</div> </div>
<!-- 任务详情弹框 --> <!-- 任务详情弹框 -->
<el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true"> <el-dialog v-model="showTaskDialog" title="绘画任务详情" :fullscreen="true">
@ -301,6 +304,7 @@ import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import BackTop from "@/components/BackTop.vue";
const data = ref({ const data = ref({
"mj": [], "mj": [],

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="index-page" :style="{height: winHeight+'px'}"> <div class="index-page" :style="{height: winHeight+'px'}">
<div class="index-bg" :style="{backgroundImage: 'url('+bgImgUrl+')'}"></div> <div :class="theme.imageBg?'color-bg image-bg':'color-bg'" :style="{backgroundImage:'url('+bgStyle.backgroundImage+')', backgroundColor:bgStyle.backgroundColor}"></div>
<div class="menu-box"> <div class="menu-box">
<el-menu <el-menu
mode="horizontal" mode="horizontal"
@ -8,19 +8,19 @@
> >
<div class="menu-item"> <div class="menu-item">
<el-image :src="logo" alt="Geek-AI"/> <el-image :src="logo" alt="Geek-AI"/>
<div class="title">{{ title }}</div> <div class="title" :style="{color:theme.textColor}">{{ title }}</div>
</div> </div>
<div class="menu-item"> <div class="menu-item">
<span v-if="!licenseConfig.de_copy"> <span v-if="!license.de_copy">
<a :href="docsURL" target="_blank"> <a :href="docsURL" target="_blank">
<el-button type="primary" round> <el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
<i class="iconfont icon-book"></i> <i class="iconfont icon-book"></i>
<span>文档</span> <span>文档</span>
</el-button> </el-button>
</a> </a>
<a :href="gitURL" target="_blank"> <a :href="gitURL" target="_blank">
<el-button type="success" round> <el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" round>
<i class="iconfont icon-github"></i> <i class="iconfont icon-github"></i>
<span>源码</span> <span>源码</span>
</el-button> </el-button>
@ -28,38 +28,29 @@
</span> </span>
<span v-if="!isLogin"> <span v-if="!isLogin">
<el-button @click="router.push('/login')" round>登录</el-button> <el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/login')" class="shadow" round>登录</el-button>
<el-button @click="router.push('/register')" round>注册</el-button> <el-button :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" @click="router.push('/register')" class="shadow" round>注册</el-button>
</span> </span>
</div> </div>
</el-menu> </el-menu>
</div> </div>
<div class="content"> <div class="content">
<h1>欢迎使用 {{ title }}</h1> <h1 :style="{color:theme.textColor}">欢迎使用 {{ title }}</h1>
<p>{{ slogan }}</p> <p :style="{color:theme.textColor}">{{ slogan }}</p>
<el-button @click="router.push('/chat')" color="#ffffff" style="color:#007bff" :dark="false">
<i class="iconfont icon-chat"></i>
<span>AI 对话</span>
</el-button>
<el-button @click="router.push('/mj')" color="#C4CCFD" style="color:#424282" :dark="false">
<i class="iconfont icon-mj"></i>
<span>MJ 绘画</span>
</el-button>
<el-button @click="router.push('/sd')" color="#4AE6DF" style="color:#424282" :dark="false"> <div class="navs">
<i class="iconfont icon-sd"></i> <el-space wrap>
<span>SD 绘画</span> <div v-for="item in navs" class="nav-item">
</el-button> <el-button @click="router.push(item.url)" :color="theme.btnBgColor" :style="{color: theme.btnTextColor}" class="shadow" :dark="false">
<el-button @click="router.push('/xmind')" color="#FFFD55" style="color:#424282" :dark="false"> <i :class="'iconfont '+iconMap[item.url]"></i>
<i class="iconfont icon-xmind"></i> <span>{{item.name}}</span>
<span>思维导图</span> </el-button>
</el-button> </div>
<!-- <div id="animation-container"></div>--> </el-space>
</div>
</div> </div>
<div class="footer" v-if="!licenseConfig.de_copy"> <footer-bar :text-color="theme.textColor" />
<footer-bar />
</div>
</div> </div>
</template> </template>
@ -79,36 +70,102 @@ if (isMobile()) {
router.push("/mobile") router.push("/mobile")
} }
const title = ref("Geek-AI 创作系统") const title = ref("")
const logo = ref("/images/logo.png") const logo = ref("")
const slogan = ref("我辈之人,先干为敬,陪您先把 AI 用起来") const slogan = ref("")
const licenseConfig = ref({}) const license = ref({de_copy: true})
const winHeight = window.innerHeight - 150 const winHeight = window.innerHeight - 150
const bgImgUrl = ref('')
const isLogin = ref(false) const isLogin = ref(false)
const docsURL = ref(process.env.VUE_APP_DOCS_URL) const docsURL = ref(process.env.VUE_APP_DOCS_URL)
const gitURL = ref(process.env.VUE_APP_GIT_URL) const gitURL = ref(process.env.VUE_APP_GIT_URL)
const navs = ref([])
const btnColors = ref([
{bgColor: "#fff143", textColor: "#50616D"},
{bgColor: "#eaff56", textColor: "#50616D"},
{bgColor: "#bddd22", textColor: "#50616D"},
{bgColor: "#1bd1a5", textColor: "#50616D"},
{bgColor: "#e0eee8", textColor: "#50616D"},
{bgColor: "#7bcfa6", textColor: "#50616D"},
{bgColor: "#bce672", textColor: "#50616D"},
{bgColor: "#44cef6", textColor: "#ffffff"},
{bgColor: "#70f3ff", textColor: "#50616D"},
{bgColor: "#fffbf0", textColor: "#50616D"},
{bgColor: "#d6ecf0", textColor: "#50616D"},
{bgColor: "#88ada6", textColor: "#50616D"},
{bgColor: "#30dff3", textColor: "#50616D"},
{bgColor: "#d3e0f3", textColor: "#50616D"},
{bgColor: "#e9e7ef", textColor: "#50616D"},
{bgColor: "#eacd76", textColor: "#50616D"},
{bgColor: "#f2be45", textColor: "#50616D"},
{bgColor: "#549688", textColor: "#ffffff"},
{bgColor: "#758a99", textColor: "#ffffff"},
{bgColor: "#41555d", textColor: "#ffffff"},
{bgColor: "#21aa93", textColor: "#ffffff"},
{bgColor: "#0aa344", textColor: "#ffffff"},
{bgColor: "#f05654", textColor: "#ffffff"},
{bgColor: "#db5a6b", textColor: "#ffffff"},
{bgColor: "#db5a6b", textColor: "#ffffff"},
{bgColor: "#8d4bbb", textColor: "#ffffff"},
{bgColor: "#426666", textColor: "#ffffff"},
{bgColor: "#177cb0", textColor: "#ffffff"},
{bgColor: "#395260", textColor: "#ffffff"},
{bgColor: "#519a73", textColor: "#ffffff"},
{bgColor: "#75878a", textColor: "#ffffff"},
])
const iconMap =ref(
{
"/chat": "icon-chat",
"/mj": "icon-mj",
"/sd": "icon-sd",
"/dalle": "icon-dalle",
"/images-wall": "icon-image",
"/suno": "icon-suno",
"/xmind": "icon-xmind",
"/apps": "icon-app",
"/member": "icon-vip-user",
"/invite": "icon-share",
}
)
const bgStyle = {}
const color = btnColors.value[Math.floor(Math.random() * btnColors.value.length)]
const theme = ref({bgColor: "#ffffff", btnBgColor: color.bgColor, btnTextColor: color.textColor, textColor: "#ffffff", imageBg:true})
onMounted(() => { onMounted(() => {
httpGet("/api/config/get?key=system").then(res => { httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title title.value = res.data.title
logo.value = res.data.logo logo.value = res.data.logo
if (res.data.index_bg_url) { if (res.data.index_bg_url === 'color') {
bgImgUrl.value = res.data.index_bg_url //
theme.value.bgColor = color.bgColor
theme.value.btnBgColor = color.bgColor
theme.value.textColor = color.textColor
theme.value.btnTextColor = color.textColor
//
bgStyle.backgroundColor = theme.value.bgColor
bgStyle.backgroundImage = "/images/transparent-bg.png"
theme.value.imageBg = false
} else if (res.data.index_bg_url) {
bgStyle.backgroundImage = res.data.index_bg_url
} else { } else {
bgImgUrl.value = "/images/index-bg.jpg" bgStyle.backgroundImage = "/images/index-bg.jpg"
}
if (res.data.slogan) {
slogan.value = res.data.slogan
} }
slogan.value = res.data.slogan
}).catch(e => { }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message) ElMessage.error("获取系统配置失败:" + e.message)
}) })
httpGet("/api/config/license").then(res => { httpGet("/api/config/license").then(res => {
licenseConfig.value = res.data license.value = res.data
}).catch(e => { }).catch(e => {
ElMessage.error("获取 License 配置:" + e.message) license.value = {de_copy: false}
ElMessage.error("获取 License 配置失败:" + e.message)
})
httpGet("/api/menu/list?index=1").then(res => {
navs.value = res.data
}).catch(e => {
ElMessage.error("获取导航菜单失败:" + e.message)
}) })
checkSession().then(() => { checkSession().then(() => {
@ -119,107 +176,5 @@ onMounted(() => {
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css' @import '@/assets/iconfont/iconfont.css'
.index-page { @import "@/assets/css/index.styl"
margin: 0
overflow hidden
color #ffffff
display flex
justify-content center
align-items baseline
padding-top 150px
.index-bg {
position absolute
top 0
left 0
width 100vw
height 100vh
filter: blur(8px);
background-size: cover;
background-position: center;
}
.menu-box {
position absolute
top 0
width 100%
display flex
.el-menu {
padding 0 30px
width 100%
display flex
justify-content space-between
background none
border none
.menu-item {
display flex
padding 20px 0
color #ffffff
.title {
font-size 24px
padding 10px 10px 0 10px
}
.el-image {
height 50px
}
.el-button {
margin-left 10px
span {
margin-left 5px
}
}
}
}
}
.content {
text-align: center;
position relative
h1 {
font-size: 5rem;
margin-bottom: 1rem;
}
p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
.el-button {
padding: 25px 20px;
font-size: 1.3rem;
transition: all 0.3s ease;
.iconfont {
font-size 1.6rem
margin-right 10px
}
}
#animation-container {
display flex
justify-content center
width 100%
height: 300px;
position: absolute;
top: 350px
}
}
.footer {
.el-link__inner {
color #ffffff
}
}
}
</style> </style>

View File

@ -55,10 +55,8 @@
</div> </div>
<reset-pass @hide="showResetPass = false" :show="showResetPass"/> <reset-pass @hide="showResetPass = false" :show="showResetPass"/>
<footer class="footer" v-if="!licenseConfig.de_copy"> <footer-bar/>
<footer-bar/>
</footer>
</div> </div>
</div> </div>
</template> </template>
@ -81,7 +79,7 @@ const title = ref('Geek-AI');
const username = ref(process.env.VUE_APP_USER); const username = ref(process.env.VUE_APP_USER);
const password = ref(process.env.VUE_APP_PASS); const password = ref(process.env.VUE_APP_PASS);
const showResetPass = ref(false) const showResetPass = ref(false)
const logo = ref("/images/logo.png") const logo = ref("")
const licenseConfig = ref({}) const licenseConfig = ref({})
const wechatLoginURL = ref('') const wechatLoginURL = ref('')

View File

@ -106,19 +106,10 @@ import {useSharedStore} from "@/store/sharedata";
const leftBoxHeight = ref(window.innerHeight - 105) const leftBoxHeight = ref(window.innerHeight - 105)
const rightBoxHeight = ref(window.innerHeight - 115) const rightBoxHeight = ref(window.innerHeight - 115)
const title = ref("")
const prompt = ref("") const prompt = ref("")
const text = ref(`# Geek-AI 助手 const text = ref("")
- 完整的开源系统前端应用和后台管理系统皆可开箱即用
- 基于 Websocket 实现完美的打字机体验
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求
- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型
- 支持 MidJourney / Stable Diffusion AI 绘画集成开箱即用
- 支持使用个人微信二维码作为充值收费的支付渠道无需企业支付通道
- 已集成支付宝支付功能微信支付支持多种会员套餐和点卡购买功能
- 集成插件 API 功能可结合大语言模型的 function 功能开发各种强大的插件
`)
const md = require('markdown-it')({breaks: true}); const md = require('markdown-it')({breaks: true});
const content = ref(text.value) const content = ref(text.value)
const html = ref("") const html = ref("")
@ -135,7 +126,20 @@ const models = ref([])
const modelID = ref(0) const modelID = ref(0)
const loading = ref(false) const loading = ref(false)
onMounted(() => { httpGet("/api/config/get?key=system").then(res => {
title.value = res.data.title??process.env.VUE_APP_TITLE
text.value = `# ${title.value}
- 完整的开源系统前端应用和后台管理系统皆可开箱即用
- 基于 Websocket 实现完美的打字机体验
- 内置了各种预训练好的角色应用,轻松满足你的各种聊天和应用需求
- 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型
- 支持 MidJourney / Stable Diffusion AI 绘画集成开箱即用
- 支持使用个人微信二维码作为充值收费的支付渠道无需企业支付通道
- 已集成支付宝支付功能微信支付支持多种会员套餐和点卡购买功能
- 集成插件 API 功能可结合大语言模型的 function 功能开发各种强大的插件
`
content.value = text.value
initData() initData()
try { try {
markMap.value = Markmap.create(svgRef.value) markMap.value = Markmap.create(svgRef.value)
@ -145,7 +149,9 @@ onMounted(() => {
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
}); }).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
const initData = () => { const initData = () => {
httpGet("/api/model/list").then(res => { httpGet("/api/model/list").then(res => {
@ -333,7 +339,6 @@ const downloadImage = () => {
a.download = "geek-ai-xmind.png" a.download = "geek-ai-xmind.png"
a.href = canvas.toDataURL(`image/png`) a.href = canvas.toDataURL(`image/png`)
a.click() a.click()
} }
} }

View File

@ -183,8 +183,8 @@ import {validateEmail, validateMobile} from "@/utils/validate";
import {showMessageError, showMessageOK} from "@/utils/dialog"; import {showMessageError, showMessageOK} from "@/utils/dialog";
const router = useRouter(); const router = useRouter();
const title = ref('Geek-AI 用户注册'); const title = ref('');
const logo = ref("/images/logo") const logo = ref("")
const data = ref({ const data = ref({
username: '', username: '',
password: '', password: '',
@ -196,7 +196,7 @@ const data = ref({
const enableMobile = ref(false) const enableMobile = ref(false)
const enableEmail = ref(false) const enableEmail = ref(false)
const enableUser = ref(false) const enableUser = ref(false)
const enableRegister = ref(false) const enableRegister = ref(true)
const activeName = ref("mobile") const activeName = ref("mobile")
const wxImg = ref("/images/wx.png") const wxImg = ref("/images/wx.png")
const licenseConfig = ref({}) const licenseConfig = ref({})

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