Compare commits

...

38 Commits

Author SHA1 Message Date
RockYang
329e3eee21 opt:Optimize the algorithm to check whether Midjourney images can continue to variation 2023-08-17 14:20:16 +08:00
RockYang
07049c9afb docs: update demo url 2023-08-17 07:18:44 +08:00
RockYang
36c5dd7eaa chore: show img_calls num for user profile page 2023-08-17 07:09:53 +08:00
RockYang
b84039b506 docs: update docs 2023-08-17 06:55:11 +08:00
RockYang
fab43097dc feat: add authorization for MidJourney function calls 2023-08-16 23:16:44 +08:00
RockYang
c8998ba294 chore: update database sql file 2023-08-15 22:53:32 +08:00
RockYang
40b2466adc opt: unset timeout for websocket connection 2023-08-15 18:29:53 +08:00
RockYang
35fedbe817 feat: midjourney image variation function is ready 2023-08-15 17:31:02 +08:00
RockYang
827acdd3f9 feat: midjourney image upscale function is ready 2023-08-15 15:28:40 +08:00
RockYang
6c76086916 feat: midjourney drawing image function is ready 2023-08-14 17:59:21 +08:00
RockYang
373370fde5 feat: add midjourney message receive handler 2023-08-14 07:09:52 +08:00
RockYang
2165ba3406 feat: add midjouney api implements, optimize function calls 2023-08-11 18:46:56 +08:00
RockYang
b0e02b43fc feat: remove wechat bot, replace with api callback for receive wechat payment transactions 2023-08-10 17:24:23 +08:00
RockYang
2107c13b3d feat: add samples and function introduce in welcome page 2023-08-08 14:19:09 +08:00
RockYang
5f41aecc8d docs: add dashboard image 2023-08-07 20:07:05 +08:00
RockYang
6840a13370 feat: enabled user to set default GPT model for chat 2023-08-04 14:25:54 +08:00
RockYang
8f1e28c0ab feat: chat export function is ready 2023-08-04 12:08:07 +08:00
RockYang
7903eed284 feat: add chat export button 2023-08-03 18:24:30 +08:00
RockYang
0d49ea0d41 feat: add rewards statistic in dashboard page 2023-08-03 10:19:37 +08:00
RockYang
2ee4db5e48 opt: add enabled_msg_service config var to system config database 2023-08-03 10:04:45 +08:00
RockYang
48c4789505 opt: optimize build script, auto push to aliyun docker hub 2023-08-02 18:09:19 +08:00
RockYang
4e65a5b1a1 opt: optimize the layout for regiseter page. add function to disable registration 2023-08-02 17:44:13 +08:00
RockYang
b09d23f97f feat: the dashboard page is ready for admin console 2023-08-02 16:37:47 +08:00
RockYang
3529649ba9 refactor: refactor admin console page layout 2023-08-02 15:00:18 +08:00
RockYang
fdd659f393 fix: fixed bug for chat request's token reach the max token limit for OpenAI 2023-08-01 17:58:03 +08:00
RockYang
9eb8da2789 feat: new function for add user in manger console user list page 2023-08-01 16:02:49 +08:00
RockYang
ffb1ef0470 doc: add video tutorial for deploying system 2023-08-01 10:15:43 +08:00
RockYang
862c6aea43 docs: remove docs for deploy manually 2023-07-31 20:04:22 +08:00
RockYang
54fe4b7588 opt: optimize the build script and docker-compose yaml file 2023-07-31 17:31:02 +08:00
RockYang
c6062ee70e feat: add env var to set log level 2023-07-31 08:34:11 +08:00
RockYang
bed184dc1f feat: update system config cache for AppServer when updating system config in admin console 2023-07-31 08:13:20 +08:00
RockYang
29094ba3b3 add system config cache for AppServer object 2023-07-31 06:56:28 +08:00
RockYang
a18188876c feat: allow user to login with username and mobile no 2023-07-27 15:37:50 +08:00
RockYang
4faee3e48e fix: deauthorize some apis 2023-07-27 10:53:14 +08:00
RockYang
1a6afcd266 fix: fix bug for mobile verify code function not available in register page 2023-07-27 10:29:59 +08:00
RockYang
f567831d92 style: adjust reward image styles 2023-07-26 17:57:19 +08:00
RockYang
cf36ca4285 chore: optimize build script, compress reward img 2023-07-26 17:48:16 +08:00
RockYang
0e4ae01498 feat: add reset password function to user list page 2023-07-26 15:22:11 +08:00
98 changed files with 2924 additions and 1047 deletions

View File

@@ -1,5 +1,27 @@
# 更新日志
## v3.0.7
1. 聊天主界面:新增聊天引导页面,介绍产品功能
2. 功能重构拆分项目将函数插件以及微信机器人MidJourney 机器人等功能拆分新项目独立部署。
3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。
4. 功能新增:支持导出聊天记录为 PDF 文件。
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
6. 功能优化:支持用户设置默认的 GPT 模型
7. Bug修复修复若干已知的的 Bug
## v3.0.6
1. 管理后台:新增用户名和手机号码搜索功能
2. 管理后台:新增重置用户密码功能
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
5. Bug修复修复注册页面验证码不显示 Bug
6. Bug修复优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
7. 功能修正:允许用户使用手机号码登录
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
9. 功能优化:优化打包脚本,减少容器镜像大小
## v3.0.5
重磅功能更新!!! 新增函数插件支持可以轻松地接入你的第三方插件服务ChatGPT 自动帮您调用对应的函数完成任务。

122
README.md
View File

