diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b42a38..23884900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 * bug修复:修复移动端修改聊天标题不生效的问题 * Bug修复:修复用户注册不显示用户名的问题 diff --git a/api/config.sample.toml b/api/config.sample.toml index 4f2819a6..eb33a5d7 100644 --- a/api/config.sample.toml +++ b/api/config.sample.toml @@ -18,7 +18,7 @@ TikaHost = "http://tika:9998" DB = 0 [ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通 - ApiURL = "" + ApiURL = "https://sapi.geekai.me" AppId = "" Token = "" diff --git a/api/core/app_server.go b/api/core/app_server.go index 12a17567..3fb32096 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -83,7 +83,7 @@ func errorHandler(c *gin.Context) { if r := recover(); r != nil { logger.Errorf("Handler Panic: %v", r) 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() } }() @@ -139,7 +139,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc { if tokenString == "" { 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() return } 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/doPay" || 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/user/clogin") || strings.HasPrefix(c.Request.URL.Path, "/api/config/") || diff --git a/api/core/types/config.go b/api/core/types/config.go index a31be482..67845571 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -155,45 +155,6 @@ func (c RedisConfig) Url() string { 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 { Title string `json:"title,omitempty"` // 网站标题 Slogan string `json:"slogan,omitempty"` // 网站 slogan @@ -219,6 +180,7 @@ type SystemConfig struct { MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力 + SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址 @@ -228,4 +190,6 @@ type SystemConfig struct { SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词 IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片 + IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单 + Copyright string `json:"copyright"` // 版权信息 } diff --git a/api/core/types/task.go b/api/core/types/task.go index 129a83b4..0fb451b7 100644 --- a/api/core/types/task.go +++ b/api/core/types/task.go @@ -78,3 +78,19 @@ type DallTask struct { 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"` // 延长秒杀 +} diff --git a/api/handler/admin/admin_handler.go b/api/handler/admin/admin_handler.go index 001b94d2..eaaaf8fd 100644 --- a/api/handler/admin/admin_handler.go +++ b/api/handler/admin/admin_handler.go @@ -55,12 +55,6 @@ func (h *ManagerHandler) Login(c *gin.Context) { return } - //// add captcha - //if !base64Captcha.DefaultMemStore.Verify(data.CaptchaId, data.Captcha, true) { - // resp.ERROR(c, "验证码错误!") - // return - //} - var manager model.AdminUser res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager) if res.Error != nil { diff --git a/api/handler/admin/api_key_handler.go b/api/handler/admin/api_key_handler.go index f412c037..fc821398 100644 --- a/api/handler/admin/api_key_handler.go +++ b/api/handler/admin/api_key_handler.go @@ -31,7 +31,6 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler { func (h *ApiKeyHandler) Save(c *gin.Context) { var data struct { Id uint `json:"id"` - Platform string `json:"platform"` Name string `json:"name"` Type string `json:"type"` Value string `json:"value"` @@ -48,7 +47,6 @@ func (h *ApiKeyHandler) Save(c *gin.Context) { if data.Id > 0 { h.DB.Find(&apiKey, data.Id) } - apiKey.Platform = data.Platform apiKey.Value = data.Value apiKey.Type = data.Type apiKey.ApiURL = data.ApiURL diff --git a/api/handler/admin/chat_model_handler.go b/api/handler/admin/chat_model_handler.go index 3a1e6328..5f84e852 100644 --- a/api/handler/admin/chat_model_handler.go +++ b/api/handler/admin/chat_model_handler.go @@ -49,28 +49,32 @@ func (h *ChatModelHandler) Save(c *gin.Context) { return } - item := model.ChatModel{ - Platform: data.Platform, - Name: data.Name, - Value: data.Value, - Enabled: data.Enabled, - Open: data.Open, - MaxTokens: data.MaxTokens, - MaxContext: data.MaxContext, - Temperature: data.Temperature, - KeyId: data.KeyId, - Power: data.Power} + item := model.ChatModel{} + // 更新 + if data.Id > 0 { + h.DB.Where("id", data.Id).First(&item) + } + + item.Name = data.Name + item.Value = data.Value + item.Enabled = data.Enabled + item.SortNum = data.SortNum + 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 if data.Id > 0 { - item.Id = data.Id - item.SortNum = data.SortNum - res = h.DB.Select("*").Omit("created_at").Updates(&item) + res = h.DB.Save(&item) } else { res = h.DB.Create(&item) } if res.Error != nil { logger.Error("error with update database:", res.Error) - resp.ERROR(c, "更新数据库失败!") + resp.ERROR(c, res.Error.Error()) return } @@ -89,12 +93,12 @@ func (h *ChatModelHandler) Save(c *gin.Context) { func (h *ChatModelHandler) List(c *gin.Context) { session := h.DB.Session(&gorm.Session{}) enable := h.GetBool(c, "enable") - platform := h.GetTrim(c, "platform") + name := h.GetTrim(c, "name") if enable { session = session.Where("enabled", enable) } - if platform != "" { - session = session.Where("platform", platform) + if name != "" { + session = session.Where("name LIKE ?", name+"%") } var items []model.ChatModel var cms = make([]vo.ChatModel, 0) diff --git a/api/handler/admin/config_handler.go b/api/handler/admin/config_handler.go index 584b026b..c50c8d6f 100644 --- a/api/handler/admin/config_handler.go +++ b/api/handler/admin/config_handler.go @@ -50,6 +50,7 @@ func (h *ConfigHandler) Update(c *gin.Context) { Content string `json:"content,omitempty"` Updated bool `json:"updated,omitempty"` } `json:"config"` + ConfigBak types.SystemConfig `json:"config_bak,omitempty"` } if err := c.ShouldBindJSON(&data); err != nil { @@ -57,6 +58,12 @@ func (h *ConfigHandler) Update(c *gin.Context) { 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) config := model.Config{Key: data.Key, Config: value} res := h.DB.FirstOrCreate(&config, model.Config{Key: data.Key}) @@ -143,10 +150,9 @@ func (h *ConfigHandler) GetLicense(c *gin.Context) { // GetAppConfig 获取内置配置 func (h *ConfigHandler) GetAppConfig(c *gin.Context) { resp.SUCCESS(c, gin.H{ - "mj_plus": h.App.Config.MjPlusConfigs, - "mj_proxy": h.App.Config.MjProxyConfigs, - "sd": h.App.Config.SdConfigs, - "platforms": Platforms, + "mj_plus": h.App.Config.MjPlusConfigs, + "mj_proxy": h.App.Config.MjProxyConfigs, + "sd": h.App.Config.SdConfigs, }) } diff --git a/api/handler/admin/types.go b/api/handler/admin/types.go deleted file mode 100644 index c06139ba..00000000 --- a/api/handler/admin/types.go +++ /dev/null @@ -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, -} diff --git a/api/handler/admin/user_handler.go b/api/handler/admin/user_handler.go index 34747d77..c2386efc 100644 --- a/api/handler/admin/user_handler.go +++ b/api/handler/admin/user_handler.go @@ -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) if res.Error != nil { logger.Error("error with update database:", res.Error) - resp.ERROR(c, "更新数据库失败!") + resp.ERROR(c, res.Error.Error()) return } // 记录算力日志 @@ -136,6 +136,13 @@ func (h *UserHandler) Save(c *gin.Context) { }) } } else { + // 检查用户是否已经存在 + h.DB.Where("username", data.Username).First(&user) + if user.Id > 0 { + resp.ERROR(c, "用户名已存在") + return + } + salt := utils.RandString(8) u := model.User{ Username: data.Username, diff --git a/api/handler/chat_model_handler.go b/api/handler/chat_model_handler.go index 555de7c8..1b74f348 100644 --- a/api/handler/chat_model_handler.go +++ b/api/handler/chat_model_handler.go @@ -31,9 +31,14 @@ func (h *ChatModelHandler) List(c *gin.Context) { var items []model.ChatModel var chatModels = make([]vo.ChatModel, 0) var res *gorm.DB + session := h.DB.Session(&gorm.Session{}).Where("enabled", true) + t := c.Query("type") + if t != "" { + session = session.Where("type", t) + } // 如果用户没有登录,则加载所有开放模型 if !h.IsLogin(c) { - res = 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 { user, _ := h.GetLoginUser(c) var models []int diff --git a/api/handler/chat_role_handler.go b/api/handler/chat_role_handler.go index e220009a..5f85e145 100644 --- a/api/handler/chat_role_handler.go +++ b/api/handler/chat_role_handler.go @@ -29,45 +29,32 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { // List 获取用户聊天应用列表 func (h *ChatRoleHandler) List(c *gin.Context) { - all := h.GetBool(c, "all") + id := h.GetInt(c, "id", 0) userId := h.GetLoginUserId(c) 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) if res.Error != nil { - resp.SUCCESS(c, roleVos) - 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, "角色解析失败!") + resp.ERROR(c, res.Error.Error()) return } + var roleVos = make([]vo.ChatRole, 0) for _, r := range roles { - if !utils.Contains(roleKeys, r.Key) { - continue - } var v vo.ChatRole err := utils.CopyObject(r, &v) if err == nil { diff --git a/api/handler/chatimpl/azure_handler.go b/api/handler/chatimpl/azure_handler.go deleted file mode 100644 index bd28d720..00000000 --- a/api/handler/chatimpl/azure_handler.go +++ /dev/null @@ -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 -} diff --git a/api/handler/chatimpl/baidu_handler.go b/api/handler/chatimpl/baidu_handler.go deleted file mode 100644 index 783ac3e9..00000000 --- a/api/handler/chatimpl/baidu_handler.go +++ /dev/null @@ -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 -} diff --git a/api/handler/chatimpl/chat_handler.go b/api/handler/chatimpl/chat_handler.go index 1aeb58ee..a8c12362 100644 --- a/api/handler/chatimpl/chat_handler.go +++ b/api/handler/chatimpl/chat_handler.go @@ -116,8 +116,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) { MaxTokens: chatModel.MaxTokens, MaxContext: chatModel.MaxContext, Temperature: chatModel.Temperature, - KeyId: chatModel.KeyId, - Platform: chatModel.Platform} + KeyId: chatModel.KeyId} logger.Infof("New websocket connected, IP: %s", c.ClientIP()) go func() { @@ -208,21 +207,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio Model: session.Model.Value, Stream: true, } - switch session.Model.Platform { - case types.Azure.Value, types.ChatGLM.Value, types.Baidu.Value, types.XunFei.Value: - req.Temperature = session.Model.Temperature - req.MaxTokens = session.Model.MaxTokens - break - case types.OpenAI.Value: - 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 - } - + req.Temperature = session.Model.Temperature + req.MaxTokens = session.Model.MaxTokens + // OpenAI 支持函数功能 + var items []model.Function + res = h.DB.Where("enabled", true).Find(&items) + if res.Error == nil { var tools = make([]types.Tool, 0) for _, v := range items { var parameters map[string]interface{} @@ -248,15 +238,6 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio req.Tools = tools 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) - if session.Model.Platform == types.QWen.Value { - req.Input = make(map[string]interface{}) - reqMgs = append(reqMgs, types.Message{ - Role: "user", - Content: fullPrompt, - }) - req.Input["messages"] = reqMgs - } else if session.Model.Platform == types.OpenAI.Value || session.Model.Platform == types.Azure.Value { // extract image for gpt-vision model - 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, - }, - }) - } + // extract images from prompt + 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": "text", - "text": strings.TrimSpace(text), + "type": "image_url", + "image_url": gin.H{ + "url": v, + }, }) - content = data - } else { - content = fullPrompt } - req.Messages = append(reqMgs, map[string]interface{}{ - "role": "user", - "content": content, + data = append(data, gin.H{ + "type": "text", + "text": strings.TrimSpace(text), }) + content = data } else { - req.Messages = append(reqMgs, map[string]interface{}{ - "role": "user", - "content": fullPrompt, - }) + content = fullPrompt } + req.Messages = append(reqMgs, map[string]interface{}{ + "role": "user", + "content": content, + }) logger.Debugf("%+v", req.Messages) - switch session.Model.Platform { - 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 + return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) } // Tokens 统计 token 数量 @@ -478,55 +431,20 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi } // use the last unused key 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 { return nil, errors.New("no available key, please import key") } // ONLY allow apiURL in blank list - if session.Model.Platform == types.OpenAI.Value { - err := h.licenseService.IsValidApiURL(apiKey.ApiURL) - if err != nil { - return nil, err - } + err := h.licenseService.IsValidApiURL(apiKey.ApiURL) + if err != nil { + 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)) + apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) // 创建 HttpClient 请求对象 var client *http.Client requestBody, err := json.Marshal(req) @@ -550,28 +468,10 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi } else { 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) - switch session.Model.Platform { - case types.Azure.Value: - request.Header.Set("api-key", apiKey.Value) - 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 - } + logger.Debugf("Sending %s request, Channel:%s, API KEY:%s, PROXY: %s, Model: %s", session.Model.Platform, apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value)) + // 更新API KEY 最后使用时间 + h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix()) return client.Do(request) } @@ -708,7 +608,7 @@ func (h *ChatHandler) extractImgUrl(text string) string { continue } - newImgURL, err := h.uploadManager.GetUploadHandler().PutImg(imageURL, false) + newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false) if err != nil { logger.Error("error with download image: ", err) continue diff --git a/api/handler/chatimpl/chatglm_handler.go b/api/handler/chatimpl/chatglm_handler.go deleted file mode 100644 index 0192abc8..00000000 --- a/api/handler/chatimpl/chatglm_handler.go +++ /dev/null @@ -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 -} diff --git a/api/handler/chatimpl/openai_handler.go b/api/handler/chatimpl/openai_handler.go index 97f76531..775c8275 100644 --- a/api/handler/chatimpl/openai_handler.go +++ b/api/handler/chatimpl/openai_handler.go @@ -65,7 +65,6 @@ func (h *ChatHandler) sendOpenAiMessage( if !strings.Contains(line, "data:") || len(line) < 30 { continue } - var responseBody = types.ApiResponse{} err = json.Unmarshal([]byte(line[6:]), &responseBody) if err != nil { // 数据解析出错 @@ -74,7 +73,7 @@ func (h *ChatHandler) sendOpenAiMessage( if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 continue } - if responseBody.Choices[0].Delta.Content == nil { + if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil { continue } diff --git a/api/handler/chatimpl/qwen_handler.go b/api/handler/chatimpl/qwen_handler.go deleted file mode 100644 index 28bf66bb..00000000 --- a/api/handler/chatimpl/qwen_handler.go +++ /dev/null @@ -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 -} diff --git a/api/handler/chatimpl/xunfei_handler.go b/api/handler/chatimpl/xunfei_handler.go deleted file mode 100644 index 7ebea8d5..00000000 --- a/api/handler/chatimpl/xunfei_handler.go +++ /dev/null @@ -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)) -} diff --git a/api/handler/markmap_handler.go b/api/handler/markmap_handler.go index 368d12f7..e6926f07 100644 --- a/api/handler/markmap_handler.go +++ b/api/handler/markmap_handler.go @@ -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) { + + session := h.DB.Session(&gorm.Session{}) // if the chat model bind a KEY, use it directly - var res *gorm.DB if chatModel.KeyId > 0 { - res = h.DB.Where("id", chatModel.KeyId).Find(apiKey) - } - // use the last unused key - if apiKey.Id == 0 { - res = h.DB.Where("platform", types.OpenAI.Value). - Where("type", "chat"). - Where("enabled", true).Order("last_used_at ASC").First(apiKey) + session = session.Where("id", chatModel.KeyId) + } else { // use the last unused key + session = session.Where("type", "chat"). + Where("enabled", true).Order("last_used_at ASC") } + + res := session.First(apiKey) if res.Error != nil { return nil, errors.New("no available key, please import key") } - apiURL := apiKey.ApiURL + apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) // 更新 API KEY 的最后使用时间 h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix()) diff --git a/api/handler/menu_handler.go b/api/handler/menu_handler.go index 647ed1e0..39de1c78 100644 --- a/api/handler/menu_handler.go +++ b/api/handler/menu_handler.go @@ -27,9 +27,15 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler { // List 数据列表 func (h *MenuHandler) List(c *gin.Context) { + index := h.GetBool(c, "index") var items []model.Menu 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 { for _, item := range items { var product vo.Menu diff --git a/api/handler/mj_handler.go b/api/handler/mj_handler.go index 56340af1..5b49f51a 100644 --- a/api/handler/mj_handler.go +++ b/api/handler/mj_handler.go @@ -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) { session := h.DB.Session(&gorm.Session{}) if finish { - session = session.Where("progress = ?", 100).Order("id DESC") + session = session.Where("progress >= ?", 100).Order("id DESC") } else { session = session.Where("progress < ?", 100).Order("id ASC") } @@ -456,15 +456,44 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) { resp.ERROR(c, "记录不存在") return } + // remove job recode - res := h.DB.Delete(&job) - if res.Error != nil { - resp.ERROR(c, res.Error.Error()) + tx := h.DB.Begin() + if err := tx.Delete(&job).Error; err != nil { + tx.Rollback() + resp.ERROR(c, err.Error()) 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 - err := h.uploader.GetUploadHandler().Delete(job.ImgURL) + err = h.uploader.GetUploadHandler().Delete(job.ImgURL) if err != nil { logger.Error("remove image failed: ", err) } diff --git a/api/handler/suno_handler.go b/api/handler/suno_handler.go new file mode 100644 index 00000000..4fbf031a --- /dev/null +++ b/api/handler/suno_handler.go @@ -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) +} diff --git a/api/main.go b/api/main.go index 3de8eceb..d8d2cdba 100644 --- a/api/main.go +++ b/api/main.go @@ -23,6 +23,7 @@ import ( "geekai/service/payment" "geekai/service/sd" "geekai/service/sms" + "geekai/service/suno" "geekai/service/wx" "geekai/store" "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.NewHuPiPay), fx.Provide(payment.NewJPayService), @@ -475,6 +484,19 @@ func main() { group.GET("remove", h.Remove) 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) { go func() { err := s.Run(db) diff --git a/api/service/dalle/service.go b/api/service/dalle/service.go index 9a915da5..20d136c7 100644 --- a/api/service/dalle/service.go +++ b/api/service/dalle/service.go @@ -110,12 +110,11 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { prompt := task.Prompt // translate prompt if utils.HasChinese(prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt)) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini") if err == nil { prompt = content logger.Debugf("重写后提示词:%s", prompt) } - } var user model.User @@ -145,7 +144,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { // get image generation API KEY var apiKey model.ApiKey - tx = s.db.Where("type", "img"). + tx = s.db.Where("type", "dalle"). Where("enabled", true). Order("last_used_at ASC").First(&apiKey) if tx.Error != nil { @@ -157,6 +156,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { if len(apiKey.ProxyURL) > 5 { s.httpClient.SetProxyURL(apiKey.ProxyURL).R() } + apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL) reqBody := imgReq{ Model: "dall-e-3", Prompt: prompt, @@ -165,14 +165,13 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) { Style: task.Style, Quality: task.Quality, } - logger.Infof("Sending %s request, ApiURL:%s, API KEY:%s, BODY: %+v", apiKey.Platform, apiKey.ApiURL, apiKey.Value, reqBody) - request := s.httpClient.R().SetHeader("Content-Type", "application/json") - if apiKey.Platform == types.Azure.Value { - request = request.SetHeader("api-key", apiKey.Value) - } else { - request = request.SetHeader("Authorization", "Bearer "+apiKey.Value) - } - r, err := request.SetBody(reqBody).SetErrorResult(&errRes).SetSuccessResult(&res).Post(apiKey.ApiURL) + logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody) + r, err := s.httpClient.R().SetHeader("Content-Type", "application/json"). + SetHeader("Authorization", "Bearer "+apiKey.Value). + SetBody(reqBody). + SetErrorResult(&errRes). + SetSuccessResult(&res). + Post(apiURL) if err != nil { 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) { // sava image - imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false) + imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false) if err != nil { return "", err } diff --git a/api/service/mj/plus_client.go b/api/service/mj/plus_client.go index beb8943c..736c52b2 100644 --- a/api/service/mj/plus_client.go +++ b/api/service/mj/plus_client.go @@ -67,25 +67,7 @@ func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) { } } - logger.Info("API URL: ", 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 + return c.doRequest(body, apiURL) } // Blend 融图 @@ -112,23 +94,7 @@ func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) { } } } - 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() { - return ImageRes{}, fmt.Errorf("API 返回错误:%s", errRes.Error.Message) - } - - return res, nil + return c.doRequest(body, apiURL) } // SwapFace 换脸 @@ -165,23 +131,7 @@ func (c *PlusClient) SwapFace(task types.MjTask) (ImageRes, error) { }, "state": "", } - var res ImageRes - 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 + return c.doRequest(body, apiURL) } // Upscale 放大指定的图片 @@ -195,24 +145,7 @@ func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) { "taskId": task.MessageId, } apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.apiURL, c.Config.Mode) - logger.Info("API URL: ", 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 + return c.doRequest(body, apiURL) } // Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效 @@ -226,9 +159,14 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) { "taskId": task.MessageId, } 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 errRes ErrRes + logger.Info("API URL: ", apiURL) r, err := req.C().R(). SetHeader("Authorization", "Bearer "+c.Config.ApiKey). SetBody(body). @@ -236,7 +174,13 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) { SetErrorResult(&errRes). Post(apiURL) 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() { diff --git a/api/service/mj/pool.go b/api/service/mj/pool.go index 7cf84a60..80ed6067 100644 --- a/api/service/mj/pool.go +++ b/api/service/mj/pool.go @@ -8,7 +8,6 @@ package mj // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( - "fmt" "geekai/core/types" logger2 "geekai/logger" "geekai/service" @@ -139,7 +138,7 @@ func (p *ServicePool) DownloadImages() { if strings.HasPrefix(v.OrgURL, "https://cdn.discordapp.com") { proxy = true } - imgURL, err := p.uploaderManager.GetUploadHandler().PutImg(v.OrgURL, proxy) + imgURL, err := p.uploaderManager.GetUploadHandler().PutUrlFile(v.OrgURL, proxy) if err != nil { logger.Errorf("error with download image %s, %v", v.OrgURL, err) @@ -188,28 +187,6 @@ func (p *ServicePool) SyncTaskProgress() { } 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 { _ = servicePlus.Notify(job) } diff --git a/api/service/mj/service.go b/api/service/mj/service.go index baccd281..60b1fc50 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -29,6 +29,7 @@ type Service struct { notifyQueue *store.RedisQueue db *gorm.DB running bool + retryCount map[uint]int } 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, Client: cli, running: true, + retryCount: make(map[uint]int), } } +const failedProgress = 101 + func (s *Service) Run() { logger.Infof("Starting MidJourney job consumer for %s", s.Name) for s.running { @@ -55,15 +59,20 @@ func (s *Service) Run() { // 如果配置了多个中转平台的 API KEY // U,V 操作必须和 Image 操作属于同一个平台,否则找不到关联任务,需重新放回任务列表 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) s.taskQueue.RPush(task) + s.retryCount[task.Id]++ time.Sleep(time.Second) continue } // translate 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 { task.Prompt = content } else { @@ -72,7 +81,7 @@ func (s *Service) Run() { } // translate negative prompt 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 { task.NegPrompt = content } else { @@ -116,7 +125,7 @@ func (s *Service) Run() { } logger.Error("绘画任务执行失败:", errMsg) - job.Progress = -1 + job.Progress = failedProgress job.ErrMsg = errMsg // update the task progress s.db.Updates(&job) @@ -164,7 +173,7 @@ func (s *Service) Notify(job model.MidJourneyJob) error { // 任务执行失败了 if task.FailReason != "" { s.db.Model(&model.MidJourneyJob{Id: job.Id}).UpdateColumns(map[string]interface{}{ - "progress": -1, + "progress": failedProgress, "err_msg": task.FailReason, }) s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed}) diff --git a/api/service/oss/aliyun_oss.go b/api/service/oss/aliyun_oss.go index 00dcc8d7..d36ad139 100644 --- a/api/service/oss/aliyun_oss.go +++ b/api/service/oss/aliyun_oss.go @@ -84,25 +84,25 @@ func (s AliYunOss) PutFile(ctx *gin.Context, name string) (File, error) { }, nil } -func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) { - var imageData []byte +func (s AliYunOss) PutUrlFile(fileURL string, useProxy bool) (string, error) { + var fileData []byte var err error if useProxy { - imageData, err = utils.DownloadImage(imageURL, s.proxyURL) + fileData, err = utils.DownloadImage(fileURL, s.proxyURL) } else { - imageData, err = utils.DownloadImage(imageURL, "") + fileData, err = utils.DownloadImage(fileURL, "") } if err != nil { return "", fmt.Errorf("error with download image: %v", err) } - parse, err := url.Parse(imageURL) + parse, err := url.Parse(fileURL) if err != nil { return "", fmt.Errorf("error with parse image URL: %v", err) } fileExt := utils.GetImgExt(parse.Path) 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 { return "", err } diff --git a/api/service/oss/localstorage.go b/api/service/oss/localstorage.go index f64ff055..642f3d0d 100644 --- a/api/service/oss/localstorage.go +++ b/api/service/oss/localstorage.go @@ -57,8 +57,8 @@ func (s LocalStorage) PutFile(ctx *gin.Context, name string) (File, error) { }, nil } -func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) { - parse, err := url.Parse(imageURL) +func (s LocalStorage) PutUrlFile(fileURL string, useProxy bool) (string, error) { + parse, err := url.Parse(fileURL) if err != nil { 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 { - err = utils.DownloadFile(imageURL, filePath, s.proxyURL) + err = utils.DownloadFile(fileURL, filePath, s.proxyURL) } else { - err = utils.DownloadFile(imageURL, filePath, "") + err = utils.DownloadFile(fileURL, filePath, "") } if err != nil { return "", fmt.Errorf("error with download image: %v", err) diff --git a/api/service/oss/minio_oss.go b/api/service/oss/minio_oss.go index 5eaca499..d095127b 100644 --- a/api/service/oss/minio_oss.go +++ b/api/service/oss/minio_oss.go @@ -44,18 +44,18 @@ func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) { return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil } -func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) { - var imageData []byte +func (s MiniOss) PutUrlFile(fileURL string, useProxy bool) (string, error) { + var fileData []byte var err error if useProxy { - imageData, err = utils.DownloadImage(imageURL, s.proxyURL) + fileData, err = utils.DownloadImage(fileURL, s.proxyURL) } else { - imageData, err = utils.DownloadImage(imageURL, "") + fileData, err = utils.DownloadImage(fileURL, "") } if err != nil { return "", fmt.Errorf("error with download image: %v", err) } - parse, err := url.Parse(imageURL) + parse, err := url.Parse(fileURL) if err != nil { 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(), s.config.Bucket, filename, - strings.NewReader(string(imageData)), - int64(len(imageData)), + strings.NewReader(string(fileData)), + int64(len(fileData)), minio.PutObjectOptions{ContentType: "image/png"}) if err != nil { return "", err diff --git a/api/service/oss/qiniu_oss.go b/api/service/oss/qiniu_oss.go index 703b6d78..310be7cf 100644 --- a/api/service/oss/qiniu_oss.go +++ b/api/service/oss/qiniu_oss.go @@ -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) { - var imageData []byte +func (s QinNiuOss) PutUrlFile(fileURL string, useProxy bool) (string, error) { + var fileData []byte var err error if useProxy { - imageData, err = utils.DownloadImage(imageURL, s.proxyURL) + fileData, err = utils.DownloadImage(fileURL, s.proxyURL) } else { - imageData, err = utils.DownloadImage(imageURL, "") + fileData, err = utils.DownloadImage(fileURL, "") } if err != nil { return "", fmt.Errorf("error with download image: %v", err) } - parse, err := url.Parse(imageURL) + parse, err := url.Parse(fileURL) if err != nil { 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{} 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 { return "", err } diff --git a/api/service/oss/uploader.go b/api/service/oss/uploader.go index 435e22d7..d4caf835 100644 --- a/api/service/oss/uploader.go +++ b/api/service/oss/uploader.go @@ -23,7 +23,7 @@ type File struct { } type Uploader interface { 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) Delete(fileURL string) error } diff --git a/api/service/sd/service.go b/api/service/sd/service.go index 468e8b3e..dbb3a3c0 100644 --- a/api/service/sd/service.go +++ b/api/service/sd/service.go @@ -63,7 +63,7 @@ func (s *Service) Run() { // translate prompt if utils.HasChinese(task.Params.Prompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt)) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt), "gpt-4o-mini") if err == nil { task.Params.Prompt = content } else { @@ -73,7 +73,7 @@ func (s *Service) Run() { // translate negative prompt if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) { - content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt)) + content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), "gpt-4o-mini") if err == nil { task.Params.NegPrompt = content } else { diff --git a/api/service/suno/service.go b/api/service/suno/service.go new file mode 100644 index 00000000..8f4defe5 --- /dev/null +++ b/api/service/suno/service.go @@ -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 +} diff --git a/api/store/model/api_key.go b/api/store/model/api_key.go index fb7ae1d4..c32ff9bd 100644 --- a/api/store/model/api_key.go +++ b/api/store/model/api_key.go @@ -3,7 +3,6 @@ package model // ApiKey OpenAI API 模型 type ApiKey struct { BaseModel - Platform string Name string Type string // 用途 chat => 聊天,img => 绘图 Value string // API Key 的值 diff --git a/api/store/model/chat_model.go b/api/store/model/chat_model.go index 134655f3..c9a644ef 100644 --- a/api/store/model/chat_model.go +++ b/api/store/model/chat_model.go @@ -2,7 +2,6 @@ package model type ChatModel struct { BaseModel - Platform string Name string Value string // API Key 的值 SortNum int diff --git a/api/store/model/suno_job.go b/api/store/model/suno_job.go new file mode 100644 index 00000000..abbbd631 --- /dev/null +++ b/api/store/model/suno_job.go @@ -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" +} diff --git a/api/store/vo/api_key.go b/api/store/vo/api_key.go index 7321b13f..800d127b 100644 --- a/api/store/vo/api_key.go +++ b/api/store/vo/api_key.go @@ -3,7 +3,6 @@ package vo // ApiKey OpenAI API 模型 type ApiKey struct { BaseVo - Platform string `json:"platform"` Name string `json:"name"` Type string `json:"type"` Value string `json:"value"` // API Key 的值 diff --git a/api/store/vo/chat_model.go b/api/store/vo/chat_model.go index 4fb21051..0c270294 100644 --- a/api/store/vo/chat_model.go +++ b/api/store/vo/chat_model.go @@ -2,7 +2,6 @@ package vo type ChatModel struct { BaseVo - Platform string `json:"platform"` Name string `json:"name"` Value string `json:"value"` Enabled bool `json:"enabled"` @@ -12,6 +11,6 @@ type ChatModel struct { MaxTokens int `json:"max_tokens"` // 最大响应长度 MaxContext int `json:"max_context"` // 最大上下文长度 Temperature float32 `json:"temperature"` // 模型温度 - KeyId int `json:"key_id"` + KeyId int `json:"key_id,omitempty"` KeyName string `json:"key_name"` } diff --git a/api/store/vo/suno_job.go b/api/store/vo/suno_job.go new file mode 100644 index 00000000..fbc752de --- /dev/null +++ b/api/store/vo/suno_job.go @@ -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" +} diff --git a/api/utils/common.go b/api/utils/common.go index 142256d8..cb76c4c1 100644 --- a/api/utils/common.go +++ b/api/utils/common.go @@ -84,6 +84,8 @@ func CopyObject(src interface{}, dst interface{}) error { case reflect.Bool: value.SetBool(v.Bool()) break + default: + value.Set(v) } } diff --git a/api/utils/openai.go b/api/utils/openai.go index 9ee01a35..fdbed5f9 100644 --- a/api/utils/openai.go +++ b/api/utils/openai.go @@ -8,12 +8,14 @@ package utils // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ import ( + "encoding/json" "fmt" "geekai/core/types" "geekai/store/model" "github.com/imroc/req/v3" "github.com/pkoukk/tiktoken-go" "gorm.io/gorm" + "io" "time" ) @@ -43,18 +45,9 @@ type apiRes struct { } `json:"choices"` } -type apiErrRes struct { - 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) { +func OpenAIRequest(db *gorm.DB, prompt string, modelName string) (string, error) { 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 { 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 errRes apiErrRes client := req.C() if len(apiKey.ProxyURL) > 5 { client.SetProxyURL(apiKey.ApiURL) } + apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) r, err := client.R().SetHeader("Content-Type", "application/json"). SetHeader("Authorization", "Bearer "+apiKey.Value). SetBody(types.ApiRequest{ - Model: "gpt-3.5-turbo", + Model: modelName, Temperature: 0.9, MaxTokens: 1024, Stream: false, Messages: messages, - }). - SetErrorResult(&errRes). - SetSuccessResult(&response).Post(apiKey.ApiURL) - if err != nil || r.IsErrorState() { - return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message) + }).Post(apiURL) + if err != nil { + return "", fmt.Errorf("请求 OpenAI API失败:%v", err) + } + body, _ := io.ReadAll(r.Body) + err = json.Unmarshal(body, &response) + if err != nil { + return "", fmt.Errorf("解析API数据失败:%v, %s", err, string(body)) } // 更新 API KEY 的最后使用时间 diff --git a/database/chatgpt_plus-v4.1.1.sql b/database/chatgpt_plus-v4.1.1.sql new file mode 100644 index 00000000..110117f5 --- /dev/null +++ b/database/chatgpt_plus-v4.1.1.sql @@ -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支持MidJourney,GPT,Claude,Google 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.5,GPT-4,DALL-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 */; diff --git a/database/update-v4.1.1.sql b/database/update-v4.1.1.sql new file mode 100644 index 00000000..f7fe282f --- /dev/null +++ b/database/update-v4.1.1.sql @@ -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`; \ No newline at end of file diff --git a/deploy/conf/config.toml b/deploy/conf/config.toml index fee98bc8..2b834a43 100644 --- a/deploy/conf/config.toml +++ b/deploy/conf/config.toml @@ -21,7 +21,7 @@ TikaHost = "http://tika:9998" DB = 0 [ApiConfig] - ApiURL = "http://service.r9it.com:9001" + ApiURL = "http://sapi.geekai.me" AppId = "" Token = "" diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml index ac0efd92..c4d21cb0 100644 --- a/deploy/docker-compose.yaml +++ b/deploy/docker-compose.yaml @@ -45,7 +45,7 @@ services: restart: always ports: - "9998:9998" - + midjourney-proxy: image: registry.cn-shenzhen.aliyuncs.com/geekmaster/midjourney-proxy:2.6.2 container_name: geekai-midjourney-proxy @@ -56,9 +56,9 @@ services: - ./conf/mj-proxy:/home/spring/config - # 后端 API 程序 + # 后端 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 restart: always depends_on: @@ -80,7 +80,7 @@ services: # 前端应用 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 restart: always depends_on: @@ -91,5 +91,4 @@ services: - ./logs/nginx:/var/log/nginx - ./conf/nginx/conf.d:/etc/nginx/conf.d - ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf - - ./conf/nginx/ssl:/etc/nginx/ssl - + - ./conf/nginx/ssl:/etc/nginx/ssl \ No newline at end of file diff --git a/web/.env.development b/web/.env.development index 5a9770cd..ba889b03 100644 --- a/web/.env.development +++ b/web/.env.development @@ -6,4 +6,6 @@ VUE_APP_ADMIN_USER=admin VUE_APP_ADMIN_PASS=admin123 VUE_APP_KEY_PREFIX=ChatPLUS_DEV_ 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 diff --git a/web/.env.production b/web/.env.production index 8afbdcae..67a06c21 100644 --- a/web/.env.production +++ b/web/.env.production @@ -2,4 +2,7 @@ VUE_APP_API_HOST= VUE_APP_WS_HOST= VUE_APP_KEY_PREFIX=ChatPLUS_ 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 + diff --git a/web/package-lock.json b/web/package-lock.json index 91d5209d..92bf98f4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -26,6 +26,7 @@ "markmap-toolbar": "^0.17.0", "markmap-view": "^0.16.0", "md-editor-v3": "^2.2.1", + "memfs": "^4.9.3", "mitt": "^3.0.1", "pinia": "^2.1.4", "qrcode": "^1.5.3", @@ -1917,6 +1918,57 @@ "@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": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz", @@ -6965,9 +7017,9 @@ } }, "node_modules/fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", "dev": true }, "node_modules/fs.realpath": { @@ -7381,6 +7433,14 @@ "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": { "version": "0.4.24", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -8480,15 +8540,21 @@ } }, "node_modules/memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "dev": true, + "version": "4.9.3", + "resolved": "https://registry.npmmirror.com/memfs/-/memfs-4.9.3.tgz", + "integrity": "sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==", "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": { "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" } }, "node_modules/memoize-one": { @@ -11394,6 +11460,17 @@ "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": { "version": "3.0.4", "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", "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": { "version": "2.4.0", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", @@ -12225,6 +12317,18 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "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": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz", @@ -14028,6 +14132,29 @@ "@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": { "version": "2.0.3", "resolved": "https://registry.npmmirror.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz", @@ -18175,9 +18302,9 @@ } }, "fs-monkey": { - "version": "1.0.3", - "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", "dev": true }, "fs.realpath": { @@ -18512,6 +18639,11 @@ "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true }, + "hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -19387,12 +19519,14 @@ "dev": true }, "memfs": { - "version": "3.4.1", - "resolved": "https://registry.npmmirror.com/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", - "dev": true, + "version": "4.9.3", + "resolved": "https://registry.npmmirror.com/memfs/-/memfs-4.9.3.tgz", + "integrity": "sha512-bsYSSnirtYTWi1+OPMFb0M048evMKyUYe0EbtuGQgq6BVQM1g1W8/KIUJCCvjgI/El0j6Q4WsmMiBwLUBSw8LA==", "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": { @@ -21633,6 +21767,12 @@ "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": { "version": "3.0.4", "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", "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": { "version": "2.4.0", "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz", @@ -22301,6 +22447,15 @@ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "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": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-4.0.0.tgz", diff --git a/web/package.json b/web/package.json index e9969bba..46943877 100644 --- a/web/package.json +++ b/web/package.json @@ -26,6 +26,7 @@ "markmap-toolbar": "^0.17.0", "markmap-view": "^0.16.0", "md-editor-v3": "^2.2.1", + "memfs": "^4.9.3", "mitt": "^3.0.1", "pinia": "^2.1.4", "qrcode": "^1.5.3", diff --git a/web/public/files/suno.mp3 b/web/public/files/suno.mp3 new file mode 100644 index 00000000..4aa2a844 Binary files /dev/null and b/web/public/files/suno.mp3 differ diff --git a/web/public/files/test.mp3 b/web/public/files/test.mp3 new file mode 100644 index 00000000..2a518c05 Binary files /dev/null and b/web/public/files/test.mp3 differ diff --git a/web/public/images/create-new.svg b/web/public/images/create-new.svg new file mode 100644 index 00000000..335cf9a6 --- /dev/null +++ b/web/public/images/create-new.svg @@ -0,0 +1,9 @@ + diff --git a/web/public/images/ext/mp3.png b/web/public/images/ext/mp3.png new file mode 100644 index 00000000..f2cdd791 Binary files /dev/null and b/web/public/images/ext/mp3.png differ diff --git a/web/public/images/ext/mp4.png b/web/public/images/ext/mp4.png new file mode 100644 index 00000000..6dc6486d Binary files /dev/null and b/web/public/images/ext/mp4.png differ diff --git a/web/public/images/menu/music.png b/web/public/images/menu/music.png new file mode 100644 index 00000000..c699e828 Binary files /dev/null and b/web/public/images/menu/music.png differ diff --git a/web/public/images/menu/suno.png b/web/public/images/menu/suno.png new file mode 100644 index 00000000..ecd4b9cd Binary files /dev/null and b/web/public/images/menu/suno.png differ diff --git a/web/public/images/play.svg b/web/public/images/play.svg new file mode 100644 index 00000000..970646c9 --- /dev/null +++ b/web/public/images/play.svg @@ -0,0 +1,3 @@ + diff --git a/web/public/images/transparent-bg.png b/web/public/images/transparent-bg.png new file mode 100644 index 00000000..8b1a375c Binary files /dev/null and b/web/public/images/transparent-bg.png differ diff --git a/web/src/assets/css/image-mj.styl b/web/src/assets/css/image-mj.styl index dd4775a8..61845709 100644 --- a/web/src/assets/css/image-mj.styl +++ b/web/src/assets/css/image-mj.styl @@ -79,7 +79,7 @@ background-color #383838 border 1px solid #454545 border-radius 5px - padding 10px + padding 5px margin-bottom 10px display flex flex-flow column @@ -91,12 +91,13 @@ } .el-image { - height 60px + height 30px width 100% } .text { - margin-top 6px + margin-top 4px + font-size 12px } } @@ -420,9 +421,27 @@ flex-flow column justify-content center align-items center - min-height 200px + min-height 220px 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 { font-size 50px margin-bottom 10px diff --git a/web/src/assets/css/index.styl b/web/src/assets/css/index.styl new file mode 100644 index 00000000..8a47c12a --- /dev/null +++ b/web/src/assets/css/index.styl @@ -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 + } + } + +} \ No newline at end of file diff --git a/web/src/assets/css/song.styl b/web/src/assets/css/song.styl new file mode 100644 index 00000000..ef8c73c2 --- /dev/null +++ b/web/src/assets/css/song.styl @@ -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 + } + +} \ No newline at end of file diff --git a/web/src/assets/css/suno.styl b/web/src/assets/css/suno.styl new file mode 100644 index 00000000..ffe39119 --- /dev/null +++ b/web/src/assets/css/suno.styl @@ -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 + } + } +} \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css index 387f2cc0..842956a9 100644 --- a/web/src/assets/iconfont/iconfont.css +++ b/web/src/assets/iconfont/iconfont.css @@ -1,8 +1,8 @@ @font-face { font-family: "iconfont"; /* Project id 4125778 */ - src: url('iconfont.woff2?t=1715987806624') format('woff2'), - url('iconfont.woff?t=1715987806624') format('woff'), - url('iconfont.ttf?t=1715987806624') format('truetype'); + src: url('iconfont.woff2?t=1721896403264') format('woff2'), + url('iconfont.woff?t=1721896403264') format('woff'), + url('iconfont.ttf?t=1721896403264') format('truetype'); } .iconfont { @@ -13,6 +13,62 @@ -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 { content: "\e624"; } diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js index 1a71104c..7048f335 100644 --- a/web/src/assets/iconfont/iconfont.js +++ b/web/src/assets/iconfont/iconfont.js @@ -1 +1 @@ -window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,s=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?s(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,m())})}function m(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}m()}}(window); \ No newline at end of file +window._iconfont_svg_string_4125778='',function(a){var l=(l=document.getElementsByTagName("script"))[l.length-1],c=l.getAttribute("data-injectcss"),l=l.getAttribute("data-disable-injectsvg");if(!l){var t,h,i,o,z,m=function(l,c){c.parentNode.insertBefore(l,c)};if(c&&!a.__iconfont__svg__cssinject__){a.__iconfont__svg__cssinject__=!0;try{document.write("")}catch(l){console&&console.log(l)}}t=function(){var l,c=document.createElement("div");c.innerHTML=a._iconfont_svg_string_4125778,(c=c.getElementsByTagName("svg")[0])&&(c.setAttribute("aria-hidden","true"),c.style.position="absolute",c.style.width=0,c.style.height=0,c.style.overflow="hidden",c=c,(l=document.body).firstChild?m(c,l.firstChild):l.appendChild(c))},document.addEventListener?~["complete","loaded","interactive"].indexOf(document.readyState)?setTimeout(t,0):(h=function(){document.removeEventListener("DOMContentLoaded",h,!1),t()},document.addEventListener("DOMContentLoaded",h,!1)):document.attachEvent&&(i=t,o=a.document,z=!1,p(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function p(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(p,50)}s()}}(window); \ No newline at end of file diff --git a/web/src/assets/iconfont/iconfont.json b/web/src/assets/iconfont/iconfont.json index d1cd109e..ffc0d972 100644 --- a/web/src/assets/iconfont/iconfont.json +++ b/web/src/assets/iconfont/iconfont.json @@ -5,6 +5,104 @@ "css_prefix_text": "icon-", "description": "", "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", "name": "control", diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf index 33495382..32d02e7b 100644 Binary files a/web/src/assets/iconfont/iconfont.ttf and b/web/src/assets/iconfont/iconfont.ttf differ diff --git a/web/src/assets/iconfont/iconfont.woff b/web/src/assets/iconfont/iconfont.woff index 4d646f56..eb517115 100644 Binary files a/web/src/assets/iconfont/iconfont.woff and b/web/src/assets/iconfont/iconfont.woff differ diff --git a/web/src/assets/iconfont/iconfont.woff2 b/web/src/assets/iconfont/iconfont.woff2 index 25f324cf..7d4d825e 100644 Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ diff --git a/web/src/assets/img/suno-create-bg.svg b/web/src/assets/img/suno-create-bg.svg new file mode 100644 index 00000000..8ac78e05 --- /dev/null +++ b/web/src/assets/img/suno-create-bg.svg @@ -0,0 +1,47 @@ + diff --git a/web/src/components/BackTop.vue b/web/src/components/BackTop.vue new file mode 100644 index 00000000..3fbab35a --- /dev/null +++ b/web/src/components/BackTop.vue @@ -0,0 +1,77 @@ + + + + + + + \ No newline at end of file diff --git a/web/src/components/ChatMidJourney.vue b/web/src/components/ChatMidJourney.vue deleted file mode 100644 index 8f3a5842..00000000 --- a/web/src/components/ChatMidJourney.vue +++ /dev/null @@ -1,244 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue index 746a9907..92dd392c 100644 --- a/web/src/components/ChatPrompt.vue +++ b/web/src/components/ChatPrompt.vue @@ -70,7 +70,7 @@
@@ -132,27 +132,38 @@ const content =ref(processPrompt(props.data.content)) const files = ref([]) onMounted(() => { - if (!finalTokens.value) { - httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => { - finalTokens.value = res.data; - }).catch(() => { - }) - } + // if (!finalTokens.value) { + // httpPost("/api/chat/tokens", {text: props.data.content, model: props.data.model}).then(res => { + // finalTokens.value = res.data; + // }).catch(() => { + // }) + // } const linkRegex = /(https?:\/\/\S+)/g; const links = props.data.content.match(linkRegex); if (links) { httpPost("/api/upload/list", {urls: links}).then(res => { files.value = res.data + + for (let link of links) { + if (isExternalImg(link, files.value)) { + files.value.push({url:link, ext: ".png"}) + } + } }).catch(() => { }) for (let link of links) { content.value = content.value.replace(link,"") } + } content.value = md.render(content.value.trim()) }) + +const isExternalImg = (link, files) => { + return isImage(link) && !files.find(file => file.url === link) +} diff --git a/web/src/components/admin/AdminSidebar.vue b/web/src/components/admin/AdminSidebar.vue index 4c0aebda..9238b58e 100644 --- a/web/src/components/admin/AdminSidebar.vue +++ b/web/src/components/admin/AdminSidebar.vue @@ -58,8 +58,8 @@ import {httpGet} from "@/utils/http"; import {ElMessage} from "element-plus"; import {useRoute} from "vue-router"; -const title = ref('Chat-Plus-Admin') -const logo = ref('/images/logo.png') +const title = ref('') +const logo = ref('') // 加载系统配置 httpGet('/api/admin/config/get?key=system').then(res => { diff --git a/web/src/components/ui/BlackDialog.vue b/web/src/components/ui/BlackDialog.vue new file mode 100644 index 00000000..7794afb0 --- /dev/null +++ b/web/src/components/ui/BlackDialog.vue @@ -0,0 +1,106 @@ + +{{ slogan }}
-{{ slogan }}
-注意:如果是百度文心一言平台,API-KEY 为 APIKey|SecretKey,中间用竖线(|)连接
-注意:如果是讯飞星火大模型,API-KEY 为 AppId|APIKey|APISecret,中间用竖线(|)连接
-