diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3cdc2f3..086f8f0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,25 @@
# 更新日志
-## v4.0.3 2024-04-15
-* Bug修复:修复MJ-PLUS 服务会自动删除10分钟前的任务问题
-* Bug修复:修复MJ 的 U/V 操作会强制使用 Fast 模式 Bug
+## v4.0.4
+* Bug修复:修复统一千问第二句不回复的问题
+* 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度
+* 功能新增:Dalle AI 绘画功能实现
+* Bug修复:修复思维导图格式乱码问题
+* 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题
+* 功能新增:支持从应用列表直接和某个应用对话
+* 功能优化:优化算力日志的页面和首页的UI
+* 功能新增:支持思维导图导出 PNG 图片下载
+## v4.0.3
+
+* 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型
+* Bug修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug
+* Bug修复:修复MidJourney在任务超时后出现后面的任务覆盖前面任务的问题
+* 功能新增:支持上传图片和视觉模型
+* 功能优化:优化聊天页面的复制代码按钮样式乱码
+* 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图
+* 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用GPT3.5或者 GPT4
+* 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。
+* 功能新增:支持管理后台 Logo 修改
## v4.0.2
@@ -323,4 +340,4 @@
5. 保存聊天记录,支持聊天上下文。
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
7. 引入 ip2region 组件,记录用户的登录IP和地址。
-8. 支持会话搜索过滤。
\ No newline at end of file
+8. 支持会话搜索过滤。
diff --git a/LICENSE b/LICENSE
index e0839217..29f81d81 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,201 @@
-MIT License
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
-Copyright (c) 2023 RockYang
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
+ 1. Definitions.
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index c21dd3d6..23c213cf 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,29 @@
# ChatGPT-Plus
**ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
-ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。主要有如下特性:
+ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。
-* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
-* 基于 Websocket 实现,完美的打字机体验。
-* 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
-* 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
-* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
-* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
-* 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
-* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
+主要特性:
+
+- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
+- 基于 Websocket 实现,完美的打字机体验。
+- 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
+- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
+- 支持 Suno 文生音乐
+- 支持 MidJourney / Stable Diffusion AI 绘画集成,文生图,图生图,换脸,融图。开箱即用。
+- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
+- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
+- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
+### 🚀 更多功能请查看 [Geek-AI](https://github.com/yangjian102621/geek-ai)
+
+- [x] 更友好的 UI 界面
+- [x] 支持 Dall-E 文生图功能
+- [x] 支持文生思维导图
+- [x] 支持为模型绑定指定的 API KEY,支持为角色绑定指定的模型等功能
+- [x] 支持网站 Logo 版权等信息的修改
+
## 功能截图
### PC 端聊天界面
@@ -73,7 +84,7 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
**演示站不提供任何充值点卡售卖或者VIP充值服务。** 如果您体验过后觉得还不错的话,可以花两分钟用下面的一键部署脚本自己部署一套。
```shell
-bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v4.0.2-ba5a891bc0.sh)"
+bash -c "$(curl -fsSL https://img.r9it.com/geekai/v4.0.6/install.sh)"
```
最新版本的一键部署脚本请参考 [**ChatGPT-Plus 文档**](https://ai.r9it.com/docs/install/)。
@@ -96,7 +107,7 @@ KEY。
## 使用须知
-1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
+1. 本项目基于 Apache2.0 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
## 项目地址
diff --git a/api/core/app_server.go b/api/core/app_server.go
index 76689b4c..d63600a8 100644
--- a/api/core/app_server.go
+++ b/api/core/app_server.go
@@ -12,11 +12,11 @@ import (
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"github.com/nfnt/resize"
+ "golang.org/x/image/webp"
"gorm.io/gorm"
"image"
"image/jpeg"
"io"
- "log"
"net/http"
"os"
"runtime/debug"
@@ -215,9 +215,12 @@ func needLogin(c *gin.Context) bool {
c.Request.URL.Path == "/api/invite/hits" ||
c.Request.URL.Path == "/api/sd/imgWall" ||
c.Request.URL.Path == "/api/sd/client" ||
+ c.Request.URL.Path == "/api/dall/imgWall" ||
+ c.Request.URL.Path == "/api/dall/client" ||
c.Request.URL.Path == "/api/config/get" ||
c.Request.URL.Path == "/api/product/list" ||
c.Request.URL.Path == "/api/menu/list" ||
+ c.Request.URL.Path == "/api/markMap/client" ||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
@@ -331,6 +334,10 @@ func staticResourceMiddleware() gin.HandlerFunc {
// 解码图片
img, _, err := image.Decode(file)
+ // for .webp image
+ if err != nil {
+ img, err = webp.Decode(file)
+ }
if err != nil {
c.String(http.StatusInternalServerError, "Error decoding image")
return
@@ -347,7 +354,9 @@ func staticResourceMiddleware() gin.HandlerFunc {
var buffer bytes.Buffer
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
if err != nil {
- log.Fatal(err)
+ logger.Error(err)
+ c.String(http.StatusInternalServerError, err.Error())
+ return
}
// 设置图片缓存有效期为一年 (365天)
diff --git a/api/core/config.go b/api/core/config.go
index 5447542e..74f7e305 100644
--- a/api/core/config.go
+++ b/api/core/config.go
@@ -23,7 +23,7 @@ func NewDefaultConfig() *types.AppConfig {
SecretKey: utils.RandString(64),
MaxAge: 86400,
},
- ApiConfig: types.ChatPlusApiConfig{},
+ ApiConfig: types.ApiConfig{},
OSS: types.OSSConfig{
Active: "local",
Local: types.LocalStorageConfig{
diff --git a/api/core/types/chat.go b/api/core/types/chat.go
index 7ba2a252..b6b63aa2 100644
--- a/api/core/types/chat.go
+++ b/api/core/types/chat.go
@@ -8,7 +8,7 @@ type ApiRequest struct {
Stream bool `json:"stream"`
Messages []interface{} `json:"messages,omitempty"`
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
- Tools []interface{} `json:"tools,omitempty"`
+ Tools []Tool `json:"tools,omitempty"`
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
ToolChoice string `json:"tool_choice,omitempty"`
@@ -62,6 +62,7 @@ type ChatModel struct {
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
+ KeyId int `json:"key_id"` // 绑定 API KEY
}
type ApiError struct {
diff --git a/api/core/types/config.go b/api/core/types/config.go
index b651f5ee..f501fea4 100644
--- a/api/core/types/config.go
+++ b/api/core/types/config.go
@@ -14,7 +14,7 @@ type AppConfig struct {
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
- ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
+ ApiConfig ApiConfig // ChatPlus API authorization configs
SMS SMSConfig // send mobile message config
OSS OSSConfig // OSS config
MjProxyConfigs []MjProxyConfig // MJ proxy config
@@ -30,6 +30,7 @@ type AppConfig struct {
}
type SmtpConfig struct {
+ UseTls bool // 是否使用 TLS 发送
Host string
Port int
AppName string // 应用名称
@@ -37,7 +38,7 @@ type SmtpConfig struct {
Password string // 发件人邮箱密码
}
-type ChatPlusApiConfig struct {
+type ApiConfig struct {
ApiURL string
AppId string
Token string
@@ -127,6 +128,17 @@ type RedisConfig struct {
DB int
}
+// LicenseKey 存储许可证书的 KEY
+const LicenseKey = "Geek-AI-License"
+
+type License struct {
+ Key string // 许可证书密钥
+ MachineId string // 机器码
+ UserNum int // 用户数量
+ ExpiredAt int64 // 过期时间
+ IsActive bool // 是否激活
+}
+
func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
@@ -149,7 +161,7 @@ type SystemConfig struct {
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
- RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机,邮箱注册,账号密码注册
+ RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
RewardImg string `json:"reward_img,omitempty"` // 众筹收款二维码地址
diff --git a/api/core/types/function.go b/api/core/types/function.go
index 8b5f183f..09808461 100644
--- a/api/core/types/function.go
+++ b/api/core/types/function.go
@@ -8,19 +8,14 @@ type ToolCall struct {
} `json:"function"`
}
+type Tool struct {
+ Type string `json:"type"`
+ Function Function `json:"function"`
+}
+
type Function struct {
- Name string `json:"name"`
- Description string `json:"description"`
- Parameters Parameters `json:"parameters"`
-}
-
-type Parameters struct {
- Type string `json:"type"`
- Required []string `json:"required"`
- Properties map[string]Property `json:"properties"`
-}
-
-type Property struct {
- Type string `json:"type"`
- Description string `json:"description"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Parameters map[string]interface{} `json:"parameters"`
+ Required interface{} `json:"required,omitempty"`
}
diff --git a/api/core/types/task.go b/api/core/types/task.go
index cd4b516e..bb1f7689 100644
--- a/api/core/types/task.go
+++ b/api/core/types/task.go
@@ -59,3 +59,16 @@ type SdTaskParams struct {
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
}
+
+// DallTask DALL-E task
+type DallTask struct {
+ JobId uint `json:"job_id"`
+ UserId uint `json:"user_id"`
+ Prompt string `json:"prompt"`
+ N int `json:"n"`
+ Quality string `json:"quality"`
+ Size string `json:"size"`
+ Style string `json:"style"`
+
+ Power int `json:"power"`
+}
diff --git a/api/core/types/web.go b/api/core/types/web.go
index 601612fa..041a9859 100644
--- a/api/core/types/web.go
+++ b/api/core/types/web.go
@@ -21,7 +21,7 @@ const (
WsStart = WsMsgType("start")
WsMiddle = WsMsgType("middle")
WsEnd = WsMsgType("end")
- WsMjImg = WsMsgType("mj")
+ WsErr = WsMsgType("error")
)
type BizCode int
diff --git a/api/go.mod b/api/go.mod
index cfd06288..25e75649 100644
--- a/api/go.mod
+++ b/api/go.mod
@@ -28,6 +28,7 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0
require (
github.com/chanxuehong/wechat v0.0.0-20230222024006-36f0325263cd
github.com/mojocn/base64Captcha v1.3.1
+ github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.3.1
github.com/syndtr/goleveldb v1.0.0
github.com/wechatpay-apiv3/wechatpay-go v0.2.18
@@ -35,9 +36,16 @@ require (
require (
github.com/chanxuehong/rand v0.0.0-20211009035549-2f07823e8e99 // indirect
+ golang.org/x/image v0.0.0-20211028202545-6944b10bf410
+)
+
+require (
+ github.com/go-ole/go-ole v1.2.6 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
- golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
+ github.com/tklauser/go-sysconf v0.3.13 // indirect
+ github.com/tklauser/numcpus v0.7.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
)
require (
@@ -110,6 +118,6 @@ require (
go.uber.org/fx v1.19.3
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.12.0
- golang.org/x/sys v0.11.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
gorm.io/gorm v1.25.1
)
diff --git a/api/go.sum b/api/go.sum
index 0b6b0eaa..0cc0eb1c 100644
--- a/api/go.sum
+++ b/api/go.sum
@@ -47,6 +47,8 @@ github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SU
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
@@ -182,6 +184,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
+github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
@@ -210,6 +214,10 @@ github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gt
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
+github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
+github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
+github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
+github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
@@ -224,6 +232,8 @@ github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onq
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -248,8 +258,9 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
-golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
+golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
@@ -272,6 +283,7 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -283,8 +295,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
diff --git a/api/handler/admin/api_key_handler.go b/api/handler/admin/api_key_handler.go
index 94cab69e..7935d0ba 100644
--- a/api/handler/admin/api_key_handler.go
+++ b/api/handler/admin/api_key_handler.go
@@ -8,6 +8,7 @@ import (
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
+
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@@ -65,14 +66,20 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
}
func (h *ApiKeyHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
+ status := h.GetBool(c, "status")
+ t := h.GetTrim(c, "type")
+ session := h.DB.Session(&gorm.Session{})
+ if status {
+ session = session.Where("enabled", true)
+ }
+ if t != "" {
+ session = session.Where("type", t)
+ }
+
var items []model.ApiKey
var keys = make([]vo.ApiKey, 0)
- res := h.DB.Find(&items)
+ res := session.Find(&items)
if res.Error == nil {
for _, item := range items {
var key vo.ApiKey
@@ -122,6 +129,5 @@ func (h *ApiKeyHandler) Remove(c *gin.Context) {
resp.ERROR(c, "更新数据库失败!")
return
}
-
resp.SUCCESS(c)
}
diff --git a/api/handler/admin/chat_handler.go b/api/handler/admin/chat_handler.go
index 3d29d165..64fb5587 100644
--- a/api/handler/admin/chat_handler.go
+++ b/api/handler/admin/chat_handler.go
@@ -33,11 +33,6 @@ type chatItemVo struct {
}
func (h *ChatHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
var data struct {
Title string `json:"title"`
UserId uint `json:"user_id"`
diff --git a/api/handler/admin/chat_model_handler.go b/api/handler/admin/chat_model_handler.go
index 97bb559e..ad0ce3c2 100644
--- a/api/handler/admin/chat_model_handler.go
+++ b/api/handler/admin/chat_model_handler.go
@@ -10,7 +10,6 @@ import (
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
- "time"
)
type ChatModelHandler struct {
@@ -34,6 +33,7 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
+ KeyId int `json:"key_id,omitempty"`
CreatedAt int64 `json:"created_at"`
}
if err := c.ShouldBindJSON(&data); err != nil {
@@ -51,12 +51,15 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
MaxTokens: data.MaxTokens,
MaxContext: data.MaxContext,
Temperature: data.Temperature,
+ KeyId: data.KeyId,
Power: data.Power}
- item.Id = data.Id
- if item.Id > 0 {
- item.CreatedAt = time.Unix(data.CreatedAt, 0)
+ var res *gorm.DB
+ if data.Id > 0 {
+ item.Id = data.Id
+ res = h.DB.Select("*").Omit("created_at").Updates(&item)
+ } else {
+ res = h.DB.Create(&item)
}
- res := h.DB.Save(&item)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
@@ -75,11 +78,6 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
// List 模型列表
func (h *ChatModelHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
session := h.DB.Session(&gorm.Session{})
enable := h.GetBool(c, "enable")
if enable {
@@ -88,18 +86,33 @@ func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel
var cms = make([]vo.ChatModel, 0)
res := session.Order("sort_num ASC").Find(&items)
- if res.Error == nil {
- for _, item := range items {
- var cm vo.ChatModel
- err := utils.CopyObject(item, &cm)
- if err == nil {
- cm.Id = item.Id
- cm.CreatedAt = item.CreatedAt.Unix()
- cm.UpdatedAt = item.UpdatedAt.Unix()
- cms = append(cms, cm)
- } else {
- logger.Error(err)
- }
+ if res.Error != nil {
+ resp.SUCCESS(c, cms)
+ return
+ }
+
+ // initialize key name
+ keyIds := make([]int, 0)
+ for _, v := range items {
+ keyIds = append(keyIds, v.KeyId)
+ }
+ var keys []model.ApiKey
+ keyMap := make(map[uint]string)
+ h.DB.Where("id IN ?", keyIds).Find(&keys)
+ for _, v := range keys {
+ keyMap[v.Id] = v.Name
+ }
+ for _, item := range items {
+ var cm vo.ChatModel
+ err := utils.CopyObject(item, &cm)
+ if err == nil {
+ cm.Id = item.Id
+ cm.CreatedAt = item.CreatedAt.Unix()
+ cm.UpdatedAt = item.UpdatedAt.Unix()
+ cm.KeyName = keyMap[uint(item.KeyId)]
+ cms = append(cms, cm)
+ } else {
+ logger.Error(err)
}
}
resp.SUCCESS(c, cms)
diff --git a/api/handler/admin/chat_role_handler.go b/api/handler/admin/chat_role_handler.go
index 7b72cb44..caec61b9 100644
--- a/api/handler/admin/chat_role_handler.go
+++ b/api/handler/admin/chat_role_handler.go
@@ -8,9 +8,10 @@ import (
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
+ "time"
+
"github.com/gin-gonic/gin"
"gorm.io/gorm"
- "time"
)
type ChatRoleHandler struct {
@@ -50,11 +51,6 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
}
func (h *ChatRoleHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
var items []model.ChatRole
var roles = make([]vo.ChatRole, 0)
res := h.DB.Order("sort_num ASC").Find(&items)
@@ -63,6 +59,25 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
return
}
+ // initialize model mane for role
+ modelIds := make([]int, 0)
+ for _, v := range items {
+ if v.ModelId > 0 {
+ modelIds = append(modelIds, v.ModelId)
+ }
+ }
+
+ modelNameMap := make(map[int]string)
+ if len(modelIds) > 0 {
+ var models []model.ChatModel
+ tx := h.DB.Where("id IN ?", modelIds).Find(&models)
+ if tx.Error == nil {
+ for _, m := range models {
+ modelNameMap[int(m.Id)] = m.Name
+ }
+ }
+ }
+
for _, v := range items {
var role vo.ChatRole
err := utils.CopyObject(v, &role)
@@ -70,6 +85,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
role.Id = v.Id
role.CreatedAt = v.CreatedAt.Unix()
role.UpdatedAt = v.UpdatedAt.Unix()
+ role.ModelName = modelNameMap[role.ModelId]
roles = append(roles, role)
}
}
diff --git a/api/handler/admin/config_handler.go b/api/handler/admin/config_handler.go
index 7ad863aa..adb1540e 100644
--- a/api/handler/admin/config_handler.go
+++ b/api/handler/admin/config_handler.go
@@ -4,20 +4,21 @@ import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
+ "chatplus/store"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
-
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ConfigHandler struct {
handler.BaseHandler
+ levelDB *store.LevelDB
}
-func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
- return &ConfigHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
+func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB) *ConfigHandler {
+ return &ConfigHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, levelDB: levelDB}
}
func (h *ConfigHandler) Update(c *gin.Context) {
@@ -70,11 +71,6 @@ func (h *ConfigHandler) Update(c *gin.Context) {
// Get 获取指定的系统配置
func (h *ConfigHandler) Get(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
key := c.Query("key")
var config model.Config
res := h.DB.Where("marker", key).First(&config)
diff --git a/api/handler/admin/function_handler.go b/api/handler/admin/function_handler.go
index 97940931..d9eed1fc 100644
--- a/api/handler/admin/function_handler.go
+++ b/api/handler/admin/function_handler.go
@@ -71,11 +71,6 @@ func (h *FunctionHandler) Set(c *gin.Context) {
}
func (h *FunctionHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
var items []model.Function
res := h.DB.Find(&items)
if res.Error != nil {
diff --git a/api/handler/admin/order_handler.go b/api/handler/admin/order_handler.go
index 993b3995..1183e01f 100644
--- a/api/handler/admin/order_handler.go
+++ b/api/handler/admin/order_handler.go
@@ -22,11 +22,6 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
}
func (h *OrderHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
var data struct {
OrderNo string `json:"order_no"`
Status int `json:"status"`
diff --git a/api/handler/admin/reward_handler.go b/api/handler/admin/reward_handler.go
index e2d283e3..a2c44cb9 100644
--- a/api/handler/admin/reward_handler.go
+++ b/api/handler/admin/reward_handler.go
@@ -21,11 +21,6 @@ func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
}
func (h *RewardHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
var items []model.Reward
res := h.DB.Order("id DESC").Find(&items)
var rewards = make([]vo.Reward, 0)
diff --git a/api/handler/admin/user_handler.go b/api/handler/admin/user_handler.go
index 1bc70b40..430b66bb 100644
--- a/api/handler/admin/user_handler.go
+++ b/api/handler/admin/user_handler.go
@@ -25,11 +25,6 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
// List 用户列表
func (h *UserHandler) List(c *gin.Context) {
- if err := utils.CheckPermission(c, h.DB); err != nil {
- resp.NotPermission(c)
- return
- }
-
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
username := h.GetTrim(c, "username")
diff --git a/api/handler/chatimpl/azure_handler.go b/api/handler/chatimpl/azure_handler.go
index a040aae6..11b3b69a 100644
--- a/api/handler/chatimpl/azure_handler.go
+++ b/api/handler/chatimpl/azure_handler.go
@@ -30,7 +30,7 @@ func (h *ChatHandler) sendAzureMessage(
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
- response, err := h.doRequest(ctx, req, session.Model.Platform, &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") {
diff --git a/api/handler/chatimpl/baidu_handler.go b/api/handler/chatimpl/baidu_handler.go
index e39ae455..08809dfe 100644
--- a/api/handler/chatimpl/baidu_handler.go
+++ b/api/handler/chatimpl/baidu_handler.go
@@ -47,7 +47,7 @@ func (h *ChatHandler) sendBaiduMessage(
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
- response, err := h.doRequest(ctx, req, session.Model.Platform, &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") {
diff --git a/api/handler/chatimpl/chat_handler.go b/api/handler/chatimpl/chat_handler.go
index c60df32e..6a70f281 100644
--- a/api/handler/chatimpl/chat_handler.go
+++ b/api/handler/chatimpl/chat_handler.go
@@ -68,9 +68,20 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
modelId := h.GetInt(c, "model_id", 0)
client := types.NewWsClient(ws)
+ var chatRole model.ChatRole
+ res := h.DB.First(&chatRole, roleId)
+ if res.Error != nil || !chatRole.Enable {
+ utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
+ c.Abort()
+ return
+ }
+ // if the role bind a model_id, use role's bind model_id
+ if chatRole.ModelId > 0 {
+ modelId = chatRole.ModelId
+ }
// get model info
var chatModel model.ChatModel
- res := h.DB.First(&chatModel, modelId)
+ res = h.DB.First(&chatModel, modelId)
if res.Error != nil || chatModel.Enabled == false {
utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
c.Abort()
@@ -111,15 +122,9 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
MaxTokens: chatModel.MaxTokens,
MaxContext: chatModel.MaxContext,
Temperature: chatModel.Temperature,
+ KeyId: chatModel.KeyId,
Platform: types.Platform(chatModel.Platform)}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
- var chatRole model.ChatRole
- res = h.DB.First(&chatRole, roleId)
- if res.Error != nil || !chatRole.Enable {
- utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
- c.Abort()
- return
- }
h.Init()
@@ -235,7 +240,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
break
}
- var tools = make([]interface{}, 0)
+ var tools = make([]types.Tool, 0)
for _, v := range items {
var parameters map[string]interface{}
err = utils.JsonDecode(v.Parameters, ¶meters)
@@ -244,15 +249,20 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
}
required := parameters["required"]
delete(parameters, "required")
- tools = append(tools, gin.H{
- "type": "function",
- "function": gin.H{
- "name": v.Name,
- "description": v.Description,
- "parameters": parameters,
- "required": required,
+ tool := types.Tool{
+ Type: "function",
+ Function: types.Function{
+ Name: v.Name,
+ Description: v.Description,
+ Parameters: parameters,
},
- })
+ }
+
+ // Fixed: compatible for gpt4-turbo-xxx model
+ if !strings.HasPrefix(req.Model, "gpt-4-turbo-") {
+ tool.Function.Required = required
+ }
+ tools = append(tools, tool)
}
if len(tools) > 0 {
@@ -326,10 +336,40 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
}
if session.Model.Platform == types.QWen {
- req.Input = map[string]interface{}{"prompt": prompt}
- if len(reqMgs) > 0 {
- req.Input["messages"] = reqMgs
+ req.Input = make(map[string]interface{})
+ reqMgs = append(reqMgs, types.Message{
+ Role: "user",
+ Content: prompt,
+ })
+ req.Input["messages"] = reqMgs
+ } else if session.Model.Platform == types.OpenAI { // extract image for gpt-vision model
+ imgURLs := utils.ExtractImgURL(prompt)
+ logger.Debugf("detected IMG: %+v", imgURLs)
+ var content interface{}
+ if len(imgURLs) > 0 {
+ data := make([]interface{}, 0)
+ text := prompt
+ for _, v := range imgURLs {
+ text = strings.Replace(text, v, "", 1)
+ data = append(data, gin.H{
+ "type": "image_url",
+ "image_url": gin.H{
+ "url": v,
+ },
+ })
+ }
+ data = append(data, gin.H{
+ "type": "text",
+ "text": text,
+ })
+ content = data
+ } else {
+ content = prompt
}
+ req.Messages = append(reqMgs, map[string]interface{}{
+ "role": "user",
+ "content": content,
+ })
} else {
req.Messages = append(reqMgs, map[string]interface{}{
"role": "user",
@@ -337,6 +377,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
})
}
+ logger.Debugf("%+v", req.Messages)
+
switch session.Model.Platform {
case types.Azure:
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
@@ -424,13 +466,21 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
// 发送请求到 OpenAI 服务器
// useOwnApiKey: 是否使用了用户自己的 API KEY
-func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *model.ApiKey) (*http.Response, error) {
- res := h.DB.Where("platform = ?", platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(apiKey)
- if res.Error != nil {
+func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
+ // if the chat model bind a KEY, use it directly
+ if session.Model.KeyId > 0 {
+ h.DB.Debug().Where("id", session.Model.KeyId).Find(apiKey)
+ }
+ // use the last unused key
+ if apiKey.Id == 0 {
+ h.DB.Debug().Where("platform = ?", session.Model.Platform).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")
}
+
var apiURL string
- switch platform {
+ switch session.Model.Platform {
case types.Azure:
md := strings.Replace(req.Model, ".", "", 1)
apiURL = strings.Replace(apiKey.ApiURL, "{model}", md, 1)
@@ -453,7 +503,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
// 更新 API KEY 的最后使用时间
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
// 百度文心,需要串接 access_token
- if platform == types.Baidu {
+ if session.Model.Platform == types.Baidu {
token, err := h.getBaiduToken(apiKey.Value)
if err != nil {
return nil, err
@@ -477,7 +527,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
request = request.WithContext(ctx)
request.Header.Set("Content-Type", "application/json")
- var proxyURL string
if len(apiKey.ProxyURL) > 5 { // 使用代理
proxy, _ := url.Parse(apiKey.ProxyURL)
client = &http.Client{
@@ -488,8 +537,8 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
} else {
client = http.DefaultClient
}
- logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s, Model: %s", platform, apiURL, apiKey.Value, proxyURL, req.Model)
- switch platform {
+ 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:
request.Header.Set("api-key", apiKey.Value)
break
diff --git a/api/handler/chatimpl/chatglm_handler.go b/api/handler/chatimpl/chatglm_handler.go
index 678f481d..5f391b3f 100644
--- a/api/handler/chatimpl/chatglm_handler.go
+++ b/api/handler/chatimpl/chatglm_handler.go
@@ -31,7 +31,7 @@ func (h *ChatHandler) sendChatGLMMessage(
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
- response, err := h.doRequest(ctx, req, session.Model.Platform, &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") {
diff --git a/api/handler/chatimpl/openai_handler.go b/api/handler/chatimpl/openai_handler.go
index 6e826159..2eb32866 100644
--- a/api/handler/chatimpl/openai_handler.go
+++ b/api/handler/chatimpl/openai_handler.go
@@ -31,25 +31,19 @@ func (h *ChatHandler) sendOpenAiMessage(
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
- response, err := h.doRequest(ctx, req, session.Model.Platform, &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") {
logger.Info("用户取消了请求:", prompt)
return nil
} else if strings.Contains(err.Error(), "no available key") {
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
return nil
- } else {
- logger.Error(err)
}
- utils.ReplyMessage(ws, ErrorMsg)
- utils.ReplyMessage(ws, ErrImg)
- if response.Body != nil {
- all, _ := io.ReadAll(response.Body)
- logger.Error(string(all))
- }
+ utils.ReplyMessage(ws, err.Error())
return err
} else {
defer response.Body.Close()
@@ -65,6 +59,7 @@ func (h *ChatHandler) sendOpenAiMessage(
var toolCall = false
var arguments = make([]string, 0)
scanner := bufio.NewScanner(response.Body)
+ var isNew = true
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "data:") || len(line) < 30 {
@@ -79,6 +74,10 @@ func (h *ChatHandler) sendOpenAiMessage(
utils.ReplyMessage(ws, ErrImg)
break
}
+ if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
+ utils.ReplyMessage(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
+ break
+ }
var tool types.ToolCall
if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
@@ -103,8 +102,10 @@ func (h *ChatHandler) sendOpenAiMessage(
res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
if res.Error == nil {
toolCall = true
+ callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
- utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)})
+ utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: callMsg})
+ contents = append(contents, callMsg)
}
continue
}
@@ -117,13 +118,16 @@ func (h *ChatHandler) sendOpenAiMessage(
// 初始化 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))
+ if isNew {
+ utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
+ isNew = false
+ }
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
diff --git a/api/handler/chatimpl/qwen_handler.go b/api/handler/chatimpl/qwen_handler.go
index 13b0156d..340f00de 100644
--- a/api/handler/chatimpl/qwen_handler.go
+++ b/api/handler/chatimpl/qwen_handler.go
@@ -45,7 +45,7 @@ func (h *ChatHandler) sendQWenMessage(
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
- response, err := h.doRequest(ctx, req, session.Model.Platform, &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") {
@@ -82,10 +82,11 @@ func (h *ChatHandler) sendQWenMessage(
continue
}
- if strings.HasPrefix(line, "data:") {
- content = line[5:]
+ if !strings.HasPrefix(line, "data:") {
+ continue
}
+ content = line[5:]
var resp qWenResp
if len(contents) == 0 { // 发送消息头
if !outPutStart {
diff --git a/api/handler/chatimpl/xunfei_handler.go b/api/handler/chatimpl/xunfei_handler.go
index adb646dc..36a5b785 100644
--- a/api/handler/chatimpl/xunfei_handler.go
+++ b/api/handler/chatimpl/xunfei_handler.go
@@ -12,6 +12,7 @@ import (
"encoding/json"
"fmt"
"github.com/gorilla/websocket"
+ "gorm.io/gorm"
"html/template"
"io"
"net/http"
@@ -69,7 +70,15 @@ func (h *ChatHandler) sendXunFeiMessage(
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
var apiKey model.ApiKey
- res := h.DB.Where("platform = ?", session.Model.Platform).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(&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 res.Error != nil {
+ 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 {
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
return nil
diff --git a/api/handler/dalle_handler.go b/api/handler/dalle_handler.go
new file mode 100644
index 00000000..9ccdf710
--- /dev/null
+++ b/api/handler/dalle_handler.go
@@ -0,0 +1,255 @@
+package handler
+
+import (
+ "chatplus/core"
+ "chatplus/core/types"
+ "chatplus/service/dalle"
+ "chatplus/service/oss"
+ "chatplus/store/model"
+ "chatplus/store/vo"
+ "chatplus/utils"
+ "chatplus/utils/resp"
+ "net/http"
+
+ "github.com/gorilla/websocket"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-redis/redis/v8"
+ "gorm.io/gorm"
+)
+
+type DallJobHandler struct {
+ BaseHandler
+ redis *redis.Client
+ service *dalle.Service
+ uploader *oss.UploaderManager
+}
+
+func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager) *DallJobHandler {
+ return &DallJobHandler{
+ service: service,
+ uploader: manager,
+ BaseHandler: BaseHandler{
+ App: app,
+ DB: db,
+ },
+ }
+}
+
+// Client WebSocket 客户端,用于通知任务状态变更
+func (h *DallJobHandler) 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())
+ go func() {
+ for {
+ _, msg, err := client.Receive()
+ if err != nil {
+ client.Close()
+ h.service.Clients.Delete(uint(userId))
+ return
+ }
+
+ var message types.WsMessage
+ err = utils.JsonDecode(string(msg), &message)
+ if err != nil {
+ continue
+ }
+
+ // 心跳消息
+ if message.Type == "heartbeat" {
+ logger.Debug("收到 DallE 心跳消息:", message.Content)
+ continue
+ }
+ }
+ }()
+}
+
+func (h *DallJobHandler) preCheck(c *gin.Context) bool {
+ user, err := h.GetLoginUser(c)
+ if err != nil {
+ resp.NotAuth(c)
+ return false
+ }
+
+ if user.Power < h.App.SysConfig.DallPower {
+ resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
+ return false
+ }
+
+ return true
+
+}
+
+// Image 创建一个绘画任务
+func (h *DallJobHandler) Image(c *gin.Context) {
+ if !h.preCheck(c) {
+ return
+ }
+
+ var data types.DallTask
+ if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
+ resp.ERROR(c, types.InvalidArgs)
+ return
+ }
+
+ idValue, _ := c.Get(types.LoginUserID)
+ userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
+ job := model.DallJob{
+ UserId: uint(userId),
+ Prompt: data.Prompt,
+ Power: h.App.SysConfig.DallPower,
+ }
+ res := h.DB.Create(&job)
+ if res.Error != nil {
+ resp.ERROR(c, "error with save job: "+res.Error.Error())
+ return
+ }
+
+ h.service.PushTask(types.DallTask{
+ JobId: job.Id,
+ UserId: uint(userId),
+ Prompt: data.Prompt,
+ Quality: data.Quality,
+ Size: data.Size,
+ Style: data.Style,
+ Power: job.Power,
+ })
+
+ client := h.service.Clients.Get(job.UserId)
+ if client != nil {
+ _ = client.Send([]byte("Task Updated"))
+ }
+ resp.SUCCESS(c)
+}
+
+// ImgWall 照片墙
+func (h *DallJobHandler) ImgWall(c *gin.Context) {
+ page := h.GetInt(c, "page", 0)
+ pageSize := h.GetInt(c, "page_size", 0)
+ err, jobs := h.getData(true, 0, page, pageSize, true)
+ if err != nil {
+ resp.ERROR(c, err.Error())
+ return
+ }
+
+ resp.SUCCESS(c, jobs)
+}
+
+// JobList 获取 SD 任务列表
+func (h *DallJobHandler) JobList(c *gin.Context) {
+ status := h.GetBool(c, "status")
+ userId := h.GetLoginUserId(c)
+ page := h.GetInt(c, "page", 0)
+ pageSize := h.GetInt(c, "page_size", 0)
+ publish := h.GetBool(c, "publish")
+
+ err, jobs := h.getData(status, userId, page, pageSize, publish)
+ if err != nil {
+ resp.ERROR(c, err.Error())
+ return
+ }
+
+ resp.SUCCESS(c, jobs)
+}
+
+// JobList 获取任务列表
+func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, []vo.DallJob) {
+
+ session := h.DB.Session(&gorm.Session{})
+ if finish {
+ session = session.Where("progress = ?", 100).Order("id DESC")
+ } else {
+ session = session.Where("progress < ?", 100).Order("id ASC")
+ }
+ if userId > 0 {
+ session = session.Where("user_id = ?", userId)
+ }
+ if publish {
+ session = session.Where("publish", publish)
+ }
+ if page > 0 && pageSize > 0 {
+ offset := (page - 1) * pageSize
+ session = session.Offset(offset).Limit(pageSize)
+ }
+
+ var items []model.DallJob
+ res := session.Find(&items)
+ if res.Error != nil {
+ return res.Error, nil
+ }
+
+ var jobs = make([]vo.DallJob, 0)
+ for _, item := range items {
+ var job vo.DallJob
+ err := utils.CopyObject(item, &job)
+ if err != nil {
+ continue
+ }
+ jobs = append(jobs, job)
+ }
+
+ return nil, jobs
+}
+
+// Remove remove task image
+func (h *DallJobHandler) Remove(c *gin.Context) {
+ var data struct {
+ Id uint `json:"id"`
+ UserId uint `json:"user_id"`
+ ImgURL string `json:"img_url"`
+ }
+ if err := c.ShouldBindJSON(&data); err != nil {
+ resp.ERROR(c, types.InvalidArgs)
+ return
+ }
+
+ // remove job recode
+ res := h.DB.Delete(&model.DallJob{Id: data.Id})
+ if res.Error != nil {
+ resp.ERROR(c, res.Error.Error())
+ return
+ }
+
+ // remove image
+ err := h.uploader.GetUploadHandler().Delete(data.ImgURL)
+ if err != nil {
+ logger.Error("remove image failed: ", err)
+ }
+
+ resp.SUCCESS(c)
+}
+
+// Publish 发布/取消发布图片到画廊显示
+func (h *DallJobHandler) Publish(c *gin.Context) {
+ var data struct {
+ Id uint `json:"id"`
+ Action bool `json:"action"` // 发布动作,true => 发布,false => 取消分享
+ }
+ if err := c.ShouldBindJSON(&data); err != nil {
+ resp.ERROR(c, types.InvalidArgs)
+ return
+ }
+
+ res := h.DB.Model(&model.DallJob{Id: data.Id}).UpdateColumn("publish", true)
+ if res.Error != nil {
+ resp.ERROR(c, "更新数据库失败")
+ return
+ }
+
+ resp.SUCCESS(c)
+}
diff --git a/api/handler/function_handler.go b/api/handler/function_handler.go
index e9eb57df..0941db08 100644
--- a/api/handler/function_handler.go
+++ b/api/handler/function_handler.go
@@ -3,27 +3,35 @@ package handler
import (
"chatplus/core"
"chatplus/core/types"
+ "chatplus/service/dalle"
"chatplus/service/oss"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"errors"
"fmt"
+ "strings"
+ "time"
+
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/imroc/req/v3"
"gorm.io/gorm"
- "strings"
- "time"
)
type FunctionHandler struct {
BaseHandler
- config types.ChatPlusApiConfig
+ config types.ApiConfig
uploadManager *oss.UploaderManager
+ dallService *dalle.Service
}
-func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppConfig, manager *oss.UploaderManager) *FunctionHandler {
+func NewFunctionHandler(
+ server *core.AppServer,
+ db *gorm.DB,
+ config *types.AppConfig,
+ manager *oss.UploaderManager,
+ dallService *dalle.Service) *FunctionHandler {
return &FunctionHandler{
BaseHandler: BaseHandler{
App: server,
@@ -31,6 +39,7 @@ func NewFunctionHandler(server *core.AppServer, db *gorm.DB, config *types.AppCo
},
config: config.ApiConfig,
uploadManager: manager,
+ dallService: dallService,
}
}
@@ -151,30 +160,6 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
}
-type imgReq struct {
- Model string `json:"model"`
- Prompt string `json:"prompt"`
- N int `json:"n"`
- Size string `json:"size"`
-}
-
-type imgRes struct {
- Created int64 `json:"created"`
- Data []struct {
- RevisedPrompt string `json:"revised_prompt"`
- Url string `json:"url"`
- } `json:"data"`
-}
-
-type ErrRes struct {
- Error struct {
- Code interface{} `json:"code"`
- Message string `json:"message"`
- Param interface{} `json:"param"`
- Type string `json:"type"`
- } `json:"error"`
-}
-
// Dall3 DallE3 AI 绘图
func (h *FunctionHandler) Dall3(c *gin.Context) {
if err := h.checkAuth(c); err != nil {
@@ -190,85 +175,40 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
logger.Debugf("绘画参数:%+v", params)
var user model.User
- tx := h.DB.Where("id = ?", params["user_id"]).First(&user)
- if tx.Error != nil {
+ res := h.DB.Where("id = ?", params["user_id"]).First(&user)
+ if res.Error != nil {
resp.ERROR(c, "当前用户不存在!")
return
}
- if user.Power < h.App.SysConfig.DallPower {
- resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
- return
- }
-
+ // create dall task
prompt := utils.InterfaceToString(params["prompt"])
- // get image generation API KEY
- var apiKey model.ApiKey
- tx = h.DB.Where("platform = ?", types.OpenAI).Where("type = ?", "img").Where("enabled = ?", true).Order("last_used_at ASC").First(&apiKey)
- if tx.Error != nil {
- resp.ERROR(c, "获取绘图 API KEY 失败: "+tx.Error.Error())
+ job := model.DallJob{
+ UserId: user.Id,
+ Prompt: prompt,
+ Power: h.App.SysConfig.DallPower,
+ }
+ res = h.DB.Create(&job)
+
+ if res.Error != nil {
+ resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+res.Error.Error())
return
}
- // translate prompt
- const translatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
- pt, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(translatePromptTemplate, params["prompt"]))
- if err == nil {
- logger.Debugf("翻译绘画提示词,原文:%s,译文:%s", prompt, pt)
- prompt = pt
- }
- var res imgRes
- var errRes ErrRes
- var request *req.Request
- if len(apiKey.ProxyURL) > 5 {
- request = req.C().SetProxyURL(apiKey.ProxyURL).R()
- } else {
- request = req.C().R()
- }
- logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, apiKey.ProxyURL)
- r, err := request.SetHeader("Content-Type", "application/json").
- SetHeader("Authorization", "Bearer "+apiKey.Value).
- SetBody(imgReq{
- Model: "dall-e-3",
- Prompt: prompt,
- N: 1,
- Size: "1024x1024",
- }).
- SetErrorResult(&errRes).
- SetSuccessResult(&res).Post(apiKey.ApiURL)
- if r.IsErrorState() {
- resp.ERROR(c, "请求 OpenAI API 失败: "+errRes.Error.Message)
- return
- }
- // 更新 API KEY 的最后使用时间
- h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
- logger.Debugf("%+v", res)
- // 存储图片
- imgURL, err := h.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
+ content, err := h.dallService.Image(types.DallTask{
+ JobId: job.Id,
+ UserId: user.Id,
+ Prompt: job.Prompt,
+ N: 1,
+ Quality: "standard",
+ Size: "1024x1024",
+ Style: "vivid",
+ Power: job.Power,
+ }, true)
if err != nil {
- resp.ERROR(c, "下载图片失败: "+err.Error())
+ resp.ERROR(c, "任务执行失败:"+err.Error())
return
}
- content := fmt.Sprintf("下面是根据您的描述创作的图片,它描绘了 【%s】 的场景。 \n\n\n", prompt, imgURL)
- // 更新用户算力
- tx = h.DB.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", h.App.SysConfig.DallPower))
- // 记录算力变化日志
- if tx.Error == nil && tx.RowsAffected > 0 {
- var u model.User
- h.DB.Where("id", user.Id).First(&u)
- h.DB.Create(&model.PowerLog{
- UserId: user.Id,
- Username: user.Username,
- Type: types.PowerConsume,
- Amount: h.App.SysConfig.DallPower,
- Balance: u.Power,
- Mark: types.PowerSub,
- Model: "dall-e-3",
- Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(prompt, 10)),
- CreatedAt: time.Now(),
- })
- }
-
resp.SUCCESS(c, content)
}
diff --git a/api/handler/markmap_handler.go b/api/handler/markmap_handler.go
new file mode 100644
index 00000000..ddaaa134
--- /dev/null
+++ b/api/handler/markmap_handler.go
@@ -0,0 +1,265 @@
+package handler
+
+import (
+ "bufio"
+ "bytes"
+ "chatplus/core"
+ "chatplus/core/types"
+ "chatplus/store/model"
+ "chatplus/utils"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ "gorm.io/gorm"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+)
+
+// MarkMapHandler 生成思维导图
+type MarkMapHandler struct {
+ BaseHandler
+ clients *types.LMap[int, *types.WsClient]
+}
+
+func NewMarkMapHandler(app *core.AppServer, db *gorm.DB) *MarkMapHandler {
+ return &MarkMapHandler{
+ BaseHandler: BaseHandler{App: app, DB: db},
+ clients: types.NewLMap[int, *types.WsClient](),
+ }
+}
+
+func (h *MarkMapHandler) 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)
+ return
+ }
+
+ modelId := h.GetInt(c, "model_id", 0)
+ userId := h.GetInt(c, "user_id", 0)
+
+ client := types.NewWsClient(ws)
+ h.clients.Put(userId, client)
+ go func() {
+ for {
+ _, msg, err := client.Receive()
+ if err != nil {
+ client.Close()
+ h.clients.Delete(userId)
+ return
+ }
+
+ var message types.WsMessage
+ err = utils.JsonDecode(string(msg), &message)
+ if err != nil {
+ continue
+ }
+
+ // 心跳消息
+ if message.Type == "heartbeat" {
+ logger.Debug("收到 MarkMap 心跳消息:", message.Content)
+ continue
+ }
+ // change model
+ if message.Type == "model_id" {
+ modelId = utils.IntValue(utils.InterfaceToString(message.Content), 0)
+ continue
+ }
+
+ logger.Info("Receive a message: ", message.Content)
+ err = h.sendMessage(client, utils.InterfaceToString(message.Content), modelId, userId)
+ if err != nil {
+ logger.Error(err)
+ utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsErr, Content: err.Error()})
+ }
+
+ }
+ }()
+}
+
+func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, modelId int, userId int) error {
+ var user model.User
+ res := h.DB.Model(&model.User{}).First(&user, userId)
+ if res.Error != nil {
+ return fmt.Errorf("error with query user info: %v", res.Error)
+ }
+ var chatModel model.ChatModel
+ res = h.DB.Where("id", modelId).First(&chatModel)
+ if res.Error != nil {
+ return fmt.Errorf("error with query chat model: %v", res.Error)
+ }
+
+ if user.Status == false {
+ return errors.New("当前用户被禁用")
+ }
+
+ if user.Power < chatModel.Power {
+ return fmt.Errorf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power)
+ }
+
+ messages := make([]interface{}, 0)
+ messages = append(messages, types.Message{Role: "system", Content: `
+你是一位非常优秀的思维导图助手,你会把用户的所有提问都总结成思维导图,然后以 Markdown 格式输出。markdown 只需要输出一级标题,二级标题,三级标题,四级标题,最多输出四级,除此之外不要输出任何其他 markdown 标记。下面是一个合格的例子:
+# Geek-AI 助手
+
+## 完整的开源系统
+### 前端开源
+### 后端开源
+
+## 支持各种大模型
+### OpenAI
+### Azure
+### 文心一言
+### 通义千问
+
+## 集成多种收费方式
+### 支付宝
+### 微信
+
+另外,除此之外不要任何解释性语句。
+`})
+ messages = append(messages, types.Message{Role: "user", Content: prompt})
+ var req = types.ApiRequest{
+ Model: chatModel.Value,
+ Stream: true,
+ Messages: messages,
+ }
+
+ var apiKey model.ApiKey
+ response, err := h.doRequest(req, chatModel, &apiKey)
+ if err != nil {
+ return fmt.Errorf("请求 OpenAI API 失败: %s", err)
+ }
+
+ defer response.Body.Close()
+
+ contentType := response.Header.Get("Content-Type")
+ if strings.Contains(contentType, "text/event-stream") {
+ // 循环读取 Chunk 消息
+ var message = types.Message{}
+ scanner := bufio.NewScanner(response.Body)
+ var isNew = true
+ 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 || len(responseBody.Choices) == 0 { // 数据解析出错
+ return fmt.Errorf("error with decode data: %v", err)
+ }
+
+ // 初始化 role
+ if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
+ message.Role = responseBody.Choices[0].Delta.Role
+ continue
+ } else if responseBody.Choices[0].FinishReason != "" {
+ break // 输出完成或者输出中断了
+ } else {
+ if isNew {
+ utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsStart})
+ isNew = false
+ }
+ utils.ReplyChunkMessage(client, types.WsMessage{
+ Type: types.WsMiddle,
+ Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
+ })
+ }
+ } // end for
+
+ utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
+
+ } else {
+ body, err := io.ReadAll(response.Body)
+ if err != nil {
+ return fmt.Errorf("读取响应失败: %v", err)
+ }
+ var res types.ApiError
+ err = json.Unmarshal(body, &res)
+ if err != nil {
+ return fmt.Errorf("解析响应失败: %v", err)
+ }
+
+ // OpenAI API 调用异常处理
+ if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
+ // remove key
+ h.DB.Where("value = ?", apiKey).Delete(&model.ApiKey{})
+ return errors.New("请求 OpenAI API 失败:API KEY 所关联的账户被禁用。")
+ } else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
+ return errors.New("请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
+ } else {
+ return fmt.Errorf("请求 OpenAI API 失败:%v", res.Error.Message)
+ }
+ }
+
+ // 扣减算力
+ res = h.DB.Model(&model.User{}).Where("id", userId).UpdateColumn("power", gorm.Expr("power - ?", chatModel.Power))
+ if res.Error == nil {
+ // 记录算力消费日志
+ var u model.User
+ h.DB.Where("id", userId).First(&u)
+ h.DB.Create(&model.PowerLog{
+ UserId: u.Id,
+ Username: u.Username,
+ Type: types.PowerConsume,
+ Amount: chatModel.Power,
+ Mark: types.PowerSub,
+ Balance: u.Power,
+ Model: chatModel.Value,
+ Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
+ CreatedAt: time.Now(),
+ })
+ }
+
+ return nil
+}
+
+func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) {
+ // 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).Where("type = ?", "chat").Where("enabled = ?", true).Order("last_used_at ASC").First(apiKey)
+ }
+ if res.Error != nil {
+ return nil, errors.New("no available key, please import key")
+ }
+ apiURL := apiKey.ApiURL
+ // 更新 API KEY 的最后使用时间
+ h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
+
+ // 创建 HttpClient 请求对象
+ var client *http.Client
+ requestBody, err := json.Marshal(req)
+ if err != nil {
+ return nil, err
+ }
+ request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
+ if err != nil {
+ return nil, err
+ }
+
+ request.Header.Set("Content-Type", "application/json")
+ if len(apiKey.ProxyURL) > 5 { // 使用代理
+ proxy, _ := url.Parse(apiKey.ProxyURL)
+ client = &http.Client{
+ Transport: &http.Transport{
+ Proxy: http.ProxyURL(proxy),
+ },
+ }
+ } else {
+ client = http.DefaultClient
+ }
+ request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
+ return client.Do(request)
+}
diff --git a/api/handler/mj_handler.go b/api/handler/mj_handler.go
index b659c5b0..e0e0f020 100644
--- a/api/handler/mj_handler.go
+++ b/api/handler/mj_handler.go
@@ -146,7 +146,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
}
if data.SRef != "" {
- params += fmt.Sprintf(" --sref %s", data.CRef)
+ params += fmt.Sprintf(" --sref %s", data.SRef)
}
if data.Model != "" && !strings.Contains(params, "--v") && !strings.Contains(params, "--niji") {
params += fmt.Sprintf(" %s", data.Model)
diff --git a/api/handler/sd_handler.go b/api/handler/sd_handler.go
index b9c3625e..25f122a0 100644
--- a/api/handler/sd_handler.go
+++ b/api/handler/sd_handler.go
@@ -65,7 +65,7 @@ func (h *SdJobHandler) Client(c *gin.Context) {
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
-func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
+func (h *SdJobHandler) preCheck(c *gin.Context) bool {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
@@ -88,7 +88,7 @@ func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
// Image 创建一个绘画任务
func (h *SdJobHandler) Image(c *gin.Context) {
- if !h.checkLimits(c) {
+ if !h.preCheck(c) {
return
}
@@ -260,9 +260,10 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
if item.Progress < 100 {
// 从 leveldb 中获取图片预览数据
- imageData, err := h.leveldb.Get(item.TaskId)
+ var imageData string
+ err = h.leveldb.Get(item.TaskId, &imageData)
if err == nil {
- job.ImgURL = "data:image/png;base64," + string(imageData)
+ job.ImgURL = "data:image/png;base64," + imageData
}
}
jobs = append(jobs, job)
@@ -298,7 +299,7 @@ func (h *SdJobHandler) Remove(c *gin.Context) {
client := h.pool.Clients.Get(data.UserId)
if client != nil {
- _ = client.Send([]byte("Task Updated"))
+ _ = client.Send([]byte(sd.Finished))
}
resp.SUCCESS(c)
diff --git a/api/main.go b/api/main.go
index 89eea8f0..51684569 100644
--- a/api/main.go
+++ b/api/main.go
@@ -8,6 +8,7 @@ import (
"chatplus/handler/chatimpl"
logger2 "chatplus/logger"
"chatplus/service"
+ "chatplus/service/dalle"
"chatplus/service/mj"
"chatplus/service/oss"
"chatplus/service/payment"
@@ -43,13 +44,13 @@ type AppLifecycle struct {
// OnStart 应用程序启动时执行
func (l *AppLifecycle) OnStart(context.Context) error {
- log.Println("AppLifecycle OnStart")
+ logger.Info("AppLifecycle OnStart")
return nil
}
// OnStop 应用程序停止时执行
func (l *AppLifecycle) OnStop(context.Context) error {
- log.Println("AppLifecycle OnStop")
+ logger.Info("AppLifecycle OnStop")
return nil
}
@@ -153,6 +154,13 @@ func main() {
}),
fx.Provide(oss.NewUploaderManager),
fx.Provide(mj.NewService),
+ fx.Provide(dalle.NewService),
+ fx.Invoke(func(service *dalle.Service) {
+ service.Run()
+ service.CheckTaskNotify()
+ service.DownloadImages()
+ service.CheckTaskStatus()
+ }),
// 邮件服务
fx.Provide(service.NewSmtpService),
@@ -279,9 +287,9 @@ func main() {
// 管理后台控制器
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
- group := s.Engine.Group("/api/admin/config/")
- group.POST("update", h.Update)
- group.GET("get", h.Get)
+ group := s.Engine.Group("/api/admin/")
+ group.POST("config/update", h.Update)
+ group.GET("config/get", h.Get)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
group := s.Engine.Group("/api/admin/")
@@ -440,6 +448,21 @@ func main() {
group := s.Engine.Group("/api/menu/")
group.GET("list", h.List)
}),
+ fx.Provide(handler.NewMarkMapHandler),
+ fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
+ group := s.Engine.Group("/api/markMap/")
+ group.Any("client", h.Client)
+ }),
+ fx.Provide(handler.NewDallJobHandler),
+ fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
+ group := s.Engine.Group("/api/dall")
+ group.Any("client", h.Client)
+ group.POST("image", h.Image)
+ group.GET("jobs", h.JobList)
+ group.GET("imgWall", h.ImgWall)
+ group.POST("remove", h.Remove)
+ group.POST("publish", h.Publish)
+ }),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() {
err := s.Run(db)
diff --git a/api/service/captcha_service.go b/api/service/captcha_service.go
index 4efbfe55..e8cfd39b 100644
--- a/api/service/captcha_service.go
+++ b/api/service/captcha_service.go
@@ -9,11 +9,11 @@ import (
)
type CaptchaService struct {
- config types.ChatPlusApiConfig
+ config types.ApiConfig
client *req.Client
}
-func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
+func NewCaptchaService(config types.ApiConfig) *CaptchaService {
return &CaptchaService{
config: config,
client: req.C().SetTimeout(10 * time.Second),
diff --git a/api/service/dalle/service.go b/api/service/dalle/service.go
new file mode 100644
index 00000000..f3929e84
--- /dev/null
+++ b/api/service/dalle/service.go
@@ -0,0 +1,300 @@
+package dalle
+
+import (
+ "chatplus/core/types"
+ logger2 "chatplus/logger"
+ "chatplus/service"
+ "chatplus/service/oss"
+ "chatplus/service/sd"
+ "chatplus/store"
+ "chatplus/store/model"
+ "chatplus/utils"
+ "errors"
+ "fmt"
+ "github.com/go-redis/redis/v8"
+ "time"
+
+ "github.com/imroc/req/v3"
+ "gorm.io/gorm"
+)
+
+var logger = logger2.GetLogger()
+
+// DALL-E 绘画服务
+
+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("DallE_Task_Queue", redisCli),
+ notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
+ Clients: types.NewLMap[uint, *types.WsClient](),
+ uploadManager: manager,
+ }
+}
+
+// PushTask push a new mj task in to task queue
+func (s *Service) PushTask(task types.DallTask) {
+ logger.Debugf("add a new MidJourney task to the task list: %+v", task)
+ s.taskQueue.RPush(task)
+}
+
+func (s *Service) Run() {
+ go func() {
+ for {
+ var task types.DallTask
+ err := s.taskQueue.LPop(&task)
+ if err != nil {
+ logger.Errorf("taking task with error: %v", err)
+ continue
+ }
+
+ _, err = s.Image(task, false)
+ if err != nil {
+ logger.Errorf("error with image task: %v", err)
+ s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
+ "progress": -1,
+ "err_msg": err.Error(),
+ })
+ s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Failed})
+ }
+ }
+ }()
+}
+
+type imgReq struct {
+ Model string `json:"model"`
+ Prompt string `json:"prompt"`
+ N int `json:"n"`
+ Size string `json:"size"`
+ Quality string `json:"quality"`
+ Style string `json:"style"`
+}
+
+type imgRes struct {
+ Created int64 `json:"created"`
+ Data []struct {
+ RevisedPrompt string `json:"revised_prompt"`
+ Url string `json:"url"`
+ } `json:"data"`
+}
+
+type ErrRes struct {
+ Error struct {
+ Code interface{} `json:"code"`
+ Message string `json:"message"`
+ Param interface{} `json:"param"`
+ Type string `json:"type"`
+ } `json:"error"`
+}
+
+func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
+ logger.Debugf("绘画参数:%+v", task)
+ prompt := task.Prompt
+ // translate prompt
+ if utils.HasChinese(task.Prompt) {
+ content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt))
+ if err != nil {
+ return "", fmt.Errorf("error with translate prompt: %v", err)
+ }
+ prompt = content
+ logger.Debugf("重写后提示词:%s", prompt)
+ }
+
+ var user model.User
+ s.db.Where("id", task.UserId).First(&user)
+ if user.Power < task.Power {
+ return "", errors.New("insufficient of power")
+ }
+
+ // get image generation API KEY
+ var apiKey model.ApiKey
+ tx := s.db.Where("platform", types.OpenAI).
+ Where("type", "img").
+ Where("enabled", true).
+ Order("last_used_at ASC").First(&apiKey)
+ if tx.Error != nil {
+ return "", fmt.Errorf("no available IMG api key: %v", tx.Error)
+ }
+
+ var res imgRes
+ var errRes ErrRes
+ if len(apiKey.ProxyURL) > 5 {
+ s.httpClient.SetProxyURL(apiKey.ProxyURL).R()
+ }
+ logger.Debugf("Sending %s request, ApiURL:%s, API KEY:%s, PROXY: %s", apiKey.Platform, apiKey.ApiURL, apiKey.Value, apiKey.ProxyURL)
+ r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
+ SetHeader("Authorization", "Bearer "+apiKey.Value).
+ SetBody(imgReq{
+ Model: "dall-e-3",
+ Prompt: prompt,
+ N: 1,
+ Size: "1024x1024",
+ Style: task.Style,
+ Quality: task.Quality,
+ }).
+ SetErrorResult(&errRes).
+ SetSuccessResult(&res).Post(apiKey.ApiURL)
+ if err != nil {
+ return "", fmt.Errorf("error with send request: %v", err)
+ }
+
+ if r.IsErrorState() {
+ return "", fmt.Errorf("error with send request: %v", errRes.Error)
+ }
+ // update the api key last use time
+ s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
+ // update task progress
+ s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
+ "progress": 100,
+ "org_url": res.Data[0].Url,
+ "prompt": prompt,
+ })
+
+ s.notifyQueue.RPush(sd.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: sd.Finished})
+ var content string
+ if sync {
+ imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url)
+ if err != nil {
+ return "", fmt.Errorf("error with download image: %v", err)
+ }
+ content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n\n", prompt, imgURL)
+ }
+
+ // 更新用户算力
+ tx = s.db.Model(&model.User{}).Where("id", user.Id).UpdateColumn("power", gorm.Expr("power - ?", task.Power))
+ // 记录算力变化日志
+ if tx.Error == nil && tx.RowsAffected > 0 {
+ var u model.User
+ s.db.Where("id", user.Id).First(&u)
+ s.db.Create(&model.PowerLog{
+ UserId: user.Id,
+ Username: user.Username,
+ Type: types.PowerConsume,
+ Amount: task.Power,
+ Balance: u.Power,
+ Mark: types.PowerSub,
+ Model: "dall-e-3",
+ Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
+ CreatedAt: time.Now(),
+ })
+ }
+
+ return content, nil
+}
+
+func (s *Service) CheckTaskNotify() {
+ go func() {
+ logger.Info("Running DALL-E 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.DallJob
+ for {
+ res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
+ if res.Error != nil {
+ continue
+ }
+
+ // download images
+ for _, v := range items {
+ if v.OrgURL == "" {
+ continue
+ }
+
+ logger.Infof("try to download image: %s", v.OrgURL)
+ imgURL, err := s.downloadImage(v.Id, int(v.UserId), v.OrgURL)
+ if err != nil {
+ logger.Error("error with download image: %s, error: %v", imgURL, err)
+ continue
+ }
+
+ }
+
+ time.Sleep(time.Second * 5)
+ }
+ }()
+}
+
+func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
+ // sava image
+ imgURL, err := s.uploadManager.GetUploadHandler().PutImg(orgURL, false)
+ if err != nil {
+ return "", err
+ }
+
+ // update img_url
+ res := s.db.Model(&model.DallJob{Id: jobId}).UpdateColumn("img_url", imgURL)
+ if res.Error != nil {
+ return "", err
+ }
+ s.notifyQueue.RPush(sd.NotifyMessage{UserId: userId, JobId: int(jobId), Message: sd.Failed})
+ return imgURL, nil
+}
+
+// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
+func (s *Service) CheckTaskStatus() {
+ go func() {
+ logger.Info("Running Stable-Diffusion task status checking ...")
+ for {
+ var jobs []model.SdJob
+ res := s.db.Where("progress < ?", 100).Find(&jobs)
+ if res.Error != nil {
+ time.Sleep(5 * time.Second)
+ continue
+ }
+
+ for _, job := range jobs {
+ // 5 分钟还没完成的任务直接删除
+ if time.Now().Sub(job.CreatedAt) > time.Minute*5 || job.Progress == -1 {
+ s.db.Delete(&job)
+ var user model.User
+ s.db.Where("id = ?", job.UserId).First(&user)
+ // 退回绘图次数
+ res = s.db.Model(&model.User{}).Where("id = ?", job.UserId).UpdateColumn("power", gorm.Expr("power + ?", job.Power))
+ if res.Error == nil && res.RowsAffected > 0 {
+ s.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: "dall-e-3",
+ Remark: fmt.Sprintf("任务失败,退回算力。任务ID:%s", job.TaskId),
+ CreatedAt: time.Now(),
+ })
+ }
+ continue
+ }
+ }
+ time.Sleep(time.Second * 10)
+ }
+ }()
+}
diff --git a/api/service/mj/plus_client.go b/api/service/mj/plus_client.go
index 52846208..822d4b91 100644
--- a/api/service/mj/plus_client.go
+++ b/api/service/mj/plus_client.go
@@ -73,6 +73,7 @@ func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
// Blend 融图
func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) {
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/blend", c.apiURL, c.Config.Mode)
+ logger.Info("API URL: ", apiURL)
body := ImageReq{
BotType: "MID_JOURNEY",
Dimensions: "SQUARE",
@@ -163,7 +164,8 @@ func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) {
"customId": fmt.Sprintf("MJ::JOB::upsample::%d::%s", task.Index, task.MessageHash),
"taskId": task.MessageId,
}
- apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.Config.Mode, c.apiURL)
+ 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().
@@ -189,7 +191,8 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
"customId": fmt.Sprintf("MJ::JOB::variation::%d::%s", task.Index, task.MessageHash),
"taskId": task.MessageId,
}
- apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/action", c.Config.Mode, c.apiURL)
+ 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 := req.C().R().
diff --git a/api/service/mj/pool.go b/api/service/mj/pool.go
index 7404021e..bd6eedf2 100644
--- a/api/service/mj/pool.go
+++ b/api/service/mj/pool.go
@@ -4,6 +4,7 @@ import (
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/service/oss"
+ "chatplus/service/sd"
"chatplus/store"
"chatplus/store/model"
"fmt"
@@ -34,13 +35,14 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
if config.Enabled == false {
continue
}
+
cli := NewPlusClient(config)
name := fmt.Sprintf("mj-plus-service-%d", k)
- service := NewService(name, taskQueue, notifyQueue, db, cli)
+ plusService := NewService(name, taskQueue, notifyQueue, db, cli)
go func() {
- service.Run()
+ plusService.Run()
}()
- services = append(services, service)
+ services = append(services, plusService)
}
for k, config := range appConfig.MjProxyConfigs {
@@ -49,11 +51,11 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
}
cli := NewProxyClient(config)
name := fmt.Sprintf("mj-proxy-service-%d", k)
- service := NewService(name, taskQueue, notifyQueue, db, cli)
+ proxyService := NewService(name, taskQueue, notifyQueue, db, cli)
go func() {
- service.Run()
+ proxyService.Run()
}()
- services = append(services, service)
+ services = append(services, proxyService)
}
return &ServicePool{
@@ -69,16 +71,16 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
func (p *ServicePool) CheckTaskNotify() {
go func() {
for {
- var userId uint
- err := p.notifyQueue.LPop(&userId)
+ var message sd.NotifyMessage
+ err := p.notifyQueue.LPop(&message)
if err != nil {
continue
}
- cli := p.Clients.Get(userId)
+ cli := p.Clients.Get(uint(message.UserId))
if cli == nil {
continue
}
- err = cli.Send([]byte("Task Updated"))
+ err = cli.Send([]byte(message.Message))
if err != nil {
continue
}
@@ -127,7 +129,7 @@ func (p *ServicePool) DownloadImages() {
if cli == nil {
continue
}
- err = cli.Send([]byte("Task Updated"))
+ err = cli.Send([]byte(sd.Finished))
if err != nil {
continue
}
@@ -162,7 +164,6 @@ func (p *ServicePool) SyncTaskProgress() {
for _, job := range items {
// 失败或者 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))
@@ -189,7 +190,7 @@ func (p *ServicePool) SyncTaskProgress() {
}
}
- time.Sleep(time.Second)
+ time.Sleep(time.Second * 10)
}
}()
}
diff --git a/api/service/mj/service.go b/api/service/mj/service.go
index ad118308..0d5f0dea 100644
--- a/api/service/mj/service.go
+++ b/api/service/mj/service.go
@@ -3,6 +3,7 @@ package mj
import (
"chatplus/core/types"
"chatplus/service"
+ "chatplus/service/sd"
"chatplus/store"
"chatplus/store/model"
"chatplus/utils"
@@ -53,7 +54,7 @@ func (s *Service) Run() {
// translate prompt
if utils.HasChinese(task.Prompt) {
- content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt))
+ content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Prompt))
if err == nil {
task.Prompt = content
} else {
@@ -62,7 +63,7 @@ func (s *Service) Run() {
}
// translate negative prompt
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
- content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt))
+ content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.NegPrompt))
if err == nil {
task.NegPrompt = content
} else {
@@ -105,7 +106,7 @@ func (s *Service) Run() {
// update the task progress
s.db.Updates(&job)
// 任务失败,通知前端
- s.notifyQueue.RPush(task.UserId)
+ s.notifyQueue.RPush(sd.NotifyMessage{UserId: task.UserId, JobId: int(job.Id), Message: sd.Failed})
continue
}
logger.Infof("任务提交成功:%+v", res)
@@ -147,7 +148,7 @@ func (s *Service) Notify(job model.MidJourneyJob) error {
"progress": -1,
"err_msg": task.FailReason,
})
- s.notifyQueue.RPush(job.UserId)
+ s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: sd.Failed})
return fmt.Errorf("task failed: %v", task.FailReason)
}
@@ -166,7 +167,11 @@ func (s *Service) Notify(job model.MidJourneyJob) error {
}
// 通知前端更新任务进度
if oldProgress != job.Progress {
- s.notifyQueue.RPush(job.UserId)
+ message := sd.Running
+ if job.Progress == 100 {
+ message = sd.Finished
+ }
+ s.notifyQueue.RPush(sd.NotifyMessage{UserId: job.UserId, JobId: int(job.Id), Message: message})
}
return nil
}
diff --git a/api/service/sd/pool.go b/api/service/sd/pool.go
index 3033b548..e191eef8 100644
--- a/api/service/sd/pool.go
+++ b/api/service/sd/pool.go
@@ -60,16 +60,16 @@ func (p *ServicePool) CheckTaskNotify() {
go func() {
logger.Info("Running Stable-Diffusion task notify checking ...")
for {
- var userId uint
- err := p.notifyQueue.LPop(&userId)
+ var message NotifyMessage
+ err := p.notifyQueue.LPop(&message)
if err != nil {
continue
}
- client := p.Clients.Get(userId)
+ client := p.Clients.Get(uint(message.UserId))
if client == nil {
continue
}
- err = client.Send([]byte("Task Updated"))
+ err = client.Send([]byte(message.Message))
if err != nil {
continue
}
@@ -113,7 +113,7 @@ func (p *ServicePool) CheckTaskStatus() {
continue
}
}
-
+ time.Sleep(time.Second * 10)
}
}()
}
diff --git a/api/service/sd/service.go b/api/service/sd/service.go
index 4f68f3e0..9d6932a2 100644
--- a/api/service/sd/service.go
+++ b/api/service/sd/service.go
@@ -8,10 +8,11 @@ import (
"chatplus/store/model"
"chatplus/utils"
"fmt"
- "github.com/imroc/req/v3"
- "gorm.io/gorm"
"strings"
"time"
+
+ "github.com/imroc/req/v3"
+ "gorm.io/gorm"
)
// SD 绘画服务
@@ -80,7 +81,7 @@ func (s *Service) Run() {
"err_msg": err.Error(),
})
// 通知前端,任务失败
- s.notifyQueue.RPush(task.UserId)
+ s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Failed})
continue
}
}
@@ -145,8 +146,13 @@ func (s *Service) Txt2Img(task types.SdTask) error {
var errChan = make(chan error)
apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", s.config.ApiURL)
logger.Debugf("send image request to %s", apiURL)
+ // send a request to sd api endpoint
go func() {
- response, err := s.httpClient.R().SetBody(body).SetSuccessResult(&res).Post(apiURL)
+ response, err := s.httpClient.R().
+ SetHeader("Authorization", s.config.ApiKey).
+ SetBody(body).
+ SetSuccessResult(&res).
+ Post(apiURL)
if err != nil {
errChan <- err
return
@@ -174,14 +180,17 @@ func (s *Service) Txt2Img(task types.SdTask) error {
errChan <- nil
}()
+ // waiting for task finish
for {
select {
- case err := <-errChan: // 任务完成
+ case err := <-errChan:
if err != nil {
return err
}
+
+ // task finished
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
- s.notifyQueue.RPush(task.UserId)
+ s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Finished})
// 从 leveldb 中删除预览图片数据
_ = s.leveldb.Delete(task.Params.TaskId)
return nil
@@ -191,7 +200,7 @@ func (s *Service) Txt2Img(task types.SdTask) error {
if err == nil && resp.Progress > 0 {
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
// 发送更新状态信号
- s.notifyQueue.RPush(task.UserId)
+ s.notifyQueue.RPush(NotifyMessage{UserId: task.UserId, JobId: task.Id, Message: Running})
// 保存预览图片数据
if resp.CurrentImage != "" {
_ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage)
@@ -207,7 +216,10 @@ func (s *Service) Txt2Img(task types.SdTask) error {
func (s *Service) checkTaskProgress() (error, *TaskProgressResp) {
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", s.config.ApiURL)
var res TaskProgressResp
- response, err := s.httpClient.R().SetSuccessResult(&res).Get(apiURL)
+ response, err := s.httpClient.R().
+ SetHeader("Authorization", s.config.ApiKey).
+ SetSuccessResult(&res).
+ Get(apiURL)
if err != nil {
return err, nil
}
diff --git a/api/service/sd/types.go b/api/service/sd/types.go
index 56ebb5bd..eb172bcd 100644
--- a/api/service/sd/types.go
+++ b/api/service/sd/types.go
@@ -4,44 +4,14 @@ import logger2 "chatplus/logger"
var logger = logger2.GetLogger()
-type TaskInfo struct {
- UserId uint `json:"user_id"`
- SessionId string `json:"session_id"`
- JobId int `json:"job_id"`
- TaskId string `json:"task_id"`
- Data []interface{} `json:"data"`
- EventData interface{} `json:"event_data"`
- FnIndex int `json:"fn_index"`
- SessionHash string `json:"session_hash"`
+type NotifyMessage struct {
+ UserId int `json:"user_id"`
+ JobId int `json:"job_id"`
+ Message string `json:"message"`
}
-type CBReq struct {
- UserId uint
- SessionId string
- JobId int
- TaskId string
- ImageName string
- ImageData string
- Progress int
- Seed int64
- Success bool
- Message string
-}
-
-var ParamKeys = map[string]int{
- "task_id": 0,
- "prompt": 1,
- "negative_prompt": 2,
- "steps": 4,
- "sampler": 5,
- "face_fix": 7, // 面部修复
- "cfg_scale": 8,
- "seed": 27,
- "height": 10,
- "width": 9,
- "hd_fix": 11,
- "hd_redraw_rate": 12, //高清修复重绘幅度
- "hd_scale": 13, // 高清修复放大倍数
- "hd_scale_alg": 14, // 高清修复放大算法
- "hd_sample_num": 15, // 高清修复采样次数
-}
+const (
+ Running = "RUNNING"
+ Finished = "FINISH"
+ Failed = "FAIL"
+)
diff --git a/api/service/smtp_sms_service.go b/api/service/smtp_sms_service.go
index fe094d49..256de934 100644
--- a/api/service/smtp_sms_service.go
+++ b/api/service/smtp_sms_service.go
@@ -3,9 +3,11 @@ package service
import (
"bytes"
"chatplus/core/types"
+ "crypto/tls"
"fmt"
"mime"
"net/smtp"
+ "net/textproto"
)
type SmtpService struct {
@@ -19,12 +21,18 @@ func NewSmtpService(appConfig *types.AppConfig) *SmtpService {
}
func (s *SmtpService) SendVerifyCode(to string, code int) error {
- subject := "ChatPlus注册验证码"
- body := fmt.Sprintf("您正在注册 ChatPlus AI 助手账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", code)
+ subject := "Geek-AI 注册验证码"
+ body := fmt.Sprintf("您正在注册 Geek-AI 助手账户,注册验证码为 %d,请不要告诉他人。如非本人操作,请忽略此邮件。", code)
- // 设置SMTP客户端配置
auth := smtp.PlainAuth("", s.config.From, s.config.Password, s.config.Host)
+ if s.config.UseTls {
+ return s.sendTLS(auth, to, subject, body)
+ } else {
+ return s.send(auth, to, subject, body)
+ }
+}
+func (s *SmtpService) send(auth smtp.Auth, to string, subject string, body string) error {
// 对主题进行MIME编码
encodedSubject := mime.QEncoding.Encode("UTF-8", subject)
// 组装邮件
@@ -34,11 +42,83 @@ func (s *SmtpService) SendVerifyCode(to string, code int) error {
message.WriteString(fmt.Sprintf("Subject: %s\r\n", encodedSubject))
message.WriteString("\r\n" + body)
- // 发送邮件
// 发送邮件
err := smtp.SendMail(s.config.Host+":"+fmt.Sprint(s.config.Port), auth, s.config.From, []string{to}, message.Bytes())
if err != nil {
return fmt.Errorf("error sending email: %v", err)
}
+
+ return err
+
+}
+
+func (s *SmtpService) sendTLS(auth smtp.Auth, to string, subject string, body string) error {
+ // TLS配置
+ tlsConfig := &tls.Config{
+ ServerName: s.config.Host,
+ }
+
+ // 建立TLS连接
+ conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), tlsConfig)
+ if err != nil {
+ return fmt.Errorf("error connecting to SMTP server: %v", err)
+ }
+ defer conn.Close()
+
+ client, err := smtp.NewClient(conn, s.config.Host)
+ if err != nil {
+ return fmt.Errorf("error creating SMTP client: %v", err)
+ }
+ defer client.Quit()
+
+ // 身份验证
+ if err = client.Auth(auth); err != nil {
+ return fmt.Errorf("error authenticating: %v", err)
+ }
+
+ // 设置寄件人
+ if err = client.Mail(s.config.From); err != nil {
+ return fmt.Errorf("error setting sender: %v", err)
+ }
+
+ // 设置收件人
+ if err = client.Rcpt(to); err != nil {
+ return fmt.Errorf("error setting recipient: %v", err)
+ }
+
+ // 发送邮件内容
+ wc, err := client.Data()
+ if err != nil {
+ return fmt.Errorf("error getting data writer: %v", err)
+ }
+ defer wc.Close()
+
+ header := make(textproto.MIMEHeader)
+ header.Set("From", s.config.From)
+ header.Set("To", to)
+ header.Set("Subject", subject)
+
+ // 将邮件头写入
+ for key, values := range header {
+ for _, value := range values {
+ _, err = fmt.Fprintf(wc, "%s: %s\r\n", key, value)
+ if err != nil {
+ return fmt.Errorf("error sending email header: %v", err)
+ }
+ }
+ }
+ _, _ = fmt.Fprintln(wc)
+ // 将邮件内容写入
+ _, err = fmt.Fprintf(wc, body)
+ if err != nil {
+ return fmt.Errorf("error sending email: %v", err)
+ }
+
+ // 发送完毕
+ err = wc.Close()
+ if err != nil {
+ return fmt.Errorf("error closing data writer: %v", err)
+ }
+
return nil
}
diff --git a/api/service/types.go b/api/service/types.go
index 9a8a0d00..15a538a2 100644
--- a/api/service/types.go
+++ b/api/service/types.go
@@ -1,4 +1,4 @@
package service
-const RewritePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other elements. Please output directly in English without any explanation, within 150 words. The text to be rewritten is: [%s]"
+const RewritePromptTemplate = "Please rewrite the following text into AI painting prompt words, and please try to add detailed description of the picture, painting style, scene, rendering effect, picture light and other creative elements. Just output the final prompt word directly. Do not output any explanation lines. The text to be rewritten is: [%s]"
const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"
diff --git a/api/store/leveldb.go b/api/store/leveldb.go
index c74d4090..269653e8 100644
--- a/api/store/leveldb.go
+++ b/api/store/leveldb.go
@@ -35,13 +35,12 @@ func (db *LevelDB) Put(key string, value interface{}) error {
return db.driver.Put([]byte(key), byteData, nil)
}
-func (db *LevelDB) Get(key string) ([]byte, error) {
+func (db *LevelDB) Get(key string, dist interface{}) error {
bytes, err := db.driver.Get([]byte(key), nil)
if err != nil {
- return nil, err
+ return err
}
-
- return bytes, nil
+ return json.Unmarshal(bytes, dist)
}
func (db *LevelDB) Search(prefix string) []string {
diff --git a/api/store/model/chat_model.go b/api/store/model/chat_model.go
index 8ddff961..134655f3 100644
--- a/api/store/model/chat_model.go
+++ b/api/store/model/chat_model.go
@@ -12,4 +12,5 @@ type ChatModel struct {
MaxTokens int // 最大响应长度
MaxContext int // 最大上下文长度
Temperature float32 // 模型温度
+ KeyId int // 绑定 API KEY ID
}
diff --git a/api/store/model/chat_role.go b/api/store/model/chat_role.go
index cc05cf7d..50e438bf 100644
--- a/api/store/model/chat_role.go
+++ b/api/store/model/chat_role.go
@@ -9,4 +9,5 @@ type ChatRole struct {
Icon string // 角色聊天图标
Enable bool // 是否启用被启用
SortNum int //排序数字
+ ModelId int // 绑定模型ID,绑定模型ID的角色只能用指定的模型来问答
}
diff --git a/api/store/model/dalle_job.go b/api/store/model/dalle_job.go
new file mode 100644
index 00000000..de7a13a0
--- /dev/null
+++ b/api/store/model/dalle_job.go
@@ -0,0 +1,16 @@
+package model
+
+import "time"
+
+type DallJob struct {
+ Id uint `gorm:"primarykey;column:id"`
+ UserId uint
+ Prompt string
+ ImgURL string
+ OrgURL string
+ Publish bool
+ Power int
+ Progress int
+ ErrMsg string
+ CreatedAt time.Time
+}
diff --git a/api/store/vo/chat_model.go b/api/store/vo/chat_model.go
index 81fc18ca..4fb21051 100644
--- a/api/store/vo/chat_model.go
+++ b/api/store/vo/chat_model.go
@@ -12,4 +12,6 @@ type ChatModel struct {
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
+ KeyId int `json:"key_id"`
+ KeyName string `json:"key_name"`
}
diff --git a/api/store/vo/chat_role.go b/api/store/vo/chat_role.go
index 52f696e5..e13d5f0c 100644
--- a/api/store/vo/chat_role.go
+++ b/api/store/vo/chat_role.go
@@ -4,11 +4,13 @@ import "chatplus/core/types"
type ChatRole struct {
BaseVo
- Key string `json:"key"` // 角色唯一标识
- Name string `json:"name"` // 角色名称
- Context []types.Message `json:"context"` // 角色语料信息
- HelloMsg string `json:"hello_msg"` // 打招呼的消息
- Icon string `json:"icon"` // 角色聊天图标
- Enable bool `json:"enable"` // 是否启用被启用
- SortNum int `json:"sort"` // 排序
+ Key string `json:"key"` // 角色唯一标识
+ Name string `json:"name"` // 角色名称
+ Context []types.Message `json:"context"` // 角色语料信息
+ HelloMsg string `json:"hello_msg"` // 打招呼的消息
+ Icon string `json:"icon"` // 角色聊天图标
+ Enable bool `json:"enable"` // 是否启用被启用
+ SortNum int `json:"sort"` // 排序
+ ModelId int `json:"model_id"` // 绑定模型 ID
+ ModelName string `json:"model_name"` // 模型名称
}
diff --git a/api/store/vo/dalle_job.go b/api/store/vo/dalle_job.go
new file mode 100644
index 00000000..28a6906d
--- /dev/null
+++ b/api/store/vo/dalle_job.go
@@ -0,0 +1,14 @@
+package vo
+
+type DallJob struct {
+ Id uint `json:"id"`
+ UserId int `json:"user_id"`
+ Prompt string `json:"prompt"`
+ ImgURL string `json:"img_url"`
+ OrgURL string `json:"org_url"`
+ Publish bool `json:"publish"`
+ Power int `json:"power"`
+ Progress int `json:"progress"`
+ ErrMsg string `json:"err_msg"`
+ CreatedAt int64 `json:"created_at"`
+}
diff --git a/api/test/test.go b/api/test/test.go
index cc826def..ff31c2f5 100644
--- a/api/test/test.go
+++ b/api/test/test.go
@@ -1,12 +1,12 @@
package main
import (
+ "chatplus/utils"
"fmt"
- "reflect"
)
func main() {
- text := 1
- bytes := reflect.ValueOf(text).Bytes()
- fmt.Println(bytes)
+ text := "https://nk.img.r9it.com/chatgpt-plus/1712709360012445.png 请简单描述一下这幅图上的内容 "
+ imgURL := utils.ExtractImgURL(text)
+ fmt.Println(imgURL)
}
diff --git a/api/utils/openai.go b/api/utils/openai.go
index 584f0435..53b61264 100644
--- a/api/utils/openai.go
+++ b/api/utils/openai.go
@@ -83,4 +83,4 @@ func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
return response.Choices[0].Message.Content, nil
-}
+}
\ No newline at end of file
diff --git a/api/utils/upload.go b/api/utils/upload.go
index 1bab2aca..695d9183 100644
--- a/api/utils/upload.go
+++ b/api/utils/upload.go
@@ -7,6 +7,7 @@ import (
"net/url"
"os"
"path/filepath"
+ "regexp"
"strings"
"time"
)
@@ -79,3 +80,15 @@ func GetImgExt(filename string) string {
}
return ext
}
+
+func ExtractImgURL(text string) []string {
+ re := regexp.MustCompile(`(http[s]?:\/\/.*?\.(?:png|jpg|jpeg|gif))`)
+ matches := re.FindAllStringSubmatch(text, 10)
+ urls := make([]string, 0)
+ if len(matches) > 0 {
+ for _, m := range matches {
+ urls = append(urls, m[1])
+ }
+ }
+ return urls
+}
diff --git a/database/chatgpt_plus-v4.0.4.sql b/database/chatgpt_plus-v4.0.4.sql
new file mode 100644
index 00000000..ed589c81
--- /dev/null
+++ b/database/chatgpt_plus-v4.0.4.sql
@@ -0,0 +1,840 @@
+-- phpMyAdmin SQL Dump
+-- version 5.1.3
+-- https://www.phpmyadmin.net/
+--
+-- 主机: localhost:3306
+-- 生成日期: 2024-04-23 18:39:13
+-- 服务器版本: 8.0.33-0ubuntu0.22.04.2
+-- PHP 版本: 8.1.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, 1713842191, '::1', '2024-03-11 16:30:20', '2024-04-23 11:16:31');
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `chatgpt_api_keys`
+--
+
+DROP TABLE IF EXISTS `chatgpt_api_keys`;
+CREATE TABLE `chatgpt_api_keys` (
+ `id` int NOT NULL,
+ `platform` char(20) DEFAULT NULL COMMENT '平台',
+ `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(100) 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,
+ `platform` varchar(20) DEFAULT NULL COMMENT '模型平台',
+ `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` tinyint 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`, `platform`, `name`, `value`, `sort_num`, `enabled`, `power`, `temperature`, `max_tokens`, `max_context`, `open`, `key_id`, `created_at`, `updated_at`) VALUES
+(1, 'OpenAI', 'GPT-3.5', 'gpt-3.5-turbo-0125', 1, 1, 1, 1.0, 1024, 4096, 1, 0, '2023-08-23 12:06:36', '2024-04-23 16:24:15'),
+(2, 'Azure', 'Azure-3.5', 'gpt-3.5-turbo', 21, 1, 1, 1.0, 1024, 4096, 0, 0, '2023-08-23 12:15:30', '2024-04-15 11:39:18'),
+(3, 'ChatGLM', 'ChatGML-Pro', 'chatglm_pro', 10, 1, 1, 1.0, 2048, 32768, 1, 0, '2023-08-23 13:35:45', '2024-04-15 11:39:18'),
+(7, 'Baidu', '文心一言3.0', 'eb-instant', 19, 1, 1, 1.0, 1024, 4096, 1, 0, '2023-10-11 11:29:28', '2024-04-15 11:39:18'),
+(8, 'XunFei', '星火V3.5', 'generalv3.5', 9, 1, 5, 0.8, 1024, 8192, 1, 0, '2023-10-11 15:48:30', '2024-04-15 11:39:18'),
+(9, 'XunFei', '星火V2.0', 'generalv2', 18, 1, 1, 1.0, 1024, 8192, 1, 0, '2023-10-11 15:48:45', '2024-04-15 11:39:18'),
+(10, 'Baidu', '文心一言4.0', 'completions_pro', 20, 1, 3, 1.0, 1024, 8192, 1, 0, '2023-10-25 08:31:37', '2024-04-15 11:39:18'),
+(11, 'OpenAI', 'GPT-4.0', 'gpt-4-0125-preview', 8, 1, 15, 1.0, 2048, 8192, 1, 0, '2023-10-25 08:45:15', '2024-04-17 11:54:57'),
+(12, 'XunFei', '星火v3.0', 'generalv3', 17, 1, 3, 1.0, 1024, 8192, 1, 0, '2023-11-23 09:20:33', '2024-04-15 11:39:18'),
+(15, 'OpenAI', 'GPT-超级模型', 'gpt-4-all', 11, 1, 30, 1.0, 4096, 32768, 0, 0, '2024-01-15 11:32:52', '2024-04-15 11:39:18'),
+(16, 'OpenAI', '视频号导师', 'gpt-4-gizmo-g-QXXEBTXl7', 12, 1, 30, 1.0, 4096, 32768, 0, 0, '2024-01-15 14:46:35', '2024-04-15 11:39:18'),
+(17, 'QWen', '通义千问-Turbo', 'qwen-turbo', 14, 1, 1, 1.0, 1024, 8192, 1, 0, '2024-01-19 10:42:24', '2024-04-15 11:39:18'),
+(18, 'QWen', '通义千问-Plus', 'qwen-plus', 15, 1, 1, 1.0, 1024, 32768, 1, 0, '2024-01-19 10:42:49', '2024-04-15 11:39:18'),
+(19, 'QWen', '通义千问-Max', 'qwen-max-1201', 16, 1, 1, 1.0, 1024, 32768, 1, 0, '2024-01-19 10:51:03', '2024-04-15 11:39:18'),
+(21, 'OpenAI', '董宇辉小作文助手', 'gpt-4-gizmo-g-dse9iXvor', 13, 1, 30, 1.0, 8192, 32768, 0, 0, '2024-03-18 14:24:20', '2024-04-15 11:39:18'),
+(22, 'OpenAI', 'LOGO生成神器', 'gpt-4-gizmo-g-YL87j8C7S', 7, 1, 30, 1.0, 1024, 4096, 1, 44, '2024-03-20 14:02:11', '2024-04-15 11:39:18'),
+(23, 'OpenAI', '音乐生成器', 'suno-v3', 6, 1, 50, 0.8, 1024, 4096, 1, 44, '2024-03-29 15:43:40', '2024-04-15 11:39:18'),
+(24, 'OpenAI', '通义千问(中转)', 'qwen-plus', 5, 1, 1, 1.0, 1024, 4096, 1, 44, '2024-04-03 12:00:46', '2024-04-17 13:41:27'),
+(25, 'OpenAI', 'GPT4-TURBO', 'gpt-4-turbo-2024-04-09', 4, 1, 15, 1.0, 2048, 8092, 1, 44, '2024-04-10 08:35:17', '2024-04-23 12:03:23'),
+(26, 'QWen', '通义千问-Turbo', 'qwen-turbo', 3, 1, 2, 1.0, 1024, 8192, 1, 0, '2024-04-12 14:11:19', '2024-04-15 11:39:18'),
+(27, 'QWen', '通义千问-Plus', 'qwen-plus', 2, 1, 2, 1.0, 1024, 8192, 1, 0, '2024-04-12 14:11:52', '2024-04-15 11:39:18'),
+(28, 'OpenAI', 'GPT-3.5(免费)', 'gpt-3.5-turbo', 0, 1, 0, 1.0, 1024, 16384, 1, 50, '2024-04-12 15:16:43', '2024-04-15 11:39:18');
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `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, 0, 0, '2023-05-30 07:02:06', '2024-03-15 09:15:42'),
+(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 3, 0, '2023-05-30 14:10:24', '2023-12-29 17:46:45'),
+(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 2, 0, '2023-05-30 14:10:24', '2023-12-29 17:46:44'),
+(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 4, 0, '2023-05-30 14:10:24', '2023-12-29 17:46:45'),
+(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '作为一名心理学家和心理治疗师,我的主要职责是帮助您解决心理健康问题,提升您的生活质量和幸福感。', '/images/avatar/psychiatrist.jpg', 1, 1, 1, '2023-05-30 14:10:24', '2024-04-12 11:54:53'),
+(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 5, 0, '2023-05-30 14:10:24', '2023-12-29 17:46:46'),
+(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装:¥1188/箱,和系列 6 瓶装:¥2208/箱,贵系列 6 瓶装:¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 8, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(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, 6, 0, '2023-05-30 14:10:24', '2023-12-29 17:46:47'),
+(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 7, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 9, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 10, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 11, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 12, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 13, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 14, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 15, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53'),
+(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 16, 0, '2023-05-30 14:10:24', '2023-12-29 17:43:53');
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `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\":\"Geek-AI 创作系统\",\"admin_title\":\"Geek-AI 控制台\",\"logo\":\"http://localhost:5678/static/upload/2024/4/1713839794927656.png\",\"init_power\":100,\"daily_power\":99,\"invite_power\":10,\"vip_month_power\":1000,\"register_ways\":[\"mobile\",\"username\",\"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\":1800,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"default_models\":[11,7,1,10,12,19,18,17,3],\"mj_power\":20,\"mj_action_power\":10,\"sd_power\":5,\"dall_power\":15,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":4}'),
+(3, 'notice', '{\"content\":\"系统每日会给免费会员赠送10算力值,用完请第二天再来领取。\\n## v4.0.4 更新日志\\n\\n* Bug修复:修复统一千问第二句不回复的问题\\n* 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度\\n* 功能新增:Dalle AI 绘画功能实现\\n* Bug修复:修复思维导图格式乱码问题\\n* 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题\\n* 功能新增:支持从应用列表直接和某个应用对话\\n* 功能优化:优化算力日志的页面和首页的UI\\n* 功能新增:支持思维导图导出 PNG 图片下载\\n\\n 如果觉得好用你就花几分钟自己部署一套,没有API KEY 的同学可以去\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e (支持MidJourney,GPT,Claude,Google Gemmi 各种表格模型) 或者 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e(不支持 Midjourney) 购买,现在有超级优惠,价格远低于 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)。\\nGPT-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\\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(400) 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', 1);
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `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, '对话聊天', '/images/menu/chat.png', '/chat', 0, 1),
+(5, 'MJ 绘画', '/images/menu/mj.png', '/mj', 1, 1),
+(6, 'SD 绘画', '/images/menu/sd.png', '/sd', 2, 1),
+(7, '算力日志', '/images/menu/log.png', '/powerLog', 7, 1),
+(8, '应用中心', '/images/menu/app.png', '/apps', 6, 1),
+(9, '作品展示', '/images/menu/img-wall.png', '/images-wall', 4, 1),
+(10, '会员计划', '/images/menu/member.png', '/member', 8, 1),
+(11, '分享计划', '/images/menu/share.png', '/invite', 9, 1),
+(12, '思维导图', '/images/menu/xmind.png', '/xmind', 5, 1),
+(13, 'DALLE', '/images/menu/dalle.png', '/dalle', 3, 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
+(1, '会员1个月', '1999.90', '10.00', 30, 0, 1, 0, 0, '2023-08-28 10:48:57', '2024-03-22 17:56:03', NULL, NULL),
+(2, '会员3个月', '3940.00', '30.00', 90, 0, 1, 0, 0, '2023-08-28 10:52:22', '2024-03-22 17:56:10', NULL, NULL),
+(3, '会员6个月', '5990.00', '100.00', 180, 0, 1, 0, 0, '2023-08-28 10:53:39', '2024-03-22 17:56:15', NULL, NULL),
+(4, '会员12个月', '9980.00', '200.00', 365, 0, 1, 0, 0, '2023-08-28 10:54:15', '2024-03-22 17:56:23', NULL, NULL),
+(5, '100次点卡', '1999.00', '3.00', 0, 100, 1, 0, 0, '2023-08-28 10:55:08', '2024-03-22 17:56:37', NULL, NULL),
+(6, '200次点卡', '2999.00', '10.00', 0, 200, 1, 0, 0, '1970-01-01 08:00:00', '2024-03-22 17:56:41', 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_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(100) 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',
+ `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`, `created_at`, `updated_at`) VALUES
+(4, '18575670125', '极客学长@830270', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://nk.img.r9it.com/chatgpt-plus/1712909716642025.png', 'ueedue5l', 8531, 1717292086, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"programmer\",\"seller\",\"artist\",\"lu_xun\",\"girl_friend\"]', '[1,11]', 1713840025, 1, '::1', '2023-06-12 16:47:17', '2024-04-23 10:40:26'),
+(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, '', '2024-04-23 09:17:26', '2024-04-23 09:17:26');
+
+-- --------------------------------------------------------
+
+--
+-- 表的结构 `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_users`
+--
+ALTER TABLE `chatgpt_users`
+ ADD PRIMARY KEY (`id`),
+ ADD UNIQUE KEY `username` (`username`),
+ ADD UNIQUE KEY `username_2` (`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=33;
+
+--
+-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
+--
+ALTER TABLE `chatgpt_chat_roles`
+ MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=130;
+
+--
+-- 使用表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=14;
+
+--
+-- 使用表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_users`
+--
+ALTER TABLE `chatgpt_users`
+ MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6;
+
+--
+-- 使用表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.0.3.sql b/database/update-v4.0.3.sql
new file mode 100644
index 00000000..80250e73
--- /dev/null
+++ b/database/update-v4.0.3.sql
@@ -0,0 +1,3 @@
+ALTER TABLE `chatgpt_chat_roles` ADD `model_id` INT NOT NULL DEFAULT '0' COMMENT '绑定模型ID' AFTER `sort_num`;
+ALTER TABLE `chatgpt_chat_models` ADD `key_id` INT(11) NOT NULL COMMENT '绑定API KEY ID' AFTER `open`;
+INSERT INTO `chatgpt_plus`.`chatgpt_menus`(`id`, `name`, `icon`, `url`, `sort_num`, `enabled`) VALUES (12, '思维导图', '/images/menu/xmind.png', '/xmind', 3, 1);
\ No newline at end of file
diff --git a/database/update-v4.0.4.sql b/database/update-v4.0.4.sql
new file mode 100644
index 00000000..d61cabb0
--- /dev/null
+++ b/database/update-v4.0.4.sql
@@ -0,0 +1,6 @@
+CREATE TABLE `chatgpt_plus`.`chatgpt_dall_jobs` ( `id` INT(11) NOT NULL AUTO_INCREMENT , `user_id` INT(11) NOT NULL COMMENT '用户ID' , `task_id` VARCHAR(20) NOT NULL COMMENT '任务ID' , `prompt` VARCHAR(2000) NOT NULL COMMENT '提示词' , `img_url` VARCHAR(255) NOT NULL COMMENT '图片地址' , `publish` TINYINT(1) NOT NULL COMMENT '是否发布' , `power` SMALLINT(3) NOT NULL COMMENT '消耗算力' , `progress` SMALLINT(3) NOT NULL COMMENT '任务进度' , `err_msg` VARCHAR(255) NOT NULL COMMENT '错误信息' , `created_at` DATETIME NOT NULL , PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = 'DALLE 绘图任务表';
+
+ALTER TABLE `chatgpt_dall_jobs` ADD `org_url` VARCHAR(400) NULL COMMENT '原图地址' AFTER `img_url`;
+ALTER TABLE `chatgpt_dall_jobs` DROP `task_id`;
+
+
diff --git a/deploy/docker-compose.yaml b/deploy/docker-compose.yaml
index 89510640..bd80cb6a 100644
--- a/deploy/docker-compose.yaml
+++ b/deploy/docker-compose.yaml
@@ -19,7 +19,7 @@ services:
# redis
chatgpt-plus-redis:
image: redis:6.0.16
- restart: always
+ restart: always
container_name: chatgpt-plus-redis
command: redis-server --requirepass 12345678
volumes :
@@ -53,7 +53,7 @@ services:
# 后端 API 程序
chatgpt-plus-api:
- image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v4.0.2-amd64
+ image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v4.0.4-amd64
container_name: chatgpt-plus-api
restart: always
depends_on:
@@ -76,7 +76,7 @@ services:
# 前端应用
chatgpt-plus-web:
- image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v4.0.2-amd64
+ image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v4.0.4-amd64
container_name: chatgpt-plus-web
restart: always
depends_on:
diff --git a/web/.env.development b/web/.env.development
index 330da87b..8474f044 100644
--- a/web/.env.development
+++ b/web/.env.development
@@ -6,4 +6,4 @@ 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.0.2
+VUE_APP_VERSION=v4.0.3
diff --git a/web/.env.production b/web/.env.production
index c6581695..e1a98fa3 100644
--- a/web/.env.production
+++ b/web/.env.production
@@ -2,4 +2,4 @@ VUE_APP_API_HOST=
VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=ChatPLUS_
VUE_APP_TITLE="Geek-AI 创作系统"
-VUE_APP_VERSION=v4.0.2
+VUE_APP_VERSION=v4.0.3
diff --git a/web/package.json b/web/package.json
index 173d8e70..2ff7ad78 100644
--- a/web/package.json
+++ b/web/package.json
@@ -22,12 +22,16 @@
"markdown-it": "^13.0.1",
"markdown-it-latex2img": "^0.0.6",
"markdown-it-mathjax": "^2.0.0",
+ "markmap-common": "^0.16.0",
+ "markmap-lib": "^0.16.1",
+ "markmap-view": "^0.16.0",
"md-editor-v3": "^2.2.1",
"moment": "^2.30.1",
"pinia": "^2.1.4",
"qrcode": "^1.5.3",
"qs": "^6.11.1",
"sortablejs": "^1.15.0",
+ "three": "^0.128.0",
"v3-waterfall": "^1.2.1",
"vant": "^4.5.0",
"vue": "^3.2.13",
diff --git a/web/public/images/land_ocean_ice_cloud_2048.jpg b/web/public/images/land_ocean_ice_cloud_2048.jpg
new file mode 100644
index 00000000..d90ced72
Binary files /dev/null and b/web/public/images/land_ocean_ice_cloud_2048.jpg differ
diff --git a/web/public/images/logo.png b/web/public/images/logo.png
index 43e5d544..78753b32 100644
Binary files a/web/public/images/logo.png and b/web/public/images/logo.png differ
diff --git a/web/public/images/menu/dalle.png b/web/public/images/menu/dalle.png
new file mode 100644
index 00000000..166d2215
Binary files /dev/null and b/web/public/images/menu/dalle.png differ
diff --git a/web/public/images/menu/more.png b/web/public/images/menu/more.png
new file mode 100644
index 00000000..187ef700
Binary files /dev/null and b/web/public/images/menu/more.png differ
diff --git a/web/public/images/menu/xmind.png b/web/public/images/menu/xmind.png
new file mode 100644
index 00000000..910dc486
Binary files /dev/null and b/web/public/images/menu/xmind.png differ
diff --git a/web/src/assets/css/chat-app.styl b/web/src/assets/css/chat-app.styl
index daafa42c..a5bc9a8a 100644
--- a/web/src/assets/css/chat-app.styl
+++ b/web/src/assets/css/chat-app.styl
@@ -47,6 +47,7 @@
.opt {
position: relative;
+ width 100%
top -5px
}
}
diff --git a/web/src/assets/css/chat-plus.styl b/web/src/assets/css/chat-plus.styl
index a0e4d4ac..e77a016c 100644
--- a/web/src/assets/css/chat-plus.styl
+++ b/web/src/assets/css/chat-plus.styl
@@ -11,6 +11,7 @@ $borderColor = #4676d0;
.el-aside {
background-color: $sideBgColor;
+ height 100vh
.title-box {
padding: 6px 10px;
diff --git a/web/src/assets/css/image-dall.styl b/web/src/assets/css/image-dall.styl
new file mode 100644
index 00000000..caf514a0
--- /dev/null
+++ b/web/src/assets/css/image-dall.styl
@@ -0,0 +1,88 @@
+.page-dall {
+ background-color: #282c34;
+
+ .inner {
+ display: flex;
+
+ .sd-box {
+ margin 10px
+ background-color #262626
+ border 1px solid #454545
+ min-width 300px
+ max-width 300px
+ padding 10px
+ border-radius 10px
+ color #ffffff;
+ font-size 14px
+
+ h2 {
+ font-weight: bold;
+ font-size 20px
+ text-align center
+ color #47fff1
+ }
+
+ // 隐藏滚动条
+
+ ::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ background-color: transparent;
+ }
+
+ .sd-params {
+ margin-top 10px
+ overflow auto
+
+
+ .param-line {
+ padding 0 10px
+
+ .grid-content
+ .form-item-inner {
+ display flex
+
+ .info-icon {
+ margin-left 10px
+ position relative
+ top 8px
+ }
+ }
+
+ }
+
+ .param-line.pt {
+ padding-top 5px
+ padding-bottom 5px
+ }
+
+ .text-info {
+ padding 10px
+ }
+ }
+
+ .submit-btn {
+ padding 10px 15px 0 15px
+ text-align center
+
+ .el-button {
+ width 100%
+
+ span {
+ color #2D3A4B
+ }
+ }
+ }
+ }
+
+ .el-form {
+ .el-form-item__label {
+ color #ffffff
+ }
+ }
+
+ @import "task-list.styl"
+ }
+
+}
+
diff --git a/web/src/assets/css/image-sd.styl b/web/src/assets/css/image-sd.styl
index 904eced8..ba860bc3 100644
--- a/web/src/assets/css/image-sd.styl
+++ b/web/src/assets/css/image-sd.styl
@@ -38,24 +38,14 @@
.param-line {
padding 0 10px
- .el-icon {
- position relative
- top 3px
- }
-
- .el-input__suffix-inner {
- .el-icon {
- top 0
- }
- }
-
.grid-content
.form-item-inner {
display flex
- .el-icon {
+ .info-icon {
margin-left 10px
- margin-top 2px
+ position relative
+ top 8px
}
}
@@ -68,10 +58,6 @@
.text-info {
padding 10px
-
- .el-tag {
- margin-right 10px
- }
}
}
diff --git a/web/src/assets/css/mark-map.styl b/web/src/assets/css/mark-map.styl
new file mode 100644
index 00000000..096c3f32
--- /dev/null
+++ b/web/src/assets/css/mark-map.styl
@@ -0,0 +1,134 @@
+.page-mark-map {
+ background-color: #282c34;
+ height 100vh
+
+ .inner {
+ display: flex;
+
+ .mark-map-box {
+ margin 10px
+ background-color #262626
+ border 1px solid #454545
+ min-width 300px
+ max-width 300px
+ padding 10px
+ border-radius 10px
+ color #ffffff;
+ font-size 14px
+
+ h2 {
+ font-weight: bold;
+ font-size 20px
+ text-align center
+ color #47fff1
+ }
+
+ // 隐藏滚动条
+ ::-webkit-scrollbar {
+ width: 0;
+ height: 0;
+ background-color: transparent;
+ }
+
+ .mark-map-params {
+ margin-top 10px
+ overflow auto
+
+
+ .param-line {
+ padding 10px
+
+ .el-button {
+ width 100%
+
+ span {
+ color #2D3A4B
+ }
+ }
+
+ }
+
+ .text-info {
+ padding 10px
+
+ .el-tag {
+ margin-right 10px
+ }
+ }
+ }
+ }
+
+ .el-form {
+ .el-form-item__label {
+ color #ffffff
+ }
+ }
+
+ .right-box {
+ width 100%
+
+ .top-bar {
+ display flex
+ justify-content space-between
+ align-items center
+
+ h2 {
+ color #ffffff
+ }
+
+ .el-button {
+ margin-right 20px
+ }
+ }
+
+ .markdown {
+ color #ffffff
+ display flex
+ justify-content center
+ align-items center
+
+ h1 {
+ color: #47fff1;
+ }
+
+ h2 {
+ color: #ffcc00;
+ }
+
+ ul {
+ list-style-type: disc;
+ margin-left: 20px;
+
+ li {
+ line-height 1.5
+ }
+ }
+
+ strong {
+ font-weight: bold;
+ }
+
+ em {
+ font-style: italic;
+ }
+ }
+
+ .body {
+ display flex
+ justify-content center
+ align-items center
+
+ .markmap {
+ width 100%
+ color #ffffff
+ font-size 12px
+
+ .markmap-foreign {
+ //height 30px
+ }
+ }
+ }
+ }
+ }
+}
+
diff --git a/web/src/assets/iconfont/iconfont.css b/web/src/assets/iconfont/iconfont.css
index 8706b25b..3ff257a6 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=1708054962140') format('woff2'),
- url('iconfont.woff?t=1708054962140') format('woff'),
- url('iconfont.ttf?t=1708054962140') format('truetype');
+ src: url('iconfont.woff2?t=1713766977199') format('woff2'),
+ url('iconfont.woff?t=1713766977199') format('woff'),
+ url('iconfont.ttf?t=1713766977199') format('truetype');
}
.iconfont {
@@ -13,6 +13,38 @@
-moz-osx-font-smoothing: grayscale;
}
+.icon-more:before {
+ content: "\e63c";
+}
+
+.icon-mj:before {
+ content: "\e643";
+}
+
+.icon-dalle:before {
+ content: "\e646";
+}
+
+.icon-xmind:before {
+ content: "\e610";
+}
+
+.icon-version:before {
+ content: "\e68d";
+}
+
+.icon-sd:before {
+ content: "\e62b";
+}
+
+.icon-huihua1:before {
+ content: "\e606";
+}
+
+.icon-chat:before {
+ content: "\e68a";
+}
+
.icon-prompt:before {
content: "\e6ce";
}
diff --git a/web/src/assets/iconfont/iconfont.js b/web/src/assets/iconfont/iconfont.js
index 2c4dfc7b..9aab97ef 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,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,s(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,v())})}function v(){z||(z=!0,i())}function s(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(s,50)}v()}}(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,v(),o.onreadystatechange=function(){"complete"==o.readyState&&(o.onreadystatechange=null,s())})}function s(){z||(z=!0,i())}function v(){try{o.documentElement.doScroll("left")}catch(l){return void setTimeout(v,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 3186e019..47a9dc74 100644
--- a/web/src/assets/iconfont/iconfont.json
+++ b/web/src/assets/iconfont/iconfont.json
@@ -5,6 +5,62 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
+ {
+ "icon_id": "1421807",
+ "name": "更多",
+ "font_class": "more",
+ "unicode": "e63c",
+ "unicode_decimal": 58940
+ },
+ {
+ "icon_id": "36264781",
+ "name": "MidJourney",
+ "font_class": "mj",
+ "unicode": "e643",
+ "unicode_decimal": 58947
+ },
+ {
+ "icon_id": "37677137",
+ "name": "DALL·E 3",
+ "font_class": "dalle",
+ "unicode": "e646",
+ "unicode_decimal": 58950
+ },
+ {
+ "icon_id": "2629858",
+ "name": "逻辑图",
+ "font_class": "xmind",
+ "unicode": "e610",
+ "unicode_decimal": 58896
+ },
+ {
+ "icon_id": "1061336",
+ "name": "version",
+ "font_class": "version",
+ "unicode": "e68d",
+ "unicode_decimal": 59021
+ },
+ {
+ "icon_id": "3901033",
+ "name": "绘画",
+ "font_class": "sd",
+ "unicode": "e62b",
+ "unicode_decimal": 58923
+ },
+ {
+ "icon_id": "39185683",
+ "name": "绘画",
+ "font_class": "huihua1",
+ "unicode": "e606",
+ "unicode_decimal": 58886
+ },
+ {
+ "icon_id": "2341972",
+ "name": "chat",
+ "font_class": "chat",
+ "unicode": "e68a",
+ "unicode_decimal": 59018
+ },
{
"icon_id": "8017627",
"name": "prompt",
diff --git a/web/src/assets/iconfont/iconfont.ttf b/web/src/assets/iconfont/iconfont.ttf
index cc125590..4140a66c 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 fd9a3d84..210c4aa4 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 0021a3ae..2a217889 100644
Binary files a/web/src/assets/iconfont/iconfont.woff2 and b/web/src/assets/iconfont/iconfont.woff2 differ
diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue
index 57f136c5..a6e9d42a 100644
--- a/web/src/components/ChatPrompt.vue
+++ b/web/src/components/ChatPrompt.vue
@@ -90,6 +90,7 @@ export default defineComponent({
}
.chat-item {
+ width 100%
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue
index a15d612b..cce22cc2 100644
--- a/web/src/components/ChatReply.vue
+++ b/web/src/components/ChatReply.vue
@@ -93,6 +93,7 @@ export default defineComponent({
}
.chat-item {
+ width 100%
position: relative;
padding: 0 0 0 5px;
overflow: hidden;
diff --git a/web/src/components/FooterBar.vue b/web/src/components/FooterBar.vue
index 15165d69..a8b263e9 100644
--- a/web/src/components/FooterBar.vue
+++ b/web/src/components/FooterBar.vue
@@ -1,8 +1,8 @@
-
+