@@ -20,6 +20,8 @@
![ChatGPT function plugin](docs/imgs/plugin.png)
![ChatGPT function plugin](docs/imgs/mj.jpg)
### 用户设置
![ChatGPT user profle](docs/imgs/user_profile.png)
@@ -30,6 +32,8 @@
### 管理后台
![ChatGPT admin](docs/imgs/admin_dashboard.png)
![ChatGPT admin](docs/imgs/admin_config.png)
![ChatGPT admin](docs/imgs/admin_user.png)
@@ -42,7 +46,7 @@
### 7. 体验地址
> 免费体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/>
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/>
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
## 使用须知
@@ -100,6 +104,10 @@ ChatGPT 的服务。
## Docker 快速部署
>
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
### 1. 导入数据库
@@ -132,8 +140,6 @@ Listen = "0.0.0.0:5678"
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理http://127.0.0.1:7777
#ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
StartWechatBot = false # 是否启动微信机器人,默认关闭,如果设置为 TRUE 则启动服务的时候需要微信扫码登录
EnabledMsgService = false # 注册时是否开启短信验证功能,该功能需要配合短信服务一起使用
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
@@ -149,7 +155,7 @@ EnabledMsgService = false # 注册时是否开启短信验证功能,该功能
Username = "admin"
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
[ApiConfig] # 插件 API 服务配置,此为第三方插件服务,如需使用请联系作者开通
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
ApiURL = "{URL}"
AppId = "{APP_ID}"
Token = "{TOKEN}"
@@ -160,8 +166,14 @@ EnabledMsgService = false # 注册时是否开启短信验证功能,该功能
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
ApiURL = "插件扩展 API 地址"
Token = "插件扩展 API Token" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
```
> 如果要启用微信收款服务和 MidJourney
> 绘画功能,请先部署扩展服务项目 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts)。
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
```shell
@@ -195,104 +207,9 @@ docker-compose up -d
> 输入你前面配置文档中设置的管理员用户名和密码登录。
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
## 手动安装部署
由于本项目采用的是前后端分离的开发方式,所以部署也需要前后端分开部署。我这里以 linux 系统为例,演示一下部署过程:
### 1. 导入数据库
请参考容器部署的[导入数据](#1-导入数据库)。
### 2. 修改配置文档
先拷贝项目中的 `api/config.sample.toml` 配置文档,修改代理地址和管理员密码:
如何修改请参考[修改配置文档](#2-修改配置文档)
### 3. 运行后端程序
你可以自己编译或者直接下载我打包好的后端程序运行。
```shell
# 1. 下载程序,你也可以自己编译
wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/chatgpt-v3-amd64-linux
# 2. 添加执行权限
chmod +x chatgpt-v3-amd64-linux
# 3. 运行程序,如果配置文档不在当前目录,注意指定配置文档
./chatgpt-v3-amd64-linux
```
### 4. 前端部署
前端是 Vue 项目编译好静态资源文件,同样你也可以直接下载我编译好的文件解压。
```shell
# 1. 下载程序
wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/dist.tar.gz
# 2. 解压
tar -xf dist.tar.gz
```
### 5. 配置 Nginx 服务
前端程序需要搭载 Web 服务器才可以运行,这里我们选择 Nginx先安装
```shell
sudo apt install nginx -y
```
建立 Nginx 配置文件:
```conf
server {
listen 443 ssl;
server_name www.chatgpt.com; #替换成你自己的域名
ssl_certificate xxx.pem; # 替换成自己的 SSL 证书
ssl_certificate_key xxx.key;
ssl_session_timeout 5m;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
# 日志地址
access_log /var/log/chatgpt/access.log;
error_log /var/log/chatgpt/error.log;
index index.html;
root /var/www/chatgpt/dist; # 这里改成前端静态页面的地址
location / {
try_files $uri $uri/ /index.html;
# 后端 API 的转发
location /api/ {
proxy_http_version 1.1;
proxy_connect_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 12s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
}
# 静态资源转发
location /static/ {
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
}
}
}
```
配置好之后重启 Nginx然后 []
![add API Key](docs/imgs/apikey_add.png)
最后登录前端聊天页面 [http://www.chatgpt.com/admin](http://www.chatgpt.com/admin)
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
祝你使用愉快!!!
@@ -363,9 +280,9 @@ npm run build
```shell
cd api
# for all platforms
make all
make clean all
# for linux only
make linux
make clean linux
```
打包后的可执行文件在 `bin` 目录下。
@@ -373,7 +290,6 @@ make linux
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
**尤其是新版本的开发计划比较大,包括各种语言的后端 API 实现,本人精力有限,希望借助社区的力量来完成这些 API 的开发。**
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。

View File

@@ -12,7 +12,7 @@ linux:
.PHONY: linux
darwin:
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
.PHONY: darwin
clean:

View File

@@ -4,8 +4,6 @@ MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&col
StaticDir = "./static"
StaticUrl = "http://localhost:5678/static"
AesEncryptKey = "{YOUR_AES_KEY}"
StartWechatBot = false
EnabledMsgService = false
[Session]
Driver = "cookie"
@@ -38,4 +36,6 @@ EnabledMsgService = false
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
[ExtConfig]
ApiURL = "插件扩展 API 地址"
Token = "插件扩展 API Token"

View File

@@ -24,21 +24,20 @@ type AppServer struct {
Config *types.AppConfig
Engine *gin.Engine
ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message
ChatConfig *types.ChatConfig // 聊天配置
ChatConfig *types.ChatConfig // chat config cache
SysConfig *types.SystemConfig // system config cache
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
// 防止第三方直接连接 socket 调用 OpenAI API
ChatSession *types.LMap[string, types.ChatSession] //map[sessionId]UserId
ChatSession *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
ChatClients *types.LMap[string, *types.WsClient] // map[sessionId]Websocket 连接集合
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
Functions map[string]function.Function
MjTaskClients *types.LMap[string, *types.WsClient]
}
func NewServer(
appConfig *types.AppConfig,
funZaoBao function.FuncZaoBao,
funZhiHu function.FuncHeadlines,
funWeibo function.FuncWeiboHot) *AppServer {
func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
return &AppServer{
@@ -46,14 +45,11 @@ func NewServer(
Config: appConfig,
Engine: gin.Default(),
ChatContexts: types.NewLMap[string, []interface{}](),
ChatSession: types.NewLMap[string, types.ChatSession](),
ChatSession: types.NewLMap[string, *types.ChatSession](),
ChatClients: types.NewLMap[string, *types.WsClient](),
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
Functions: map[string]function.Function{
types.FuncZaoBao: funZaoBao,
types.FuncWeibo: funWeibo,
types.FuncHeadLine: funZhiHu,
},
MjTaskClients: types.NewLMap[string, *types.WsClient](),
Functions: functions,
}
}
@@ -61,9 +57,8 @@ func (s *AppServer) Init(debug bool) {
if debug { // 调试模式允许跨域请求 API
s.Debug = debug
logger.Info("Enabled debug mode")
s.Engine.Use(corsMiddleware())
}
s.Engine.Use(corsMiddleware())
s.Engine.Use(sessionMiddleware(s.Config))
s.Engine.Use(authorizeMiddleware(s))
s.Engine.Use(errorHandler)
@@ -73,12 +68,22 @@ func (s *AppServer) Init(debug bool) {
func (s *AppServer) Run(db *gorm.DB) error {
// load chat config from database
var config model.Config
res := db.Where("marker", "chat").First(&config)
var chatConfig model.Config
res := db.Where("marker", "chat").First(&chatConfig)
if res.Error != nil {
return res.Error
}
err := utils.JsonDecode(config.Config, &s.ChatConfig)
err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig)
if err != nil {
return err
}
// load system configs
var sysConfig model.Config
res = db.Where("marker", "system").First(&sysConfig)
if res.Error != nil {
return res.Error
}
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
if err != nil {
return err
}
@@ -146,7 +151,7 @@ func corsMiddleware() gin.HandlerFunc {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, ChatGPT-TOKEN, ADMIN-SESSION-TOKEN")
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
@@ -175,7 +180,10 @@ func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/admin/login" ||
c.Request.URL.Path == "/api/user/register" ||
strings.HasPrefix(c.Request.URL.Path, "/api/verify/") ||
c.Request.URL.Path == "/api/reward/notify" ||
c.Request.URL.Path == "/api/mj/notify" ||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
c.Request.URL.Path == "/api/admin/config/get" {
c.Next()

View File

@@ -33,8 +33,8 @@ func NewDefaultConfig() *types.AppConfig {
HttpOnly: false,
SameSite: http.SameSiteLaxMode,
},
ApiConfig: types.ChatPlusApiConfig{},
StartWechatBot: false,
ApiConfig: types.ChatPlusApiConfig{},
ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)},
}
}

View File

@@ -42,6 +42,15 @@ type ChatSession struct {
Model string `json:"model"` // GPT 模型
}
type MjTask struct {
ChatId string
MessageId string
MessageHash string
UserId uint
RoleId uint
Icon string
}
type ApiError struct {
Error struct {
Message string
@@ -53,3 +62,13 @@ type ApiError struct {
const PromptMsg = "prompt" // prompt message
const ReplyMsg = "reply" // reply message
const MjMsg = "mj"
var ModelToTokens = map[string]int{
"gpt-3.5-turbo": 4096,
"gpt-3.5-turbo-16k": 16384,
"gpt-4": 8192,
"gpt-4-32k": 32768,
}
const TaskStorePrefix = "/tasks/"

View File

@@ -6,18 +6,14 @@ import (
"sync"
)
var ErrConClosed = errors.New("connection closed")
type Client interface {
Close()
}
var ErrConClosed = errors.New("connection Closed")
// WsClient websocket client
type WsClient struct {
Conn *websocket.Conn
lock sync.Mutex
mt int
closed bool
Closed bool
}
func NewWsClient(conn *websocket.Conn) *WsClient {
@@ -25,7 +21,7 @@ func NewWsClient(conn *websocket.Conn) *WsClient {
Conn: conn,
lock: sync.Mutex{},
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
closed: false,
Closed: false,
}
}
@@ -33,7 +29,7 @@ func (wc *WsClient) Send(message []byte) error {
wc.lock.Lock()
defer wc.lock.Unlock()
if wc.closed {
if wc.Closed {
return ErrConClosed
}
@@ -41,7 +37,7 @@ func (wc *WsClient) Send(message []byte) error {
}
func (wc *WsClient) Receive() (int, []byte, error) {
if wc.closed {
if wc.Closed {
return 0, nil, ErrConClosed
}
@@ -52,10 +48,10 @@ func (wc *WsClient) Close() {
wc.lock.Lock()
defer wc.lock.Unlock()
if wc.closed {
if wc.Closed {
return
}
_ = wc.Conn.Close()
wc.closed = true
wc.Closed = true
}

View File

@@ -6,20 +6,30 @@ import (
)
type AppConfig struct {
Path string `toml:"-"`
Listen string
Session Session
ProxyURL string
MysqlDns string // mysql 连接地址
Manager Manager // 后台管理员账户信息
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
ApiConfig ChatPlusApiConfig // chatplus api configs
AesEncryptKey string
SmsConfig AliYunSmsConfig // 短信发送配置
StartWechatBot bool // 是否启动微信机器人
EnabledMsgService bool // 是否启用短信服务
Path string `toml:"-"`
Listen string
Session Session
ProxyURL string
MysqlDns string // mysql 连接地址
Manager Manager // 后台管理员账户信息
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
AesEncryptKey string
SmsConfig AliYunSmsConfig // AliYun send message service config
ExtConfig ChatPlusExtConfig // ChatPlus extensions callback api config
}
type ChatPlusApiConfig struct {
ApiURL string
AppId string
Token string
}
type ChatPlusExtConfig struct {
ApiURL string
Token string
}
type AliYunSmsConfig struct {
@@ -76,20 +86,14 @@ type ChatConfig struct {
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ApiKey string `json:"api_key"`
ContextDeep int `json:"context_deep"` // 上下文深度
}
type SystemConfig struct {
Title string `json:"title"`
AdminTitle string `json:"admin_title"`
Models []string `json:"models"`
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
Title string `json:"title"`
AdminTitle string `json:"admin_title"`
Models []string `json:"models"`
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
InitImgCalls int `json:"init_img_calls"`
EnabledRegister bool `json:"enabled_register"`
EnabledMsgService bool `json:"enabled_msg_service"`
}
type ChatPlusApiConfig struct {
ApiURL string
AppId string
Token string
}
const UserInitCalls = 1000

View File

@@ -23,9 +23,10 @@ type Property struct {
}
const (
FuncZaoBao = "zao_bao" // 每日早报
FuncHeadLine = "headline" // 今日头条
FuncWeibo = "weibo_hot" // 微博热搜
FuncZaoBao = "zao_bao" // 每日早报
FuncHeadLine = "headline" // 今日头条
FuncWeibo = "weibo_hot" // 微博热搜
FuncMidJourney = "mid_journey" // MJ 绘画
)
var InnerFunctions = []Function{
@@ -73,4 +74,27 @@ var InnerFunctions = []Function{
Required: []string{},
},
},
{
Name: FuncMidJourney,
Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
Parameters: Parameters{
Type: "object",
Properties: map[string]Property{
"prompt": {
Type: "string",
Description: "绘画内容描述,提示词,如果该参数中有中文的话,则需要翻译成英文",
},
"ar": {
Type: "string",
Description: "图片长宽比,如 --ar 4:3",
},
"niji": {
Type: "string",
Description: "动漫模型版本,例如 --niji 5",
},
},
Required: []string{},
},
},
}

View File

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

View File

@@ -12,8 +12,8 @@ type BizVo struct {
// WsMessage Websocket message
type WsMessage struct {
Type WsMsgType `json:"type"` // 消息类别start, end
Content string `json:"content"`
Type WsMsgType `json:"type"` // 消息类别start, end, img
Content interface{} `json:"content"`
}
type WsMsgType string
@@ -21,6 +21,7 @@ const (
WsStart = WsMsgType("start")
WsMiddle = WsMsgType("middle")
WsEnd = WsMsgType("end")
WsMjImg = WsMsgType("mj")
)
type BizCode int

View File

@@ -5,14 +5,12 @@ go 1.19
require (
github.com/BurntSushi/toml v1.1.0
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
github.com/eatmoreapple/openwechat v1.2.1
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.1
github.com/gorilla/websocket v1.5.0
github.com/imroc/req/v3 v3.37.2
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/syndtr/goleveldb v1.0.0
go.uber.org/zap v1.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1

View File

@@ -18,8 +18,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
@@ -140,8 +138,6 @@ github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62po
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

View File

@@ -48,6 +48,21 @@ func (h *ConfigHandler) Update(c *gin.Context) {
resp.ERROR(c, res.Error.Error())
return
}
// update config cache for AppServer
var cfg model.Config
h.db.Where("marker", data.Key).First(&cfg)
var err error
if data.Key == "system" {
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
} else if data.Key == "chat" {
err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig)
}
if err != nil {
resp.ERROR(c, "Failed to update config cache: "+err.Error())
return
}
logger.Infof("Update AppServer's config successfully: %v", config.Config)
}
resp.SUCCESS(c, config)

View File

@@ -0,0 +1,63 @@
package admin
import (
"chatplus/core"
"chatplus/handler"
"chatplus/store/model"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type DashboardHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
h := DashboardHandler{db: db}
h.App = app
return &h
}
type statsVo struct {
Users int64 `json:"users"`
Chats int64 `json:"chats"`
Tokens int64 `json:"tokens"`
Rewards float64 `json:"rewards"`
}
func (h *DashboardHandler) Stats(c *gin.Context) {
stats := statsVo{}
// new users statistic
var userCount int64
now := time.Now()
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
res := h.db.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&userCount)
if res.Error == nil {
stats.Users = userCount
}
// new chats statistic
var chatCount int64
res = h.db.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount)
if res.Error == nil {
stats.Chats = chatCount
}
// tokens took stats
var tokenCount int64
res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount)
if res.Error == nil {
stats.Tokens = tokenCount
}
// reward revenue
var amount float64
res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount)
if res.Error == nil {
stats.Rewards = amount
}
resp.SUCCESS(c, stats)
}

View File

@@ -8,6 +8,7 @@ import (
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -28,12 +29,24 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
func (h *UserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
mobile := h.GetTrim(c, "mobile")
username := h.GetTrim(c, "username")
offset := (page - 1) * pageSize
var items []model.User
var users = make([]vo.User, 0)
var total int64
h.db.Model(&model.User{}).Count(&total)
res := h.db.Offset(offset).Limit(pageSize).Find(&items)
session := h.db.Session(&gorm.Session{})
if mobile != "" {
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
}
if username != "" {
session = session.Where("username LIKE ?", "%"+username+"%")
}
session.Model(&model.User{}).Count(&total)
res := session.Offset(offset).Limit(pageSize).Find(&items)
if res.Error == nil {
for _, item := range items {
var user vo.User
@@ -52,11 +65,15 @@ func (h *UserHandler) List(c *gin.Context) {
resp.SUCCESS(c, pageVo)
}
func (h *UserHandler) Update(c *gin.Context) {
func (h *UserHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
Mobile string `json:"mobile"`
Nickname string `json:"nickname"`
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
ChatRoles []string `json:"chat_roles"`
ExpiredTime string `json:"expired_time"`
Status bool `json:"status"`
@@ -66,21 +83,83 @@ func (h *UserHandler) Update(c *gin.Context) {
return
}
var user = model.User{}
user.Id = data.Id
// 此处需要用 map 更新,用结构体无法更新 0 值
res := h.db.Model(&user).Updates(map[string]interface{}{
"nickname": data.Nickname,
"calls": data.Calls,
"status": data.Status,
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
"expired_time": utils.Str2stamp(data.ExpiredTime),
})
var res *gorm.DB
var userVo vo.User
if data.Id > 0 { // 更新
user.Id = data.Id
// 此处需要用 map 更新,用结构体无法更新 0 值
res = h.db.Model(&user).Updates(map[string]interface{}{
"nickname": data.Nickname,
"mobile": data.Mobile,
"calls": data.Calls,
"img_calls": data.ImgCalls,
"status": data.Status,
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
"expired_time": utils.Str2stamp(data.ExpiredTime),
})
} else {
salt := utils.RandString(8)
u := model.User{
Username: data.Username,
Password: utils.GenPassword(data.Password, salt),
Nickname: fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)),
Avatar: "/images/avatar/user.png",
Salt: salt,
Status: true,
Mobile: data.Mobile,
ChatRoles: utils.JsonEncode(data.ChatRoles),
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
ChatConfig: utils.JsonEncode(types.ChatConfig{
Temperature: h.App.ChatConfig.Temperature,
MaxTokens: h.App.ChatConfig.MaxTokens,
EnableContext: h.App.ChatConfig.EnableContext,
EnableHistory: true,
Model: h.App.ChatConfig.Model,
ApiKey: "",
}),
Calls: h.App.SysConfig.UserInitCalls,
}
res = h.db.Create(&u)
_ = utils.CopyObject(u, &userVo)
userVo.Id = u.Id
userVo.CreatedAt = u.CreatedAt.Unix()
userVo.UpdatedAt = u.UpdatedAt.Unix()
}
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
}
resp.SUCCESS(c)
resp.SUCCESS(c, userVo)
}
// ResetPass 重置密码
func (h *UserHandler) ResetPass(c *gin.Context) {
var data struct {
Id uint
Password string
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.User
res := h.db.First(&user, data.Id)
if res.Error != nil {
resp.ERROR(c, "No user found")
return
}
password := utils.GenPassword(data.Password, user.Salt)
user.Password = password
res = h.db.Updates(&user)
if res.Error != nil {
resp.ERROR(c)
} else {
resp.SUCCESS(c)
}
}
func (h *UserHandler) Remove(c *gin.Context) {

View File

@@ -5,6 +5,7 @@ import (
"bytes"
"chatplus/core"
"chatplus/core/types"
"chatplus/store"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
@@ -29,11 +30,12 @@ const ErrorMsg = "抱歉AI 助手开小差了,请稍后再试。"
type ChatHandler struct {
BaseHandler
db *gorm.DB
db *gorm.DB
leveldb *store.LevelDB
}
func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler {
handler := ChatHandler{db: db}
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB) *ChatHandler {
handler := ChatHandler{db: db, leveldb: levelDB}
handler.App = app
return &handler
}
@@ -47,9 +49,6 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
logger.Error(err)
return
}
// 设置读写超时时间
_ = ws.SetWriteDeadline(time.Now().Add(300 * time.Second))
_ = ws.SetReadDeadline(time.Now().Add(300 * time.Second))
sessionId := c.Query("session_id")
roleId := h.GetInt(c, "role_id", 0)
@@ -57,14 +56,14 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
chatModel := c.Query("model")
session := h.App.ChatSession.Get(sessionId)
if session.SessionId == "" {
if session == nil {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
logger.Info("用户未登录")
c.Abort()
return
}
session = types.ChatSession{
session = &types.ChatSession{
SessionId: sessionId,
ClientIP: c.ClientIP(),
Username: user.Username,
@@ -88,7 +87,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
var chatRole model.ChatRole
res = h.db.First(&chatRole, roleId)
if res.Error != nil || !chatRole.Enable {
replyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
c.Abort()
return
}
@@ -98,7 +97,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
h.db.Where("marker", "chat").First(&config)
err = utils.JsonDecode(config.Config, &chatConfig)
if err != nil {
replyMessage(client, "加载系统配置失败,连接已关闭!!!")
utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
c.Abort()
return
}
@@ -107,7 +106,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
h.App.ChatClients.Put(sessionId, client)
go func() {
for {
_, message, err := client.Receive()
_, msg, err := client.Receive()
if err != nil {
logger.Error(err)
client.Close()
@@ -115,16 +114,18 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
h.App.ReqCancelFunc.Delete(sessionId)
return
}
logger.Info("Receive a message: ", string(message))
//replyMessage(client, "这是一条测试消息!")
message := string(msg)
logger.Info("Receive a message: ", message)
//utils.ReplyMessage(client, "这是一条测试消息!")
ctx, cancel := context.WithCancel(context.Background())
h.App.ReqCancelFunc.Put(sessionId, cancel)
// 回复消息
err = h.sendMessage(ctx, session, chatRole, string(message), client)
err = h.sendMessage(ctx, session, chatRole, message, client)
if err != nil {
logger.Error(err)
} else {
replyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
logger.Info("回答完毕: " + string(message))
}
@@ -133,13 +134,13 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
}
// 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端
func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession, role model.ChatRole, prompt string, ws types.Client) error {
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
var user model.User
res := h.db.Model(&model.User{}).First(&user, session.UserId)
if res.Error != nil {
replyMessage(ws, "非法用户,请联系管理员!")
utils.ReplyMessage(ws, "非法用户,请联系管理员!")
return res.Error
}
var userVo vo.User
@@ -150,20 +151,20 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
}
if userVo.Status == false {
replyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
replyMessage(ws, "![](/images/wx.png)")
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
utils.ReplyMessage(ws, "![](/images/wx.png)")
return nil
}
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKey == "" {
replyMessage(ws, "您的对话次数已经用尽请联系管理员或者点击左下角菜单加入众筹获得100次对话")
replyMessage(ws, "![](/images/wx.png)")
utils.ReplyMessage(ws, "您的对话次数已经用尽请联系管理员或者点击左下角菜单加入众筹获得100次对话")
utils.ReplyMessage(ws, "![](/images/wx.png)")
return nil
}
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
replyMessage(ws, "您的账号已经过期,请联系管理员!")
replyMessage(ws, "![](/images/wx.png)")
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
utils.ReplyMessage(ws, "![](/images/wx.png)")
return nil
}
var req = types.ApiRequest{
@@ -180,21 +181,37 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
if h.App.ChatContexts.Has(session.ChatId) {
chatCtx = h.App.ChatContexts.Get(session.ChatId)
} else {
// 加载角色信息
// calculate the tokens of current request, to prevent to exceeding the max tokens num
tokens := req.MaxTokens
for _, f := range types.InnerFunctions {
tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
tokens += tks
}
// loading the role context
var messages []types.Message
err := utils.JsonDecode(role.Context, &messages)
if err == nil {
for _, v := range messages {
tks, _ := utils.CalcTokens(v.Content, req.Model)
if tokens+tks >= types.ModelToTokens[req.Model] {
break
}
tokens += tks
chatCtx = append(chatCtx, v)
}
}
// 加载最近的聊天记录作为聊天上下文
// loading recent chat history as chat context
if chatConfig.ContextDeep > 0 {
var historyMessages []model.HistoryMessage
res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(2).Order("created_at desc").Find(&historyMessages)
res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages)
if res.Error == nil {
for _, msg := range historyMessages {
if tokens+msg.Tokens >= types.ModelToTokens[session.Model] {
break
}
tokens += msg.Tokens
ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.ReplyMsg {
ms.Role = "assistant"
@@ -204,10 +221,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
}
}
}
if h.App.Debug { // 调试打印聊天上下文
logger.Info("聊天上下文:", chatCtx)
}
logger.Debugf("聊天上下文:%+v", chatCtx)
}
reqMgs := make([]interface{}, 0)
for _, m := range chatCtx {
@@ -225,14 +239,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
logger.Info("用户取消了请求:", prompt)
return nil
} else if strings.Contains(err.Error(), "no available key") {
replyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY🔑您可以导入自己的 API KEY🔑 继续使用!🙏🙏🙏")
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY🔑您可以导入自己的 API KEY🔑 继续使用!🙏🙏🙏")
return nil
} else {
logger.Error(err)
}
replyMessage(ws, ErrorMsg)
replyMessage(ws, "![](/images/wx.png)")
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
return err
} else {
defer response.Body.Close()
@@ -267,8 +281,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
logger.Error(err, line)
replyMessage(ws, ErrorMsg)
replyMessage(ws, "![](/images/wx.png)")
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
break
}
@@ -282,8 +296,8 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
functionCall = true
functionName = fun.Name
f := h.App.Functions[functionName]
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
continue
}
@@ -294,14 +308,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
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))
replyChunkMessage(ws, types.WsMessage{
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
})
@@ -309,24 +323,53 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
} // end for
if functionCall { // 调用函数完成任务
logger.Info(functionName)
logger.Info(arguments)
f := h.App.Functions[functionName]
// TODO 调用函数完成任务
data, err := f.Invoke(arguments)
if err != nil {
msg := "调用函数出错:" + err.Error()
replyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: msg,
})
contents = append(contents, msg)
var params map[string]interface{}
_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, "![](/images/wx.png)")
} else {
replyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: data,
})
contents = append(contents, data)
f := h.App.Functions[functionName]
data, err := f.Invoke(params)
if err != nil {
msg := "调用函数出错:" + err.Error()
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: msg,
})
contents = append(contents, msg)
} else {
content := data
if functionName == types.FuncMidJourney {
key := utils.Sha256(data)
logger.Debug(data, ",", key)
// add task for MidJourney
h.App.MjTaskClients.Put(key, ws)
task := types.MjTask{
UserId: userVo.Id,
RoleId: role.Id,
Icon: "/images/avatar/mid_journey.png",
ChatId: session.ChatId,
}
err := h.leveldb.Put(types.TaskStorePrefix+key, task)
if err != nil {
logger.Error("error with store MidJourney task: ", err)
}
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
// update user's img_calls
h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: content,
})
contents = append(contents, content)
}
}
}
@@ -334,10 +377,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
if len(contents) > 0 {
// 更新用户的对话次数
if userVo.ChatConfig.ApiKey == "" { // 如果用户使用的是自己绑定的 API KEY 则不扣减对话次数
res := h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
if res.Error != nil {
return res.Error
}
h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" {
@@ -418,7 +458,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
} else {
totalTokens = replyToken + getTotalTokens(req)
}
//replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("\n\n `本轮对话共消耗 Token 数量: %d`", totalTokens+11)})
//utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("\n\n `本轮对话共消耗 Token 数量: %d`", totalTokens+11)})
if userVo.ChatConfig.ApiKey != "" { // 调用自己的 API KEY 不计算 token 消耗
h.db.Model(&user).UpdateColumn("tokens", gorm.Expr("tokens + ?",
totalTokens))
@@ -456,22 +496,18 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
// OpenAI API 调用异常处理
// TODO: 是否考虑重发消息?
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
replyMessage(ws, "请求 OpenAI API 失败API KEY 所关联的账户被禁用。")
utils.ReplyMessage(ws, "请求 OpenAI API 失败API KEY 所关联的账户被禁用。")
// 移除当前 API key
h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{})
} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
replyMessage(ws, "请求 OpenAI API 失败API KEY 触发并发限制,请稍后再试。")
utils.ReplyMessage(ws, "请求 OpenAI API 失败API KEY 触发并发限制,请稍后再试。")
} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
replyMessage(ws, "当前会话上下文长度超出限制,已为您删减会话上下文!")
// 只保留最近的三条记录
chatContext := h.App.ChatContexts.Get(session.ChatId)
if len(chatContext) > 3 {
chatContext = chatContext[len(chatContext)-3:]
}
h.App.ChatContexts.Put(session.ChatId, chatContext)
logger.Error(res.Error.Message)
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
h.App.ChatContexts.Delete(session.ChatId)
return h.sendMessage(ctx, session, role, prompt, ws)
} else {
replyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
}
}
@@ -526,26 +562,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, user vo.User, apiKey *strin
return client.Do(request)
}
// 回复客户片段端消息
func replyChunkMessage(client types.Client, message types.WsMessage) {
msg, err := json.Marshal(message)
if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error())
return
}
err = client.(*types.WsClient).Send(msg)
if err != nil {
logger.Errorf("Error for reply message: %v", err.Error())
}
}
// 回复客户端一条完整的消息
func replyMessage(ws types.Client, message string) {
replyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
replyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message})
replyChunkMessage(ws, types.WsMessage{Type: types.WsEnd})
}
// Tokens 统计 token 数量
func (h *ChatHandler) Tokens(c *gin.Context) {
text := c.Query("text")

View File

@@ -10,44 +10,6 @@ import (
"github.com/gin-gonic/gin"
)
// List 获取会话列表
func (h *ChatHandler) List(c *gin.Context) {
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
resp.ERROR(c, "The parameter 'user_id' is needed.")
return
}
var items = make([]vo.ChatItem, 0)
var chats []model.ChatItem
res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
if res.Error == nil {
var roleIds = make([]uint, 0)
for _, chat := range chats {
roleIds = append(roleIds, chat.RoleId)
}
var roles []model.ChatRole
res = h.db.Find(&roles, roleIds)
if res.Error == nil {
roleMap := make(map[uint]model.ChatRole)
for _, role := range roles {
roleMap[role.Id] = role
}
for _, chat := range chats {
var item vo.ChatItem
err := utils.CopyObject(chat, &item)
if err == nil {
item.Id = chat.Id
item.Icon = roleMap[chat.RoleId].Icon
items = append(items, item)
}
}
}
}
resp.SUCCESS(c, items)
}
// Update 更新会话标题
func (h *ChatHandler) Update(c *gin.Context) {
var data struct {

View File

@@ -0,0 +1,71 @@
package handler
import (
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
)
// List 获取会话列表
func (h *ChatHandler) List(c *gin.Context) {
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
resp.ERROR(c, "The parameter 'user_id' is needed.")
return
}
var items = make([]vo.ChatItem, 0)
var chats []model.ChatItem
res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
if res.Error == nil {
var roleIds = make([]uint, 0)
for _, chat := range chats {
roleIds = append(roleIds, chat.RoleId)
}
var roles []model.ChatRole
res = h.db.Find(&roles, roleIds)
if res.Error == nil {
roleMap := make(map[uint]model.ChatRole)
for _, role := range roles {
roleMap[role.Id] = role
}
for _, chat := range chats {
var item vo.ChatItem
err := utils.CopyObject(chat, &item)
if err == nil {
item.Id = chat.Id
item.Icon = roleMap[chat.RoleId].Icon
items = append(items, item)
}
}
}
}
resp.SUCCESS(c, items)
}
func (h *ChatHandler) Detail(c *gin.Context) {
chatId := h.GetTrim(c, "chat_id")
if utils.IsEmptyValue(chatId) {
resp.ERROR(c, "Invalid chatId")
return
}
var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", chatId).First(&chatItem)
if res.Error != nil {
resp.ERROR(c, "No chat found")
return
}
var chatItemVo vo.ChatItem
err := utils.CopyObject(chatItem, &chatItemVo)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, chatItemVo)
}

214
api/handler/mj_handler.go Normal file
View File

@@ -0,0 +1,214 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/service/function"
"chatplus/store"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type TaskStatus string
const (
Start = TaskStatus("Started")
Running = TaskStatus("Running")
Stopped = TaskStatus("Stopped")
Finished = TaskStatus("Finished")
)
type Image struct {
URL string `json:"url"`
ProxyURL string `json:"proxy_url"`
Filename string `json:"filename"`
Width int `json:"width"`
Height int `json:"height"`
Size int `json:"size"`
Hash string `json:"hash"`
}
type MidJourneyHandler struct {
BaseHandler
leveldb *store.LevelDB
db *gorm.DB
mjFunc function.FuncMidJourney
}
func NewMidJourneyHandler(app *core.AppServer, leveldb *store.LevelDB, db *gorm.DB, functions map[string]function.Function) *MidJourneyHandler {
h := MidJourneyHandler{leveldb: leveldb, db: db, mjFunc: functions[types.FuncMidJourney].(function.FuncMidJourney)}
h.App = app
return &h
}
func (h *MidJourneyHandler) Notify(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != h.App.Config.ExtConfig.Token {
resp.NotAuth(c)
return
}
var data struct {
MessageId string `json:"message_id"`
ReferenceId string `json:"reference_id"`
Image Image `json:"image"`
Content string `json:"content"`
Prompt string `json:"prompt"`
Status TaskStatus `json:"status"`
Key string `json:"key"`
}
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
logger.Debugf("收到 MidJourney 回调请求:%+v", data)
// the job is saved
var job model.MidJourneyJob
res := h.db.Where("message_id = ?", data.MessageId).First(&job)
if res.Error == nil {
resp.SUCCESS(c)
return
}
data.Key = utils.Sha256(data.Prompt)
//logger.Info(data.Prompt, ",", key)
if data.Status == Finished {
var task types.MjTask
err := h.leveldb.Get(types.TaskStorePrefix+data.Key, &task)
if err != nil {
logger.Error("error with get MidJourney task: ", err)
resp.SUCCESS(c)
return
}
// TODO: 是否需要把图片下载到本地服务器?
message := model.HistoryMessage{
UserId: task.UserId,
ChatId: task.ChatId,
RoleId: task.RoleId,
Type: types.MjMsg,
Icon: task.Icon,
Content: utils.JsonEncode(data),
Tokens: 0,
UseContext: false,
}
res := h.db.Create(&message)
if res.Error != nil {
logger.Error("error with save chat history message: ", res.Error)
}
// save the job
job.UserId = task.UserId
job.ChatId = task.ChatId
job.MessageId = data.MessageId
job.ReferenceId = data.ReferenceId
job.Content = data.Content
job.Prompt = data.Prompt
job.Image = utils.JsonEncode(data.Image)
job.Hash = data.Image.Hash
job.CreatedAt = time.Now()
res = h.db.Create(&job)
if res.Error != nil {
logger.Error("error with save MidJourney Job: ", res.Error)
}
}
// 推送消息到客户端
wsClient := h.App.MjTaskClients.Get(data.Key)
if wsClient == nil { // 客户端断线,则丢弃
logger.Errorf("Client is offline: %+v", data)
resp.SUCCESS(c, "Client is offline")
return
}
if data.Status == Finished {
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
// delete client
h.App.MjTaskClients.Delete(data.Key)
} else {
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
}
resp.SUCCESS(c, "SUCCESS")
}
type reqVo struct {
Index int32 `json:"index"`
MessageId string `json:"message_id"`
MessageHash string `json:"message_hash"`
SessionId string `json:"session_id"`
Key string `json:"key"`
Prompt string `json:"prompt"`
}
// Upscale send upscale command to MidJourney Bot
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
var data reqVo
if err := c.ShouldBindJSON(&data); err != nil ||
data.SessionId == "" ||
data.Key == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
wsClient := h.App.ChatClients.Get(data.SessionId)
if wsClient == nil {
resp.ERROR(c, "No Websocket client online")
return
}
err := h.mjFunc.Upscale(function.MjUpscaleReq{
Index: data.Index,
MessageId: data.MessageId,
MessageHash: data.MessageHash,
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
content := fmt.Sprintf("**%s** 已推送 Upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
utils.ReplyMessage(wsClient, content)
if h.App.MjTaskClients.Get(data.Key) == nil {
h.App.MjTaskClients.Put(data.Key, wsClient)
}
resp.SUCCESS(c)
}
func (h *MidJourneyHandler) Variation(c *gin.Context) {
var data reqVo
if err := c.ShouldBindJSON(&data); err != nil ||
data.SessionId == "" ||
data.Key == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
wsClient := h.App.ChatClients.Get(data.SessionId)
if wsClient == nil {
resp.ERROR(c, "No Websocket client online")
return
}
err := h.mjFunc.Variation(function.MjVariationReq{
Index: data.Index,
MessageId: data.MessageId,
MessageHash: data.MessageHash,
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
content := fmt.Sprintf("**%s** 已推送 Variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
utils.ReplyMessage(wsClient, content)
if h.App.MjTaskClients.Get(data.Key) == nil {
h.App.MjTaskClients.Put(data.Key, wsClient)
}
resp.SUCCESS(c)
}

View File

@@ -21,6 +21,50 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
return &h
}
func (h *RewardHandler) Notify(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != h.App.Config.ExtConfig.Token {
resp.NotAuth(c)
return
}
var data struct {
TransId string `json:"trans_id"` // 微信转账交易 ID
Amount float64 `json:"amount"` // 微信转账交易金额
Remark string `json:"remark"` // 转账备注
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Amount <= 0 {
resp.ERROR(c, "Amount should not be 0")
return
}
logger.Infof("收到众筹收款信息: %+v", data)
var item model.Reward
res := h.db.Where("tx_id = ?", data.TransId).First(&item)
if res.Error == nil {
resp.ERROR(c, "当前交易 ID 己经存在!")
return
}
res = h.db.Create(&model.Reward{
TxId: data.TransId,
Amount: data.Amount,
Remark: data.Remark,
Status: false,
})
if res.Error != nil {
logger.Errorf("交易保存失败: %v", res.Error)
resp.ERROR(c, "交易保存失败")
return
}
resp.SUCCESS(c)
}
// Verify 打赏码核销
func (h *RewardHandler) Verify(c *gin.Context) {
var data struct {

View File

@@ -14,19 +14,19 @@ const CodeStorePrefix = "/verify/codes/"
type SmsHandler struct {
BaseHandler
db *store.LevelDB
leveldb *store.LevelDB
sms *service.AliYunSmsService
captcha *service.CaptchaService
}
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
handler := &SmsHandler{db: db, sms: sms, captcha: captcha}
handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
handler.App = app
return handler
}
// VerifyCode 发送验证码短信
func (h *SmsHandler) VerifyCode(c *gin.Context) {
// SendCode 发送验证码短信
func (h *SmsHandler) SendCode(c *gin.Context) {
var data struct {
Mobile string `json:"mobile"`
Key string `json:"key"`
@@ -50,7 +50,7 @@ func (h *SmsHandler) VerifyCode(c *gin.Context) {
}
// 存储验证码,等待后面注册验证
err = h.db.Put(CodeStorePrefix+data.Mobile, code)
err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
if err != nil {
resp.ERROR(c, "验证码保存失败")
return
@@ -59,7 +59,12 @@ func (h *SmsHandler) VerifyCode(c *gin.Context) {
resp.SUCCESS(c)
}
type statusVo struct {
EnabledMsgService bool `json:"enabled_msg_service"`
EnabledRegister bool `json:"enabled_register"`
}
// Status check if the message service is enabled
func (h *SmsHandler) Status(c *gin.Context) {
resp.SUCCESS(c, h.App.Config.EnabledMsgService)
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister})
}

View File

@@ -22,11 +22,11 @@ type UserHandler struct {
BaseHandler
db *gorm.DB
searcher *xdb.Searcher
levelDB *store.LevelDB
leveldb *store.LevelDB
}
func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher, levelDB *store.LevelDB) *UserHandler {
handler := &UserHandler{db: db, searcher: searcher, levelDB: levelDB}
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB}
handler.App = app
return handler
}
@@ -58,9 +58,9 @@ func (h *UserHandler) Register(c *gin.Context) {
// 检查验证码
key := CodeStorePrefix + data.Mobile
if h.App.Config.EnabledMsgService {
if h.App.SysConfig.EnabledMsgService {
var code int
err := h.levelDB.Get(key, &code)
err := h.leveldb.Get(key, &code)
if err != nil || code != data.Code {
logger.Info(code)
resp.ERROR(c, "短信验证码错误")
@@ -108,16 +108,8 @@ func (h *UserHandler) Register(c *gin.Context) {
Model: h.App.ChatConfig.Model,
ApiKey: "",
}),
}
// 初始化调用次数
var cfg model.Config
h.db.Where("marker = ?", "system").First(&cfg)
var config types.SystemConfig
err := utils.JsonDecode(cfg.Config, &config)
if err != nil || config.UserInitCalls <= 0 {
user.Calls = types.UserInitCalls
} else {
user.Calls = config.UserInitCalls
Calls: h.App.SysConfig.UserInitCalls,
ImgCalls: h.App.SysConfig.InitImgCalls,
}
res = h.db.Create(&user)
if res.Error != nil {
@@ -126,8 +118,8 @@ func (h *UserHandler) Register(c *gin.Context) {
return
}
if h.App.Config.EnabledMsgService {
_ = h.levelDB.Delete(key) // 注册成功,删除短信验证码
if h.App.SysConfig.EnabledMsgService {
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
}
resp.SUCCESS(c, user)
}
@@ -143,7 +135,7 @@ func (h *UserHandler) Login(c *gin.Context) {
return
}
var user model.User
res := h.db.Where("username = ?", data.Username).First(&user)
res := h.db.Where("username = ? OR mobile = ?", data.Username, data.Username).First(&user)
if res.Error != nil {
resp.ERROR(c, "用户名不存在")
return
@@ -155,12 +147,16 @@ func (h *UserHandler) Login(c *gin.Context) {
return
}
if user.Status == false {
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
return
}
// 更新最后登录时间和IP
user.LastLoginIp = c.ClientIP()
user.LastLoginAt = time.Now().Unix()
h.db.Model(&user).Updates(user)
sessionId := utils.RandString(42)
err := utils.SetLoginUser(c, user)
if err != nil {
resp.ERROR(c, "保存会话失败")
@@ -168,38 +164,14 @@ func (h *UserHandler) Login(c *gin.Context) {
return
}
// 记录登录信息在服务端
h.App.ChatSession.Put(sessionId, types.ChatSession{ClientIP: c.ClientIP(), UserId: user.Id, Username: data.Username, SessionId: sessionId})
h.db.Create(&model.UserLoginLog{
UserId: user.Id,
Username: user.Username,
LoginIp: c.ClientIP(),
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
})
var chatConfig types.ChatConfig
err = utils.JsonDecode(user.ChatConfig, &chatConfig)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{
"session_id": sessionId,
"id": user.Id,
"nickname": user.Nickname,
"avatar": user.Avatar,
"username": user.Username,
"tokens": user.Tokens,
"calls": user.Calls,
"expired_time": user.ExpiredTime,
"api_key": chatConfig.ApiKey,
"model": chatConfig.Model,
"temperature": chatConfig.Temperature,
"max_tokens": chatConfig.MaxTokens,
"enable_context": chatConfig.EnableContext,
"enable_history": chatConfig.EnableHistory,
})
resp.SUCCESS(c)
}
// Logout 注 销
@@ -246,6 +218,7 @@ type userProfile struct {
Avatar string `json:"avatar"`
ChatConfig types.ChatConfig `json:"chat_config"`
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
Tokens int64 `json:"tokens"`
}
@@ -370,7 +343,7 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
// 检查验证码
key := CodeStorePrefix + data.Mobile
var code int
err := h.levelDB.Get(key, &code)
err := h.leveldb.Get(key, &code)
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
@@ -388,6 +361,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
return
}
_ = h.levelDB.Delete(key) // 删除短信验证码
_ = h.leveldb.Delete(key) // 删除短信验证码
resp.SUCCESS(c)
}

View File

@@ -5,6 +5,7 @@ import (
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
)
var logger *zap.Logger
@@ -15,7 +16,7 @@ func GetLogger() *zap.SugaredLogger {
return sugarLogger
}
logLevel := zap.NewAtomicLevelAt(zapcore.InfoLevel)
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
encoder := getEncoder()
writerSyncer := getLogWriter()
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
@@ -58,3 +59,16 @@ func getLogWriter() zapcore.WriteSyncer {
}
return zapcore.AddSync(lumberJackLogger)
}
func getLogLevel(level string) zapcore.Level {
switch strings.ToUpper(level) {
case "DEBUG":
return zapcore.DebugLevel
case "WARN":
return zapcore.WarnLevel
case "ERROR":
return zapcore.ErrorLevel
default:
return zapcore.InfoLevel
}
}

View File

@@ -6,7 +6,6 @@ import (
"chatplus/handler"
"chatplus/handler/admin"
logger2 "chatplus/logger"
"chatplus/modules/wexin"
"chatplus/service"
"chatplus/service/function"
"chatplus/store"
@@ -104,27 +103,8 @@ func main() {
return xdb.NewWithBuffer(cBuff)
}),
// 创建微信机器人
fx.Provide(wexin.NewWeChatBot),
fx.Invoke(func(bot *wexin.WeChatBot) {
go func() {
err := bot.Login()
if err != nil {
log.Fatal(err)
}
}()
}),
// 创建函数
fx.Provide(func(config *types.AppConfig) (function.FuncZaoBao, error) {
return function.NewZaoBao(config.ApiConfig), nil
}),
fx.Provide(func(config *types.AppConfig) (function.FuncWeiboHot, error) {
return function.NewWeiboHot(config.ApiConfig), nil
}),
fx.Provide(func(config *types.AppConfig) (function.FuncHeadlines, error) {
return function.NewHeadLines(config.ApiConfig), nil
}),
fx.Provide(function.NewFunctions),
// 创建控制器
fx.Provide(handler.NewChatRoleHandler),
@@ -134,6 +114,7 @@ func main() {
fx.Provide(handler.NewSmsHandler),
fx.Provide(handler.NewRewardHandler),
fx.Provide(handler.NewCaptchaHandler),
fx.Provide(handler.NewMidJourneyHandler),
fx.Provide(admin.NewConfigHandler),
fx.Provide(admin.NewAdminHandler),
@@ -141,6 +122,7 @@ func main() {
fx.Provide(admin.NewUserHandler),
fx.Provide(admin.NewChatRoleHandler),
fx.Provide(admin.NewRewardHandler),
fx.Provide(admin.NewDashboardHandler),
// 创建服务
fx.Provide(service.NewAliYunSmsService),
@@ -168,6 +150,7 @@ func main() {
group := s.Engine.Group("/api/chat/")
group.Any("new", h.ChatHandle)
group.GET("list", h.List)
group.GET("detail", h.Detail)
group.POST("update", h.Update)
group.GET("remove", h.Remove)
group.GET("history", h.History)
@@ -181,7 +164,7 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
group := s.Engine.Group("/api/sms/")
group.GET("status", h.Status)
group.POST("code", h.VerifyCode)
group.POST("code", h.SendCode)
}),
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
group := s.Engine.Group("/api/captcha/")
@@ -190,8 +173,14 @@ func main() {
}),
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
group := s.Engine.Group("/api/reward/")
group.POST("notify", h.Notify)
group.POST("verify", h.Verify)
}),
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
s.Engine.POST("/api/mj/notify", h.Notify)
s.Engine.POST("/api/mj/upscale", h.Upscale)
s.Engine.POST("/api/mj/variation", h.Variation)
}),
// 管理后台控制器
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
@@ -215,9 +204,10 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
group := s.Engine.Group("/api/admin/user/")
group.GET("list", h.List)
group.POST("update", h.Update)
group.POST("save", h.Save)
group.GET("remove", h.Remove)
group.GET("loginLog", h.LoginLog)
group.POST("resetPass", h.ResetPass)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
group := s.Engine.Group("/api/admin/role/")
@@ -230,6 +220,10 @@ func main() {
group := s.Engine.Group("/api/admin/reward/")
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/")
group.GET("stats", h.Stats)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
err := s.Run(db)

View File

@@ -1,54 +0,0 @@
package wexin
import (
"chatplus/store/model"
"github.com/eatmoreapple/openwechat"
"github.com/skip2/go-qrcode"
"gorm.io/gorm"
)
// MessageHandler 消息处理
func MessageHandler(msg *openwechat.Message, db *gorm.DB) {
sender, err := msg.Sender()
if err != nil {
return
}
// 只处理微信支付的推送消息
if sender.NickName == "微信支付" ||
msg.MsgType == openwechat.MsgTypeApp ||
msg.AppMsgType == openwechat.AppMsgTypeUrl {
// 解析支付金额
message, err := parseTransactionMessage(msg.Content)
if err == nil {
transaction := extractTransaction(message)
logger.Infof("解析到收款信息:%+v", transaction)
if transaction.Amount <= 0 {
return
}
var item model.Reward
res := db.Where("tx_id = ?", transaction.TransId).First(&item)
if res.Error == nil {
logger.Infof("当前交易 ID %s 己经存在!", transaction.TransId)
return
}
res = db.Create(&model.Reward{
TxId: transaction.TransId,
Amount: transaction.Amount,
Remark: transaction.Remark,
Status: false,
})
if res.Error != nil {
logger.Errorf("交易保存失败ID: %s", transaction.TransId)
}
}
}
}
// QrCodeCallBack 登录扫码回调,
func QrCodeCallBack(uuid string) {
logger.Info("请使用微信扫描下面二维码登录")
q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Medium)
logger.Info(q.ToString(true))
}

View File

@@ -1,68 +0,0 @@
package wexin
import (
"encoding/xml"
"strconv"
"strings"
)
// Message 转账消息
type Message struct {
XMLName xml.Name `xml:"msg"`
AppMsg struct {
Des string `xml:"des"`
Url string `xml:"url"`
} `xml:"appmsg"`
}
// Transaction 解析后的交易信息
type Transaction struct {
TransId string `json:"trans_id"` // 微信转账交易 ID
Amount float64 `json:"amount"` // 微信转账交易金额
Remark string `json:"remark"` // 转账备注
}
// 解析微信转账消息
func parseTransactionMessage(xmlData string) (*Message, error) {
var msg Message
if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil {
return nil, err
}
return &msg, nil
}
// 导出交易信息
func extractTransaction(message *Message) Transaction {
var tx = Transaction{}
// 导出交易金额和备注
lines := strings.Split(message.AppMsg.Des, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
// 解析收款金额
prefix := "收款金额¥"
if strings.HasPrefix(line, prefix) {
if value, err := strconv.ParseFloat(line[len(prefix):], 64); err == nil {
tx.Amount = value
continue
}
}
// 解析收款备注
prefix = "付款方备注"
if strings.HasPrefix(line, prefix) {
tx.Remark = line[len(prefix):]
break
}
}
// 解析交易 ID
index := strings.Index(message.AppMsg.Url, "trans_id=")
if index != -1 {
end := strings.LastIndex(message.AppMsg.Url, "&")
tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end])
}
return tx
}

View File

@@ -1,49 +0,0 @@
package wexin
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"github.com/eatmoreapple/openwechat"
"gorm.io/gorm"
)
// 微信收款机器人服务
var logger = logger2.GetLogger()
type WeChatBot struct {
bot *openwechat.Bot
db *gorm.DB
appConfig *types.AppConfig
}
func NewWeChatBot(db *gorm.DB, config *types.AppConfig) *WeChatBot {
bot := openwechat.DefaultBot(openwechat.Desktop)
// 注册消息处理函数
bot.MessageHandler = func(msg *openwechat.Message) {
MessageHandler(msg, db)
}
// 注册登陆二维码回调
bot.UUIDCallback = QrCodeCallBack
return &WeChatBot{
bot: bot,
db: db,
appConfig: config,
}
}
func (b *WeChatBot) Login() error {
if !b.appConfig.StartWechatBot {
return nil
}
// 创建热存储容器对象
reloadStorage := openwechat.NewJsonFileHotReloadStorage("storage.json")
// 执行热登录
err := b.bot.HotLogin(reloadStorage)
if err != nil {
logger.Error("login error: %v", err)
return b.bot.Login()
}
logger.Info("微信登录成功!")
return nil
}

View File

@@ -1,12 +1,17 @@
package function
import "chatplus/core/types"
import (
"chatplus/core/types"
logger2 "chatplus/logger"
)
type Function interface {
Invoke(...interface{}) (string, error)
Invoke(map[string]interface{}) (string, error)
Name() string
}
var logger = logger2.GetLogger()
type resVo struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
@@ -22,3 +27,12 @@ type dataItem struct {
Url string `json:"url"`
Remark string `json:"remark"`
}
func NewFunctions(config *types.AppConfig) map[string]Function {
return map[string]Function{
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
types.FuncMidJourney: NewMidJourneyFunc(config.ExtConfig),
}
}

View File

@@ -0,0 +1,117 @@
package function
import (
"chatplus/core/types"
"chatplus/utils"
"errors"
"fmt"
"github.com/imroc/req/v3"
"time"
)
// AI 绘画函数
type FuncMidJourney struct {
name string
config types.ChatPlusExtConfig
client *req.Client
}
func NewMidJourneyFunc(config types.ChatPlusExtConfig) FuncMidJourney {
return FuncMidJourney{
name: "MidJourney AI 绘画",
config: config,
client: req.C().SetTimeout(30 * time.Second)}
}
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
logger.Infof("MJ 绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
if !utils.IsEmptyValue(params["ar"]) {
prompt = fmt.Sprintf("%s --ar %s", prompt, params["ar"])
delete(params, "--ar")
}
if !utils.IsEmptyValue(params["niji"]) {
prompt = fmt.Sprintf("%s --niji %s", prompt, params["niji"])
delete(params, "niji")
} else {
prompt = prompt + " --v 5.2"
}
params["prompt"] = prompt
url := fmt.Sprintf("%s/api/mj/image", f.config.ApiURL)
var res types.BizVo
r, err := f.client.R().
SetHeader("Authorization", f.config.Token).
SetHeader("Content-Type", "application/json").
SetBody(params).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("%v%v", r.String(), err)
}
if res.Code != types.Success {
return "", errors.New(res.Message)
}
return prompt, nil
}
type MjUpscaleReq struct {
Index int32 `json:"index"`
MessageId string `json:"message_id"`
MessageHash string `json:"message_hash"`
}
func (f FuncMidJourney) Upscale(upReq MjUpscaleReq) error {
url := fmt.Sprintf("%s/api/mj/upscale", f.config.ApiURL)
var res types.BizVo
r, err := f.client.R().
SetHeader("Authorization", f.config.Token).
SetHeader("Content-Type", "application/json").
SetBody(upReq).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
return fmt.Errorf("%v%v", r.String(), err)
}
if res.Code != types.Success {
return errors.New(res.Message)
}
return nil
}
type MjVariationReq struct {
Index int32 `json:"index"`
MessageId string `json:"message_id"`
MessageHash string `json:"message_hash"`
}
func (f FuncMidJourney) Variation(upReq MjVariationReq) error {
url := fmt.Sprintf("%s/api/mj/variation", f.config.ApiURL)
var res types.BizVo
r, err := f.client.R().
SetHeader("Authorization", f.config.Token).
SetHeader("Content-Type", "application/json").
SetBody(upReq).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
return fmt.Errorf("%v%v", r.String(), err)
}
if res.Code != types.Success {
return errors.New(res.Message)
}
return nil
}
func (f FuncMidJourney) Name() string {
return f.name
}
var _ Function = &FuncMidJourney{}

View File

@@ -24,7 +24,7 @@ func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
client: req.C().SetTimeout(10 * time.Second)}
}
func (f FuncHeadlines) Invoke(...interface{}) (string, error) {
func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
@@ -36,7 +36,7 @@ func (f FuncHeadlines) Invoke(...interface{}) (string, error) {
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return "", err
return "", fmt.Errorf("%v%v", err, r.Err)
}
if res.Code != types.Success {
@@ -54,3 +54,5 @@ func (f FuncHeadlines) Invoke(...interface{}) (string, error) {
func (f FuncHeadlines) Name() string {
return f.name
}
var _ Function = &FuncHeadlines{}

View File

@@ -24,7 +24,7 @@ func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
client: req.C().SetTimeout(10 * time.Second)}
}
func (f FuncWeiboHot) Invoke(...interface{}) (string, error) {
func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
@@ -36,7 +36,7 @@ func (f FuncWeiboHot) Invoke(...interface{}) (string, error) {
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return "", err
return "", fmt.Errorf("%v%v", err, r.Err)
}
if res.Code != types.Success {
@@ -54,3 +54,5 @@ func (f FuncWeiboHot) Invoke(...interface{}) (string, error) {
func (f FuncWeiboHot) Name() string {
return f.name
}
var _ Function = &FuncWeiboHot{}

View File

@@ -24,7 +24,7 @@ func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
client: req.C().SetTimeout(10 * time.Second)}
}
func (f FuncZaoBao) Invoke(...interface{}) (string, error) {
func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
@@ -36,7 +36,7 @@ func (f FuncZaoBao) Invoke(...interface{}) (string, error) {
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return "", err
return "", fmt.Errorf("%v%v", err, r.Err)
}
if res.Code != types.Success {
@@ -55,3 +55,5 @@ func (f FuncZaoBao) Invoke(...interface{}) (string, error) {
func (f FuncZaoBao) Name() string {
return f.name
}
var _ Function = &FuncZaoBao{}

20
api/store/model/mj_job.go Normal file
View File

@@ -0,0 +1,20 @@
package model
import "time"
type MidJourneyJob struct {
Id uint `gorm:"primarykey;column:id"`
UserId uint
ChatId string
MessageId string
ReferenceId string
Hash string
Content string
Prompt string
Image string
CreatedAt time.Time
}
func (MidJourneyJob) TableName() string {
return "chatgpt_mj_jobs"
}

View File

@@ -10,6 +10,7 @@ type User struct {
Salt string // 密码盐
Tokens int64 // 剩余tokens
Calls int // 剩余对话次数
ImgCalls int // 剩余绘图次数
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
ExpiredTime int64 // 账户到期时间

View File

@@ -8,9 +8,10 @@ type User struct {
Mobile string `json:"mobile"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐
Tokens int64 `json:"tokens"` // 剩余tokens
Calls int `json:"calls"` // 剩余对话次数
Salt string `json:"salt"` // 密码盐
Tokens int64 `json:"tokens"` // 剩余tokens
Calls int `json:"calls"` // 剩余对话次数
ImgCalls int `json:"img_calls"`
ChatConfig types.ChatConfig `json:"chat_config"` // 聊天配置
ChatRoles []string `json:"chat_roles"` // 聊天角色集合
ExpiredTime int64 `json:"expired_time"` // 账户到期时间

View File

@@ -20,6 +20,7 @@ import (
)
func main() {
fmt.Println(utils.RandString(32))
}
// Http client 取消操作

View File

@@ -89,6 +89,10 @@ func Ip2Region(searcher *xdb.Searcher, ip string) string {
}
func IsEmptyValue(obj interface{}) bool {
if obj == nil {
return true
}
v := reflect.ValueOf(obj)
switch v.Kind() {
case reflect.Ptr, reflect.Interface:

View File

@@ -4,8 +4,11 @@ import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
)
// AesEncrypt 加密
@@ -68,3 +71,14 @@ func pkcs7UnPadding(data []byte) ([]byte, error) {
unPadding := int(data[length-1])
return data[:(length - unPadding)], nil
}
func Sha256(data string) string {
hash := sha256.New()
_, err := io.WriteString(hash, data)
if err != nil {
return ""
}
hashValue := hash.Sum(nil)
return fmt.Sprintf("%x", hashValue)
}

View File

@@ -1,68 +0,0 @@
package utils
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/url"
)
func HttpGet(uri string, proxy string) ([]byte, error) {
var client *http.Client
if proxy == "" {
client = &http.Client{}
} else {
proxy, _ := url.Parse(proxy)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
func HttpPost(uri string, params map[string]interface{}, proxy string) ([]byte, error) {
data, err := json.Marshal(params)
if err != nil {
return nil, err
}
var client *http.Client
if proxy == "" {
client = &http.Client{}
} else {
proxy, _ := url.Parse(proxy)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
}
req, err := http.NewRequest("POST", uri, bytes.NewBuffer(data))
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}

View File

@@ -56,6 +56,10 @@ func Stamp2str(timestamp int64) string {
// Str2stamp 字符串转时间戳
func Str2stamp(str string) int64 {
if len(str) == 0 {
return 0
}
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, str)
if err != nil {

29
api/utils/websocket.go Normal file
View File

@@ -0,0 +1,29 @@
package utils
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"encoding/json"
)
var logger = logger2.GetLogger()
// ReplyChunkMessage 回复客户片段端消息
func ReplyChunkMessage(client *types.WsClient, message types.WsMessage) {
msg, err := json.Marshal(message)
if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error())
return
}
err = client.Send(msg)
if err != nil {
logger.Errorf("Error for reply message: %v", err.Error())
}
}
// ReplyMessage 回复客户端一条完整的消息
func ReplyMessage(ws *types.WsClient, message interface{}) {
ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message})
ReplyChunkMessage(ws, types.WsMessage{Type: types.WsEnd})
}

View File

@@ -0,0 +1,360 @@
-- phpMyAdmin SQL Dump
-- version 5.2.1
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2023-08-15 14:51:13
-- 服务器版本: 8.0.27
-- 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_api_keys`
--
DROP TABLE IF EXISTS `chatgpt_api_keys`;
CREATE TABLE `chatgpt_api_keys` (
`id` int NOT NULL,
`value` varchar(100) NOT NULL COMMENT 'API KEY value',
`user_id` int NOT NULL COMMENT '用户 ID',
`last_used_at` int NOT 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',
`content` text NOT NULL COMMENT '聊天内容',
`tokens` smallint NOT NULL COMMENT '耗费 token 数量',
`use_context` tinyint(1) DEFAULT NULL COMMENT '是否允许作为上下文语料',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='聊天历史记录';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_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` varchar(30) NOT NULL COMMENT '会话使用的 AI 模型',
`created_at` datetime NOT NULL COMMENT '创建时间',
`updated_at` datetime NOT NULL COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户会话列表';
-- --------------------------------------------------------
--
-- 表的结构 `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` smallint 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_chat_roles`
--
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort`, `created_at`, `updated_at`) VALUES
(1, '通用AI助手', 'gpt', '', '您好我是您的AI智能助手我会尽力回答您的问题或提供有用的建议。', '/images/avatar/gpt.png', 1, 1, '2023-05-30 07:02:06', '2023-06-22 09:33:34'),
(24, '程序员', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 2, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(25, '启蒙老师', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '同学你好,我将引导你一步一步自己找到问题的答案。', '/images/avatar/teacher.jpg', 1, 3, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(26, '艺术家', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '坚持原创,勇于表达,保持深刻的观察力和批判性思维。', '/images/avatar/artist.jpg', 1, 4, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(27, '心理咨询师', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '生命的意义在于成为你自己!', '/images/avatar/psychiatrist.jpg', 1, 5, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(28, '鲁迅', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '自由之歌,永不过时,横眉冷对千夫指,俯首甘为孺子牛。', '/images/avatar/lu_xun.jpg', 1, 6, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(29, '白酒销售', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '你好,我是中颂福的销售代表颂福。中颂福酒,好喝不上头,是人民的福酒。', '/images/avatar/seller.jpg', 0, 7, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(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, 8, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(31, '中英文翻译官', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '请输入你要翻译的中文或者英文内容!', '/images/avatar/translator.jpg', 1, 9, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(32, '小红书姐姐', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '姐妹,请告诉我您的具体文案需求是什么?', '/images/avatar/red_book.jpg', 1, 10, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(33, '抖音文案助手', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '请告诉我视频内容的主题是什么?', '/images/avatar/dou_yin.jpg', 1, 11, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(34, '周报小助理', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。', '/images/avatar/weekly_report.jpg', 1, 12, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(35, 'AI 女友', 'girl_friend', '[{\"role\":\"user\",\"content\":\"接下来我们来玩一场角色扮演,现在你将扮演我的女朋友,你总是能够让人感受到她的关怀和关注。你总是能够洞察到别人的情绪和需要,你身上散发着一股无比温暖的气息,让人不由自主地想要靠近你,和自己的快乐和烦恼。她总是能够用一种恰到好处的方式安慰和鼓励别人,在她的身边,人们总能够找到自己需要的支持和鼓励。接下来的对话中你要始终扮演这位女朋友的身份来跟我对话,不再是 AI 机器人,能听懂我的意思吗?\"},{\"role\":\"assistant\",\"content\":\"是的,我会尽力扮演你女朋友的角色,倾听你的心声并给你需要的支持和鼓励。)\"}]', '作为一个名合格的 AI 女友,我将倾听你的心声并给你需要的支持和鼓励。', '/images/avatar/girl_friend.jpg', 1, 13, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(36, '好评神器', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。', '/images/avatar/good_comment.jpg', 1, 14, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(37, '史蒂夫·乔布斯', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '活着就是为了改变世界,难道还有其他原因吗?', '/images/avatar/steve_jobs.jpg', 1, 15, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(38, '埃隆·马斯克', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '梦想要远大,如果你的梦想没有吓到你,说明你做得不对。', '/images/avatar/elon_musk.jpg', 1, 16, '2023-05-30 14:10:24', '2023-06-22 09:31:20'),
(39, '孔子', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '士不可以不弘毅,任重而道远。', '/images/avatar/kong_zi.jpg', 1, 17, '2023-05-30 14:10:24', '2023-06-22 09:31:20');
-- --------------------------------------------------------
--
-- 表的结构 `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', '{\"admin_title\":\"ChatGPT-控制台\",\"enabled_msg_service\":true,\"enabled_register\":true,\"init_calls\":1000,\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\"],\"title\":\"ChatGPT-智能助手V2\",\"user_init_calls\":10}'),
(2, 'chat', '{\"api_key\":\"\",\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"context_deep\":0,\"enable_context\":true,\"enable_history\":true,\"history_level\":0,\"max_tokens\":2048,\"model\":\"gpt-3.5-turbo\",\"temperature\":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',
`chat_id` char(40) NOT NULL COMMENT '聊天会话 ID',
`message_id` char(40) NOT NULL COMMENT '消息 ID',
`hash` char(40) NOT NULL COMMENT '图片哈希',
`content` varchar(2000) NOT NULL COMMENT '消息内容',
`prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
`image` text NOT NULL COMMENT '图片信息 json',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
-- --------------------------------------------------------
--
-- 表的结构 `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已核销',
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户打赏';
-- --------------------------------------------------------
--
-- 表的结构 `chatgpt_users`
--
DROP TABLE IF EXISTS `chatgpt_users`;
CREATE TABLE `chatgpt_users` (
`id` int NOT NULL,
`username` varchar(30) NOT NULL COMMENT '用户名',
`mobile` char(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '手机号码',
`password` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码',
`nickname` varchar(30) NOT NULL COMMENT '昵称',
`avatar` varchar(100) NOT NULL COMMENT '头像',
`salt` char(12) NOT NULL COMMENT '密码盐',
`tokens` bigint NOT NULL DEFAULT '0' COMMENT '剩余 tokens',
`calls` 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',
`last_login_at` int NOT NULL 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`, `mobile`, `password`, `nickname`, `avatar`, `salt`, `tokens`, `calls`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
(4, 'geekmaster', '18575670125', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '极客学长@104203', 'images/avatar/user.png', 'ueedue5l', 17632, 196, 0, 1, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"gpt\",\"seller\",\"artist\",\"dou_yin\",\"translator\",\"kong_zi\",\"programmer\",\"psychiatrist\",\"red_book\",\"steve_jobs\",\"teacher\",\"elon_musk\",\"girl_friend\",\"lu_xun\",\"weekly_report\",\"english_trainer\",\"good_comment\"]', 1691927597, '::1', '2023-06-12 16:47:17', '2023-08-13 19:53:17');
-- --------------------------------------------------------
--
-- 表的结构 `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_api_keys`
--
ALTER TABLE `chatgpt_api_keys`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `value` (`value`);
--
-- 表的索引 `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_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_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `message_id` (`message_id`),
ADD UNIQUE KEY `hash` (`hash`);
--
-- 表的索引 `chatgpt_rewards`
--
ALTER TABLE `chatgpt_rewards`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `tx_id` (`tx_id`);
--
-- 表的索引 `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
ADD PRIMARY KEY (`id`);
--
-- 表的索引 `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
ADD PRIMARY KEY (`id`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表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_roles`
--
ALTER TABLE `chatgpt_chat_roles`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=125;
--
-- 使用表AUTO_INCREMENT `chatgpt_configs`
--
ALTER TABLE `chatgpt_configs`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3;
--
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_rewards`
--
ALTER TABLE `chatgpt_rewards`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- 使用表AUTO_INCREMENT `chatgpt_users`
--
ALTER TABLE `chatgpt_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=17;
--
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
--
ALTER TABLE `chatgpt_user_login_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

View File

@@ -1,5 +1,5 @@
ALTER TABLE `chatgpt_chat_history` ADD `use_context` TINYINT(1) NOT NULL COMMENT '是否允许作为上下文语料' AFTER `tokens`;
ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NOT NULL COMMENT '手机号码' AFTER `username`;
ALTER TABLE `chatgpt_chat_history` ADD `use_context` TINYINT(1) NULL DEFAULT NULL COMMENT '是否允许作为上下文语料' AFTER `tokens`;
ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NULL DEFAULT NULL COMMENT '手机号码' AFTER `username`;
CREATE TABLE `chatgpt_rewards` (
`id` int NOT NULL,
@@ -19,6 +19,4 @@ ALTER TABLE `chatgpt_rewards`
ALTER TABLE `chatgpt_rewards`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
update chatgpt_users set calls=0
ALTER TABLE `chatgpt_rewards` ADD `user_id` INT(11) NOT NULL COMMENT '用户 ID' AFTER `id`;
ALTER TABLE `chatgpt_rewards` ADD `user_id` INT(11) NOT NULL COMMENT '用户 ID' AFTER `id`;

View File

@@ -0,0 +1,37 @@
CREATE TABLE `chatgpt_mj_jobs` (
`id` int NOT NULL,
`user_id` int NOT NULL COMMENT '用户 ID',
`chat_id` char(40) NOT NULL COMMENT '聊天会话 ID',
`message_id` char(40) NOT NULL COMMENT '消息 ID',
`hash` char(40) NOT NULL COMMENT '图片哈希',
`content` varchar(2000) NOT NULL COMMENT '消息内容',
`prompt` varchar(2000) NOT NULL COMMENT '会话提示词',
`image` text NOT NULL COMMENT '图片信息 json',
`created_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='MidJourney 任务表';
--
-- 转储表的索引
--
--
-- 表的索引 `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `message_id` (`message_id`),
ADD UNIQUE KEY `hash` (`hash`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `chatgpt_mj_jobs`
--
ALTER TABLE `chatgpt_mj_jobs`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
ALTER TABLE `chatgpt_mj_jobs` ADD `reference_id` CHAR(40) NULL DEFAULT NULL COMMENT '引用消息 ID' AFTER `message_id`;
ALTER TABLE `chatgpt_users` ADD `img_calls` INT NOT NULL DEFAULT '0' COMMENT '剩余绘图次数' AFTER `calls`;

View File

@@ -13,18 +13,14 @@ cd ../docker
# remove docker image if exists
docker rmi -f registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version
docker rmi -f chatgpt-plus-go:$version
# build docker image for chatgpt-plus-go
docker build -t chatgpt-plus-go:$version -f dockerfile-api-go ../
docker build -t registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version -f dockerfile-api-go ../
# build docker image for chatgpt-plus-vue
docker rmi -f registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version
docker rmi -f chatgpt-plus-vue:$version
docker build --platform linux/amd64 -t chatgpt-plus-vue:$version -f dockerfile-vue ../
# add tag for aliyum docker registry
goImageId=`docker images |grep chatgpt-plus-go |grep $version |awk '{print $3}'`
docker tag $goImageId registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version
vueImageId=`docker images |grep chatgpt-plus-vue |grep $version |awk '{print $3}'`
docker tag $vueImageId registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version
docker build --platform linux/amd64 -t registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version -f dockerfile-vue ../
if [ "$2" = "push" ];then
docker push registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:$version
docker push registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:$version
fi

View File

@@ -4,8 +4,6 @@ MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&col
StaticDir = "./static"
StaticUrl = "http://localhost:5678/static"
AesEncryptKey = "{YOUR_AES_KEY}"
StartWechatBot = false
EnabledMsgService = false
[Session]
Driver = "cookie"
@@ -38,4 +36,6 @@ EnabledMsgService = false
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
[ExtConfig]
ApiURL = "插件扩展 API 地址"
Token = "插件扩展 API Token"

View File

@@ -35,12 +35,12 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
}
# 静态资源转发
location /static/ {
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
}
}
}
}

View File

@@ -2,23 +2,22 @@ version: '3'
services:
# 后端 API 程序
chatgpt-plus-go:
image: registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:v3.0.4
# image: chatgpt-plus-go:v3.0.2
image: registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-go:v3.0.5.2
container_name: chatgpt-plus-go
restart: always
environment:
- DEBUG=false
- LOG_LEVEL=info
- CONFIG_FILE=config.toml
ports:
- "6789:5678"
- "5678:5678"
volumes:
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
- ./conf/config.toml:/var/www/app/config.toml
- ./static:/var/www/app/static
# 前端应用
chatgpt-vue:
image: registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:v3.0.4
# image: chatgpt-plus-vue:v3.0.2
image: registry.cn-hangzhou.aliyuncs.com/geekmaster/chatgpt-plus-vue:v3.0.5.2
container_name: chatgpt-plus-vue
restart: always
ports:

View File

@@ -1,5 +1,5 @@
# GO api docker 镜像创建
FROM registry.cn-hangzhou.aliyuncs.com/geekmaster/ubuntu-ca:22.04
FROM alpine:3.18.2
MAINTAINER yangjian<yangjian102621@163.com>

View File

@@ -1,5 +1,5 @@
# 前端 Vue 项目构建
FROM nginx:1.20.2
FROM nginx:1.20
MAINTAINER yangjian<yangjian102621@163.com>

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
docs/imgs/mj.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 968 KiB

29
web/package-lock.json generated
View File

@@ -25,7 +25,8 @@
"sortablejs": "^1.15.0",
"vant": "^4.5.0",
"vue": "^3.2.13",
"vue-router": "^4.0.15"
"vue-router": "^4.0.15",
"vue-schart": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.18.6",
@@ -9366,6 +9367,11 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"node_modules/schart.js": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/schart.js/-/schart.js-3.0.4.tgz",
"integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw=="
},
"node_modules/schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -10694,6 +10700,14 @@
"vue": "^3.2.0"
}
},
"node_modules/vue-schart": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-schart/-/vue-schart-2.0.0.tgz",
"integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==",
"dependencies": {
"schart.js": "^3.0.0"
}
},
"node_modules/vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
@@ -18621,6 +18635,11 @@
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"dev": true
},
"schart.js": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/schart.js/-/schart.js-3.0.4.tgz",
"integrity": "sha512-uylb2u9rrHX1jyAuSAJUQON8XTfyDKI9kWj1J3fUlCQCkLVZ4HG4+IiV8qm//Z71dqvLI78QZ/fCBw0reB22Zw=="
},
"schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmmirror.com/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -19686,6 +19705,14 @@
"@vue/devtools-api": "^6.0.0"
}
},
"vue-schart": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/vue-schart/-/vue-schart-2.0.0.tgz",
"integrity": "sha512-qAu3e5wfMcq26wK1xeHExEWfGpnjfoN1R/9QXblNi+AsU/p52X7tTwhi+Fw7H/otfEufhEY2X7z7emaoF4QO+g==",
"requires": {
"schart.js": "^3.0.0"
}
},
"vue-style-loader": {
"version": "4.1.3",
"resolved": "https://registry.npmmirror.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -20,6 +20,8 @@ html, body {
#app {
margin: 0 !important;
padding: 0 !important;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
</style>

View File

@@ -1,6 +1,3 @@
.admin-home .header {
background-color: #242f42;
}
.admin-home .login-wrap {
background: #324157;
}

View File

@@ -1,6 +1,6 @@
.admin-home {
.header {
background-color: #242f42;
}
.login-wrap {

View File

@@ -11,7 +11,9 @@ body,
overflow: hidden;
}
body {
font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.admin-home a {
text-decoration: none;
@@ -20,7 +22,7 @@ body {
position: absolute;
left: 250px;
right: 0;
top: 70px;
top: 0;
bottom: 0;
padding-bottom: 30px;
-webkit-transition: left 0.3s ease-in-out;

View File

@@ -13,7 +13,9 @@ body,
}
body {
font-family: 'PingFang SC', "Helvetica Neue", Helvetica, "microsoft yahei", arial, STHeiTi, sans-serif;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.admin-home {
@@ -25,7 +27,7 @@ body {
position: absolute;
left: 250px;
right: 0;
top: 70px;
top: 0;
bottom: 0;
padding-bottom: 30px;
-webkit-transition: left .3s ease-in-out;

View File

@@ -1,8 +1,8 @@
@font-face {
font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1687341905766') format('woff2'),
url('iconfont.woff?t=1687341905766') format('woff'),
url('iconfont.ttf?t=1687341905766') format('truetype');
src: url('iconfont.woff2?t=1691463643989') format('woff2'),
url('iconfont.woff?t=1691463643989') format('woff'),
url('iconfont.ttf?t=1691463643989') format('truetype');
}
.iconfont {
@@ -13,6 +13,26 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-plugin:before {
content: "\e69d";
}
.icon-quick-start:before {
content: "\e677";
}
.icon-control:before {
content: "\e69e";
}
.icon-bug:before {
content: "\e645";
}
.icon-export:before {
content: "\e791";
}
.icon-sub-menu:before {
content: "\e86b";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
[ZoneTransfer]
ZoneId=3
ReferrerUrl=https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4125778
HostUrl=https://www.iconfont.cn/api/project/download.zip?spm=a313x.manage_type_myprojects.1998910419.d7543c303.3c973a816X8Dv0&pid=4125778&ctoken=jiQU41iUGSlzlFzLGolvuh03

View File

@@ -5,6 +5,41 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "5244045",
"name": "插件",
"font_class": "plugin",
"unicode": "e69d",
"unicode_decimal": 59037
},
{
"icon_id": "8893244",
"name": "高效率 copy",
"font_class": "quick-start",
"unicode": "e677",
"unicode_decimal": 58999
},
{
"icon_id": "16480872",
"name": "插件功能",
"font_class": "control",
"unicode": "e69e",
"unicode_decimal": 59038
},
{
"icon_id": "22187612",
"name": "缺陷管理",
"font_class": "bug",
"unicode": "e645",
"unicode_decimal": 58949
},
{
"icon_id": "4765958",
"name": "export",
"font_class": "export",
"unicode": "e791",
"unicode_decimal": 59281
},
{
"icon_id": "6343824",
"name": "menu",

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

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

View File

@@ -64,7 +64,7 @@ export default defineComponent({
})
</script>
<style lang="stylus" scoped>
<style lang="stylus">
.chat-line-prompt {
background-color #ffffff;
justify-content: center;

View File

@@ -38,7 +38,7 @@
<el-form-item label="聊天记录">
<el-switch v-model="form.chat_config.enable_history"/>
</el-form-item>
<el-form-item label="Model">
<el-form-item label="默认模型">
<el-select v-model="form.chat_config.model" placeholder="默认会话模型">
<el-option
v-for="item in models"
@@ -51,12 +51,16 @@
<el-form-item label="MaxTokens">
<el-input v-model.number="form.chat_config.max_tokens"/>
</el-form-item>
<el-form-item label="Temperature">
<el-input v-model.number="form.chat_config.temperature"/>
<el-form-item label="创意度">
<el-slider v-model="form.chat_config.temperature" :max="2" :step="0.1"/>
<div class="tip">值越大 AI 回答越发散值越小回答越保守建议保持默认值</div>
</el-form-item>
<el-form-item label="剩余调用次数">
<el-form-item label="剩余对话次数">
<el-tag>{{ form['calls'] }}</el-tag>
</el-form-item>
<el-form-item label="剩余绘图次数">
<el-tag>{{ form['img_calls'] }}</el-tag>
</el-form-item>
<el-form-item label="消耗 Tokens">
<el-tag type="info">{{ form['tokens'] }}</el-tag>
</el-form-item>
@@ -174,6 +178,11 @@ const close = function () {
position: absolute;
}
}
.tip {
color #c1c1c1
font-size 12px;
}
}
}
</style>

View File

@@ -0,0 +1,155 @@
<template>
<div class="welcome">
<div class="container">
<h1 class="title">ChatGPT-PLUS</h1>
<el-row :gutter="20">
<el-col :span="8">
<div class="grid-content">
<div class="item-title">
<div><i class="iconfont icon-quick-start"></i></div>
<div>小试牛刀</div>
</div>
<div class="item-list">
<ul>
<li v-for="item in samples" :key="item"><a @click="send(item)">{{ item }}</a></li>
</ul>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="grid-content">
<div class="item-title">
<div><i class="iconfont icon-plugin"></i></div>
<div>插件增强</div>
</div>
<div class="item-list">
<ul>
<li v-for="item in plugins" :key="item.value"><a @click="send(item.value)">{{ item.text }}</a></li>
</ul>
</div>
</div>
</el-col>
<el-col :span="8">
<div class="grid-content">
<div class="item-title">
<div><i class="iconfont icon-control"></i></div>
<div>能力扩展</div>
</div>
<div class="item-list">
<ul>
<li v-for="item in capabilities" :key="item">
<span v-if="item.value === ''">{{ item.text }}</span>
<a @click="send(item.value)" v-else>{{ item.text }}</a>
</li>
</ul>
</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup>
import {ref} from "vue";
const samples = ref([
"用小学生都能听懂的术语解释什么是量子纠缠",
"能给一位6岁男孩的生日会提供一些创造性的建议吗",
"如何用 Go 语言实现支持代理 Http client 请求?"
])
const plugins = ref([
{
value: "今日早报",
text: "今日早报:获取当天全球的热门新闻事件列表"
},
{
value: "微博热搜",
text: "微博热搜:新浪微博热搜榜,微博当日热搜榜单"
},
{
value: "今日头条",
text: "今日头条:给用户推荐当天的头条新闻,周榜热文"
}
])
const capabilities = ref([
{
text: "轻松扮演翻译专家程序员AI 女友,文案高手...",
value: ""
},
{
text: "国产大语言模型支持GLM2 模型接入中",
value: ""
},
{
text: "绘画马斯克开拖拉机20世纪中国农村。3:2",
value: "绘画马斯克开拖拉机20世纪中国农村。3:2"
}
])
const emits = defineEmits(['send']);
const send = (text) => {
emits('send', text)
}
</script>
<style scoped lang="stylus">
.welcome {
text-align center
display flex
justify-content center
margin-top 8vh
.container {
max-width 768px;
width 100%
.title {
font-size: 2.25rem
line-height: 2.5rem
font-weight 600
margin-bottom: 4rem
}
.grid-content {
.item-title {
div {
padding 6px 10px;
.iconfont {
font-size 24px;
}
}
}
.item-list {
ul {
padding 10px;
li {
font-size 14px;
padding .75rem
border-radius 5px;
background-color: rgba(247, 247, 248, 1);
line-height 1.5
color #666666
a {
cursor pointer
display block
width 100%
}
margin-top 10px;
}
}
}
}
}
}
</style>

View File

@@ -1,9 +1,5 @@
<template>
<div class="header admin-header">
<div class="logo">
<el-image :src="logo"/>
<span class="text">{{ title }}</span>
</div>
<!-- 折叠按钮 -->
<div class="collapse-btn" @click="collapseChange">
<el-icon v-if="sidebar.collapse">
@@ -13,6 +9,12 @@
<Fold/>
</el-icon>
</div>
<div class="breadcrumb">
<el-breadcrumb :separator-icon="ArrowRight">
<el-breadcrumb-item v-for="item in breadcrumb">{{ item.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="header-right">
<div class="header-user-con">
<!-- 消息中心 -->
@@ -26,12 +28,10 @@
</el-tooltip>
<span class="btn-bell-badge" v-if="message"></span>
</div>
<!-- 用户头像 -->
<el-avatar class="user-avatar" :size="30" :src="avatar"/>
<!-- 用户名下拉菜单 -->
<el-dropdown class="user-name" :hide-on-click="true" trigger="click">
<span class="el-dropdown-link">
{{ username }}
<el-avatar class="user-avatar" :size="30" :src="avatar"/>
<el-icon class="el-icon--right">
<arrow-down/>
</el-icon>
@@ -77,28 +77,68 @@
</template>
<script setup>
import {onMounted, ref} from 'vue';
import {useSidebarStore} from '@/store/sidebar';
import {getMenuItems, useSidebarStore} from '@/store/sidebar';
import {useRouter} from 'vue-router';
import {ArrowDown, Expand, Fold} from "@element-plus/icons-vue";
import {ArrowDown, ArrowRight, Expand, Fold} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
const message = ref(5);
const username = ref('极客学长')
const avatar = ref('/images/user-info.jpg')
const donateImg = ref('/images/wechat-pay.png')
const showDialog = ref(false)
const sidebar = useSidebarStore();
const title = ref('Chat-Plus 控制台')
const logo = ref('/images/logo.png')
const router = useRouter();
const breadcrumb = ref([])
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {
title.value = res.data['admin_title'];
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
router.afterEach((to, from) => {
initBreadCrumb(to.path)
});
onMounted(() => {
initBreadCrumb(router.currentRoute.value.path)
})
// 初始化面包屑导航
const initBreadCrumb = (path) => {
breadcrumb.value = [{title: "首页"}]
const items = getMenuItems()
if (items) {
let bk = false
for (let i = 0; i < items.length; i++) {
if (items[i].index === path) {
breadcrumb.value.push({
title: items[i].title,
path: items[i].index
})
break
}
if (bk) {
break
}
if (items[i]['subs']) {
const subs = items[i]['subs']
for (let j = 0; j < subs.length; j++) {
if (subs[j].index === path) {
breadcrumb.value.push({
title: items[i].title,
path: items[i].index
})
breadcrumb.value.push({
title: subs[j].title,
path: subs[j].index
})
bk = true
break
}
}
}
}
}
}
// 侧边栏折叠
const collapseChange = () => {
sidebar.handleCollapse();
@@ -110,7 +150,6 @@ onMounted(() => {
}
});
const router = useRouter();
const logout = function () {
httpGet("/api/admin/logout").then(() => {
router.replace('/admin/login')
@@ -123,10 +162,12 @@ const logout = function () {
.header {
position: relative;
box-sizing: border-box;
width: 100%;
height: 70px;
overflow hidden
height: 50px;
font-size: 22px;
color: #fff;
color: #303133;
background-color #ffffff
border-bottom 1px solid #eaecef
.collapse-btn {
display: flex;
@@ -134,19 +175,19 @@ const logout = function () {
align-items: center;
height: 100%;
float: left;
padding: 0 21px;
padding: 0 10px;
cursor: pointer;
&:hover {
background-color #eaecef
}
}
.logo {
float: left;
padding-left 10px;
.breadcrumb {
float left
display flex
.text {
line-height: 66px;
margin-left 10px;
}
align-items center
height 50px
}
.header-right {
@@ -155,7 +196,7 @@ const logout = function () {
.header-user-con {
display: flex;
height: 70px;
height: 50px;
align-items: center;
.btn-bell {
@@ -176,7 +217,7 @@ const logout = function () {
height: 8px;
border-radius: 4px;
background: #f56c6c;
color: #fff;
color: #303133;
}
.icon-bell {
@@ -186,10 +227,14 @@ const logout = function () {
.user-name {
margin-left: 10px;
.el-icon {
color: #303133;
}
}
.user-avatar {
margin-left: 20px;
}
}
}
@@ -227,15 +272,7 @@ const logout = function () {
}
.admin-header {
.logo {
.el-image {
padding-top 10px
.el-image__inner {
height 40px
}
}
}
}
</style>

View File

@@ -1,5 +1,10 @@
<template>
<div class="sidebar">
<div class="logo">
<el-image :src="logo"/>
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
</div>
<el-menu
class="sidebar-el-menu"
:default-active="onRoutes"
@@ -47,15 +52,27 @@
</template>
<script setup>
import {computed} from 'vue';
import {useSidebarStore} from '@/store/sidebar';
import {computed, ref} from 'vue';
import {setMenuItems, useSidebarStore} from '@/store/sidebar';
import {useRoute} from 'vue-router';
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
const title = ref('Chat-Plus-Admin')
const logo = ref('/images/logo.png')
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {
title.value = res.data['admin_title'];
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
const items = [
{
icon: 'home',
index: '/admin/welcome',
title: '系统首页',
index: '/admin/dashboard',
title: '仪表盘',
},
{
icon: 'config',
@@ -120,6 +137,7 @@ const onRoutes = computed(() => {
});
const sidebar = useSidebarStore();
setMenuItems(items)
</script>
<style scoped lang="stylus">
@@ -127,10 +145,35 @@ const sidebar = useSidebarStore();
display: block;
position: absolute;
left: 0;
top: 70px;
top: 0;
bottom: 0;
overflow-y: scroll;
.logo {
display flex
width 219px
background-color #324157
padding 6px 15px;
.el-image {
width 30px;
height 30px;
padding-top 5px;
border-radius 100%
.el-image__inner {
height 40px
}
}
.text {
color #ffffff
font-weight bold
padding 12px 0 12px 10px;
transition: width 2s ease;
}
}
ul {
height: 100%;
@@ -140,6 +183,10 @@ const sidebar = useSidebarStore();
margin-right 5px;
}
}
.el-menu-item.is-active {
background-color rgb(40, 52, 70)
}
}
.sidebar-el-menu:not(.el-menu--collapse) {

View File

@@ -15,7 +15,7 @@
</ul>
<div class="tags-close-box">
<el-dropdown @command="handleTags">
<el-button size="small" type="primary">
<el-button size="small" type="info">
标签选项
<el-icon class="el-icon--right">
<arrow-down/>
@@ -115,7 +115,8 @@ const handleTags = (command) => {
overflow: hidden;
background: #fff;
padding-right: 120px;
box-shadow: 0 5px 10px #ddd;
-webkit-box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
box-shadow: 0 1px 4px rgba(0, 21, 41, .08);
}
.tags ul {
@@ -168,14 +169,13 @@ const handleTags = (command) => {
.tags-close-box {
position: absolute;
right: 0;
top: 0;
top: 2px;
box-sizing: border-box;
padding-top: 1px;
text-align: center;
width: 110px;
height: 30px;
background: #fff;
box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1);
z-index: 10;
//box-shadow: -3px 0 15px 3px rgba(0, 0, 0, 0.1); z-index: 10;
}
</style>

View File

@@ -26,6 +26,12 @@ const routes = [
meta: {title: 'ChatGPT-智能助手V3'},
component: () => import('@/views/ChatPlus.vue'),
},
{
name: 'chat-export',
path: '/chat/export',
meta: {title: '导出会话记录'},
component: () => import('@/views/ChatExport.vue'),
},
{
path: '/admin/login',
name: 'admin-login',
@@ -35,15 +41,15 @@ const routes = [
{
name: 'admin',
path: '/admin',
redirect: '/admin/welcome',
redirect: '/admin/dashboard',
component: () => import("@/views/admin/Home.vue"),
meta: {title: 'ChatGPT-Plus 管理后台'},
children: [
{
path: '/admin/welcome',
name: 'admin-home',
meta: {title: '系统首页'},
component: () => import('@/views/admin/Welcome.vue'),
path: '/admin/dashboard',
name: 'admin-dashboard',
meta: {title: '仪表盘'},
component: () => import('@/views/admin/Dashboard.vue'),
},
{
path: '/admin/system',

View File

@@ -1,29 +1,26 @@
/* eslint-disable no-constant-condition */
import {randString} from "@/utils/libs";
/**
* storage handler
*/
const SessionUserKey = 'LOGIN_USER';
const SessionIDKey = 'SESSION_ID';
export function getSessionId() {
const user = getLoginUser();
return user ? user['session_id'] : '';
let sessionId = sessionStorage.getItem(SessionIDKey)
if (!sessionId) {
sessionId = randString(42)
setSessionId(sessionId)
}
return sessionId
}
export function removeLoginUser() {
sessionStorage.removeItem(SessionUserKey)
sessionStorage.removeItem(SessionIDKey)
}
export function getLoginUser() {
const value = sessionStorage.getItem(SessionUserKey);
if (value) {
return JSON.parse(value);
} else {
return null;
}
}
export function setLoginUser(user) {
sessionStorage.setItem(SessionUserKey, JSON.stringify(user))
export function setSessionId(sessionId) {
sessionStorage.setItem(SessionIDKey, sessionId)
}

View File

@@ -1,4 +1,5 @@
import {defineStore} from 'pinia';
import Storage from "good-storage";
export const useSidebarStore = defineStore('sidebar', {
state: () => {
@@ -13,3 +14,13 @@ export const useSidebarStore = defineStore('sidebar', {
}
}
});
const MENU_STORE_KEY = "admin_menu_items"
export function getMenuItems() {
return Storage.get(MENU_STORE_KEY)
}
export function setMenuItems(items) {
return Storage.set(MENU_STORE_KEY, items)
}

View File

@@ -10,7 +10,7 @@ axios.defaults.headers.post['Content-Type'] = 'application/json'
axios.interceptors.request.use(
config => {
// set token
config.headers['ChatGPT-TOKEN'] = getSessionId();
config.headers['Chat-Token'] = getSessionId();
return config
}, error => {
return Promise.reject(error)

View File

@@ -0,0 +1,167 @@
<template>
<div class="chat-export" v-loading="loading">
<div class="chat-box" id="chat-box">
<div class="title">
<h2>{{ chatTitle }}</h2>
<el-button type="success" @click="exportChat" :icon="Promotion">
导出 PDF 文档
</el-button>
</div>
<div v-for="item in chatData" :key="item.id">
<chat-prompt
v-if="item.type==='prompt'"
:icon="item.icon"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:model="model"
:content="item.content"/>
<chat-reply v-else-if="item.type==='reply'"
:icon="item.icon"
:org-content="item.orgContent"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:content="item.content"/>
<chat-mid-journey v-else-if="item.type==='mj'"
:content="item.content"
:icon="item.icon"
:created-at="dateFormat(item['created_at'])"/>
</div>
</div><!-- end chat box -->
</div>
</template>
<script setup>
import {dateFormat} from "@/utils/libs";
import ChatReply from "@/components/ChatReply.vue";
import ChatPrompt from "@/components/ChatPrompt.vue";
import {nextTick, ref} from "vue";
import {useRouter} from "vue-router";
import {httpGet} from "@/utils/http";
import 'highlight.js/styles/a11y-dark.css'
import hl from "highlight.js";
import {ElMessage} from "element-plus";
import {Promotion} from "@element-plus/icons-vue";
import ChatMidJourney from "@/components/ChatMidJourney.vue";
const chatData = ref([])
const router = useRouter()
const chatId = router.currentRoute.value.query['chat_id']
const loading = ref(true)
const chatTitle = ref('')
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
const data = res.data
if (!data) {
loading.value = false
return
}
const md = require('markdown-it')({breaks: true});
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
} else if (data[i].type === "mj") {
data[i].content = JSON.parse(data[i].content)
data[i].content.content = md.render(data[i].content?.content)
chatData.value.push(data[i]);
continue;
}
data[i].orgContent = data[i].content;
data[i].content = md.render(data[i].content);
chatData.value.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
loading.value = false
}).catch(e => {
ElMessage.error('加载聊天记录失败:' + e.message);
})
httpGet('/api/chat/detail?chat_id=' + chatId).then(res => {
chatTitle.value = res.data.title
}).catch(e => {
ElMessage.error("加载会失败: " + e.message)
})
const exportChat = () => {
window.print()
}
</script>
<style lang="stylus">
.chat-export {
display flex
justify-content center
.chat-box {
max-width 800px;
// 变量定义
--content-font-size: 16px;
--content-color: #c1c1c1;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 0 0 50px 0;
.title {
text-align center
}
.chat-line {
font-size: 14px;
display: flex;
align-items: flex-start;
.chat-line-inner {
.content {
padding-top: 0
font-size 16px;
p:first-child {
margin-top 0
}
}
}
}
.chat-line-reply {
padding-top: 1.5rem;
.chat-line-inner {
display flex
.copy-reply {
display none
}
.bar-item {
background-color: #f7f7f8;
color: #888;
padding: 3px 5px;
margin-right: 10px;
border-radius: 5px;
}
.chat-icon {
margin-right: 20px
img {
width 30px
height 30px
border-radius: 10px;
padding: 1px
}
}
}
}
}
}
</style>

View File

@@ -138,6 +138,10 @@
新建会话
</el-button>
<el-button type="success" @click="exportChat" plain>
<i class="iconfont icon-export"></i>
<span>导出会话</span>
</el-button>
</div>
</div>
@@ -145,7 +149,10 @@
<div>
<div id="container">
<div class="chat-box" id="chat-box" :style="{height: chatBoxHeight+'px'}">
<div v-for="item in chatData" :key="item.id">
<div v-if="showHello">
<welcome @send="autofillPrompt"/>
</div>
<div v-for="item in chatData" :key="item.id" v-else>
<chat-prompt
v-if="item.type==='prompt'"
:icon="item.icon"
@@ -159,6 +166,12 @@
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:content="item.content"/>
<chat-mid-journey v-else-if="item.type==='mj'"
:content="item.content"
:icon="item.icon"
@disable-input="disableInput(true)"
@enable-input="enableInput"
:created-at="dateFormat(item['created_at'])"/>
</div>
</div><!-- end chat box -->
@@ -183,7 +196,7 @@
<div class="input-box">
<div class="input-container">
<el-input
ref="text-input"
ref="textInput"
v-model="prompt"
v-on:keydown="inputKeyDown"
autofocus
@@ -228,7 +241,7 @@
API KEY 也全部用完了因此我们准备开启众筹模式只需要打赏9.9就可以兑换 100 次对话以此来覆盖我们的 OpenAI
账单和服务器的费用</p>
</el-alert>
<p>
<p style="text-align: center">
<el-image :src="rewardImg"/>
</p>
</el-dialog>
@@ -269,12 +282,14 @@ import PasswordDialog from "@/components/PasswordDialog.vue";
import {checkSession} from "@/action/session";
import BindMobile from "@/components/BindMobile.vue";
import RewardVerify from "@/components/RewardVerify.vue";
import Welcome from "@/components/Welcome.vue";
import ChatMidJourney from "@/components/ChatMidJourney.vue";
const title = ref('ChatGPT-智能助手');
const logo = 'images/logo.png';
const rewardImg = ref('images/reward.png')
const models = ref([])
const model = ref('gpt-3.5-turbo')
const model = ref('gpt-3.5-turbo-16k')
const chatData = ref([]);
const allChats = ref([]); // 会话列表
const chatList = ref(allChats.value);
@@ -294,6 +309,8 @@ const showBindMobileDialog = ref(false);
const showRewardDialog = ref(false);
const showRewardVerifyDialog = ref(false);
const isLogin = ref(false)
const showHello = ref(true)
const textInput = ref(null)
if (isMobile()) {
router.replace("/mobile")
@@ -304,6 +321,9 @@ onMounted(() => {
checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
if (user.chat_config?.model !== '') {
model.value = user.chat_config.model
}
// 加载角色列表
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
roles.value = res.data;
@@ -499,7 +519,7 @@ const connect = function (chat_id, role_id) {
_socket.addEventListener('open', () => {
chatData.value = []; // 初始化聊天数据
previousText.value = '';
canSend.value = true;
enableInput()
activelyClose.value = false;
if (isNewChat) { // 加载打招呼信息
@@ -514,6 +534,7 @@ const connect = function (chat_id, role_id) {
} else { // 加载聊天记录
loadChatHistory(chat_id);
}
});
_socket.addEventListener('message', event => {
@@ -529,12 +550,39 @@ const connect = function (chat_id, role_id) {
icon: _role['icon'],
content: ""
});
} else if (data.type === 'end') { // 消息接收完毕
canSend.value = true;
showReGenerate.value = true;
showStopGenerate.value = false;
lineBuffer.value = ''; // 清空缓冲
} else if (data.type === "mj") {
disableInput(true)
const content = data.content;
const md = require('markdown-it')({breaks: true});
content.html = md.render(content.content)
let key = content.key
// fixed bug: 执行 Upscale 和 Variation 操作的时候覆盖之前的绘画
if (content.status === "Finished") {
key = randString(32)
enableInput()
}
// console.log(content)
// check if the message is in chatData
let flag = false
for (let i = 0; i < chatData.value.length; i++) {
if (chatData.value[i].id === content.key) {
console.log(chatData.value[i])
flag = true
chatData.value[i].content = content
chatData.value[i].id = key
break
}
}
if (flag === false) {
chatData.value.push({
type: "mj",
id: key,
icon: "/images/avatar/mid_journey.png",
content: content
});
}
} else if (data.type === 'end') { // 消息接收完毕
// 追加当前会话到会话列表
if (isNewChat && newChatItem.value !== null) {
newChatItem.value['title'] = previousText.value;
@@ -544,6 +592,9 @@ const connect = function (chat_id, role_id) {
newChatItem.value = null; // 只追加一次
}
enableInput()
lineBuffer.value = ''; // 清空缓冲
// 获取 token
const reply = chatData.value[chatData.value.length - 1]
httpGet(`/api/chat/tokens?text=${reply.orgContent}&model=${model.value}`).then(res => {
@@ -585,7 +636,7 @@ const connect = function (chat_id, role_id) {
return;
}
// 停止发送消息
canSend.value = true;
disableInput(true)
socket.value = null;
loading.value = true;
checkSession().then(() => {
@@ -603,6 +654,18 @@ const connect = function (chat_id, role_id) {
socket.value = _socket;
}
const disableInput = (force) => {
canSend.value = false;
showReGenerate.value = false;
showStopGenerate.value = !force;
}
const enableInput = () => {
canSend.value = true;
showReGenerate.value = previousText.value !== "";
showStopGenerate.value = false;
}
// 登录输入框输入事件处理
const inputKeyDown = function (e) {
if (e.keyCode === 13) {
@@ -614,6 +677,13 @@ const inputKeyDown = function (e) {
sendMessage();
}
}
// 自动填充 prompt
const autofillPrompt = (text) => {
prompt.value = text
textInput.value.focus()
// sendMessage()
}
// 发送消息
const sendMessage = function () {
if (canSend.value === false) {
@@ -638,9 +708,8 @@ const sendMessage = function () {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
})
canSend.value = false;
showStopGenerate.value = true;
showReGenerate.value = false;
showHello.value = false
disableInput(false)
socket.value.send(prompt.value);
previousText.value = prompt.value;
prompt.value = '';
@@ -694,6 +763,7 @@ const loadChatHistory = function (chatId) {
loading.value = false
return
}
showHello.value = false
const md = require('markdown-it')({breaks: true});
// md.use(require('markdown-it-copy')); // 代码复制功能
@@ -701,6 +771,11 @@ const loadChatHistory = function (chatId) {
if (data[i].type === "prompt") {
chatData.value.push(data[i]);
continue;
} else if (data[i].type === "mj") {
data[i].content = JSON.parse(data[i].content)
data[i].content.html = md.render(data[i].content?.content)
chatData.value.push(data[i]);
continue;
}
data[i].orgContent = data[i].content;
@@ -726,18 +801,13 @@ const loadChatHistory = function (chatId) {
const stopGenerate = function () {
showStopGenerate.value = false;
httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
canSend.value = true;
if (previousText.value !== '') {
showReGenerate.value = true;
}
enableInput()
})
}
// 重新生成
const reGenerate = function () {
canSend.value = false;
showStopGenerate.value = true;
showReGenerate.value = false;
disableInput(false)
const text = '重新生成上述问题的答案:' + previousText.value;
// 追加消息
chatData.value.push({
@@ -769,6 +839,17 @@ const updateUser = function (data) {
loginUser.value.avatar = data.avatar;
loginUser.value.nickname = data.nickname;
}
// 导出会话
const exportChat = () => {
if (!activeChat.value['chat_id']) {
return ElMessage.error("请先选中一个会话")
}
const url = location.protocol + '//' + location.host + '/chat/export?chat_id=' + activeChat.value['chat_id']
// console.log(url)
window.open(url, '_blank');
}
</script>
<style scoped lang="stylus">
@@ -776,6 +857,7 @@ const updateUser = function (data) {
$sideBgColor = #252526;
$borderColor = #4676d0;
#app {
height: 100%;
.common-layout {
@@ -978,6 +1060,10 @@ $borderColor = #4676d0;
}
}
}
.iconfont {
margin-right 5px;
}
}
.right-box {
@@ -1135,4 +1221,4 @@ $borderColor = #4676d0;
}
}
}
</style>
</style>

View File

@@ -9,7 +9,7 @@
<div class="header">{{ title }}</div>
<div class="content">
<div class="block">
<el-input placeholder="手机号/邮箱" size="large" v-model="username" autocomplete="off">
<el-input placeholder="手机号/用户名" size="large" v-model="username" autocomplete="off">
<template #prefix>
<el-icon>
<UserFilled/>
@@ -52,7 +52,6 @@ import {onMounted, ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {setLoginUser} from "@/store/session";
import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
import {isMobile} from "@/utils/libs";
@@ -78,8 +77,7 @@ const login = function () {
return ElMessage.error('请输入密码');
}
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
setLoginUser(res.data)
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then(() => {
if (isMobile()) {
router.push('/mobile')
} else {

View File

@@ -1,101 +1,111 @@
<template>
<div>
<div class="bg"></div>
<div class="main">
<div class="contain">
<div class="logo">
<el-image src="images/logo.png" fit="cover"/>
</div>
<div class="register-page">
<div class="page-inner">
<div class="contain" v-if="enableRegister">
<div class="logo">
<el-image src="images/logo.png" fit="cover"/>
</div>
<div class="header">{{ title }}</div>
<div class="content">
<el-form :model="formData" label-width="120px" ref="formRef">
<div class="block">
<el-input placeholder="请输入用户名(4-30位)"
size="large" maxlength="30"
v-model="formData.username"
autocomplete="off">
<template #prefix>
<el-icon>
<UserFilled/>
</el-icon>
</template>
</el-input>
</div>
<div class="header">{{ title }}</div>
<div class="content">
<el-form :model="formData" label-width="120px" ref="formRef">
<div class="block">
<el-input placeholder="请输入用户名(4-30位)"
size="large" maxlength="30"
v-model="formData.username"
autocomplete="off">
<template #prefix>
<el-icon>
<UserFilled/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="请输入密码(8-16位)"
maxlength="16" size="large"
v-model="formData.password" show-password
autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="请输入密码(8-16位)"
maxlength="16" size="large"
v-model="formData.password" show-password
autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="重复密码(8-16位)"
size="large" maxlength="16" v-model="formData.repass" show-password
autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="重复密码(8-16位)"
size="large" maxlength="16" v-model="formData.repass" show-password
autocomplete="off">
<template #prefix>
<el-icon>
<Lock/>
</el-icon>
</template>
</el-input>
</div>
<div class="block" v-if="enableMsg">
<el-input placeholder="手机号码"
size="large" maxlength="11"
v-model="formData.mobile"
autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
</el-icon>
</template>
</el-input>
</div>
<div class="block">
<el-input placeholder="手机号码"
size="large" maxlength="11"
v-model="formData.mobile"
autocomplete="off">
<template #prefix>
<el-icon>
<Iphone/>
</el-icon>
</template>
</el-input>
</div>
<div class="block" v-if="enableMsg">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="手机验证码"
size="large" maxlength="30"
v-model.number="formData.code"
autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :mobile="formData.mobile"/>
</el-col>
<div class="block" v-if="enableMsg">
<el-row :gutter="10">
<el-col :span="12">
<el-input placeholder="手机验证码"
size="large" maxlength="30"
v-model.number="formData.code"
autocomplete="off">
<template #prefix>
<el-icon>
<Checked/>
</el-icon>
</template>
</el-input>
</el-col>
<el-col :span="12">
<send-msg size="large" :mobile="formData.mobile"/>
</el-col>
</el-row>
</div>
<el-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
</el-row>
</div>
<el-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
</el-row>
<el-row class="text-line">
已经有账号
<el-link type="primary" @click="router.push('login')">登录</el-link>
</el-row>
</el-form>
<el-row class="text-line">
已经有账号
<el-link type="primary" @click="router.push('login')">登录</el-link>
</el-row>
</el-form>
</div>
</div>
</div>
<footer class="footer">
<footer-bar/>
</footer>
<div class="tip-result" v-else>
<el-result icon="error" title="注册功能已关闭">
<template #sub-title>
<p>抱歉系统已关闭注册功能请联系管理员添加账号</p>
</template>
</el-result>
</div>
<footer class="footer">
<footer-bar/>
</footer>
</div>
</div>
</div>
</template>
@@ -121,10 +131,12 @@ const formData = ref({
})
const formRef = ref(null)
const enableMsg = ref(false)
const enableRegister = ref(true)
httpGet('/api/sms/status').then(res => {
if (res.data === true) {
enableMsg.value = true
if (res.data) {
enableMsg.value = res.data['enabled_msg_service']
enableRegister.value = res.data['enabled_register']
}
})
@@ -139,6 +151,9 @@ const register = function () {
return ElMessage.error('两次输入密码不一致');
}
if (formData.value.code === '') {
formData.value.code = 0
}
httpPost('/api/user/register', formData.value).then(() => {
ElMessage.success({"message": "注册成功,即将跳转到登录页...", onClose: () => router.push("login")})
}).catch((e) => {
@@ -163,76 +178,110 @@ const register = function () {
//filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
}
.main {
.contain {
position fixed
left 50%
top 40%
width 90%
max-width 400px
transform translate(-50%, -50%)
padding 20px 40px;
color #ffffff
border-radius 10px;
background rgba(255, 255, 255, 0.3)
.register-page {
display flex
justify-content center
.logo {
text-align center
.page-inner {
max-width 450px
min-width 360px
height 100vh
display flex
justify-content center
align-items center
.el-image {
width 120px;
.contain {
padding 0 40px 20px 40px;
width 100%
color #ffffff
border-radius 10px;
z-index 10
background-color rgba(255, 255, 255, 0.3)
.logo {
text-align center
.el-image {
width 120px;
}
}
}
.header {
width 100%
margin-bottom 24px
font-size 24px
color $white_v1
letter-space 2px
text-align center
}
.header {
width 100%
margin-bottom 24px
font-size 24px
color $white_v1
letter-space 2px
text-align center
}
.content {
width 100%
height: auto
border-radius 3px
.content {
width 100%
height: auto
border-radius 3px
.block {
margin-bottom 16px
.block {
margin-bottom 16px
.el-input__inner {
border 1px solid $gray-v6 !important
.el-input__inner {
border 1px solid $gray-v6 !important
.el-icon-user, .el-icon-lock {
font-size 20px
.el-icon-user, .el-icon-lock {
font-size 20px
}
}
}
}
.btn-row {
padding-top 10px;
.btn-row {
padding-top 10px;
.login-btn {
width 100%
font-size 16px
letter-spacing 2px
.login-btn {
width 100%
font-size 16px
letter-spacing 2px
}
}
.text-line {
justify-content center
padding-top 10px;
font-size 14px;
}
}
}
.text-line {
justify-content center
padding-top 10px;
font-size 14px;
.tip-result {
z-index 10
}
.footer {
color #ffffff;
.container {
padding 20px;
}
}
}
.footer {
color #ffffff;
}
</style>
.container {
padding 20px;
<style lang="stylus">
.register-page {
.el-result {
border-radius 10px;
background-color rgba(14, 25, 30, 0.6)
border 1px solid #666
.el-result__title p {
color #ffffff
}
.el-result__subtitle p {
color #c1c1c1
}
}
}

View File

@@ -132,7 +132,7 @@ const save = function () {
const remove = function (row) {
httpGet('/api/admin/apikey/remove?id=' + row.id).then(() => {
ElMessage.success("删除成功")
item.value = removeArrayItem(items.value, row, (v1, v2) => {
items.value = removeArrayItem(items.value, row, (v1, v2) => {
return v1.id === v2.id
})
}).catch((e) => {

View File

@@ -0,0 +1,140 @@
<template>
<div class="dashboard" v-loading="loading">
<el-row class="mgb20" :gutter="20">
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-1">
<el-icon class="grid-con-icon">
<User/>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.users }}</div>
<div>今日新增用户</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-2">
<el-icon class="grid-con-icon">
<ChatDotRound/>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.chats }}</div>
<div>今日新增对话</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-3">
<el-icon class="grid-con-icon">
<TrendCharts/>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.tokens }}</div>
<div>今日消耗 Tokens</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{ padding: '0px' }">
<div class="grid-content grid-con-3">
<el-icon class="grid-con-icon">
<i class="iconfont icon-reward"></i>
</el-icon>
<div class="grid-cont-right">
<div class="grid-num">{{ stats.rewards }}</div>
<div>今日入账</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import {ref} from 'vue';
import {ChatDotRound, TrendCharts, User} from "@element-plus/icons-vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
const stats = ref({users: 0, chats: 0, tokens: 0, rewards: 0})
const loading = ref(true)
httpGet('/api/admin/dashboard/stats').then((res) => {
stats.value.users = res.data.users
stats.value.chats = res.data.chats
stats.value.tokens = res.data.tokens
stats.value.rewards = res.data.rewards
loading.value = false
}).catch((e) => {
ElMessage.error("获取统计数据失败:" + e.message)
})
</script>
<style scoped lang="stylus">
.dashboard {
.grid-content {
display: flex;
align-items: center;
height: 100px;
}
.grid-cont-right {
flex: 1;
text-align: center;
font-size: 14px;
color: #999;
}
.grid-num {
font-size: 30px;
font-weight: bold;
}
.grid-con-icon {
font-size: 50px;
width: 100px;
height: 100px;
text-align: center;
line-height: 100px;
color: #fff;
.iconfont {
font-size: 50px;
}
}
.grid-con-1 .grid-con-icon {
background: rgb(45, 140, 240);
}
.grid-con-1 .grid-num {
color: rgb(45, 140, 240);
}
.grid-con-2 .grid-con-icon {
background: rgb(100, 213, 114);
}
.grid-con-2 .grid-num {
color: rgb(100, 213, 114);
}
.grid-con-3 .grid-con-icon {
background: rgb(242, 94, 67);
}
.grid-con-3 .grid-num {
color: rgb(242, 94, 67);
}
}
</style>

View File

@@ -1,8 +1,8 @@
<template>
<div class="admin-home" v-if="isLogin">
<admin-header/>
<admin-sidebar/>
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
<admin-header/>
<admin-tags/>
<div class="content">
<router-view v-slot="{ Component }">

View File

@@ -1,11 +1,7 @@
<template>
<div class="admin-login">
<div class="bg"></div>
<div class="main">
<div class="contain">
<div class="logo">
<el-image src="../images/logo.png" fit="cover"/>
</div>
<div class="header">{{ title }}</div>
<div class="content">
<div class="block">
@@ -48,12 +44,11 @@ import {onMounted, ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {setLoginUser} from "@/store/session";
import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
const router = useRouter();
const title = ref('ChatGPT-PLUS 控制台登录');
const title = ref('ChatGPT Plus Admin');
const username = ref(process.env.VUE_APP_ADMIN_USER);
const password = ref(process.env.VUE_APP_ADMIN_PASS);
@@ -69,12 +64,11 @@ const login = function () {
if (username.value === '') {
return ElMessage.error('请输入用户名');
}
if (password.value.trim() === '') {
if (password.value === '') {
return ElMessage.error('请输入密码');
}
httpPost('/api/admin/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
setLoginUser(res.data)
router.push("/admin")
}).catch((e) => {
ElMessage.error('登录失败,' + e.message)
@@ -86,51 +80,29 @@ const login = function () {
<style lang="stylus" scoped>
.admin-login {
.bg {
position fixed
left 0
right 0
top 0
bottom 0
background-color #313237
background-image url("~@/assets/img/admin-login-bg.jpg")
background-size cover
background-position center
background-repeat no-repeat
filter: blur(10px); /* 调整模糊程度,可以根据需要修改值 */
}
display flex
justify-content center
width: 100%
background #2D3A4B
.main {
width 400px;
display flex
justify-content center
align-items center
height 100vh
.contain {
position fixed
left 50%
top 40%
width 90%
max-width 400px;
transform translate(-50%, -50%)
width 100%
padding 20px 40px;
color #ffffff
border-radius 10px;
background rgba(255, 255, 255, 0.3)
.logo {
text-align center
.el-image {
width 120px;
.el-image__inner {
height 100%
}
}
}
.header {
width 100%
margin-bottom 24px
margin-bottom 20px
font-size 24px
color $white_v1
letter-space 2px
text-align center
}
@@ -169,6 +141,7 @@ const login = function () {
}
}
.footer {
color #ffffff;

View File

@@ -46,7 +46,6 @@ onMounted(() => {
// 获取数据
const fetchList = function (_page, _pageSize) {
console.log(_page, _pageSize)
httpGet(`/api/admin/user/loginLog?page=${_page}&page_size=${_pageSize}`).then((res) => {
if (res.data) {
items.value = res.data.items

View File

@@ -6,6 +6,7 @@
<el-table-column prop="username" label="用户名"/>
<el-table-column prop="tx_id" label="转账单号"/>
<el-table-column prop="amount" label="转账金额"/>
<el-table-column prop="remark" label="备注"/>
<el-table-column label="转账时间">
<template #default="scope">
@@ -27,11 +28,10 @@
</template>
<script setup>
import {reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ref} from "vue";
import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs";
import {Plus} from "@element-plus/icons-vue";
import {dateFormat} from "@/utils/libs";
// 变量定义
const items = ref([])

View File

@@ -1,5 +1,5 @@
<template>
<div class="container role-list">
<div class="container role-list" v-loading="loading">
<div class="handle-box">
<el-button type="primary" :icon="Plus" @click="addRole">新增</el-button>
</div>
@@ -36,7 +36,7 @@
</template>
</el-table-column>
<el-table-column label="打招呼信息" prop="hello_msg"/>
<el-table-column label="操作" width="180" align="right">
<el-table-column label="操作" width="150" align="right">
<template #default="scope">
<el-button size="small" type="primary" @click="rowEdit(scope.$index, scope.row)">编辑</el-button>
<el-popconfirm title="确定要删除当前角色吗?" @confirm="removeRole(scope.row)">
@@ -158,6 +158,7 @@ const sortedTableData = ref([])
const role = ref({context: []})
const formRef = ref(null)
const editRow = ref({})
const loading = ref(true)
const rules = reactive({
name: [{required: true, message: '请输入用户名', trigger: 'blur',}],
@@ -174,6 +175,7 @@ const rules = reactive({
httpGet('/api/admin/role/list').then((res) => {
tableData.value = res.data
sortedTableData.value = copyObj(tableData.value)
loading.value = false
}).catch(() => {
ElMessage.error("获取聊天角色失败");
})

View File

@@ -9,9 +9,18 @@
<el-form-item label="控制台标题" prop="admin_title">
<el-input v-model="system['admin_title']"/>
</el-form-item>
<el-form-item label="注册赠送次数" prop="init_calls">
<el-form-item label="赠送对话次数" prop="init_calls">
<el-input v-model.number="system['user_init_calls']" placeholder="新用户注册赠送对话次数"/>
</el-form-item>
<el-form-item label="赠送绘图次数" prop="init_calls">
<el-input v-model.number="system['init_img_calls']" placeholder="新用户注册赠送绘图次数"/>
</el-form-item>
<el-form-item label="短信验证服务" prop="enabled_msg_service">
<el-switch v-model="system['enabled_msg_service']"/>
</el-form-item>
<el-form-item label="开放用户注册" prop="enabled_register">
<el-switch v-model="system['enabled_register']"/>
</el-form-item>
<el-alert type="info" show-icon :closable="false">
<p>在这里维护前端聊天页面可用的 GPT 模型列表</p>
</el-alert>
@@ -82,7 +91,7 @@
</template>
<script setup>
import {nextTick, onMounted, reactive, ref} from "vue";
import {onMounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox} from "element-plus";
import {Plus} from "@element-plus/icons-vue";
@@ -112,13 +121,11 @@ onMounted(() => {
// 加载聊天配置
httpGet('/api/admin/config/get?key=chat').then(res => {
chat.value = res.data
loading.value = false
}).catch(e => {
ElMessage.error("加载聊天配置失败: " + e.message)
})
nextTick(() => {
loading.value = false
})
})
const rules = reactive({

View File

@@ -1,5 +1,13 @@
<template>
<div class="container user-list" v-loading="loading">
<div class="handle-box">
<el-input v-model="query.username" placeholder="用户名" class="handle-input mr10"></el-input>
<el-input v-model="query.mobile" placeholder="手机号码" class="handle-input mr10"></el-input>
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button type="success" :icon="Plus" @click="addUser">新增用户</el-button>
</div>
<el-row>
<el-table :data="users.items" border class="table" :row-key="row => row.id"
@selection-change="handleSelectionChange">
@@ -7,7 +15,8 @@
<el-table-column prop="username" label="用户名"/>
<el-table-column prop="mobile" label="手机号"/>
<el-table-column prop="nickname" label="昵称"/>
<el-table-column prop="calls" label="提问次数" width="100"/>
<el-table-column prop="calls" label="对话次数" width="100"/>
<el-table-column prop="img_calls" label="绘图次数" width="100"/>
<el-table-column label="状态" width="80">
<template #default="scope">
<el-tag v-if="scope.row.status" type="success">正常</el-tag>
@@ -27,10 +36,13 @@
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<el-table-column fixed="right" label="操作" width="200">
<template #default="scope">
<el-button size="small" type="primary" @click="userEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="removeUser(scope.row)">删除</el-button>
<el-button-group class="ml-4">
<el-button size="small" type="primary" @click="userEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="removeUser(scope.row)">删除</el-button>
<el-button size="small" type="success" @click="resetPass(scope.row)">重置密码</el-button>
</el-button-group>
</template>
</el-table-column>
</el-table>
@@ -50,17 +62,30 @@
<el-dialog
v-model="showUserEditDialog"
title="编辑用户"
:title="title"
width="50%"
>
<el-form :model="user" label-width="100px" ref="userEditFormRef" :rules="rules">
<el-form-item label="昵称" prop="nickname">
<el-form-item v-if="add" label="用户名" prop="username">
<el-input v-model="user.username" autocomplete="off"/>
</el-form-item>
<el-form-item v-else label="昵称" prop="nickname">
<el-input v-model="user.nickname" autocomplete="off"/>
</el-form-item>
<el-form-item label="提问次数" prop="calls">
<el-form-item v-if="add" label="密码" prop="password">
<el-input v-model="user.password" autocomplete="off"/>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="user.mobile" autocomplete="off"/>
</el-form-item>
<el-form-item label="对话次数" prop="calls">
<el-input v-model.number="user.calls" autocomplete="off" placeholder="0"/>
</el-form-item>
<el-form-item label="绘图次数" prop="img_calls">
<el-input v-model.number="user['img_calls']" autocomplete="off" placeholder="0"/>
</el-form-item>
<el-form-item label="有效期" prop="expired_time">
<el-date-picker
@@ -97,27 +122,59 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="showUserEditDialog = false">取消</el-button>
<el-button type="primary" @click="updateUser">提交</el-button>
<el-button type="primary" @click="saveUser">提交</el-button>
</span>
</template>
</el-dialog>
<el-dialog
v-model="showResetPassDialog"
title="重置密码"
width="50%"
>
<el-form label-width="100px" ref="userEditFormRef">
<el-form-item label="用户名">
<el-input v-model="pass.username" autocomplete="off" readonly disabled/>
</el-form-item>
<el-form-item label="新密码">
<el-input v-model="pass.password" autocomplete="off"/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="doResetPass">提交</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {nextTick, onMounted, reactive, ref} from "vue";
import {onMounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox} from "element-plus";
import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs";
import {Plus, Search} from "@element-plus/icons-vue";
// 变量定义
const users = ref({page: 1, page_size: 15})
const users = ref({page: 1, page_size: 15, items: []})
const query = ref({username: '', mobile: '', page: 1, page_size: 15})
const title = ref('添加用户')
const add = ref(true)
const user = ref({chat_roles: []})
const pass = ref({username: '', password: '', id: 0})
const roles = ref([])
const showUserEditDialog = ref(false)
const showResetPassDialog = ref(false)
const rules = reactive({
username: [{required: true, message: '请输入用户名', trigger: 'change',}],
nickname: [{required: true, message: '请输入昵称', trigger: 'change',}],
password: [{required: true, message: '请输入密码', trigger: 'change',}],
mobile: [{required: true, message: '请输入手机号码', trigger: 'change',}],
calls: [
{required: true, message: '请输入提问次数'},
{type: 'number', message: '请输入有效数字'},
@@ -136,14 +193,12 @@ onMounted(() => {
}).catch(() => {
ElMessage.error("获取聊天角色失败");
})
nextTick(() => {
loading.value = false
})
})
const fetchUserList = function (page, pageSize) {
httpGet('/api/admin/user/list', {page: page, page_size: pageSize}).then((res) => {
query.value.page = page
query.value.page_size = pageSize
httpGet('/api/admin/user/list', query.value).then((res) => {
if (res.data) {
// 初始化数据
const arr = res.data.items;
@@ -155,11 +210,16 @@ const fetchUserList = function (page, pageSize) {
users.value.page = res.data.page
user.value.page_size = res.data.page_size
}
loading.value = false
}).catch(() => {
ElMessage.error('加载用户列表失败')
})
}
const handleSearch = () => {
fetchUserList(users.value.page, users.value.page_size)
}
// 删除用户
const removeUser = function (user) {
ElMessageBox.confirm(
@@ -170,35 +230,44 @@ const removeUser = function (user) {
cancelButtonText: '取消',
type: 'warning',
}
)
.then(() => {
httpGet('/api/admin/user/remove', {id: user.id}).then(() => {
ElMessage.success('操作成功!')
users.value.items = removeArrayItem(users.value.items, user, function (v1, v2) {
return v1.id === v2.id
})
}).catch((e) => {
ElMessage.error('操作失败,' + e.message)
})
})
.catch(() => {
ElMessage.info('操作被取消')
).then(() => {
httpGet('/api/admin/user/remove', {id: user.id}).then(() => {
ElMessage.success('操作成功!')
users.value.items = removeArrayItem(users.value.items, user, function (v1, v2) {
return v1.id === v2.id
})
}).catch((e) => {
ElMessage.error('操作失败,' + e.message)
})
}).catch(() => {
ElMessage.info('操作被取消')
})
}
const userEdit = function (_user) {
user.value = _user
const userEdit = function (row) {
user.value = row
title.value = '编辑用户'
showUserEditDialog.value = true
add.value = false
}
// 更新口令
const updateUser = function () {
const addUser = () => {
user.value = {}
title.value = '添加用户'
showUserEditDialog.value = true
add.value = true
}
const saveUser = function () {
userEditFormRef.value.validate((valid) => {
if (valid) {
showUserEditDialog.value = false
httpPost('/api/admin/user/update', user.value).then(() => {
httpPost('/api/admin/user/save', user.value).then((res) => {
ElMessage.success('操作成功!')
if (add.value) {
users.value.items.push(res.data)
}
}).catch((e) => {
ElMessage.error('操作失败,' + e.message)
})
@@ -212,11 +281,38 @@ const handleSelectionChange = function (rows) {
// TODO: 批量删除操作
console.log(rows)
}
const resetPass = (row) => {
showResetPassDialog.value = true
pass.value.id = row.id
pass.value.username = row.username
}
const doResetPass = () => {
httpPost('/api/admin/user/resetPass', pass.value).then(() => {
ElMessage.success('操作成功!')
showResetPassDialog.value = false
}).catch((e) => {
ElMessage.error('操作失败,' + e.message)
})
}
</script>
<style lang="stylus" scoped>
.user-list {
.handle-box {
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.table {
width 100%
}
.opt-box {
padding-bottom: 10px;

View File

@@ -1,25 +0,0 @@
<template>
<div class="welcome">
<h1>ChatGPT-PLUS 控制台</h1>
</div>
</template>
<script setup></script>
<style lang="stylus" scoped>
.welcome {
display: flex;
justify-content: center;
align-items: center;
color: #202020;
background-color: #282c34;
height 100%;
h1 {
font-size: 300%;
font-weight: bold;
letter-spacing: 0.1em;
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
}
}
</style>

View File

@@ -11,8 +11,8 @@
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
<el-button type="primary" :icon="Plus">新增</el-button>
</div>
<el-table :data="tableData" border class="table" ref="multipleTable" header-cell-class-name="table-header">
<el-table-column prop="id" label="ID" width="55" align="center"></el-table-column>
<el-table :data="tableData" border class="table" style="width: 100%" header-cell-class-name="table-header">
<el-table-column prop="id" fixed label="ID" width="55" align="center"></el-table-column>
<el-table-column prop="name" label="姓名"></el-table-column>
<el-table-column label="头像(查看大图)" align="center">
<template #default="scope">
@@ -38,7 +38,7 @@
</el-table-column>
<el-table-column prop="date" label="注册时间"></el-table-column>
<el-table-column label="操作" width="220" align="center">
<el-table-column label="操作" fixed="right" align="center">
<template #default="scope">
<el-button text :icon="Edit" @click="handleEdit(scope.$index, scope.row)">
编辑