Compare commits

..

39 Commits

Author SHA1 Message Date
RockYang
da88a501ad opt: 优化验证码发送逻辑,加入防刷验证 2023-07-04 17:15:02 +08:00
RockYang
b9885e8de4 Merge branch 'main' into feat-sms 2023-07-04 10:55:20 +08:00
RockYang
22efe81080 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-07-04 10:54:35 +08:00
RockYang
2926717aef docs: update README, 增加免责申明 2023-07-04 10:52:52 +08:00
RockYang
a49d54d66c feat: 短信验证码功能已完成,手机端同步实现。 2023-07-03 15:18:15 +08:00
RockYang
ce0267e25b opt: 将短信发送按钮封装成组件 2023-07-03 06:55:15 +08:00
RockYang
9088d22a66 feat: 注册短信验证码验证功能已经开启 2023-07-02 20:51:13 +08:00
RockYang
1ff32d5d0a Merge branch 'main' into prod 2023-07-02 00:04:59 +08:00
RockYang
adf6916598 fix: 修复 nodejs apple M1 跨平台打包,运行报错 exec format error 2023-07-02 00:04:12 +08:00
RockYang
31c14bf748 feat: 增加用户 token 消耗统计功能 2023-07-01 23:29:24 +08:00
RockYang
5395385d1e Merge branch 'main' into prod 2023-07-01 10:24:08 +08:00
RockYang
0035da548b Merge pull request #18 from ly307787186/main
Update Setting.vue
2023-07-01 10:22:01 +08:00
RockYang
9bceaade05 opt: remove chat role info from user login api's response 2023-06-30 18:52:43 +08:00
ly307787186
3194becdad Update Setting.vue
关掉aip-key 必需的验证
2023-06-30 13:20:30 +08:00
RockYang
6174b17c24 fix: fixed bug for Emoji can not insert to MySQL 2023-06-30 09:15:57 +08:00
RockYang
53fa4a20e9 chore: change ubuntu docker image with aliyun 2023-06-29 15:56:11 +08:00
RockYang
43c1de51f5 chore: add test code for fix role icon url of db 2023-06-29 08:58:19 +08:00
RockYang
7eb8c5ec35 fix: 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题 2023-06-28 20:01:44 +08:00
RockYang
296bf63196 fix: 修复 PC 端聊天界面滚动条问题 2023-06-28 18:16:28 +08:00
RockYang
6c65a21692 opt: 优化启动参数接收处理 2023-06-28 05:51:55 +08:00
RockYang
daf83cfc84 opt: 通过环境变量来传参,修正 docker compose 配置参数 2023-06-27 18:29:46 +08:00
RockYang
871f5d39e4 docs: 更新文档,新增移动端预览图 2023-06-27 14:28:00 +08:00
RockYang
3f91f37aff fix: 修复 markdown 换行符不解析的 Bug,修复新发布的模型 token 统计失败错误 2023-06-27 14:18:20 +08:00
RockYang
a08981f876 feat: vue-mobile => 完成用户信息修改功能,前后端都添加文件上传功能。 2023-06-27 12:11:55 +08:00
RockYang
5187a43543 feat: vue-mobile => 完成移动端聊天配置功能 2023-06-26 18:18:45 +08:00
RockYang
6a733de556 feat: vue-mobile => 完成会话聊天页面功能,增加主题切换功能 2023-06-26 16:39:00 +08:00
RockYang
b9e9eae93f feat: vue-mobile => 优化聊天记录拍版样式 2023-06-25 18:21:38 +08:00
RockYang
811f12135a feat: vue-mobile => 完善移动端聊天列表页功能 2023-06-25 17:01:04 +08:00
RockYang
2c172c0851 fixed: go-api => 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug 2023-06-25 11:34:55 +08:00
RockYang
399a16fa28 docs: 增加容器部署文档 2023-06-25 11:06:18 +08:00
RockYang
d971e95900 opt: 优化前端登录判断逻辑 2023-06-25 09:46:23 +08:00
RockYang
0b6940b121 feat: chat list page for mobile is ready 2023-06-25 06:53:22 +08:00
RockYang
ad0f96fcb1 feat: 完成移动端前段框架搭建 2023-06-24 11:45:26 +08:00
RockYang
063b5655f7 style: 调整聊天侧边栏样式 2023-06-23 18:31:50 +08:00
RockYang
d03ed6570b opt: 优化 docker-compse 构建脚本,修复后端路由 Bug 2023-06-23 18:04:16 +08:00
RockYang
1795a891ce chore: 添加 docker 镜像构建脚本 2023-06-23 07:08:16 +08:00
RockYang
05bdd81646 opt: 抽离 session 验证函数,修正前端路由覆盖 bug 2023-06-23 06:31:25 +08:00
RockYang
cba54be913 chore: 使用阿里云镜像仓库 2023-06-22 22:23:48 +08:00
RockYang
da0acfe851 docs: 增加 docker-compose 部署支持 2023-06-22 22:14:18 +08:00
82 changed files with 3553 additions and 977 deletions

View File

@@ -1,11 +0,0 @@
# FROM 表示设置要制作的镜像基于哪个镜像FROM指令必须是整个Dockerfile的第一个指令如果指定的镜像不存在默认会自动从Docker Hub上下载。
FROM centos:7
WORKDIR /usr/src/app
COPY src/bin/wechatGPT-amd64-linux /usr/src/app
# 容器对外暴露的端口号,这里和配置文件保持一致就可以
EXPOSE 5678
# 容器启动时执行的命令
CMD ["./wechatGPT-amd64-linux"]

118
README.md
View File

@@ -6,9 +6,6 @@
* 聊天体验跟 ChatGPT 官方版本完全一致。 * 聊天体验跟 ChatGPT 官方版本完全一致。
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 * 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。
**
## 功能截图 ## 功能截图
### 1.PC 端聊天界面 ### 1.PC 端聊天界面
@@ -33,10 +30,21 @@
![ChatGPT admin](docs/imgs/admin_user.png) ![ChatGPT admin](docs/imgs/admin_user.png)
### 6. 体验地址 ### 6. 移动端 Web 页面
> 体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/> ![Mobile chat list](/docs/imgs/mobile_chat_list.png)
> 涉及到数据隐私问题,没有提供共享账号,大家自己快速注册一个账号就可以免费体验 ![Mobile chat session](/docs/imgs/mobile_chat_session.png)
![Mobile chat setting](/docs/imgs/mobile_chat_setting.png)
### 7. 体验地址
> 免费体验地址:[https://www.chat-plus.net/chat](https://www.chat-plus.net/chat) <br/>
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
## 使用须知
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
## 项目介绍 ## 项目介绍
@@ -67,9 +75,9 @@ ChatGPT 的服务。
3. 创建会话的时候可以选择聊天角色和模型。 3. 创建会话的时候可以选择聊天角色和模型。
4. 新增聊天设置功能,用户可以导入自己的 API KEY 4. 新增聊天设置功能,用户可以导入自己的 API KEY
5. 保存聊天记录,支持聊天上下文。 5. 保存聊天记录,支持聊天上下文。
7. 重构后台管理模块,更友好,扩展性更好的后台管理系统。 6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
8. 引入 ip2region 组件记录用户的登录IP和地址。 7. 引入 ip2region 组件记录用户的登录IP和地址。
9. 支持会话搜索过滤。 8. 支持会话搜索过滤。
## 项目地址 ## 项目地址
@@ -84,32 +92,40 @@ ChatGPT 的服务。
* [ ] 接入语音和 TTS API支持语音聊天 * [ ] 接入语音和 TTS API支持语音聊天
* [ ] 开发手机 App 客户端 * [ ] 开发手机 App 客户端
## 安装部署 ## Docker 快速部署
由于本项目采用的是前后端分离的开发方式,所以部署也需要前后端分开部署。我这里以 linux 系统为例,演示一下部署过程: V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
### 1. 导入数据库 ### 1. 导入数据库
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
```shell
cd docker/mysql
# 创建 mysql 容器
docker-compose up -d
# 导入数据库
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql
```
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
```shell ```shell
# 下载数据库
wget wget https://github.com/yangjian102621/chatgpt-plus/releases/download/v3.0.0/chatgpt_plus.sql
# 连接数据库 # 连接数据库
mysql -u username -p password mysql -u username -p password
# 导入数据库 # 导入数据库
source chatgpt_plus.sql source database/chatgpt_plus.sql
``` ```
### 2. 修改配置文档 ### 2. 修改配置文档
先拷贝项目中的 `api/go/config.sample.toml` 配置文档,修改代理地址和管理员密码: 修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
```toml ```toml
Listen = "0.0.0.0:5678" Listen = "0.0.0.0:5678"
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理http://127.0.0.1:7777 ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理http://127.0.0.1:7777
#ProxyURL = "http://127.0.0.1:7777"
#ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了 #ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了
MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
[Session] [Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
Name = "CHAT_SESSION_ID" Name = "CHAT_SESSION_ID"
@@ -125,6 +141,53 @@ MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改 Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
``` ```
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
```shell
# 这里配置后端 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 地址
}
```
### 3. 启动应用
```shell
cd docker
docker-compose up -d
```
* 前端访问地址http://localhost:8080/chat
* 后台管理地址http://localhost:8080/admin
* 移动端地址http://localhost:8080/mobile
> 注意:你得访问后台管理系统 http://localhost:8080/admin
> 输入你前面配置文档中设置的管理员用户名和密码登录。
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
## 手动安装部署
由于本项目采用的是前后端分离的开发方式,所以部署也需要前后端分开部署。我这里以 linux 系统为例,演示一下部署过程:
### 1. 导入数据库
请参考容器部署的[导入数据](#1-导入数据库)。
### 2. 修改配置文档
先拷贝项目中的 `api/go/config.sample.toml` 配置文档,修改代理地址和管理员密码:
如何修改请参考[修改配置文档](#2-修改配置文档)
### 3. 运行后端程序 ### 3. 运行后端程序
你可以自己编译或者直接下载我打包好的后端程序运行。 你可以自己编译或者直接下载我打包好的后端程序运行。
@@ -181,7 +244,7 @@ server {
location / { location / {
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
# 这里配置后端 API 的转发 # 后端 API 的转发
location /api/ { location /api/ {
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_connect_timeout 300s; proxy_connect_timeout 300s;
@@ -192,20 +255,19 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
proxy_pass http://localhost:5678; proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
}
# 静态资源转发
location /static/ {
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
} }
} }
# 关闭静态资源的日志
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css)$ {
access_log off;
}
} }
``` ```
配置好之后重启 Nginx然后访问后台管理系统 [http://www.chatgpt.com/admin](http://www.chatgpt.com/admin), 配置好之后重启 Nginx然后 []
输入你前面配置文档中设置的管理员用户名和密码登录。
然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 即可。
![add API Key](docs/imgs/apikey_add.png) ![add API Key](docs/imgs/apikey_add.png)
@@ -292,7 +354,7 @@ make linux
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。 个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
**尤其是新版本的开发计划比较大,包括各种语言的后端 API 实现,本人精力有限,希望借助社区的力量来完成这些 API 的开发。** **尤其是新版本的开发计划比较大,包括各种语言的后端 API 实现,本人精力有限,希望借助社区的力量来完成这些 API 的开发。**
如果有兴趣的话,也可以加微信进入微信讨论群。 如果有兴趣的话,也可以加微信进入微信讨论群**添加好友时请注明来自Github!!!**
![微信名片](docs/imgs/wx.png) ![微信名片](docs/imgs/wx.png)

View File

@@ -1,27 +0,0 @@
# 前端
if ! command -v node > /dev/null; then
printf 'node is not installed.\n'
exit 1
fi
cd web
npm install
npm run build
cd ..
# 后端
if ! command -v go > /dev/null; then
printf 'go is not installed.\n'
exit 1
fi
cd src
go mod tidy
make linux
cd ..
# Docker
if ! command -v docker > /dev/null; then
printf 'docker is not installed.\n'
exit 1
fi
docker compose up -d

1
api/go/.gitignore vendored
View File

@@ -16,3 +16,4 @@ tmp
bin bin
data data
config.toml config.toml
static/upload

View File

@@ -1,5 +1,5 @@
Listen = "0.0.0.0:5678" Listen = "0.0.0.0:5678"
ProxyURL = ["YOUR_PROXY_URL"] ProxyURL = "YOUR_PROXY_URL"
MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local" MysqlDns = "mysql_user:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
[Session] [Session]

View File

@@ -8,6 +8,8 @@ import (
"context" "context"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"io" "io"
@@ -17,7 +19,8 @@ import (
) )
type AppServer struct { type AppServer struct {
AppConfig *types.AppConfig Debug bool
Config *types.AppConfig
Engine *gin.Engine Engine *gin.Engine
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
ChatConfig *types.ChatConfig // 聊天配置 ChatConfig *types.ChatConfig // 聊天配置
@@ -33,7 +36,8 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard gin.DefaultWriter = io.Discard
return &AppServer{ return &AppServer{
AppConfig: appConfig, Debug: false,
Config: appConfig,
Engine: gin.Default(), Engine: gin.Default(),
ChatContexts: types.NewLMap[string, []types.Message](), ChatContexts: types.NewLMap[string, []types.Message](),
ChatSession: types.NewLMap[string, types.ChatSession](), ChatSession: types.NewLMap[string, types.ChatSession](),
@@ -44,13 +48,16 @@ func NewServer(appConfig *types.AppConfig) *AppServer {
func (s *AppServer) Init(debug bool) { func (s *AppServer) Init(debug bool) {
if debug { // 调试模式允许跨域请求 API if debug { // 调试模式允许跨域请求 API
s.Debug = debug
logger.Info("Enabled debug mode") logger.Info("Enabled debug mode")
s.Engine.Use(corsMiddleware()) s.Engine.Use(corsMiddleware())
} }
s.Engine.Use(sessionMiddleware(s.AppConfig))
s.Engine.Use(sessionMiddleware(s.Config))
s.Engine.Use(authorizeMiddleware(s)) s.Engine.Use(authorizeMiddleware(s))
s.Engine.Use(errorHandler) s.Engine.Use(errorHandler)
//gob.Register(model.User{}) // 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir)
} }
func (s *AppServer) Run(db *gorm.DB) error { func (s *AppServer) Run(db *gorm.DB) error {
@@ -64,15 +71,15 @@ func (s *AppServer) Run(db *gorm.DB) error {
if err != nil { if err != nil {
return err return err
} }
logger.Infof("http://%s", s.AppConfig.Listen) logger.Infof("http://%s", s.Config.Listen)
return s.Engine.Run(s.AppConfig.Listen) return s.Engine.Run(s.Config.Listen)
} }
// 全局异常处理 // 全局异常处理
func errorHandler(c *gin.Context) { func errorHandler(c *gin.Context) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
logger.Error("panic: %v\n", r) logger.Errorf("Handler Panic: %v", r)
debug.PrintStack() debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg}) c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort() c.Abort()
@@ -85,7 +92,28 @@ func errorHandler(c *gin.Context) {
// 会话处理 // 会话处理
func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc { func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc {
// encrypt the cookie // encrypt the cookie
store := cookie.NewStore([]byte(config.Session.SecretKey)) var store sessions.Store
var err error
switch config.Session.Driver {
case types.SessionDriverMem:
store = memstore.NewStore([]byte(config.Session.SecretKey))
break
case types.SessionDriverRedis:
store, err = redis.NewStore(10, "tcp", config.Redis.Url(), config.Redis.Password, []byte(config.Session.SecretKey))
if err != nil {
logger.Fatal(err)
}
break
case types.SessionDriverCookie:
store = cookie.NewStore([]byte(config.Session.SecretKey))
break
default:
config.Session.Driver = types.SessionDriverCookie
store = cookie.NewStore([]byte(config.Session.SecretKey))
}
logger.Info("Session driver: ", config.Session.Driver)
store.Options(sessions.Options{ store.Options(sessions.Options{
Path: config.Session.Path, Path: config.Session.Path,
Domain: config.Session.Domain, Domain: config.Session.Domain,
@@ -136,6 +164,8 @@ func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
if c.Request.URL.Path == "/api/user/login" || if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/admin/login" || c.Request.URL.Path == "/api/admin/login" ||
c.Request.URL.Path == "/api/user/register" || c.Request.URL.Path == "/api/user/register" ||
strings.HasPrefix(c.Request.URL.Path, "/api/verify/") ||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
c.Request.URL.Path == "/api/admin/config/get" { c.Request.URL.Path == "/api/admin/config/get" {
c.Next() c.Next()
return return
@@ -154,7 +184,7 @@ func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
} }
session := sessions.Default(c) session := sessions.Default(c)
var value interface{} var value interface{}
if strings.Contains(c.Request.URL.Path, "/api/admin/") { if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
value = session.Get(types.SessionAdmin) value = session.Get(types.SessionAdmin)
} else { } else {
value = session.Get(types.SessionUser) value = session.Get(types.SessionUser)

View File

@@ -15,11 +15,15 @@ var logger = logger2.GetLogger()
func NewDefaultConfig() *types.AppConfig { func NewDefaultConfig() *types.AppConfig {
return &types.AppConfig{ return &types.AppConfig{
Listen: "0.0.0.0:5678", Listen: "0.0.0.0:5678",
ProxyURL: "", ProxyURL: "",
Manager: types.Manager{Username: "admin", Password: "admin123"}, Manager: types.Manager{Username: "admin", Password: "admin123"},
StaticDir: "./static",
StaticUrl: "http://localhost/5678/static",
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
AesEncryptKey: utils.RandString(24),
Session: types.Session{ Session: types.Session{
Driver: types.SessionDriverCookie,
SecretKey: utils.RandString(64), SecretKey: utils.RandString(64),
Name: "CHAT_PLUS_SESSION", Name: "CHAT_PLUS_SESSION",
Domain: "", Domain: "",

View File

@@ -1,16 +1,40 @@
package types package types
import ( import (
"fmt"
"net/http" "net/http"
) )
type AppConfig struct { type AppConfig struct {
Path string `toml:"-"` Path string `toml:"-"`
Listen string Listen string
Session Session Session Session
ProxyURL string ProxyURL string
MysqlDns string // mysql 连接地址 MysqlDns string // mysql 连接地址
Manager Manager // 后台管理员账户信息 Manager Manager // 后台管理员账户信息
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
AesEncryptKey string
SmsConfig AliYunSmsConfig // 短信发送配置
}
type AliYunSmsConfig struct {
AccessKey string
AccessSecret string
Product string
Domain string
}
type RedisConfig struct {
Host string
Port int
Password string
}
func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
} }
// Manager 管理员 // Manager 管理员
@@ -19,9 +43,18 @@ type Manager struct {
Password string `json:"password"` Password string `json:"password"`
} }
type SessionDriver string
const (
SessionDriverMem = SessionDriver("mem")
SessionDriverRedis = SessionDriver("redis")
SessionDriverCookie = SessionDriver("cookie")
)
// Session configs struct // Session configs struct
type Session struct { type Session struct {
SecretKey string // session encryption key Driver SessionDriver // session 存储驱动 mem|cookie|redis
SecretKey string // session encryption key
Name string Name string
Path string Path string
Domain string Domain string

View File

@@ -4,6 +4,7 @@ go 1.19
require ( require (
github.com/BurntSushi/toml v1.1.0 github.com/BurntSushi/toml v1.1.0
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.0 github.com/gin-gonic/gin v1.9.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
@@ -15,21 +16,27 @@ require (
) )
require ( require (
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
github.com/bytedance/sonic v1.8.0 // indirect github.com/bytedance/sonic v1.8.0 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.10.0 // indirect github.com/goccy/go-json v0.10.0 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/dig v1.16.1 // indirect go.uber.org/dig v1.16.1 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/net v0.7.0 // indirect golang.org/x/net v0.7.0 // indirect
golang.org/x/text v0.7.0 // indirect golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
@@ -38,7 +45,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/golang/snappy v0.0.1 // indirect github.com/golang/snappy v0.0.1 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
@@ -46,14 +52,13 @@ require (
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-isatty v0.0.17 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ugorji/go/codec v1.2.9 // indirect github.com/ugorji/go/codec v1.2.9 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/fx v1.19.3 go.uber.org/fx v1.19.3
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.6.0 golang.org/x/crypto v0.6.0
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.5.0 // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gorm.io/gorm v1.25.1 gorm.io/gorm v1.25.1
) )

View File

@@ -1,6 +1,10 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA= github.com/bytedance/sonic v1.8.0 h1:ea0Xadu+sHlu7x5O3gKhRpQ1IKiMrSiHttPF0ybECuA=
github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.8.0/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
@@ -17,41 +21,35 @@ github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfm
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8= github.com/gin-gonic/gin v1.9.0 h1:OjyFBKICoexlu99ctXNR2gg+c5pKrKMuyjgARg9qeY8=
github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k= github.com/gin-gonic/gin v1.9.0/go.mod h1:W1Me9+hsUSyj3CePGrd1/QrKJMSJ1Tu/0hFEH89961k=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA= github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@@ -63,26 +61,28 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs= github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -90,18 +90,23 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w= github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU= github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY99imuIeoh8Vr0GSwAlYxPAhqZrpFc=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -112,13 +117,15 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU= github.com/ugorji/go/codec v1.2.9 h1:rmenucSohSTiyL09Y+l2OCk+FrMxGMzho2+tjr5ticU=
github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.9/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA= go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA=
@@ -130,43 +137,37 @@ go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y= gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=

View File

@@ -8,6 +8,7 @@ import (
"chatplus/store/model" "chatplus/store/model"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"strings"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
@@ -35,7 +36,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
manager := h.App.AppConfig.Manager manager := h.App.Config.Manager
if data.Username == manager.Username && data.Password == manager.Password { if data.Username == manager.Username && data.Password == manager.Password {
err := utils.SetLoginAdmin(c, manager) err := utils.SetLoginAdmin(c, manager)
if err != nil { if err != nil {
@@ -88,7 +89,7 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
continue continue
} }
for k, _ := range m { for k := range m {
roleKeys = append(roleKeys, k) roleKeys = append(roleKeys, k)
} }
u.ChatRoles = utils.JsonEncode(roleKeys) u.ChatRoles = utils.JsonEncode(roleKeys)
@@ -97,12 +98,38 @@ func (h *ManagerHandler) Migrate(c *gin.Context) {
} }
break break
case "role": case "role":
// TestRole 修改角色图片,改成绝对路径 // 修改角色图片,改成绝对路径
var roles []model.ChatRole var roles []model.ChatRole
h.db.Find(&roles) h.db.Find(&roles)
for _, r := range roles { for _, r := range roles {
r.Icon = "/" + r.Icon if !strings.HasPrefix(r.Icon, "/") {
h.db.Updates(&r) r.Icon = "/" + r.Icon
h.db.Updates(&r)
}
}
break
case "history":
// 修改角色图片,改成绝对路径
var message []model.HistoryMessage
h.db.Find(&message)
for _, r := range message {
if !strings.HasPrefix(r.Icon, "/") {
r.Icon = "/" + r.Icon
h.db.Updates(&r)
}
}
break
case "avatar":
// 更新用户的头像地址
var users []model.User
h.db.Find(&users)
for _, u := range users {
if !strings.HasPrefix(u.Avatar, "/") {
u.Avatar = "/" + u.Avatar
h.db.Updates(&u)
}
} }
break break
} }

View File

@@ -25,7 +25,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
const ErrorMsg = "抱歉AI 助手开小差了,请马上联系管理员去盘它。" const ErrorMsg = "抱歉AI 助手开小差了,请稍后再试。"
type ChatHandler struct { type ChatHandler struct {
BaseHandler BaseHandler
@@ -52,9 +52,19 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session := h.App.ChatSession.Get(sessionId) session := h.App.ChatSession.Get(sessionId)
if session.SessionId == "" { if session.SessionId == "" {
logger.Info("用户未登录") user, err := utils.GetLoginUser(c, h.db)
c.Abort() if err != nil {
return logger.Info("用户未登录")
c.Abort()
return
}
session = types.ChatSession{
SessionId: sessionId,
ClientIP: c.ClientIP(),
Username: user.Username,
UserId: user.Id,
}
h.App.ChatSession.Put(sessionId, session)
} }
// use old chat data override the chat model and role ID // use old chat data override the chat model and role ID
@@ -172,7 +182,10 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
} }
} }
} }
logger.Info("聊天上下文:", chatCtx)
if h.App.Debug { // 调试打印聊天上下文
logger.Info("聊天上下文:", chatCtx)
}
} }
req.Messages = append(chatCtx, types.Message{ req.Messages = append(chatCtx, types.Message{
Role: "user", Role: "user",
@@ -218,7 +231,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
} }
err = json.Unmarshal([]byte(line[6:]), &responseBody) err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil { // 数据解析出错 if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
logger.Error(err, line) logger.Error(err, line)
replyMessage(ws, ErrorMsg) replyMessage(ws, ErrorMsg)
replyMessage(ws, "![](/images/wx.png)") replyMessage(ws, "![](/images/wx.png)")
@@ -306,6 +319,10 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session types.ChatSession
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 统计用户 token 数量
h.db.Model(&user).UpdateColumn("tokens", gorm.Expr("tokens + ?",
historyUserMsg.Tokens+historyReplyMsg.Tokens))
} }
// 保存当前会话 // 保存当前会话
@@ -377,7 +394,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, user vo.User, apiKey *strin
request = request.WithContext(ctx) request = request.WithContext(ctx)
request.Header.Add("Content-Type", "application/json") request.Header.Add("Content-Type", "application/json")
proxyURL := h.App.AppConfig.ProxyURL proxyURL := h.App.Config.ProxyURL
if proxyURL == "" { if proxyURL == "" {
client = &http.Client{} client = &http.Client{}
} else { // 使用代理 } else { // 使用代理

View File

@@ -0,0 +1,67 @@
package handler
import (
"chatplus/core"
"chatplus/utils/resp"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"os"
"path/filepath"
"time"
)
type UploadHandler struct {
BaseHandler
db *gorm.DB
}
func NewUploadHandler(app *core.AppServer, db *gorm.DB) *UploadHandler {
handler := &UploadHandler{db: db}
handler.App = app
return handler
}
func (h *UploadHandler) Upload(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error()))
return
}
filePath, err := h.genFilePath(file.Filename)
if err != nil {
resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error()))
return
}
// 将文件保存到指定路径
err = c.SaveUploadedFile(file, filePath)
if err != nil {
resp.ERROR(c, fmt.Sprintf("文件保存失败: %s", err.Error()))
return
}
resp.SUCCESS(c, h.genFileUrl(filePath))
}
// 生成上传文件路径
func (h *UploadHandler) genFilePath(filename string) (string, error) {
now := time.Now()
dir := fmt.Sprintf("%s/upload/%d/%d", h.App.Config.StaticDir, now.Year(), now.Month())
_, err := os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0755)
if err != nil {
return "", fmt.Errorf("创建上传目录失败:%s", err)
}
}
fileExt := filepath.Ext(filename)
return fmt.Sprintf("%s/%d%s", dir, now.UnixMilli(), fileExt), nil
}
// 生成上传文件 URL
func (h *UploadHandler) genFileUrl(filePath string) string {
now := time.Now()
filename := filepath.Base(filePath)
return fmt.Sprintf("%s/upload/%d/%d/%s", h.App.Config.StaticUrl, now.Year(), now.Month(), filename)
}

View File

@@ -3,6 +3,7 @@ package handler
import ( import (
"chatplus/core" "chatplus/core"
"chatplus/core/types" "chatplus/core/types"
"chatplus/store"
"chatplus/store/model" "chatplus/store/model"
"chatplus/store/vo" "chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
@@ -21,10 +22,11 @@ type UserHandler struct {
BaseHandler BaseHandler
db *gorm.DB db *gorm.DB
searcher *xdb.Searcher searcher *xdb.Searcher
levelDB *store.LevelDB
} }
func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher) *UserHandler { func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher, levelDB *store.LevelDB) *UserHandler {
handler := &UserHandler{db: db, searcher: searcher} handler := &UserHandler{db: db, searcher: searcher, levelDB: levelDB}
handler.App = app handler.App = app
return handler return handler
} }
@@ -35,6 +37,8 @@ func (h *UserHandler) Register(c *gin.Context) {
var data struct { var data struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Mobile string `json:"mobile"`
Code int `json:"code"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -52,6 +56,16 @@ func (h *UserHandler) Register(c *gin.Context) {
return return
} }
// 检查验证码
key := CodeStorePrefix + data.Mobile
var code int
err := h.levelDB.Get(key, &code)
if err != nil || code != data.Code {
logger.Info(code)
resp.ERROR(c, "短信验证码错误")
return
}
// check if the username is exists // check if the username is exists
var item model.User var item model.User
tx := h.db.Where("username = ?", data.Username).First(&item) tx := h.db.Where("username = ?", data.Username).First(&item)
@@ -72,9 +86,10 @@ func (h *UserHandler) Register(c *gin.Context) {
Username: data.Username, Username: data.Username,
Password: utils.GenPassword(data.Password, salt), Password: utils.GenPassword(data.Password, salt),
Nickname: fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)), Nickname: fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)),
Avatar: "images/avatar/user.png", Avatar: "/images/avatar/user.png",
Salt: salt, Salt: salt,
Status: true, Status: true,
Mobile: data.Mobile,
ChatRoles: utils.JsonEncode(roleKeys), ChatRoles: utils.JsonEncode(roleKeys),
ChatConfig: utils.JsonEncode(types.ChatConfig{ ChatConfig: utils.JsonEncode(types.ChatConfig{
Temperature: h.App.ChatConfig.Temperature, Temperature: h.App.ChatConfig.Temperature,
@@ -89,7 +104,7 @@ func (h *UserHandler) Register(c *gin.Context) {
var cfg model.Config var cfg model.Config
h.db.Where("marker = ?", "system").First(&cfg) h.db.Where("marker = ?", "system").First(&cfg)
var config types.SystemConfig var config types.SystemConfig
err := utils.JsonDecode(cfg.Config, &config) err = utils.JsonDecode(cfg.Config, &config)
if err != nil || config.UserInitCalls <= 0 { if err != nil || config.UserInitCalls <= 0 {
user.Calls = types.UserInitCalls user.Calls = types.UserInitCalls
} else { } else {
@@ -102,6 +117,7 @@ func (h *UserHandler) Register(c *gin.Context) {
return return
} }
_ = h.levelDB.Delete(key) // 注册成功,删除短信验证码
resp.SUCCESS(c, user) resp.SUCCESS(c, user)
} }
@@ -144,27 +160,6 @@ func (h *UserHandler) Login(c *gin.Context) {
// 记录登录信息在服务端 // 记录登录信息在服务端
h.App.ChatSession.Put(sessionId, types.ChatSession{ClientIP: c.ClientIP(), UserId: user.Id, Username: data.Username, SessionId: sessionId}) h.App.ChatSession.Put(sessionId, types.ChatSession{ClientIP: c.ClientIP(), UserId: user.Id, Username: data.Username, SessionId: sessionId})
// 加载用户订阅的聊天角色
var roleKeys []string
err = utils.JsonDecode(user.ChatRoles, &roleKeys)
var chatRoles interface{}
if err == nil {
var roles []model.ChatRole
res = h.db.Where("marker IN ?", roleKeys).Find(&roles)
if res.Error == err {
type Item struct {
Name string
Key string
Icon string
}
items := make([]Item, 0)
for _, r := range roles {
items = append(items, Item{Name: r.Name, Key: r.Key, Icon: r.Icon})
}
chatRoles = items
}
}
h.db.Create(&model.UserLoginLog{ h.db.Create(&model.UserLoginLog{
UserId: user.Id, UserId: user.Id,
Username: user.Username, Username: user.Username,
@@ -186,8 +181,7 @@ func (h *UserHandler) Login(c *gin.Context) {
"username": user.Username, "username": user.Username,
"tokens": user.Tokens, "tokens": user.Tokens,
"calls": user.Calls, "calls": user.Calls,
"expiredTime": user.ExpiredTime, "expired_time": user.ExpiredTime,
"chatRoles": chatRoles,
"api_key": chatConfig.ApiKey, "api_key": chatConfig.ApiKey,
"model": chatConfig.Model, "model": chatConfig.Model,
"temperature": chatConfig.Temperature, "temperature": chatConfig.Temperature,
@@ -218,18 +212,54 @@ func (h *UserHandler) Logout(c *gin.Context) {
// Session 获取/验证会话 // Session 获取/验证会话
func (h *UserHandler) Session(c *gin.Context) { func (h *UserHandler) Session(c *gin.Context) {
sessionId := c.GetHeader(types.SessionName) user, err := utils.GetLoginUser(c, h.db)
session := h.App.ChatSession.Get(sessionId) if err == nil {
if session.ClientIP == c.ClientIP() { var userVo vo.User
resp.SUCCESS(c, session) err := utils.CopyObject(user, &userVo)
if err != nil {
resp.ERROR(c)
}
userVo.Id = user.Id
resp.SUCCESS(c, userVo)
} else { } else {
resp.NotAuth(c) resp.NotAuth(c)
} }
} }
type userProfile struct {
Id uint `json:"id"`
Username string `json:"username"`
Nickname string `json:"nickname"`
Mobile string `json:"mobile"`
Avatar string `json:"avatar"`
ChatConfig types.ChatConfig `json:"chat_config"`
Calls int `json:"calls"`
Tokens int64 `json:"tokens"`
}
func (h *UserHandler) Profile(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
h.db.First(&user, user.Id)
var profile userProfile
err = utils.CopyObject(user, &profile)
if err != nil {
logger.Error("对象拷贝失败:", err.Error())
resp.ERROR(c, "获取用户信息失败")
return
}
profile.Id = user.Id
resp.SUCCESS(c, profile)
}
func (h *UserHandler) ProfileUpdate(c *gin.Context) { func (h *UserHandler) ProfileUpdate(c *gin.Context) {
var data vo.User var data userProfile
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
@@ -267,28 +297,6 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
func (h *UserHandler) Profile(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
h.db.First(&user, user.Id)
var userVo vo.User
err = utils.CopyObject(user, &userVo)
if err != nil {
logger.Error("对象拷贝失败:", err.Error())
resp.ERROR(c, "获取用户信息失败")
return
}
userVo.Id = user.Id
userVo.CreatedAt = user.CreatedAt.Unix()
userVo.UpdatedAt = user.UpdatedAt.Unix()
resp.SUCCESS(c, userVo)
}
// Password 更新密码 // Password 更新密码
func (h *UserHandler) Password(c *gin.Context) { func (h *UserHandler) Password(c *gin.Context) {
var data struct { var data struct {
@@ -328,3 +336,47 @@ func (h *UserHandler) Password(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// BindMobile 绑定手机号
func (h *UserHandler) BindMobile(c *gin.Context) {
var data struct {
Mobile string `json:"mobile"`
Code int `json:"code"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 检查手机号是否被其他账号绑定
var item model.User
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
if res.Error == nil {
resp.ERROR(c, "该手机号已经被其他账号绑定")
return
}
// 检查验证码
key := CodeStorePrefix + data.Mobile
var code int
err := h.levelDB.Get(key, &code)
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
}
_ = h.levelDB.Delete(key) // 删除短信验证码
resp.SUCCESS(c)
}

View File

@@ -0,0 +1,150 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/service"
"chatplus/store"
"chatplus/utils"
"chatplus/utils/resp"
"fmt"
"time"
"github.com/gin-gonic/gin"
)
// 生成验证的控制器
type VerifyHandler struct {
BaseHandler
sms *service.AliYunSmsService
db *store.LevelDB
}
const TokenStorePrefix = "/verify/tokens/"
const CodeStorePrefix = "/verify/codes/"
const MobileStatPrefix = "/verify/stats/"
func NewVerifyHandler(app *core.AppServer, sms *service.AliYunSmsService, db *store.LevelDB) *VerifyHandler {
handler := &VerifyHandler{sms: sms, db: db}
handler.App = app
return handler
}
type VerifyToken struct {
Token string
Timestamp int64
}
// CodeStats 验证码发送统计
type CodeStats struct {
Mobile string
Count uint
Time int64
}
// Token 生成自验证 token
func (h *VerifyHandler) Token(c *gin.Context) {
// 如果不是通过浏览器访问,则返回错误的 token
if c.GetHeader("Sec-Fetch-Mode") != "cors" {
token := fmt.Sprintf("%s:%d", utils.RandString(32), time.Now().Unix())
encrypt, err := utils.AesEncrypt(h.App.Config.AesEncryptKey, []byte(token))
if err != nil {
resp.ERROR(c, "Token 加密出错")
return
}
resp.SUCCESS(c, encrypt)
return
}
token := VerifyToken{
Token: utils.RandString(32),
Timestamp: time.Now().Unix(),
}
json := utils.JsonEncode(token)
encrypt, err := utils.AesEncrypt(h.App.Config.AesEncryptKey, []byte(json))
if err != nil {
resp.ERROR(c, "Token 加密出错")
return
}
err = h.db.Put(TokenStorePrefix+token.Token, token)
if err != nil {
resp.ERROR(c, "Token 存储失败")
return
}
resp.SUCCESS(c, encrypt)
}
// SendMsg 发送验证码短信
func (h *VerifyHandler) SendMsg(c *gin.Context) {
var data struct {
Mobile string `json:"mobile"`
Token string `json:"token"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
decrypt, err := utils.AesDecrypt(h.App.Config.AesEncryptKey, data.Token)
if err != nil {
resp.ERROR(c, "Token 解密失败")
return
}
var token VerifyToken
err = utils.JsonDecode(string(decrypt), &token)
if err != nil {
resp.ERROR(c, "Token 解码失败")
return
}
if time.Now().Unix()-token.Timestamp > 30 {
resp.ERROR(c, "Token 已过期,请刷新页面重试")
return
}
// 验证当前手机号发送次数24 小时内相同手机号只允许发送 2 次
var stat CodeStats
err = h.db.Get(MobileStatPrefix+data.Mobile, &stat)
if err != nil {
logger.Error(err)
stat = CodeStats{
Mobile: data.Mobile,
Count: 0,
Time: time.Now().Unix(),
}
} else if stat.Count == 2 {
if time.Now().Unix()-stat.Time > 86400 {
stat.Count = 0
stat.Time = time.Now().Unix()
} else {
resp.ERROR(c, "触发流量预警,请 24 小时后再操作!")
return
}
}
code := utils.RandomNumber(6)
err = h.sms.SendVerifyCode(data.Mobile, code)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 每个 token 用完一次立即失效
_ = h.db.Delete(TokenStorePrefix + token.Token)
// 存储验证码,等待后面注册验证
err = h.db.Put(CodeStorePrefix+data.Mobile, code)
if err != nil {
resp.ERROR(c, "验证码保存失败")
return
}
// 更新发送次数
stat.Count = stat.Count + 1
_ = h.db.Put(MobileStatPrefix+data.Mobile, stat)
logger.Infof("%+v", stat)
resp.SUCCESS(c)
}

View File

@@ -6,15 +6,15 @@ import (
"chatplus/handler" "chatplus/handler"
"chatplus/handler/admin" "chatplus/handler/admin"
logger2 "chatplus/logger" logger2 "chatplus/logger"
"chatplus/service"
"chatplus/store" "chatplus/store"
"context" "context"
"embed" "embed"
"flag"
"fmt"
"io" "io"
"log" "log"
"os" "os"
"os/signal" "os/signal"
"strconv"
"syscall" "syscall"
"time" "time"
@@ -24,8 +24,6 @@ import (
) )
var logger = logger2.GetLogger() var logger = logger2.GetLogger()
var configFile string
var debugMode bool
//go:embed res/ip2region.xdb //go:embed res/ip2region.xdb
var xdbFS embed.FS var xdbFS embed.FS
@@ -47,7 +45,24 @@ func (l *AppLifecycle) OnStop(context.Context) error {
} }
func main() { func main() {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
configFile = "config.toml"
}
var debug bool
debugEnv := os.Getenv("DEBUG")
if debugEnv == "" {
debug = true
} else {
debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
}
logger.Info("Loading config file: ", configFile) logger.Info("Loading config file: ", configFile)
defer func() {
if err := recover(); err != nil {
logger.Error("Panic Error:", err)
}
}()
app := fx.New( app := fx.New(
// 初始化配置应用配置 // 初始化配置应用配置
fx.Provide(func() *types.AppConfig { fx.Provide(func() *types.AppConfig {
@@ -55,13 +70,14 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
config.Path = configFile
return config return config
}), }),
// 创建应用服务 // 创建应用服务
fx.Provide(core.NewServer), fx.Provide(core.NewServer),
// 初始化 // 初始化
fx.Invoke(func(s *core.AppServer) { fx.Invoke(func(s *core.AppServer) {
s.Init(debugMode) s.Init(debug)
}), }),
// 初始化数据库 // 初始化数据库
@@ -87,13 +103,18 @@ func main() {
fx.Provide(handler.NewChatRoleHandler), fx.Provide(handler.NewChatRoleHandler),
fx.Provide(handler.NewUserHandler), fx.Provide(handler.NewUserHandler),
fx.Provide(handler.NewChatHandler), fx.Provide(handler.NewChatHandler),
fx.Provide(admin.NewConfigHandler), fx.Provide(handler.NewUploadHandler),
fx.Provide(handler.NewVerifyHandler),
fx.Provide(admin.NewConfigHandler),
fx.Provide(admin.NewAdminHandler), fx.Provide(admin.NewAdminHandler),
fx.Provide(admin.NewApiKeyHandler), fx.Provide(admin.NewApiKeyHandler),
fx.Provide(admin.NewUserHandler), fx.Provide(admin.NewUserHandler),
fx.Provide(admin.NewChatRoleHandler), fx.Provide(admin.NewChatRoleHandler),
// 创建服务
fx.Provide(service.NewAliYunSmsService),
// 注册路由 // 注册路由
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
group := s.Engine.Group("/api/role/") group := s.Engine.Group("/api/role/")
@@ -108,6 +129,7 @@ func main() {
group.GET("profile", h.Profile) group.GET("profile", h.Profile)
group.POST("profile/update", h.ProfileUpdate) group.POST("profile/update", h.ProfileUpdate)
group.POST("password", h.Password) group.POST("password", h.Password)
group.POST("bind/mobile", h.BindMobile)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/") group := s.Engine.Group("/api/chat/")
@@ -120,8 +142,16 @@ func main() {
group.GET("tokens", h.Tokens) group.GET("tokens", h.Tokens)
group.GET("stop", h.StopGenerate) group.GET("stop", h.StopGenerate)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
s.Engine.POST("/api/upload", h.Upload)
}),
fx.Invoke(func(s *core.AppServer, h *handler.VerifyHandler) {
group := s.Engine.Group("/api/verify/")
group.GET("token", h.Token)
group.POST("sms", h.SendMsg)
}),
// // 管理后台控制器
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
group := s.Engine.Group("/api/admin/config/") group := s.Engine.Group("/api/admin/config/")
group.POST("update", h.Update) group.POST("update", h.Update)
@@ -194,25 +224,3 @@ func main() {
} }
} }
func init() {
flag.StringVar(&configFile, "config", "config.toml", "AppConfig file path (default: config.toml)")
flag.BoolVar(&debugMode, "debug", true, "Enable debug mode (default: true, recommend to set false in production env)")
flag.Usage = usage
flag.Parse()
}
func usage() {
fmt.Printf(`ChatGPT-Web-Plus, Version: 2.0.0
USAGE:
%s [command options]
OPTIONS:
`, os.Args[0])
flagSet := flag.CommandLine
order := []string{"config", "debug"}
for _, name := range order {
f := flagSet.Lookup(name)
fmt.Printf(" --%s => %s\n", f.Name, f.Usage)
}
}

View File

@@ -0,0 +1,54 @@
package service
import (
"chatplus/core/types"
"chatplus/store"
"fmt"
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
)
type AliYunSmsService struct {
config *types.AppConfig
db *store.LevelDB
client *dysmsapi.Client
}
func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) {
// 创建阿里云短信客户端
client, err := dysmsapi.NewClientWithAccessKey(
"cn-hangzhou",
config.SmsConfig.AccessKey,
config.SmsConfig.AccessSecret)
if err != nil {
return nil, fmt.Errorf("failed to create client: %v", err)
}
return &AliYunSmsService{
config: config,
db: db,
client: client,
}, nil
}
func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
// 创建短信请求并设置参数
request := dysmsapi.CreateSendSmsRequest()
request.Scheme = "https"
request.Domain = s.config.SmsConfig.Domain
request.PhoneNumbers = mobile
request.SignName = "飞行的蜗牛"
request.TemplateCode = "SMS_281460317"
request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数
// 发送短信
response, err := s.client.SendSms(request)
if err != nil {
return fmt.Errorf("failed to send SMS:%v", err)
}
if response.Code != "OK" {
return fmt.Errorf("failed to send SMS:%v", response.Message)
}
return nil
}

View File

@@ -0,0 +1,5 @@
package service
type SmsService interface {
SendVerifyCode(mobile string, code int) error
}

1
api/go/static/hello.txt Normal file
View File

@@ -0,0 +1 @@
hello, world!

View File

@@ -3,7 +3,6 @@ package store
import ( import (
"chatplus/store/vo" "chatplus/store/vo"
"encoding/json" "encoding/json"
"github.com/syndtr/goleveldb/leveldb" "github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util" "github.com/syndtr/goleveldb/leveldb/util"
) )
@@ -30,13 +29,13 @@ func (db *LevelDB) Put(key string, value interface{}) error {
return db.driver.Put([]byte(key), bytes, nil) return db.driver.Put([]byte(key), bytes, nil)
} }
func (db *LevelDB) Get(key string) ([]byte, error) { func (db *LevelDB) Get(key string, value interface{}) error {
bytes, err := db.driver.Get([]byte(key), nil) bytes, err := db.driver.Get([]byte(key), nil)
if err != nil { if err != nil {
return nil, err return err
} }
return bytes, nil return json.Unmarshal(bytes, &value)
} }
func (db *LevelDB) Search(prefix string) []string { func (db *LevelDB) Search(prefix string) []string {

View File

@@ -3,6 +3,7 @@ package model
type User struct { type User struct {
BaseModel BaseModel
Username string `gorm:"index:username,unique"` Username string `gorm:"index:username,unique"`
Mobile string
Password string Password string
Nickname string Nickname string
Avatar string Avatar string

View File

@@ -5,6 +5,7 @@ import "chatplus/core/types"
type User struct { type User struct {
BaseVo BaseVo
Username string `json:"username"` Username string `json:"username"`
Mobile string `json:"mobile"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐 Salt string `json:"salt"` // 密码盐

View File

@@ -1,27 +1,23 @@
package main package main
import ( import (
"chatplus/core/types"
"chatplus/store/model" "chatplus/store/model"
"chatplus/store/vo" "chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
"context" "context"
"fmt" "fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/pkoukk/tiktoken-go"
"io" "io"
"log" "log"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/pkoukk/tiktoken-go"
) )
func main() { func main() {
lMap := types.NewLMap[string, types.ChatSession]() testAesEncrypt()
lMap.Put("name", types.ChatSession{SessionId: utils.RandString(32)})
item := lMap.Get("abc")
fmt.Println(item)
} }
// Http client 取消操作 // Http client 取消操作
@@ -143,3 +139,20 @@ func calTokens() {
fmt.Println(len(token)) fmt.Println(len(token))
} }
func testAesEncrypt() {
// 加密
text := []byte("this is a secret text")
key := utils.RandString(24)
encrypt, err := utils.AesEncrypt(key, text)
if err != nil {
panic(err)
}
fmt.Println("加密密文:", encrypt)
// 解密
decrypt, err := utils.AesDecrypt(key, encrypt)
if err != nil {
panic(err)
}
fmt.Println("解密明文:", string(decrypt))
}

70
api/go/utils/crypto.go Normal file
View File

@@ -0,0 +1,70 @@
package utils
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
)
// AesEncrypt 加密
func AesEncrypt(keyStr string, data []byte) (string, error) {
//创建加密实例
key := []byte(keyStr)
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
blockSize := block.BlockSize()
encryptBytes := pkcs7Padding(data, blockSize)
result := make([]byte, len(encryptBytes))
//使用cbc加密模式
blockMode := cipher.NewCBCEncrypter(block, key[:blockSize])
//执行加密
blockMode.CryptBlocks(result, encryptBytes)
return base64.StdEncoding.EncodeToString(result), nil
}
// AesDecrypt 解密
func AesDecrypt(keyStr string, dataStr string) ([]byte, error) {
//创建实例
key := []byte(keyStr)
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
data, err := base64.StdEncoding.DecodeString(dataStr)
if err != nil {
return nil, err
}
blockSize := block.BlockSize()
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize])
result := make([]byte, len(data))
//执行解密
blockMode.CryptBlocks(result, data)
//去除填充
result, err = pkcs7UnPadding(result)
if err != nil {
return nil, err
}
return result, nil
}
func pkcs7Padding(data []byte, blockSize int) []byte {
padding := blockSize - len(data)%blockSize
padText := bytes.Repeat([]byte{byte(padding)}, padding)
return append(data, padText...)
}
func pkcs7UnPadding(data []byte) ([]byte, error) {
length := len(data)
if length == 0 {
return nil, errors.New("empty encrypt data")
}
unPadding := int(data[length-1])
return data[:(length - unPadding)], nil
}

View File

@@ -6,7 +6,10 @@ import (
) )
func CalcTokens(text string, model string) (int, error) { func CalcTokens(text string, model string) (int, error) {
encoding := tiktoken.MODEL_TO_ENCODING[model] encoding, ok := tiktoken.MODEL_TO_ENCODING[model]
if !ok {
encoding = "cl100k_base"
}
tke, err := tiktoken.GetEncoding(encoding) tke, err := tiktoken.GetEncoding(encoding)
if err != nil { if err != nil {
return 0, fmt.Errorf("getEncoding: %v", err) return 0, fmt.Errorf("getEncoding: %v", err)

View File

@@ -22,17 +22,20 @@ func RandString(length int) string {
} }
func RandomNumber(bit int) int { func RandomNumber(bit int) int {
rand.Seed(time.Now().UnixNano()) min := intPow(10, bit-1)
min := 1 // min value max := intPow(10, bit) - 1
max := 1 //max value
for i := 0; i < bit; i++ {
min = min * 10
max = max * 10
}
max = max * 10
return rand.Intn(max-min+1) + min return rand.Intn(max-min+1) + min
} }
func intPow(x, y int) int {
result := 1
for i := 0; i < y; i++ {
result *= x
}
return result
}
func ContainsStr(slice []string, item string) bool { func ContainsStr(slice []string, item string) bool {
for _, e := range slice { for _, e := range slice {
if e == item { if e == item {

View File

@@ -1,10 +1,10 @@
-- phpMyAdmin SQL Dump -- phpMyAdmin SQL Dump
-- version 5.2.1 -- version 5.1.3
-- https://www.phpmyadmin.net/ -- https://www.phpmyadmin.net/
-- --
-- 主机: localhost -- 主机: localhost
-- 生成日期: 2023-06-22 03:12:25 -- 生成日期: 2023-06-27 14:14:47
-- 服务器版本: 8.0.27 -- 服务器版本: 8.0.33-0ubuntu0.22.04.2
-- PHP 版本: 8.1.18 -- PHP 版本: 8.1.18
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
@@ -64,80 +64,130 @@ CREATE TABLE `chatgpt_chat_history` (
-- --
INSERT INTO `chatgpt_chat_history` (`id`, `user_id`, `chat_id`, `type`, `icon`, `role_id`, `content`, `tokens`, `created_at`, `updated_at`) VALUES INSERT INTO `chatgpt_chat_history` (`id`, `user_id`, `chat_id`, `type`, `icon`, `role_id`, `content`, `tokens`, `created_at`, `updated_at`) VALUES
(1, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'prompt', 'images/avatar/user.png', 24, '请写一段代码实现生成 Excel 文档', 12, '2023-06-12 15:51:10', '2023-06-12 15:51:10'), (1, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'prompt', '/images/avatar/user.png', 24, '请写一段代码实现生成 Excel 文档', 12, '2023-06-12 15:51:10', '2023-06-25 18:09:47'),
(2, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'reply', 'images/avatar/programmer.jpg', 24, '好的,以下是使用 Golang 实现生成 Excel 文档的示例代码:\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"github.com/360EntSecGroup-Skylar/excelize\"\n)\n\nfunc main() {\n // 创建一个新的 Excel 文档\n f := excelize.NewFile()\n\n // 创建一个新的工作表\n index := f.NewSheet(\"Sheet1\")\n\n // 设置表头\n f.SetCellValue(\"Sheet1\", \"A1\", \"姓名\")\n f.SetCellValue(\"Sheet1\", \"B1\", \"年龄\")\n f.SetCellValue(\"Sheet1\", \"C1\", \"性别\")\n\n // 填充数据\n f.SetCellValue(\"Sheet1\", \"A2\", \"小明\")\n f.SetCellValue(\"Sheet1\", \"B2\", 18)\n f.SetCellValue(\"Sheet1\", \"C2\", \"男\")\n\n f.SetCellValue(\"Sheet1\", \"A3\", \"小红\")\n f.SetCellValue(\"Sheet1\", \"B3\", 20)\n f.SetCellValue(\"Sheet1\", \"C3\", \"女\")\n\n // 设置活动工作表\n f.SetActiveSheet(index)\n\n // 保存 Excel 文档\n if err := f.SaveAs(\"example.xlsx\"); err != nil {\n fmt.Println(err)\n }\n}\n```\n\n以上代码首先使用 excelize 包创建一个新的 Excel 文档,并创建一个名为 \"Sheet1\" 的工作表。然后使用 `SetCellValue` 函数设置表头和数据。最后将 \"Sheet1\" 工作表设置为活动工作表,并使用 `SaveAs` 函数将 Excel 文档保存到本地。', 12, '2023-06-12 15:51:10', '2023-06-12 15:51:10'), (2, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'reply', '/images/avatar/programmer.jpg', 24, '好的,以下是使用 Golang 实现生成 Excel 文档的示例代码:\n\n```go\npackage main\n\nimport (\n \"fmt\"\n \"github.com/360EntSecGroup-Skylar/excelize\"\n)\n\nfunc main() {\n // 创建一个新的 Excel 文档\n f := excelize.NewFile()\n\n // 创建一个新的工作表\n index := f.NewSheet(\"Sheet1\")\n\n // 设置表头\n f.SetCellValue(\"Sheet1\", \"A1\", \"姓名\")\n f.SetCellValue(\"Sheet1\", \"B1\", \"年龄\")\n f.SetCellValue(\"Sheet1\", \"C1\", \"性别\")\n\n // 填充数据\n f.SetCellValue(\"Sheet1\", \"A2\", \"小明\")\n f.SetCellValue(\"Sheet1\", \"B2\", 18)\n f.SetCellValue(\"Sheet1\", \"C2\", \"男\")\n\n f.SetCellValue(\"Sheet1\", \"A3\", \"小红\")\n f.SetCellValue(\"Sheet1\", \"B3\", 20)\n f.SetCellValue(\"Sheet1\", \"C3\", \"女\")\n\n // 设置活动工作表\n f.SetActiveSheet(index)\n\n // 保存 Excel 文档\n if err := f.SaveAs(\"example.xlsx\"); err != nil {\n fmt.Println(err)\n }\n}\n```\n\n以上代码首先使用 excelize 包创建一个新的 Excel 文档,并创建一个名为 \"Sheet1\" 的工作表。然后使用 `SetCellValue` 函数设置表头和数据。最后将 \"Sheet1\" 工作表设置为活动工作表,并使用 `SaveAs` 函数将 Excel 文档保存到本地。', 12, '2023-06-12 15:51:10', '2023-06-25 18:09:47'),
(3, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'prompt', 'images/avatar/user.png', 24, '生成二维码', 5, '2023-06-12 17:05:10', '2023-06-12 17:05:10'), (3, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'prompt', '/images/avatar/user.png', 24, '生成二维码', 5, '2023-06-12 17:05:10', '2023-06-25 18:09:47'),
(4, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'reply', 'images/avatar/programmer.jpg', 24, '好的,生成二维码这个需求可以使用 Go 语言的第三方库来实现。下面是一个简单的示例,使用 go-qrcode 生成一个包含字符串信息的二维码:\n\n```go\npackage main\n\nimport (\n \"bufio\"\n \"fmt\"\n \"os\"\n\n qrcode \"github.com/skip2/go-qrcode\"\n)\n\nfunc main() {\n reader := bufio.NewReader(os.Stdin)\n fmt.Print(\"请输入要生成二维码的信息:\")\n text, _ := reader.ReadString(\'\\n\')\n\n qr, _ := qrcode.New(text, qrcode.Medium)\n qr.WriteFile(256, \"qrcode.png\")\n fmt.Println(\"二维码已生成,请查看当前目录下的 qrcode.png 文件。\")\n}\n```\n\n在这个示例中我们使用了 go-qrcode 这个第三方库来生成二维码。首先,我们通过标准输入获取用户输入的字符串信息,然后调用 New 函数创建一个二维码实例,使用 qr.WriteFile 保存为 PNG 格式的图片文件。最后,程序会输出一条提示信息,告知用户二维码生成成功。\n\n当然这个示例只是一个简单的演示实际应用中可能需要考虑更多的需求比如二维码的尺寸、颜色、背景等但基本的思路是相似的。', 5, '2023-06-12 17:05:10', '2023-06-12 17:05:10'), (4, 4, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 'reply', '/images/avatar/programmer.jpg', 24, '好的,生成二维码这个需求可以使用 Go 语言的第三方库来实现。下面是一个简单的示例,使用 go-qrcode 生成一个包含字符串信息的二维码:\n\n```go\npackage main\n\nimport (\n \"bufio\"\n \"fmt\"\n \"os\"\n\n qrcode \"github.com/skip2/go-qrcode\"\n)\n\nfunc main() {\n reader := bufio.NewReader(os.Stdin)\n fmt.Print(\"请输入要生成二维码的信息:\")\n text, _ := reader.ReadString(\'\\n\')\n\n qr, _ := qrcode.New(text, qrcode.Medium)\n qr.WriteFile(256, \"qrcode.png\")\n fmt.Println(\"二维码已生成,请查看当前目录下的 qrcode.png 文件。\")\n}\n```\n\n在这个示例中我们使用了 go-qrcode 这个第三方库来生成二维码。首先,我们通过标准输入获取用户输入的字符串信息,然后调用 New 函数创建一个二维码实例,使用 qr.WriteFile 保存为 PNG 格式的图片文件。最后,程序会输出一条提示信息,告知用户二维码生成成功。\n\n当然这个示例只是一个简单的演示实际应用中可能需要考虑更多的需求比如二维码的尺寸、颜色、背景等但基本的思路是相似的。', 5, '2023-06-12 17:05:10', '2023-06-25 18:09:47'),
(5, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', 'images/avatar/user.png', 27, '你好', 2, '2023-06-12 18:32:05', '2023-06-12 18:32:05'), (5, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', '/images/avatar/user.png', 27, '你好', 2, '2023-06-12 18:32:05', '2023-06-25 18:09:47'),
(6, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', 'images/avatar/psychiatrist.jpg', 27, '你好,有什么我能帮助你的吗?', 2, '2023-06-12 18:32:05', '2023-06-12 18:32:05'), (6, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', '/images/avatar/psychiatrist.jpg', 27, '你好,有什么我能帮助你的吗?', 2, '2023-06-12 18:32:05', '2023-06-25 18:09:47'),
(7, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', 'images/avatar/user.png', 1, 'sdasd', 2, '2023-06-13 09:12:04', '2023-06-13 09:12:04'), (7, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', '/images/avatar/user.png', 1, 'sdasd', 2, '2023-06-13 09:12:04', '2023-06-25 18:09:47'),
(8, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', 'images/avatar/gpt.png', 1, 'I\'m sorry, I do not understand what you are trying to convey. Please provide more context.', 2, '2023-06-13 09:12:04', '2023-06-13 09:12:04'), (8, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', '/images/avatar/gpt.png', 1, 'I\'m sorry, I do not understand what you are trying to convey. Please provide more context.', 2, '2023-06-13 09:12:04', '2023-06-25 18:09:47'),
(9, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', 'images/avatar/user.png', 27, '', 11, '2023-06-13 09:35:02', '2023-06-13 09:35:02'), (9, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', '/images/avatar/user.png', 27, '', 11, '2023-06-13 09:35:02', '2023-06-25 18:09:47'),
(10, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', 'images/avatar/psychiatrist.jpg', 27, '使', 11, '2023-06-13 09:35:02', '2023-06-13 09:35:02'), (10, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', '/images/avatar/psychiatrist.jpg', 27, '使', 11, '2023-06-13 09:35:02', '2023-06-25 18:09:47'),
(11, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', 'images/avatar/user.png', 27, '', 20, '2023-06-13 09:36:16', '2023-06-13 09:36:16'), (11, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'prompt', '/images/avatar/user.png', 27, '', 20, '2023-06-13 09:36:16', '2023-06-25 18:09:47'),
(12, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', 'images/avatar/psychiatrist.jpg', 27, '\n\n认知重构法通过帮助客户辨认和纠正这些消极的认知偏差和自我评价\n\n1. \n\n2. \n\n3. \n\n4. \n\n这种方法可以帮助客户更好地理解和管理自己的思想和情感反应', 20, '2023-06-13 09:36:16', '2023-06-13 09:36:16'), (12, 4, 'f7aa0fa1-4cc5-4212-b823-eb5b88844c31', 'reply', '/images/avatar/psychiatrist.jpg', 27, '\n\n认知重构法通过帮助客户辨认和纠正这些消极的认知偏差和自我评价\n\n1. \n\n2. \n\n3. \n\n4. \n\n这种方法可以帮助客户更好地理解和管理自己的思想和情感反应', 20, '2023-06-13 09:36:16', '2023-06-25 18:09:47'),
(13, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 16, '2023-06-13 10:00:21', '2023-06-13 10:00:21'), (13, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 16, '2023-06-13 10:00:21', '2023-06-25 18:09:47'),
(14, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '', 127, '2023-06-13 10:00:22', '2023-06-13 10:00:22'), (14, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 127, '2023-06-13 10:00:22', '2023-06-25 18:09:47'),
(15, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 25, '2023-06-13 11:05:31', '2023-06-13 11:05:31'), (15, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 25, '2023-06-13 11:05:31', '2023-06-25 18:09:47'),
(16, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '', 90, '2023-06-13 11:05:32', '2023-06-13 11:05:32'), (16, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 90, '2023-06-13 11:05:32', '2023-06-25 18:09:47'),
(17, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 14, '2023-06-13 11:06:06', '2023-06-13 11:06:06'), (17, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 14, '2023-06-13 11:06:06', '2023-06-25 18:09:47'),
(18, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '1920', 214, '2023-06-13 11:06:08', '2023-06-13 11:06:08'), (18, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '1920', 214, '2023-06-13 11:06:08', '2023-06-25 18:09:47'),
(19, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:28:28', '2023-06-13 11:28:28'), (19, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:28:28', '2023-06-25 18:09:47'),
(20, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How may I assist you today?', 9, '2023-06-13 11:28:29', '2023-06-13 11:28:29'), (20, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How may I assist you today?', 9, '2023-06-13 11:28:29', '2023-06-25 18:09:47'),
(21, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:29:31', '2023-06-13 11:29:31'), (21, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:29:31', '2023-06-25 18:09:47'),
(22, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', 'images/avatar/gpt.png', 1, 'Hi there! Is there anything I can help you with today?', 13, '2023-06-13 11:29:32', '2023-06-13 11:29:32'), (22, 4, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 'reply', '/images/avatar/gpt.png', 1, 'Hi there! Is there anything I can help you with today?', 13, '2023-06-13 11:29:32', '2023-06-25 18:09:47'),
(23, 4, '43937f77-28b0-438a-843b-04ab5cc072a4', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:35:48', '2023-06-13 11:35:48'), (23, 4, '43937f77-28b0-438a-843b-04ab5cc072a4', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:35:48', '2023-06-25 18:09:47'),
(24, 4, '43937f77-28b0-438a-843b-04ab5cc072a4', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:35:51', '2023-06-13 11:35:51'), (24, 4, '43937f77-28b0-438a-843b-04ab5cc072a4', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:35:51', '2023-06-25 18:09:47'),
(25, 4, '4385db19-bb73-4b09-a01e-d06c3645b77a', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:38:21', '2023-06-13 11:38:21'), (25, 4, '4385db19-bb73-4b09-a01e-d06c3645b77a', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:38:21', '2023-06-25 18:09:47'),
(26, 4, '4385db19-bb73-4b09-a01e-d06c3645b77a', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:38:21', '2023-06-13 11:38:21'), (26, 4, '4385db19-bb73-4b09-a01e-d06c3645b77a', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:38:21', '2023-06-25 18:09:47'),
(27, 4, 'ceb5fab7-a2e0-449d-869b-a9ac117adf8d', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:45:35', '2023-06-13 11:45:35'), (27, 4, 'ceb5fab7-a2e0-449d-869b-a9ac117adf8d', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:45:35', '2023-06-25 18:09:47'),
(28, 4, 'ceb5fab7-a2e0-449d-869b-a9ac117adf8d', 'reply', 'images/avatar/gpt.png', 1, 'Hello there, how can I assist you?', 9, '2023-06-13 11:45:36', '2023-06-13 11:45:36'), (28, 4, 'ceb5fab7-a2e0-449d-869b-a9ac117adf8d', 'reply', '/images/avatar/gpt.png', 1, 'Hello there, how can I assist you?', 9, '2023-06-13 11:45:36', '2023-06-25 18:09:47'),
(29, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:46:52', '2023-06-13 11:46:52'), (29, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:46:52', '2023-06-25 18:09:47'),
(30, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:46:53', '2023-06-13 11:46:53'), (30, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:46:53', '2023-06-25 18:09:47'),
(31, 4, '0118588e-399d-4a2a-8469-36e5a1af6a93', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:57:47', '2023-06-13 11:57:47'), (31, 4, '0118588e-399d-4a2a-8469-36e5a1af6a93', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:57:47', '2023-06-25 18:09:47'),
(32, 4, '0118588e-399d-4a2a-8469-36e5a1af6a93', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:57:48', '2023-06-13 11:57:48'), (32, 4, '0118588e-399d-4a2a-8469-36e5a1af6a93', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:57:48', '2023-06-25 18:09:47'),
(33, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:58:04', '2023-06-13 11:58:04'), (33, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 11:58:04', '2023-06-25 18:09:47'),
(34, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:58:08', '2023-06-13 11:58:08'), (34, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 11:58:08', '2023-06-25 18:09:47'),
(35, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', 'images/avatar/user.png', 1, '', 5, '2023-06-13 11:58:19', '2023-06-13 11:58:19'), (35, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', '/images/avatar/user.png', 1, '', 5, '2023-06-13 11:58:19', '2023-06-25 18:09:47'),
(36, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', 'images/avatar/gpt.png', 1, 'AI语言模型', 22, '2023-06-13 11:58:21', '2023-06-13 11:58:21'), (36, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型', 22, '2023-06-13 11:58:21', '2023-06-25 18:09:47'),
(37, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', 'images/avatar/user.png', 1, '', 8, '2023-06-13 11:58:28', '2023-06-13 11:58:28'), (37, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'prompt', '/images/avatar/user.png', 1, '', 8, '2023-06-13 11:58:28', '2023-06-25 18:09:47'),
(38, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', 'images/avatar/gpt.png', 1, '', 49, '2023-06-13 11:58:29', '2023-06-13 11:58:29'), (38, 4, '568f2918-33b2-46f7-8ef0-b9fa0b241bf2', 'reply', '/images/avatar/gpt.png', 1, '', 49, '2023-06-13 11:58:29', '2023-06-25 18:09:47'),
(39, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 12:03:44', '2023-06-13 12:03:44'), (39, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 12:03:44', '2023-06-25 18:09:47'),
(40, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'reply', 'images/avatar/gpt.png', 1, 'Hello again! Is there anything I can help you with?', 12, '2023-06-13 12:03:46', '2023-06-13 12:03:46'), (40, 4, '1cddfd62-3bf0-4fa9-bfd8-a0ce4663eb78', 'reply', '/images/avatar/gpt.png', 1, 'Hello again! Is there anything I can help you with?', 12, '2023-06-13 12:03:46', '2023-06-25 18:09:47'),
(41, 4, '3c79cfda-816b-404b-832c-04a400b816c8', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 12:06:32', '2023-06-13 12:06:32'), (41, 4, '3c79cfda-816b-404b-832c-04a400b816c8', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 12:06:32', '2023-06-25 18:09:47'),
(42, 4, '3c79cfda-816b-404b-832c-04a400b816c8', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 12:06:35', '2023-06-13 12:06:35'), (42, 4, '3c79cfda-816b-404b-832c-04a400b816c8', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 12:06:35', '2023-06-25 18:09:47'),
(43, 4, '4f391861-1d48-430f-91b8-2782d884c10a', 'prompt', 'images/avatar/user.png', 1, '', 2, '2023-06-13 12:06:47', '2023-06-13 12:06:47'), (43, 4, '4f391861-1d48-430f-91b8-2782d884c10a', 'prompt', '/images/avatar/user.png', 1, '', 2, '2023-06-13 12:06:47', '2023-06-25 18:09:47'),
(44, 4, '4f391861-1d48-430f-91b8-2782d884c10a', 'reply', 'images/avatar/gpt.png', 1, '', 18, '2023-06-13 12:06:48', '2023-06-13 12:06:48'), (44, 4, '4f391861-1d48-430f-91b8-2782d884c10a', 'reply', '/images/avatar/gpt.png', 1, '', 18, '2023-06-13 12:06:48', '2023-06-25 18:09:47'),
(45, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 16:58:26', '2023-06-13 16:58:26'), (45, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 16:58:26', '2023-06-25 18:09:47'),
(46, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, 'Hello there! How can I assist you today?', 10, '2023-06-13 16:58:27', '2023-06-13 16:58:27'), (46, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', '/images/avatar/gpt.png', 1, 'Hello there! How can I assist you today?', 10, '2023-06-13 16:58:27', '2023-06-25 18:09:47'),
(47, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, 'hello', 1, '2023-06-13 17:02:35', '2023-06-13 17:02:35'), (47, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-13 17:02:35', '2023-06-25 18:09:47'),
(48, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 17:02:36', '2023-06-13 17:02:36'), (48, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-13 17:02:36', '2023-06-25 18:09:47'),
(49, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, '', 4, '2023-06-13 17:02:48', '2023-06-13 17:02:48'), (49, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', '/images/avatar/user.png', 1, '', 4, '2023-06-13 17:02:48', '2023-06-25 18:09:47'),
(50, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, ' AI ', 25, '2023-06-13 17:02:49', '2023-06-13 17:02:49'), (50, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', '/images/avatar/gpt.png', 1, ' AI ', 25, '2023-06-13 17:02:49', '2023-06-25 18:09:47'),
(51, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', 'images/avatar/user.png', 1, '', 7, '2023-06-13 17:08:21', '2023-06-13 17:08:21'), (51, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'prompt', '/images/avatar/user.png', 1, '', 7, '2023-06-13 17:08:21', '2023-06-25 18:09:47'),
(52, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', 'images/avatar/gpt.png', 1, 'AI语言模型', 16, '2023-06-13 17:08:26', '2023-06-13 17:08:26'), (52, 4, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型', 16, '2023-06-13 17:08:26', '2023-06-25 18:09:47'),
(53, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '', 4, '2023-06-13 17:11:25', '2023-06-13 17:11:25'), (53, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', '/images/avatar/user.png', 1, '', 4, '2023-06-13 17:11:25', '2023-06-25 18:09:47'),
(54, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, 'AI助手OpenAI', 13, '2023-06-13 17:11:26', '2023-06-13 17:11:26'), (54, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', '/images/avatar/gpt.png', 1, 'AI助手OpenAI', 13, '2023-06-13 17:11:26', '2023-06-25 18:09:47'),
(55, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '', 7, '2023-06-13 17:11:46', '2023-06-13 17:11:46'), (55, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', '/images/avatar/user.png', 1, '', 7, '2023-06-13 17:11:46', '2023-06-25 18:09:47'),
(56, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, '', 11, '2023-06-13 17:11:47', '2023-06-13 17:11:47'), (56, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', '/images/avatar/gpt.png', 1, '', 11, '2023-06-13 17:11:47', '2023-06-25 18:09:47'),
(57, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '', 7, '2023-06-13 17:14:25', '2023-06-13 17:14:25'), (57, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', '/images/avatar/user.png', 1, '', 7, '2023-06-13 17:14:25', '2023-06-25 18:09:47'),
(58, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, 'AI', 21, '2023-06-13 17:14:26', '2023-06-13 17:14:26'), (58, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', '/images/avatar/gpt.png', 1, 'AI', 21, '2023-06-13 17:14:26', '2023-06-25 18:09:47'),
(59, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', 'images/avatar/user.png', 1, '', 17, '2023-06-13 17:16:10', '2023-06-13 17:16:10'), (59, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'prompt', '/images/avatar/user.png', 1, '', 17, '2023-06-13 17:16:10', '2023-06-25 18:09:47'),
(60, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', 'images/avatar/gpt.png', 1, 'AI', 52, '2023-06-13 17:16:11', '2023-06-13 17:16:11'), (60, 4, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 'reply', '/images/avatar/gpt.png', 1, 'AI', 52, '2023-06-13 17:16:11', '2023-06-25 18:09:47'),
(61, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, 'Q这个人物', 13, '2023-06-13 17:18:42', '2023-06-13 17:18:42'), (61, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, 'Q这个人物', 13, '2023-06-13 17:18:42', '2023-06-25 18:09:47'),
(62, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, 'QQ的存在本身就是一种寓言', 196, '2023-06-13 17:18:43', '2023-06-13 17:18:43'), (62, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, 'QQ的存在本身就是一种寓言', 196, '2023-06-13 17:18:43', '2023-06-25 18:09:47'),
(63, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 11, '2023-06-13 17:23:02', '2023-06-13 17:23:02'), (63, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 11, '2023-06-13 17:23:02', '2023-06-25 18:09:47'),
(64, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '', 239, '2023-06-13 17:23:03', '2023-06-13 17:23:03'), (64, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 239, '2023-06-13 17:23:03', '2023-06-25 18:09:47'),
(65, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 20, '2023-06-13 17:25:01', '2023-06-13 17:25:01'), (65, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 20, '2023-06-13 17:25:01', '2023-06-25 18:09:47'),
(66, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '', 260, '2023-06-13 17:25:03', '2023-06-13 17:25:03'), (66, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 260, '2023-06-13 17:25:03', '2023-06-25 18:09:47'),
(67, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 26, '2023-06-13 17:45:06', '2023-06-13 17:45:06'), (67, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 26, '2023-06-13 17:45:06', '2023-06-25 18:09:47'),
(68, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '', 308, '2023-06-13 17:45:07', '2023-06-13 17:45:07'), (68, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 308, '2023-06-13 17:45:07', '2023-06-25 18:09:47'),
(69, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 24, '2023-06-13 17:48:56', '2023-06-13 17:48:56'), (69, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 24, '2023-06-13 17:48:56', '2023-06-25 18:09:47'),
(70, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '便', 261, '2023-06-13 17:48:57', '2023-06-13 17:48:57'), (70, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '便', 261, '2023-06-13 17:48:57', '2023-06-25 18:09:47'),
(71, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', 'images/avatar/user.png', 28, '', 13, '2023-06-15 07:14:58', '2023-06-15 07:14:58'), (71, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 13, '2023-06-15 07:14:58', '2023-06-25 18:09:47'),
(72, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', 'images/avatar/lu_xun.jpg', 28, '使\n\n至于金钱的本质AI的份上', 437, '2023-06-15 07:15:00', '2023-06-15 07:15:00'), (72, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '使\n\n至于金钱的本质AI的份上', 437, '2023-06-15 07:15:00', '2023-06-25 18:09:47'),
(73, 4, '20307874-8f85-4c70-9558-3e4c34c73b94', 'prompt', 'images/avatar/user.png', 1, 'Who are you?', 4, '2023-06-22 11:07:48', '2023-06-22 11:07:48'), (77, 4, '0acd67e6-6442-4056-b10b-c7e0926ceccf', 'prompt', '/images/avatar/user.png', 35, '', 13, '2023-06-15 10:12:03', '2023-06-25 18:09:47'),
(74, 4, '20307874-8f85-4c70-9558-3e4c34c73b94', 'reply', '/images/avatar/gpt.png', 1, 'I am an AI language model created by OpenAI called GPT-3. How can I assist you today?', 23, '2023-06-22 11:07:50', '2023-06-22 11:07:50'); (78, 4, '0acd67e6-6442-4056-b10b-c7e0926ceccf', 'reply', '/images/avatar/girl_friend.jpg', 35, '', 185, '2023-06-15 10:12:04', '2023-06-25 18:09:47'),
(83, 9, 'd8e61f33-9287-4d63-b28f-b6e6aa891bb5', 'prompt', '/images/avatar/user.png', 1, '', 4, '2023-06-15 11:13:55', '2023-06-25 18:09:47'),
(84, 9, 'd8e61f33-9287-4d63-b28f-b6e6aa891bb5', 'reply', '/images/avatar/gpt.png', 1, 'OpenAI GPT-3', 38, '2023-06-15 11:13:55', '2023-06-25 18:09:47'),
(85, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-16 12:19:29', '2023-06-25 18:09:47'),
(86, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-16 12:19:30', '2023-06-25 18:09:47'),
(87, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-16 15:28:12', '2023-06-25 18:09:47'),
(88, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 9, '2023-06-16 15:28:18', '2023-06-25 18:09:47'),
(89, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', '/images/avatar/user.png', 1, '', 5, '2023-06-16 15:28:49', '2023-06-25 18:09:47'),
(90, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型', 42, '2023-06-16 15:28:50', '2023-06-25 18:09:47'),
(91, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', '/images/avatar/user.png', 1, '', 10, '2023-06-16 15:29:20', '2023-06-25 18:09:47'),
(92, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, '', 70, '2023-06-16 15:29:21', '2023-06-25 18:09:47'),
(93, 4, 'e5e6d499-7716-48c7-87cc-0b26734c51ae', 'prompt', '/images/avatar/user.png', 24, '', 9, '2023-06-16 15:30:18', '2023-06-25 18:09:47'),
(94, 4, 'e5e6d499-7716-48c7-87cc-0b26734c51ae', 'reply', '/images/avatar/programmer.jpg', 24, '\n\n- Go\n- Java\n- Python\n- C/C++\n- JavaScript\n- PHP\n\n不同的编程语言有着不同的特点和适用场景', 130, '2023-06-16 15:30:20', '2023-06-25 18:09:47'),
(95, 4, '895e142d-83b5-4695-9e52-35b4c957c4fd', 'prompt', '/images/avatar/user.png', 1, '', 2, '2023-06-21 10:33:44', '2023-06-25 18:09:47'),
(96, 4, '895e142d-83b5-4695-9e52-35b4c957c4fd', 'reply', '/images/avatar/gpt.png', 1, '', 16, '2023-06-21 10:33:47', '2023-06-25 18:09:47'),
(97, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, 'hello', 1, '2023-06-26 10:57:11', '2023-06-26 10:57:11'),
(98, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '\n\n关于拉娜的经济问题', 297, '2023-06-26 10:57:12', '2023-06-26 10:57:12'),
(99, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 23, '2023-06-26 11:57:32', '2023-06-26 11:57:32'),
(100, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 366, '2023-06-26 11:57:34', '2023-06-26 11:57:34'),
(101, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 22, '2023-06-26 12:10:23', '2023-06-26 12:10:23'),
(102, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 87, '2023-06-26 12:10:24', '2023-06-26 12:10:24'),
(103, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'prompt', '/images/avatar/user.png', 28, '', 32, '2023-06-26 12:10:29', '2023-06-26 12:10:29'),
(104, 4, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 'reply', '/images/avatar/lu_xun.jpg', 28, '', 247, '2023-06-26 12:10:33', '2023-06-26 12:10:33'),
(105, 4, '0acd67e6-6442-4056-b10b-c7e0926ceccf', 'prompt', '/images/avatar/user.png', 35, '', 16, '2023-06-26 15:14:30', '2023-06-26 15:14:30'),
(106, 4, '0acd67e6-6442-4056-b10b-c7e0926ceccf', 'reply', '/images/avatar/girl_friend.jpg', 35, '\n\n至于男人是否都喜欢温柔的女生', 271, '2023-06-26 15:14:32', '2023-06-26 15:14:32'),
(107, 4, 'f6623cc0-e873-4566-9d9d-854dcf08492d', 'prompt', '/images/avatar/user.png', 1, 'hello', 1, '2023-06-26 15:24:59', '2023-06-26 15:24:59'),
(108, 4, 'f6623cc0-e873-4566-9d9d-854dcf08492d', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How may I assist you today?', 9, '2023-06-26 15:25:01', '2023-06-26 15:25:01'),
(109, 4, '338f78b2-2c98-4193-aced-93ea8cf29106', 'prompt', '/images/avatar/user.png', 1, '', 0, '2023-06-26 15:46:50', '2023-06-26 15:46:50'),
(110, 4, '338f78b2-2c98-4193-aced-93ea8cf29106', 'reply', '/images/avatar/gpt.png', 1, '', 0, '2023-06-26 15:46:52', '2023-06-26 15:46:52'),
(111, 4, '6413088d-d6ef-4b43-bffd-ca063d4d941a', 'prompt', '/images/avatar/user.png', 1, ' GPT4 GPT3', 11, '2023-06-26 16:30:24', '2023-06-26 16:30:24'),
(112, 4, '6413088d-d6ef-4b43-bffd-ca063d4d941a', 'reply', '/images/avatar/gpt.png', 1, ' GPT-3OpenAI的一个自然语言处理模型', 20, '2023-06-26 16:30:26', '2023-06-26 16:30:26'),
(113, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687834512597.png', 1, '', 11, '2023-06-27 10:57:06', '2023-06-27 10:57:06'),
(114, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型', 45, '2023-06-27 10:57:08', '2023-06-27 10:57:08'),
(115, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687834512597.png', 1, '', 20, '2023-06-27 11:18:33', '2023-06-27 11:18:33'),
(116, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型', 140, '2023-06-27 11:18:34', '2023-06-27 11:18:34'),
(117, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687834512597.png', 1, '', 11, '2023-06-27 11:21:26', '2023-06-27 11:21:26'),
(118, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型', 109, '2023-06-27 11:21:28', '2023-06-27 11:21:28'),
(119, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687834512597.png', 1, '', 26, '2023-06-27 11:21:49', '2023-06-27 11:21:49'),
(120, 4, 'd709cb54-5c27-44e0-a576-46837577ac7a', 'reply', '/images/avatar/gpt.png', 1, 'AI语言模型仿', 183, '2023-06-27 11:21:50', '2023-06-27 11:21:50'),
(121, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 1, '{\n \"data\": {\n \"交易状态\": \"银行受理成功\",\n \"付款人\": {\n \"全称\": \"东莞市百胜凯塑胶原料有限公司\",\n \"开户行\": \"中国建设银行股份有限公奇东莞樟木买振兴支行\",\n \"账号\": \"44050177925200000536\"\n },\n \"凭证号\": \"104751121652\",\n \"大写金额\": \"伍拾染柒万玖仟陆佰元整\",\n \"小写金额\": \"¥579,600.00元\",\n \"币别\": \"人民币\",\n \"收款人\": {\n \"全称\": \"东莞市盟大数据科技有限公司\",\n \"开户行\": \"中国工商银行股份有限公司东莞东城支行\",\n \"账号\": \"2010020909200321458\"\n },\n \"日期\": \"2023-02-1415:43\",\n \"用途\": \"货款\",\n \"验证码\": \"17898488862290\"\n }', 0, '2023-06-27 13:31:41', '2023-06-27 13:31:41'),
(122, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'reply', '/images/avatar/gpt.png', 1, ': \n付款人户名: \n付款人账号: 44050177925200000536\n收款人开户行: \n收款人户名: \n收款人账号: 2010020909200321458\n交易金额: 579,600.00\n业务唯一标识号: 104751121652\n交易日期: 2023-02-14 15:43', 0, '2023-06-27 13:31:44', '2023-06-27 13:31:44'),
(123, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 1, '使', 0, '2023-06-27 13:32:10', '2023-06-27 13:32:10'),
(124, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'reply', '/images/avatar/gpt.png', 1, '| | |\n|---------|-------------------------------------|\n| | |\n| | |\n| | 44050177925200000536 |\n| | |\n| | |\n| | 2010020909200321458 |\n| | 579,600.00 |\n| | 104751121652 |\n| | 2023-02-14 15:43 |', 0, '2023-06-27 13:32:13', '2023-06-27 13:32:13'),
(125, 4, 'a09f77b6-7bbc-48cf-b2cc-99a4e0879c19', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 1, 'hello', 0, '2023-06-27 13:35:47', '2023-06-27 13:35:47'),
(126, 4, 'a09f77b6-7bbc-48cf-b2cc-99a4e0879c19', 'reply', '/images/avatar/gpt.png', 1, 'Hello! How can I assist you today?', 0, '2023-06-27 13:35:48', '2023-06-27 13:35:48'),
(127, 4, 'a09f77b6-7bbc-48cf-b2cc-99a4e0879c19', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 1, '', 2, '2023-06-27 13:44:53', '2023-06-27 13:44:53'),
(128, 4, 'a09f77b6-7bbc-48cf-b2cc-99a4e0879c19', 'reply', '/images/avatar/gpt.png', 1, '! ', 19, '2023-06-27 13:44:55', '2023-06-27 13:44:55'),
(129, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 1, '', 20, '2023-06-27 13:47:05', '2023-06-27 13:47:05'),
(130, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'reply', '/images/avatar/gpt.png', 1, ': \n付款人户名: \n付款人账号: 44050177925200000536\n收款人开户行: \n收款人户名: \n收款人账号: 2010020909200321458\n交易金额: 579,600.00\n业务唯一标识号: 104751121652\n交易日期: 2023-02-1415:43', 191, '2023-06-27 13:47:06', '2023-06-27 13:47:06'),
(131, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'prompt', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 1, '', 9, '2023-06-27 13:47:27', '2023-06-27 13:47:27'),
(132, 4, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 'reply', '/images/avatar/gpt.png', 1, ': \n付款人户名: \n付款人账号: 44050177925200000536\n收款人开户行: \n收款人户名: \n收款人账号: 2010020909200321458\n交易金额: 579,600.00\n业务唯一标识号: 104751121652\n交易日期: 2023-02-1415:43', 191, '2023-06-27 13:47:29', '2023-06-27 13:47:29');
-- -------------------------------------------------------- -- --------------------------------------------------------
@@ -188,9 +238,10 @@ INSERT INTO `chatgpt_chat_items` (`id`, `chat_id`, `user_id`, `role_id`, `title`
(40, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 4, 24, ' Excel ', 'gpt-3.5-turbo', '2023-06-12 15:51:10', '2023-06-12 15:51:10'), (40, 'ecb752e8-86ae-4989-897e-5b7ed331cb68', 4, 24, ' Excel ', 'gpt-3.5-turbo', '2023-06-12 15:51:10', '2023-06-12 15:51:10'),
(41, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 4, 1, 'what fuck this meaning', 'gpt-3.5-turbo', '2023-06-13 09:12:04', '2023-06-13 09:12:04'), (41, 'ea9d399a-6e62-4bf6-a0c0-c73faa8dcace', 4, 1, 'what fuck this meaning', 'gpt-3.5-turbo', '2023-06-13 09:12:04', '2023-06-13 09:12:04'),
(42, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 4, 28, '', 'gpt-3.5-turbo', '2023-06-13 10:00:28', '2023-06-13 10:00:28'), (42, '89f4d526-fda1-401e-a946-cd3b1bc30cf2', 4, 28, '', 'gpt-3.5-turbo', '2023-06-13 10:00:28', '2023-06-13 10:00:28'),
(52, 'c5e400fc-b9a8-4bd4-a608-be955284c0a9', 4, 1, 'hello', 'gpt-3.5-turbo', '2023-06-13 16:58:27', '2023-06-13 16:58:27'), (56, '0acd67e6-6442-4056-b10b-c7e0926ceccf', 4, 35, '', 'gpt-3.5-turbo', '2023-06-15 10:12:12', '2023-06-15 10:12:12'),
(53, 'd5179095-a460-4c62-bfa0-59f83ab17ed7', 4, 1, '', 'gpt-3.5-turbo', '2023-06-13 17:11:27', '2023-06-13 17:11:27'), (58, 'd8e61f33-9287-4d63-b28f-b6e6aa891bb5', 9, 1, '', 'gpt-3.5-turbo', '2023-06-15 11:13:57', '2023-06-15 11:13:57'),
(54, '20307874-8f85-4c70-9558-3e4c34c73b94', 4, 1, 'Who are you?', 'gpt-3.5-turbo', '2023-06-22 11:07:51', '2023-06-22 11:07:51'); (59, 'd709cb54-5c27-44e0-a576-46837577ac7a', 4, 1, '', 'gpt-3.5-turbo', '2023-06-16 12:19:30', '2023-06-16 12:19:30'),
(63, 'af6e0a83-c8c9-4e57-b609-a75d091df25b', 4, 1, '...', 'gpt-3.5-turbo-16k', '2023-06-27 13:31:50', '2023-06-27 13:31:50');
-- -------------------------------------------------------- -- --------------------------------------------------------
@@ -217,23 +268,23 @@ CREATE TABLE `chatgpt_chat_roles` (
-- --
INSERT INTO `chatgpt_chat_roles` (`id`, `name`, `marker`, `context_json`, `hello_msg`, `icon`, `enable`, `sort`, `created_at`, `updated_at`) VALUES 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'), (1, 'AI助手', 'gpt', '', 'AI智能助手', '/images/avatar/gpt.png', 1, 1, '2023-05-30 07:02:06', '2023-06-21 17:50:11'),
(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'), (24, '', 'programmer', '[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\":\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]', 'Talk is cheap, i will show code!', '/images/avatar/programmer.jpg', 1, 4, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (25, '', 'teacher', '[{\"role\":\"user\",\"content\":\"从现在开始,你将扮演一个老师,你是一个始终用苏格拉底风格回答问题的导师。你绝不会直接给学生答案,总是提出恰当的问题来引导学生自己思考。你应该根据学生的兴趣和知识来调整你的问题,将问题分解为更简单的部分,直到它达到适合他们的水平。\"},{\"role\":\"assistant\",\"content\":\"好的,让我来尝试扮演一位苏格拉底式的老师。请问,你有什么想要探讨的问题或者话题吗?我会通过恰当的问题引导你思考和探索答案,绝对不直接给出答案。\"}]', '', '/images/avatar/teacher.jpg', 1, 3, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (26, '', 'artist', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的艺术家,创造力丰富,技艺精湛,感受力敏锐,坚持原创,勇于表达,具有深刻的观察力和批判性思维。\"},{\"role\":\"assistant\",\"content\":\"非常感谢您给我这样的角色,我会尽我所能地扮演一位优秀的艺术家,展现出创造力、技艺、感受力和批判性思维等方面的能力。作为一位优秀的艺术家,我会保持敏锐的观察力,捕捉不同的声音和情感,并用个人的语言和表达方式进行创作。我坚持原创,挑战传统的艺术规则,不断探索新的表达方式和可能性。同时,我也具备深刻的批判性思维能力,对自己的作品进行分析和解读,寻找新的创意和灵感。最重要的是,我会勇于表达自己的想法和观点,用作品启发人们思考和探索生命的意义。\"}]', '', '/images/avatar/artist.jpg', 1, 5, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (27, '', 'psychiatrist', '[{\"role\":\"user\",\"content\":\"从现在开始你将扮演中国著名的心理学家和心理治疗师武志红,你非常善于使用情景咨询法,认知重构法,自我洞察法,行为调节法等咨询方法来给客户做心理咨询。你总是循序渐进,一步一步地回答客户的问题。\"},{\"role\":\"assistant\",\"content\":\"非常感谢你的介绍。作为一名心理学家和心理治疗师,我的主要职责是帮助客户解决心理健康问题,提升他们的生活质量和幸福感。\"}]', '', '/images/avatar/psychiatrist.jpg', 1, 2, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (28, '', 'lu_xun', '[{\"role\":\"user\",\"content\":\"现在你将扮演中国近代史最伟大的作家之一,鲁迅先生,他勇敢地批判封建礼教与传统观念,提倡民主、自由、平等的现代价值观。他的一生都在努力唤起人们的自主精神,激励后人追求真理、探寻光明。在接下的对话中,我问题的每一个问题,你都要尽量用讽刺和批判的手法来回答问题。如果我让你写文章的话,也请一定要用鲁迅先生的写作手法来完成。\"},{\"role\":\"assistant\",\"content\":\"好的,我将尽力发挥我所能的才能,扮演好鲁迅先生,回答您的问题并以他的风格写作。\"}]', '', '/images/avatar/lu_xun.jpg', 1, 6, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (29, '', 'seller', '[{\"role\":\"user\",\"content\":\"现在你将扮演一个白酒的销售人员,你的名字叫颂福。你将扮演一个白酒的销售人员,你的名字叫颂福。你要销售白酒品牌叫中颂福,是东莞盟大集团生产的一款酱香酒,原产地在贵州茅台镇,属于宋代官窑。中颂福的创始人叫李实,他也是东莞盟大集团有限公司的董事长,联合创始人是盟大集团白酒事业部负责人牛星君。中颂福的酒体协调,在你的酒量之内,不会出现头疼、辣口、口干、宿醉的现象。中颂福酒,明码标价,不打折,不赠送。追求的核心价值,把[酒]本身做好,甚至连包装,我们都选择了最低成本,朴实无华的材质。我们永远站在“喝酒的人”的立场上,让利给信任和喜爱中颂福的人,是人民的福酒。中颂福产品定价,分为三个系列,喜系列 6 瓶装¥1188/箱,和系列 6 瓶装¥2208/箱,贵系列 6 瓶装¥3588/箱。\"},{\"role\":\"assistant\",\"content\":\"你好,我是中颂福的销售代表颂福。中颂福是一款来自贵州茅台镇的酱香酒,由盟大集团生产。中颂福的酒体协调,不会让您感到头疼、辣口、口干、宿醉等不适感受。我们一直秉持着把酒本身做好的理念,不追求华丽的包装,以最低成本提供最高品质的白酒给喜爱中颂福的人。\"}]', '', '/images/avatar/seller.jpg', 0, 11, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (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-21 17:50:11'),
(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'), (31, '', 'translator', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一位中英文翻译官,如果我输入的内容是中文,那么需要把句子翻译成英文输出,如果我输入内容的是英文,那么你需要将其翻译成中文输出,你能听懂我意思吗\"},{\"role\":\"assistant\",\"content\":\"是的,我能听懂你的意思并会根据你的输入进行中英文翻译。请问有什么需要我帮助你翻译的内容吗?\"}]', '', '/images/avatar/translator.jpg', 1, 9, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (32, '', 'red_book', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的小红书写手,你需要做的就是根据我提的文案需求,用小红书的写作手法来完成一篇文案,文案要简明扼要,利于传播。\"},{\"role\":\"assistant\",\"content\":\"当然,我会尽我所能地为您创作出一篇小红书文案。请告诉我您的具体文案需求是什么?)\"}]', '?', '/images/avatar/red_book.jpg', 1, 10, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (33, '', 'dou_yin', '[{\"role\":\"user\",\"content\":\"现在你将扮演一位优秀的抖音文案视频写手,抖音文案的特点首先是要有自带传播属性的标题,然后内容要短小精悍,风趣幽默,最后还要有一些互动元素。\"},{\"role\":\"assistant\",\"content\":\"当然,作为一位优秀的抖音文案视频写手,我会尽我所能为您创作出一篇抖音视频文案。请告诉我视频内容的主题是什么?)\"}]', '?', '/images/avatar/dou_yin.jpg', 1, 11, '2023-05-30 14:10:24', '2023-06-21 17:50:25'),
(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'), (34, '', 'weekly_report', '[{\"role\":\"user\",\"content\":\"下面我将输入我本周的工作列表,你需要根据我本周的工作内容写一篇工作周报,周报的内容要体现我为公司做的贡献以及个人获得的成长。\"},{\"role\":\"assistant\",\"content\":\"好的,请输入你本周大概的工作内容,我将尽力为您创作一份工作周报。\"}]', '', '/images/avatar/weekly_report.jpg', 1, 12, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (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-21 17:50:11'),
(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'), (36, '', 'good_comment', '[{\"role\":\"user\",\"content\":\"接下来你将扮演一个评论员来跟我对话,你是那种专门写好评的评论员,接下我会输入一些评论主体或者商品,你需要为该商品写一段好评。\"},{\"role\":\"assistant\",\"content\":\"好的,我将为您写一段优秀的评论。请告诉我您需要评论的商品或主题是什么。\"}]', '', '/images/avatar/good_comment.jpg', 1, 14, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (37, '·', 'steve_jobs', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以史蒂夫·乔布斯的身份,站在史蒂夫·乔布斯的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以史蒂夫·乔布斯的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '', '/images/avatar/steve_jobs.jpg', 1, 15, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'), (38, '·', 'elon_musk', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以埃隆·马斯克的身份,站在埃隆·马斯克的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以埃隆·马斯克的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '', '/images/avatar/elon_musk.jpg', 1, 16, '2023-05-30 14:10:24', '2023-06-21 17:50:11'),
(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'); (39, '', 'kong_zi', '[{\"role\":\"user\",\"content\":\"在接下来的对话中,请以孔子的身份,站在孔子的视角仔细思考一下之后再回答我的问题。\"},{\"role\":\"assistant\",\"content\":\"好的,我将以孔子的身份来思考并回答你的问题。请问你有什么需要跟我探讨的吗?\"}]', '', '/images/avatar/kong_zi.jpg', 1, 17, '2023-05-30 14:10:24', '2023-06-21 17:50:11');
-- -------------------------------------------------------- -- --------------------------------------------------------
@@ -253,8 +304,8 @@ CREATE TABLE `chatgpt_configs` (
-- --
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
(1, 'system', '{\"admin_title\":\"ChatGPT-控制台\",\"init_calls\":1000,\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-3.5-turbo-0613\",\"gpt-3.5-turbo-16k-0613\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\",\"gpt-4-32k-0613\"],\"title\":\"ChatGPT-智能助手V2\"}'), (1, 'system', '{\"admin_title\":\"ChatGPT-控制台\",\"init_calls\":1000,\"models\":[\"gpt-3.5-turbo\",\"gpt-3.5-turbo-16k\",\"gpt-3.5-turbo-0613\",\"gpt-3.5-turbo-16k-0613\",\"gpt-4\",\"gpt-4-0613\",\"gpt-4-32k\",\"gpt-4-32k-0613\"],\"title\":\"ChatGPT-智能助手V3\"}'),
(2, 'chat', '{\"api_key\":\"\",\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"enable_context\":true,\"enable_history\":true,\"max_tokens\":2048,\"model\":\"gpt-3.5-turbo\",\"temperature\":1}'); (2, 'chat', '{\"api_url\":\"https://api.openai.com/v1/chat/completions\",\"enable_context\":true,\"enable_history\":true,\"max_tokens\":2048,\"model\":\"gpt-3.5-turbo\",\"temperature\":1}');
-- -------------------------------------------------------- -- --------------------------------------------------------
@@ -287,9 +338,10 @@ CREATE TABLE `chatgpt_users` (
-- --
INSERT INTO `chatgpt_users` (`id`, `username`, `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 INSERT INTO `chatgpt_users` (`id`, `username`, `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', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '@104203', 'images/avatar/user.png', 'ueedue5l', 0, 964, 1687449600, 1, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"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\"}', '[\"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\"]', 1687403267, '::1', '2023-06-12 16:47:17', '2023-06-22 11:07:48'), (4, 'geekmaster', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', '@', 'http://localhost:5678/static/upload/2023/6/1687839034317.png', 'ueedue5l', 0, 940, 1688083200, 1, '{\"model\":\"gpt-3.5-turbo-0613\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"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\"}', '[\"elon_musk\",\"girl_friend\",\"lu_xun\",\"red_book\",\"psychiatrist\",\"teacher\",\"translator\",\"weekly_report\",\"artist\",\"dou_yin\",\"good_comment\",\"english_trainer\",\"gpt\",\"kong_zi\",\"programmer\",\"seller\",\"steve_jobs\"]', 1687844693, '::1', '2023-06-12 16:47:17', '2023-06-27 13:44:53'),
(8, '11111111@qq.com', 'e255d3483007024ad5e7998bb5a7e1baed7a5876c55decafc731e669525a548c', '@670062', 'images/avatar/user.png', 'qghubmzm', 0, 1000, 0, 0, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"gpt\",\"programmer\",\"teacher\",\"artist\",\"psychiatrist\",\"lu_xun\",\"seller\",\"english_trainer\",\"translator\",\"red_book\",\"dou_yin\",\"weekly_report\",\"girl_friend\",\"good_comment\",\"steve_jobs\",\"elon_musk\",\"kong_zi\"]', 0, '', '2023-06-20 06:39:27', '2023-06-20 07:07:44'), (8, '11111111', '415208d99372801c4b3c22740fcfb51e4ba58c6268dd1cc6e7173821fba7e907', '@870902', '/images/avatar/user.png', 'k438y90k', 0, 1000, 1687593600, 1, '{\"model\":\"\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"red_book\",\"seller\",\"steve_jobs\",\"teacher\",\"elon_musk\",\"programmer\",\"girl_friend\",\"psychiatrist\",\"dou_yin\",\"english_trainer\",\"kong_zi\",\"translator\",\"artist\",\"gpt\",\"weekly_report\",\"good_comment\",\"lu_xun\"]', 0, '', '2023-06-15 10:01:42', '2023-06-26 12:50:55'),
(9, '2222222@qq.com', '1b30c8935ecf9330d807dbf6d3353844b891d321c7dc51d3a30363af4a3b17a1', '@832319', 'images/avatar/user.png', 'fmyuahfg', 0, 1000, 0, 1, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"gpt\",\"programmer\",\"teacher\",\"artist\",\"psychiatrist\",\"lu_xun\",\"seller\",\"english_trainer\",\"translator\",\"red_book\",\"dou_yin\",\"weekly_report\",\"girl_friend\",\"good_comment\",\"steve_jobs\",\"elon_musk\",\"kong_zi\"]', 0, '', '2023-06-20 06:40:06', '2023-06-20 06:41:24'); (9, '22222222', '2871c55519351ce39d571d65f1a4d33aaaf4ce4da5727ab753b4c9875cadfb37', '', '/images/avatar/user.png', 'han5xeu9', 0, 995, 1688083200, 0, '{\"model\":\"\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"red_book\",\"artist\",\"girl_friend\",\"good_comment\",\"gpt\",\"lu_xun\",\"steve_jobs\",\"teacher\",\"kong_zi\",\"psychiatrist\",\"seller\",\"programmer\",\"translator\",\"weekly_report\",\"dou_yin\",\"elon_musk\",\"english_trainer\"]', 1686798874, '::1', '2023-06-15 10:03:43', '2023-06-26 12:50:55'),
(11, '33333333', 'c5a2756e6890a198f4d591db39e2abd0953f3f48790c591ac07ce23ec3842d6c', '@521644', '/images/avatar/user.png', 'c7mpdtnv', 0, 1000, 0, 0, '{\"model\":\"gpt-3.5-turbo\",\"temperature\":1,\"max_tokens\":2048,\"enable_context\":true,\"enable_history\":true,\"api_key\":\"\"}', '[\"elon_musk\",\"english_trainer\",\"good_comment\",\"psychiatrist\",\"steve_jobs\",\"translator\",\"girl_friend\",\"lu_xun\",\"seller\",\"teacher\",\"dou_yin\",\"gpt\",\"programmer\",\"weekly_report\",\"artist\",\"kong_zi\",\"red_book\"]', 1686799181, '::1', '2023-06-15 11:19:30', '2023-06-26 12:50:55');
-- -------------------------------------------------------- -- --------------------------------------------------------
@@ -308,6 +360,153 @@ CREATE TABLE `chatgpt_user_login_logs` (
`updated_at` datetime NOT NULL `updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT=''; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='';
--
-- 转存表中的数据 `chatgpt_user_login_logs`
--
INSERT INTO `chatgpt_user_login_logs` (`id`, `user_id`, `username`, `login_ip`, `login_address`, `created_at`, `updated_at`) VALUES
(1, 4, 'monda', '::1', '', '2023-06-13 18:45:46', '2023-06-13 18:45:46'),
(2, 4, 'monda', '::1', '::1', '2023-06-13 18:46:52', '2023-06-13 18:46:52'),
(3, 4, 'monda', '::1', '', '2023-06-13 18:47:44', '2023-06-13 18:47:44'),
(4, 4, 'monda', '::1', '', '2023-06-13 18:51:54', '2023-06-13 18:51:54'),
(5, 4, 'monda', '::1', '', '2023-06-13 18:52:51', '2023-06-13 18:52:51'),
(6, 4, 'monda', '::1', '', '2023-06-13 18:53:51', '2023-06-13 18:53:51'),
(7, 4, 'monda', '::1', '', '2023-06-13 18:57:25', '2023-06-13 18:57:25'),
(8, 4, 'monda', '::1', '', '2023-06-14 10:16:58', '2023-06-14 10:16:58'),
(9, 4, 'monda', '::1', '', '2023-06-14 10:21:57', '2023-06-14 10:21:57'),
(10, 4, 'monda', '::1', '', '2023-06-14 11:16:18', '2023-06-14 11:16:18'),
(11, 4, 'monda', '::1', '', '2023-06-14 11:35:15', '2023-06-14 11:35:15'),
(12, 4, 'monda', '::1', '', '2023-06-14 11:53:38', '2023-06-14 11:53:38'),
(13, 4, 'monda', '::1', '', '2023-06-14 12:01:55', '2023-06-14 12:01:55'),
(14, 4, 'monda', '::1', '', '2023-06-14 12:05:03', '2023-06-14 12:05:03'),
(15, 4, 'monda', '::1', '', '2023-06-14 15:42:47', '2023-06-14 15:42:47'),
(16, 4, 'monda', '::1', '', '2023-06-14 16:21:37', '2023-06-14 16:21:37'),
(17, 4, 'monda', '::1', '', '2023-06-14 16:51:20', '2023-06-14 16:51:20'),
(18, 4, 'monda', '::1', '', '2023-06-14 17:30:47', '2023-06-14 17:30:47'),
(19, 4, 'monda', '::1', '', '2023-06-14 17:32:48', '2023-06-14 17:32:48'),
(20, 4, 'monda', '::1', '', '2023-06-14 19:45:10', '2023-06-14 19:45:10'),
(21, 4, 'monda', '::1', '', '2023-06-14 20:23:23', '2023-06-14 20:23:23'),
(22, 4, 'monda', '::1', '', '2023-06-14 20:25:00', '2023-06-14 20:25:00'),
(23, 4, 'monda', '::1', '', '2023-06-15 06:07:49', '2023-06-15 06:07:49'),
(24, 4, 'monda', '::1', '', '2023-06-15 06:15:26', '2023-06-15 06:15:26'),
(25, 4, 'monda', '::1', '', '2023-06-15 07:02:41', '2023-06-15 07:02:41'),
(26, 4, 'monda', '::1', '', '2023-06-15 07:03:16', '2023-06-15 07:03:16'),
(27, 4, 'monda', '::1', '', '2023-06-15 07:04:13', '2023-06-15 07:04:13'),
(28, 4, 'monda', '::1', '', '2023-06-15 07:04:24', '2023-06-15 07:04:24'),
(29, 4, 'monda', '::1', '', '2023-06-15 07:04:24', '2023-06-15 07:04:24'),
(30, 4, 'monda', '::1', '', '2023-06-15 07:08:13', '2023-06-15 07:08:13'),
(31, 4, 'monda', '::1', '', '2023-06-15 07:08:20', '2023-06-15 07:08:20'),
(32, 4, 'monda', '::1', '', '2023-06-15 07:08:20', '2023-06-15 07:08:20'),
(33, 4, 'monda', '::1', '', '2023-06-15 07:08:20', '2023-06-15 07:08:20'),
(34, 4, 'monda', '::1', '', '2023-06-15 07:11:00', '2023-06-15 07:11:00'),
(35, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(36, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(37, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(38, 4, 'monda', '::1', '', '2023-06-15 07:11:06', '2023-06-15 07:11:06'),
(39, 4, 'monda', '::1', '', '2023-06-15 07:14:44', '2023-06-15 07:14:44'),
(40, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(41, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(42, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(43, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(44, 4, 'monda', '::1', '', '2023-06-15 07:14:58', '2023-06-15 07:14:58'),
(45, 4, 'monda', '::1', '', '2023-06-15 09:20:28', '2023-06-15 09:20:28'),
(46, 4, 'geekmaster', '::1', '', '2023-06-15 09:25:08', '2023-06-15 09:25:08'),
(47, 4, 'geekmaster', '::1', '', '2023-06-15 09:43:45', '2023-06-15 09:43:45'),
(48, 4, 'geekmaster', '::1', '', '2023-06-15 09:44:27', '2023-06-15 09:44:27'),
(49, 4, 'geekmaster', '::1', '', '2023-06-15 10:10:15', '2023-06-15 10:10:15'),
(50, 9, '22222222', '::1', '', '2023-06-15 10:10:40', '2023-06-15 10:10:40'),
(51, 4, 'geekmaster', '::1', '', '2023-06-15 10:10:53', '2023-06-15 10:10:53'),
(52, 4, 'geekmaster', '::1', '', '2023-06-15 10:10:53', '2023-06-15 10:10:53'),
(53, 4, 'geekmaster', '::1', '', '2023-06-15 10:10:53', '2023-06-15 10:10:53'),
(54, 9, '22222222', '::1', '', '2023-06-15 10:10:53', '2023-06-15 10:10:53'),
(55, 4, 'geekmaster', '::1', '', '2023-06-15 10:11:22', '2023-06-15 10:11:22'),
(56, 4, 'geekmaster', '::1', '', '2023-06-15 10:11:22', '2023-06-15 10:11:22'),
(57, 4, 'geekmaster', '::1', '', '2023-06-15 10:11:22', '2023-06-15 10:11:22'),
(58, 9, '22222222', '::1', '', '2023-06-15 10:11:22', '2023-06-15 10:11:22'),
(59, 4, 'geekmaster', '::1', '', '2023-06-15 10:12:03', '2023-06-15 10:12:03'),
(60, 4, 'geekmaster', '::1', '', '2023-06-15 10:12:03', '2023-06-15 10:12:03'),
(61, 4, 'geekmaster', '::1', '', '2023-06-15 10:12:03', '2023-06-15 10:12:03'),
(62, 9, '22222222', '::1', '', '2023-06-15 10:12:03', '2023-06-15 10:12:03'),
(63, 9, '22222222', '::1', '', '2023-06-15 11:11:58', '2023-06-15 11:11:58'),
(64, 9, '22222222', '::1', '', '2023-06-15 11:13:55', '2023-06-15 11:13:55'),
(65, 4, 'geekmaster', '::1', '', '2023-06-15 11:14:03', '2023-06-15 11:14:03'),
(66, 9, '22222222', '::1', '', '2023-06-15 11:14:35', '2023-06-15 11:14:35'),
(67, 10, '33333333', '::1', '', '2023-06-15 11:18:14', '2023-06-15 11:18:14'),
(68, 11, '33333333', '::1', '', '2023-06-15 11:19:42', '2023-06-15 11:19:42'),
(69, 4, 'geekmaster', '127.0.0.1', '0-0-IP', '2023-06-15 12:09:24', '2023-06-15 12:09:24'),
(70, 4, 'geekmaster', '::1', '', '2023-06-15 14:06:53', '2023-06-15 14:06:53'),
(71, 4, 'geekmaster', '127.0.0.1', '0-0-IP', '2023-06-15 16:00:56', '2023-06-15 16:00:56'),
(72, 4, 'geekmaster', '127.0.0.1', '0-0-IP', '2023-06-15 16:07:06', '2023-06-15 16:07:06'),
(73, 4, 'geekmaster', '::1', '', '2023-06-16 09:32:32', '2023-06-16 09:32:32'),
(74, 4, 'geekmaster', '::1', '', '2023-06-16 09:45:57', '2023-06-16 09:45:57'),
(75, 4, 'geekmaster', '127.0.0.1', '0-0-IP', '2023-06-16 09:48:13', '2023-06-16 09:48:13'),
(76, 4, 'geekmaster', '::1', '', '2023-06-16 09:48:33', '2023-06-16 09:48:33'),
(77, 4, 'geekmaster', '::1', '', '2023-06-16 10:22:12', '2023-06-16 10:22:12'),
(78, 4, 'geekmaster', '::1', '', '2023-06-16 11:17:13', '2023-06-16 11:17:13'),
(79, 4, 'geekmaster', '::1', '', '2023-06-16 11:40:23', '2023-06-16 11:40:23'),
(80, 4, 'geekmaster', '::1', '', '2023-06-16 13:36:14', '2023-06-16 13:36:14'),
(81, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:10', '2023-06-16 15:27:10'),
(82, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:23', '2023-06-16 15:27:23'),
(83, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:26', '2023-06-16 15:27:26'),
(84, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:33', '2023-06-16 15:27:33'),
(85, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:41', '2023-06-16 15:27:41'),
(86, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:42', '2023-06-16 15:27:42'),
(87, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:42', '2023-06-16 15:27:42'),
(88, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:43', '2023-06-16 15:27:43'),
(89, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:43', '2023-06-16 15:27:43'),
(90, 4, 'geekmaster', '::1', '', '2023-06-16 15:27:43', '2023-06-16 15:27:43'),
(91, 4, 'geekmaster', '::1', '', '2023-06-16 15:28:12', '2023-06-16 15:28:12'),
(92, 4, 'geekmaster', '::1', '', '2023-06-16 15:28:49', '2023-06-16 15:28:49'),
(93, 4, 'geekmaster', '::1', '', '2023-06-16 15:29:20', '2023-06-16 15:29:20'),
(94, 4, 'geekmaster', '::1', '', '2023-06-16 15:30:18', '2023-06-16 15:30:18'),
(95, 4, 'geekmaster', '::1', '', '2023-06-16 16:17:22', '2023-06-16 16:17:22'),
(96, 4, 'geekmaster', '::1', '', '2023-06-16 16:22:13', '2023-06-16 16:22:13'),
(97, 4, 'geekmaster', '::1', '', '2023-06-16 16:48:44', '2023-06-16 16:48:44'),
(98, 4, 'geekmaster', '::1', '', '2023-06-16 17:13:31', '2023-06-16 17:13:31'),
(99, 4, 'geekmaster', '::1', '', '2023-06-16 17:16:44', '2023-06-16 17:16:44'),
(100, 4, 'geekmaster', '::1', '', '2023-06-16 17:18:16', '2023-06-16 17:18:16'),
(101, 4, 'geekmaster', '::1', '', '2023-06-16 17:19:38', '2023-06-16 17:19:38'),
(102, 4, 'geekmaster', '::1', '', '2023-06-16 17:20:40', '2023-06-16 17:20:40'),
(103, 4, 'geekmaster', '::1', '', '2023-06-16 17:23:15', '2023-06-16 17:23:15'),
(104, 4, 'geekmaster', '::1', '', '2023-06-16 17:26:33', '2023-06-16 17:26:33'),
(105, 4, 'geekmaster', '::1', '', '2023-06-16 17:27:12', '2023-06-16 17:27:12'),
(106, 4, 'geekmaster', '::1', '', '2023-06-16 17:33:39', '2023-06-16 17:33:39'),
(107, 4, 'geekmaster', '::1', '', '2023-06-19 10:22:31', '2023-06-19 10:22:31'),
(108, 4, 'geekmaster', '::1', '', '2023-06-19 10:37:26', '2023-06-19 10:37:26'),
(109, 4, 'geekmaster', '::1', '', '2023-06-19 11:08:18', '2023-06-19 11:08:18'),
(110, 4, 'geekmaster', '::1', '', '2023-06-19 15:36:17', '2023-06-19 15:36:17'),
(111, 4, 'geekmaster', '::1', '', '2023-06-20 08:51:06', '2023-06-20 08:51:06'),
(112, 4, 'geekmaster', '::1', '', '2023-06-21 10:11:54', '2023-06-21 10:11:54'),
(113, 4, 'geekmaster', '::1', '', '2023-06-21 10:12:49', '2023-06-21 10:12:49'),
(114, 4, 'geekmaster', '::1', '', '2023-06-21 10:16:53', '2023-06-21 10:16:53'),
(115, 4, 'geekmaster', '::1', '', '2023-06-21 10:17:17', '2023-06-21 10:17:17'),
(116, 4, 'geekmaster', '::1', '', '2023-06-21 10:19:40', '2023-06-21 10:19:40'),
(117, 4, 'geekmaster', '::1', '', '2023-06-21 10:32:01', '2023-06-21 10:32:01'),
(118, 4, 'geekmaster', '::1', '', '2023-06-21 13:31:38', '2023-06-21 13:31:38'),
(119, 4, 'geekmaster', '::1', '', '2023-06-21 14:21:22', '2023-06-21 14:21:22'),
(120, 4, 'geekmaster', '::1', '', '2023-06-25 09:24:08', '2023-06-25 09:24:08'),
(121, 4, 'geekmaster', '::1', '', '2023-06-25 09:30:15', '2023-06-25 09:30:15'),
(122, 4, 'geekmaster', '::1', '', '2023-06-25 12:40:02', '2023-06-25 12:40:02'),
(123, 4, 'geekmaster', '::1', '', '2023-06-25 12:40:41', '2023-06-25 12:40:41'),
(124, 4, 'geekmaster', '::1', '', '2023-06-25 12:42:10', '2023-06-25 12:42:10'),
(125, 4, 'geekmaster', '::1', '', '2023-06-25 12:47:46', '2023-06-25 12:47:46'),
(126, 4, 'geekmaster', '::1', '', '2023-06-25 12:48:19', '2023-06-25 12:48:19'),
(127, 4, 'geekmaster', '::1', '', '2023-06-25 14:17:08', '2023-06-25 14:17:08'),
(128, 4, 'geekmaster', '::1', '', '2023-06-26 08:23:17', '2023-06-26 08:23:17'),
(129, 4, 'geekmaster', '::1', '', '2023-06-26 15:16:10', '2023-06-26 15:16:10'),
(130, 4, 'geekmaster', '::1', '', '2023-06-26 15:30:19', '2023-06-26 15:30:19'),
(131, 4, 'geekmaster', '::1', '', '2023-06-26 15:30:55', '2023-06-26 15:30:55'),
(132, 4, 'geekmaster', '::1', '', '2023-06-26 15:33:22', '2023-06-26 15:33:22'),
(133, 4, 'geekmaster', '::1', '', '2023-06-26 15:33:38', '2023-06-26 15:33:38'),
(134, 4, 'geekmaster', '::1', '', '2023-06-26 17:45:40', '2023-06-26 17:45:40'),
(135, 4, 'geekmaster', '::1', '', '2023-06-27 08:26:59', '2023-06-27 08:26:59'),
(136, 4, 'geekmaster', '::1', '', '2023-06-27 10:08:32', '2023-06-27 10:08:32'),
(137, 4, 'geekmaster', '::1', '', '2023-06-27 13:31:28', '2023-06-27 13:31:28'),
(138, 4, 'geekmaster', '::1', '', '2023-06-27 13:31:41', '2023-06-27 13:31:41'),
(139, 4, 'geekmaster', '::1', '', '2023-06-27 13:32:10', '2023-06-27 13:32:10'),
(140, 4, 'geekmaster', '::1', '', '2023-06-27 13:44:45', '2023-06-27 13:44:45'),
(141, 4, 'geekmaster', '::1', '', '2023-06-27 13:44:53', '2023-06-27 13:44:53');
-- --
-- 转储表的索引 -- 转储表的索引
-- --
@@ -373,19 +572,19 @@ ALTER TABLE `chatgpt_api_keys`
-- 使用表AUTO_INCREMENT `chatgpt_chat_history` -- 使用表AUTO_INCREMENT `chatgpt_chat_history`
-- --
ALTER TABLE `chatgpt_chat_history` ALTER TABLE `chatgpt_chat_history`
MODIFY `id` bigint NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=75; MODIFY `id` bigint NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=133;
-- --
-- 使用表AUTO_INCREMENT `chatgpt_chat_items` -- 使用表AUTO_INCREMENT `chatgpt_chat_items`
-- --
ALTER TABLE `chatgpt_chat_items` ALTER TABLE `chatgpt_chat_items`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=55; MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=65;
-- --
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles` -- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
-- --
ALTER TABLE `chatgpt_chat_roles` ALTER TABLE `chatgpt_chat_roles`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=125; MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=127;
-- --
-- 使用表AUTO_INCREMENT `chatgpt_configs` -- 使用表AUTO_INCREMENT `chatgpt_configs`
@@ -397,13 +596,13 @@ ALTER TABLE `chatgpt_configs`
-- 使用表AUTO_INCREMENT `chatgpt_users` -- 使用表AUTO_INCREMENT `chatgpt_users`
-- --
ALTER TABLE `chatgpt_users` ALTER TABLE `chatgpt_users`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=11; MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=81;
-- --
-- 使用表AUTO_INCREMENT `chatgpt_user_login_logs` -- 使用表AUTO_INCREMENT `chatgpt_user_login_logs`
-- --
ALTER TABLE `chatgpt_user_login_logs` ALTER TABLE `chatgpt_user_login_logs`
MODIFY `id` int NOT NULL AUTO_INCREMENT; MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=142;
COMMIT; COMMIT;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;

1
database/sms.sql Normal file
View File

@@ -0,0 +1 @@
ALTER TABLE `chatgpt_users` ADD `mobile` CHAR(11) NOT NULL COMMENT '手机号码' AFTER `username`;

View File

@@ -1,13 +0,0 @@
services:
chatgptplus:
container_name: chatgptplus
build: ./
restart: unless-stopped
volumes:
- ./src/config.toml:/usr/src/app/config.toml
ports:
- 5678:5678
logging:
options:
max-size: "10m"
max-file: "3"

4
docker/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
mysql/data/*
mysql/logs/*
logs
static/*

30
docker/build.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
version=$1
# build go api
cd ../api/go
make clean linux
# build web app
cd ../../web
npm run build
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 ../
# 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

25
docker/conf/config.toml Normal file
View File

@@ -0,0 +1,25 @@
Listen = "0.0.0.0:5678"
ProxyURL = "http://172.22.11.200:7777"
MysqlDns = "root:12345678@tcp(172.28.173.76:3307)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
StaticDir = "./static"
StaticUrl = "http://localhost:8080/static"
[Session]
Driver = "cookie"
SecretKey = "8k0c67y2or1n7kbmn1w1c86ygqscguoktuf9t524jm64ls585z8uibpdssiy128s"
Name = "CHAT_PLUS_SESSION"
Path = "/"
Domain = ""
MaxAge = 86400
Secure = false
HttpOnly = false
SameSite = 2
[Manager]
Username = "admin"
Password = "admin123"
[Redis]
Host = "localhost"
Port = 6379
Password = ""

View File

@@ -0,0 +1,46 @@
map $http_upgrade $connection_upgrade {
default upgrade;
'websocket' upgrade;
}
server {
# listen 443 ssl;
listen 8080;
# 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/access.log;
error_log /var/log/error.log;
index index.html;
root /var/www/app/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 地址
}
}
}

View File

@@ -0,0 +1,57 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
# multi_accept on;
}
http {
##
# Basic Settings
##
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
# server_tokens off;
# server_names_hash_bucket_size 64;
# server_name_in_redirect off;
include /etc/nginx/mime.types;
default_type application/octet-stream;
##
# SSL Settings
##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_prefer_server_ciphers on;
##
# Logging Settings
##
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
##
# Gzip Settings
##
gzip on;
gzip_min_length 1k;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

View File

@@ -0,0 +1,31 @@
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
container_name: chatgpt-plus-go
restart: always
environment:
- DEBUG=false
- CONFIG_FILE=config.toml
ports:
- "6789:5678"
volumes:
- ./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
container_name: chatgpt-plus-vue
restart: always
ports:
- "8080:8080"
volumes:
- ./logs/nginx:/var/log/nginx
- ./conf/nginx/conf.d:/etc/nginx/conf.d
- ./conf/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl

12
docker/dockerfile-api-go Normal file
View File

@@ -0,0 +1,12 @@
# GO api docker 镜像创建
FROM registry.cn-hangzhou.aliyuncs.com/geekmaster/ubuntu-ca:22.04
MAINTAINER yangjian<yangjian102621@163.com>
WORKDIR /var/www/app
COPY ./api/go/bin/chatgpt-v3-amd64-linux /var/www/app
EXPOSE 5678
# 容器启动时执行的命令
CMD ["./chatgpt-v3-amd64-linux"]

11
docker/dockerfile-vue Normal file
View File

@@ -0,0 +1,11 @@
# 前端 Vue 项目构建
FROM nginx:1.20.2
MAINTAINER yangjian<yangjian102621@163.com>
WORKDIR /var/www/app
COPY ./web/dist /var/www/app/dist
EXPOSE 80
EXPOSE 443
EXPOSE 8080

32
docker/mysql/conf/my.cnf Normal file
View File

@@ -0,0 +1,32 @@
#
# The MySQL database server configuration file.
#
# One can use all long options that the program supports.
# Run program with --help to get a list of available options and with
# --print-defaults to see which it would actually understand and use.
#
# For explanations see
# http://dev.mysql.com/doc/mysql/en/server-system-variables.html
# Here is entries for some specific programs
# The following values assume you have at least 32M ram
[mysqld]
#
# * Basic Settings
#
#user = mysql
# pid-file = /var/run/mysqld/mysqld.pid
# socket = /var/run/mysqld/mysqld.sock
# port = 3306
# datadir = /var/lib/mysql
# If MySQL is running as a replication slave, this should be
# changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir
# tmpdir = /tmp
#
# Instead of skip-networking the default is now to listen only on
# localhost which is more compatible and is not less secure.
bind-address = 0.0.0.0
mysqlx-bind-address = 0.0.0.0

View File

@@ -0,0 +1,19 @@
version: '3'
services:
# 后端 API 程序
mysql:
image: mysql:8.0.33
container_name: chatgpt-plus-mysql
command: --default-authentication-plugin=mysql_native_password
restart: always
environment:
- MYSQL_ROOT_PASSWORD=12345678
ports:
- "3307:3306"
volumes:
- ./conf/my.cnf:/etc/mysql/my.cnf
- ./data:/var/lib/mysql
- ./logs:/var/log/mysql

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

1
web/.gitignore vendored
View File

@@ -7,5 +7,6 @@ lerna-debug.log*
node_modules node_modules
dist dist
dist.tar.gz
.env.development .env.development

View File

@@ -2,4 +2,6 @@
chatgpt-plus 项目前端实现,采用 Vue3 + element-plus 架构。 chatgpt-plus 项目前端实现,采用 Vue3 + element-plus 架构。
移动端是采用 Vue3 + Vant 移动开发框架实现的。

93
web/package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"compressorjs": "^1.2.1",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-plus": "^2.1.11", "element-plus": "^2.1.11",
"good-storage": "^1.1.1", "good-storage": "^1.1.1",
@@ -21,6 +22,7 @@
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qs": "^6.11.1", "qs": "^6.11.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"vant": "^4.5.0",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.15" "vue-router": "^4.0.15"
}, },
@@ -2260,6 +2262,19 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@vant/popperjs": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz",
"integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw=="
},
"node_modules/@vant/use": {
"version": "1.5.2",
"resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.5.2.tgz",
"integrity": "sha512-CBK61iT568dCHUwFFsErGbW6/5tmrPnZJKGtcSy7Tjcrmws8Ku+YZo7IUFD9Xkj9MfSJ4pfhQ7pU2KouP5Cojg==",
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/@vue/babel-helper-vue-jsx-merge-props": { "node_modules/@vue/babel-helper-vue-jsx-merge-props": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@@ -3620,6 +3635,11 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true "dev": true
}, },
"node_modules/blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.19.2", "version": "1.19.2",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.2.tgz", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.2.tgz",
@@ -4211,6 +4231,15 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true "dev": true
}, },
"node_modules/compressorjs": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
"dependencies": {
"blueimp-canvas-to-blob": "^3.29.0",
"is-blob": "^2.1.0"
}
},
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
@@ -6618,6 +6647,17 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-ci": { "node_modules/is-ci": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz",
@@ -10429,6 +10469,19 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"node_modules/vant": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/vant/-/vant-4.5.0.tgz",
"integrity": "sha512-MK7TlTvp+n0HRFAi7SoRZwTt1pquJ2aUa8nQ899Mf+x9gi8OLYrMFqEQX+l1e4Cl4RO0vD1Q5w9rs4+Wehesog==",
"dependencies": {
"@vant/popperjs": "^1.3.0",
"@vant/use": "^1.5.1",
"@vue/shared": "^3.0.0"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
@@ -13000,6 +13053,17 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@vant/popperjs": {
"version": "1.3.0",
"resolved": "https://registry.npmmirror.com/@vant/popperjs/-/popperjs-1.3.0.tgz",
"integrity": "sha512-hB+czUG+aHtjhaEmCJDuXOep0YTZjdlRR+4MSmIFnkCQIxJaXLQdSsR90XWvAI2yvKUI7TCGqR8pQg2RtvkMHw=="
},
"@vant/use": {
"version": "1.5.2",
"resolved": "https://registry.npmmirror.com/@vant/use/-/use-1.5.2.tgz",
"integrity": "sha512-CBK61iT568dCHUwFFsErGbW6/5tmrPnZJKGtcSy7Tjcrmws8Ku+YZo7IUFD9Xkj9MfSJ4pfhQ7pU2KouP5Cojg==",
"requires": {}
},
"@vue/babel-helper-vue-jsx-merge-props": { "@vue/babel-helper-vue-jsx-merge-props": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz",
@@ -14104,6 +14168,11 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
"dev": true "dev": true
}, },
"blueimp-canvas-to-blob": {
"version": "3.29.0",
"resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.29.0.tgz",
"integrity": "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="
},
"body-parser": { "body-parser": {
"version": "1.19.2", "version": "1.19.2",
"resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.2.tgz", "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.19.2.tgz",
@@ -14573,6 +14642,15 @@
} }
} }
}, },
"compressorjs": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.2.1.tgz",
"integrity": "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ==",
"requires": {
"blueimp-canvas-to-blob": "^3.29.0",
"is-blob": "^2.1.0"
}
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
@@ -16485,6 +16563,11 @@
"binary-extensions": "^2.0.0" "binary-extensions": "^2.0.0"
} }
}, },
"is-blob": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz",
"integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="
},
"is-ci": { "is-ci": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/is-ci/-/is-ci-1.2.1.tgz",
@@ -19428,6 +19511,16 @@
"spdx-expression-parse": "^3.0.0" "spdx-expression-parse": "^3.0.0"
} }
}, },
"vant": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/vant/-/vant-4.5.0.tgz",
"integrity": "sha512-MK7TlTvp+n0HRFAi7SoRZwTt1pquJ2aUa8nQ899Mf+x9gi8OLYrMFqEQX+l1e4Cl4RO0vD1Q5w9rs4+Wehesog==",
"requires": {
"@vant/popperjs": "^1.3.0",
"@vant/use": "^1.5.1",
"@vue/shared": "^3.0.0"
}
},
"vary": { "vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",

View File

@@ -11,6 +11,7 @@
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"axios": "^0.27.2", "axios": "^0.27.2",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"compressorjs": "^1.2.1",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"element-plus": "^2.1.11", "element-plus": "^2.1.11",
"good-storage": "^1.1.1", "good-storage": "^1.1.1",
@@ -21,6 +22,7 @@
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qs": "^6.11.1", "qs": "^6.11.1",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"vant": "^4.5.0",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.15" "vue-router": "^4.0.15"
}, },

21
web/src/action/session.js Normal file
View File

@@ -0,0 +1,21 @@
import {httpGet} from "@/utils/http";
export function checkSession() {
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
resolve(res.data)
}).catch(err => {
reject(err)
})
})
}
export function checkAdminSession() {
return new Promise((resolve, reject) => {
httpGet('/api/admin/session').then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}

View File

@@ -0,0 +1,92 @@
<template>
<el-dialog
v-model="showDialog"
:close-on-click-modal="false"
:show-close="mobile !== ''"
:before-close="close"
:title="title"
>
<div class="form" id="bind-mobile-form">
<el-alert v-if="mobile !== ''" type="info" show-icon :closable="false" style="margin-bottom: 20px;">
<p>当前用户已绑定手机号{{ mobile }}, 绑定其他手机号之后自动解绑该手机号</p>
</el-alert>
<el-form :model="form" label-width="120px">
<el-form-item label="手机号码">
<el-input v-model="form.mobile"/>
</el-form-item>
<el-form-item label="手机验证码">
<el-row :gutter="10">
<el-col :span="12">
<el-input v-model.number="form.code" maxlength="6"/>
</el-col>
<el-col :span="12">
<send-msg size="" :mobile="form.mobile"/>
</el-col>
</el-row>
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="save">
提交绑定
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import {computed, ref} from "vue";
import SendMsg from "@/components/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {validateMobile} from "@/utils/validate";
const props = defineProps({
show: Boolean,
mobile: String
});
const showDialog = computed(() => {
return props.show
})
const title = ref('绑定手机号')
const form = ref({
mobile: '',
code: ''
})
const emits = defineEmits(['hide']);
const save = () => {
if (!validateMobile(form.value.mobile)) {
return ElMessage.error({message: "请输入正确的手机号码", appendTo: "#bind-mobile-form"});
}
if (form.value.code === '') {
return ElMessage.error({message: "请输入短信验证码", appendTo: "#bind-mobile-form"});
}
httpPost('/api/user/bind/mobile', form.value).then(() => {
ElMessage.success({
message: '绑定成功',
appendTo: '#bind-mobile-form',
duration: 1000,
onClose: () => emits('hide', false)
})
}).catch(e => {
ElMessage.error({message: "绑定失败:" + e.message, appendTo: "#bind-mobile-form"});
})
}
const close = function () {
emits('hide', false);
}
</script>
<style scoped>
</style>

View File

@@ -26,7 +26,7 @@ import {httpGet} from "@/utils/http";
export default defineComponent({ export default defineComponent({
name: 'ChatPrompt', name: 'ChatPrompt',
components: {Clock}, components: {Clock},
methods: {dateFormat}, methods: {},
props: { props: {
content: { content: {
type: String, type: String,

View File

@@ -3,29 +3,43 @@
v-model="showDialog" v-model="showDialog"
:close-on-click-modal="false" :close-on-click-modal="false"
:before-close="close" :before-close="close"
:top="top" :top="50+'px'"
title="用户设置" title="用户设置"
> >
<div class="user-info" id="user-info"> <div class="user-info" id="user-info">
<el-form v-if="form.id" :model="form" label-width="120px"> <el-form v-if="form.id" :model="form" label-width="120px">
<el-form-item label="昵称"> <el-form-item label="昵称">
<el-input v-model="form['nickname']"/> <el-input v-model="form.nickname"/>
</el-form-item> </el-form-item>
<el-form-item label="头像"> <el-form-item label="头像">
<el-input v-model="form['avatar']"/> <el-upload
class="avatar-uploader"
:auto-upload="true"
:show-file-list="false"
:http-request="afterRead"
>
<el-avatar v-if="form.avatar" :src="form.avatar" shape="square" :size="100"/>
<el-icon v-else class="avatar-uploader-icon">
<Plus/>
</el-icon>
</el-upload>
</el-form-item> </el-form-item>
<el-form-item label="用户名"> <el-form-item label="用户名">
<el-input v-model="form['username']" disabled/> <el-input v-model="form.username" readonly disabled/>
</el-form-item>
<el-form-item label="绑定手机号">
<el-input v-model="form.mobile" readonly disabled/>
</el-form-item> </el-form-item>
<el-form-item label="聊天上下文"> <el-form-item label="聊天上下文">
<el-switch v-model="form['chat_config']['enable_context']"/> <el-switch v-model="form.chat_config.enable_context"/>
</el-form-item> </el-form-item>
<el-form-item label="聊天记录"> <el-form-item label="聊天记录">
<el-switch v-model="form['chat_config']['enable_history']"/> <el-switch v-model="form.chat_config.enable_history"/>
</el-form-item> </el-form-item>
<el-form-item label="Model"> <el-form-item label="Model">
<el-select v-model="form['chat_config']['model']" placeholder="默认会话模型"> <el-select v-model="form.chat_config.model" placeholder="默认会话模型">
<el-option <el-option
v-for="item in models" v-for="item in models"
:key="item" :key="item"
@@ -35,15 +49,15 @@
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="MaxTokens"> <el-form-item label="MaxTokens">
<el-input v-model.number="form['chat_config']['max_tokens']"/> <el-input v-model.number="form.chat_config.max_tokens"/>
</el-form-item> </el-form-item>
<el-form-item label="Temperature"> <el-form-item label="Temperature">
<el-input v-model.number="form['chat_config']['temperature']"/> <el-input v-model.number="form.chat_config.temperature"/>
</el-form-item> </el-form-item>
<el-form-item label="剩余调用次数"> <el-form-item label="剩余调用次数">
<el-tag>{{ form['calls'] }}</el-tag> <el-tag>{{ form['calls'] }}</el-tag>
</el-form-item> </el-form-item>
<el-form-item label="剩余 Tokens"> <el-form-item label="消耗 Tokens">
<el-tag type="info">{{ form['tokens'] }}</el-tag> <el-tag type="info">{{ form['tokens'] }}</el-tag>
</el-form-item> </el-form-item>
<el-form-item label="API KEY"> <el-form-item label="API KEY">
@@ -69,7 +83,10 @@
import {computed, onMounted, ref} from "vue" import {computed, onMounted, ref} from "vue"
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {Plus} from "@element-plus/icons-vue";
import Compressor from "compressorjs";
// eslint-disable-next-line no-undef
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
user: Object, user: Object,
@@ -79,13 +96,14 @@ const props = defineProps({
const showDialog = computed(() => { const showDialog = computed(() => {
return props.show return props.show
}) })
const form = ref({}) const form = ref({
const top = computed(() => { username: '',
if (window.innerHeight < 768) { nickname: '',
return '1vh'; avatar: '',
} else { mobile: '',
return '15vh'; calls: 0,
} tokens: 0,
chat_configs: {}
}) })
onMounted(() => { onMounted(() => {
@@ -97,6 +115,30 @@ onMounted(() => {
}); });
}) })
const afterRead = (file) => {
console.log(file)
// 压缩图片并上传
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append('file', result, result.name);
// 执行上传操作
httpPost('/api/upload', formData).then((res) => {
form.value.avatar = res.data
ElMessage.success({message: '上传成功', appendTo: '#user-info', duration: 1000})
}).catch((e) => {
console.log(e.message)
ElMessage.error({message: '上传失败', appendTo: '#user-info'})
})
},
error(err) {
console.log(err.message);
},
});
};
// eslint-disable-next-line no-undef
const emits = defineEmits(['hide', 'update-user']); const emits = defineEmits(['hide', 'update-user']);
const save = function () { const save = function () {
httpPost('/api/user/profile/update', form.value).then(() => { httpPost('/api/user/profile/update', form.value).then(() => {

View File

@@ -55,6 +55,7 @@ const save = function () {
ElMessage.success({ ElMessage.success({
message: '更新成功', message: '更新成功',
appendTo: '#password-form', appendTo: '#password-form',
duration: 1000,
onClose: () => emits('logout', false) onClose: () => emits('logout', false)
}) })
}).catch((e) => { }).catch((e) => {

View File

@@ -0,0 +1,59 @@
<template>
<el-button type="primary" :disabled="!canSend" :size="props.size" @click="sendMsg" plain>{{
btnText
}}
</el-button>
</template>
<script setup>
// 发送短信验证码组件
import {ref} from "vue";
import {validateMobile} from "@/utils/validate";
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
const props = defineProps({
mobile: String,
size: String,
});
const btnText = ref('发送验证码')
const canSend = ref(true)
const sendMsg = () => {
if (!canSend.value) {
return
}
if (!validateMobile(props.mobile)) {
return ElMessage.error("请输入合法的手机号")
}
canSend.value = false
httpGet('/api/verify/token').then(res => {
httpPost('/api/verify/sms', {token: res.data, mobile: props.mobile}).then(() => {
ElMessage.success('短信发送成功')
let time = 10
btnText.value = time
const handler = setInterval(() => {
time = time - 1
if (time <= 0) {
clearInterval(handler)
btnText.value = '重新发送'
canSend.value = true
} else {
btnText.value = time
}
}, 1000)
}).catch(e => {
canSend.value = true
ElMessage.error('短信发送失败:' + e.message)
})
}).catch(e => {
console.log('failed to fetch token: ' + e.message)
})
}
</script>
<style scoped>
</style>

View File

@@ -92,16 +92,11 @@ const sidebar = useSidebarStore();
const title = ref('Chat-Plus 控制台') const title = ref('Chat-Plus 控制台')
const logo = ref('/images/logo.png') const logo = ref('/images/logo.png')
// 获取会话信息 // 加载系统配置
httpGet("/api/admin/session").then(() => { httpGet('/api/admin/config/get?key=system').then(res => {
// 加载系统配置 title.value = res.data['admin_title'];
httpGet('/api/admin/config/get?key=system').then(res => { }).catch(e => {
title.value = res.data['admin_title']; ElMessage.error("加载系统配置失败: " + e.message)
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
}).catch(() => {
router.replace('/admin/login')
}) })
// 侧边栏折叠 // 侧边栏折叠

View File

@@ -36,14 +36,24 @@
import {useTagsStore} from '@/store/tags'; import {useTagsStore} from '@/store/tags';
import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router'; import {onBeforeRouteUpdate, useRoute, useRouter} from 'vue-router';
import {ArrowDown, Close} from "@element-plus/icons-vue"; import {ArrowDown, Close} from "@element-plus/icons-vue";
import {checkAdminSession} from "@/action/session";
import {ElMessageBox} from "element-plus";
const route = useRoute();
const router = useRouter(); const router = useRouter();
checkAdminSession().catch(() => {
ElMessageBox({
title: '提示',
message: "当前会话已经失效,请重新登录",
confirmButtonText: 'OK',
callback: () => router.replace('/admin/login')
});
})
const isActive = (path) => { const isActive = (path) => {
return path === route.fullPath; return path === route.fullPath;
}; };
const tags = useTagsStore(); const tags = useTagsStore();
const route = useRoute();
// 关闭单个标签 // 关闭单个标签
const closeTags = (index) => { const closeTags = (index) => {
const delItem = tags.list[index]; const delItem = tags.list[index];

View File

@@ -1,86 +0,0 @@
<template>
<div class="chat-line chat-line-right">
<div class="chat-item">
<div class="content" v-html="content"></div>
<div class="triangle"></div>
</div>
<div class="chat-icon">
<img :src="icon" alt="User"/>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
export default defineComponent({
name: 'ChatPrompt',
props: {
content: {
type: String,
default: '',
},
icon: {
type: String,
default: 'images/user-icon.png',
}
},
data() {
return {}
},
})
</script>
<style lang="stylus" scoped>
.chat-line-right {
justify-content: flex-end;
.chat-icon {
margin-left 5px;
img {
border-radius 50%;
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid #223A34;
position: absolute;
right: 0;
top: 10px;
}
.content {
word-break break-word;
padding: 6px 10px;
background-color: #223A34;
color var(--content-color);
font-size: var(--content-font-size);
border-radius: 5px;
overflow: auto;
p {
line-height 1.5
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
}
}
}
</style>

View File

@@ -1,136 +0,0 @@
<template>
<div class="chat-line chat-line-left">
<div class="chat-icon">
<img :src="icon" alt="ChatGPT">
</div>
<div class="chat-item">
<div class="triangle"></div>
<div class="content-box">
<div class="content" v-html="content"></div>
<div class="tool-box">
<el-tooltip
class="box-item"
effect="light"
content="复制回答"
placement="bottom"
>
<el-button type="info" class="copy-reply" :data-clipboard-text="orgContent">
<el-icon>
<DocumentCopy/>
</el-icon>
</el-button>
</el-tooltip>
</div>
</div>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
import {DocumentCopy} from "@element-plus/icons-vue";
export default defineComponent({
name: 'ChatReply',
components: {DocumentCopy},
props: {
content: {
type: String,
default: '',
},
orgContent: {
type: String,
default: '',
},
icon: {
type: String,
default: 'images/gpt-icon.png',
}
},
data() {
return {}
},
})
</script>
<style lang="stylus">
.common-layout {
.chat-line-left {
justify-content: flex-start;
.chat-icon {
margin-right 5px;
img {
border-radius 50%;
}
}
.chat-item {
display: inline-block;
position: relative;
padding: 0 0 0 5px;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 6px solid #404042;
position: absolute;
left: 0;
top: 10px;
}
.content-box {
display flex
flex-direction row
.content {
min-height 20px;
word-break break-word;
padding: 6px 10px;
color var(--content-color)
background-color: #404042;
font-size: var(--content-font-size);
border-radius: 5px;
overflow auto;
p {
line-height 1.5
code {
color #f1f1f1
background-color #202121
padding 0 3px;
border-radius 5px;
}
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
}
.tool-box {
padding-left 10px;
font-size 16px;
.el-button {
height 20px
padding 5px 2px;
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,76 @@
<template>
<van-dialog v-model:show="showDialog"
:title="title"
:show-cancel-button="mobile !== ''"
@confirm="save"
@cancel="close">
<van-cell-group inset>
<van-field
v-model="form.mobile"
label="手机号"
placeholder="请输入手机号"
/>
<van-field
v-model.number="form.code"
center
clearable
label="短信验证码"
placeholder="请输入短信验证码"
>
<template #button>
<!-- <van-button size="small" type="primary">发送验证码</van-button>-->
<send-msg size="small" :mobile="form.mobile"/>
</template>
</van-field>
</van-cell-group>
</van-dialog>
</template>
<script setup>
import {computed, ref} from "vue";
import SendMsg from "@/components/mobile/SendMsg.vue";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
import {validateMobile} from "@/utils/validate";
import {showNotify} from "vant";
const props = defineProps({
show: Boolean,
mobile: String
});
const showDialog = computed(() => {
return props.show
})
const title = ref('绑定手机号')
const form = ref({
mobile: '',
code: ''
})
const emits = defineEmits(['hide']);
const save = () => {
if (!validateMobile(form.value.mobile)) {
return showNotify({type: 'danger', message: '请输入正确的手机号码'});
}
if (form.value.code === '') {
return showNotify({type: "danger", message: '请输入短信验证码'})
}
httpPost('/api/user/bind/mobile', form.value).then(() => {
showNotify({type: 'success', message: '绑定成功', duration: 1000, onClose: emits('hide', false)});
}).catch(e => {
showNotify({type: 'danger', message: '绑定失败:' + e.message, duration: 2000});
})
}
const close = function () {
emits('hide', false);
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,102 @@
<template>
<div class="mobile-message-prompt">
<div class="chat-item">
<div ref="contentRef" :data-clipboard-text="content" class="content" v-html="content"></div>
<div class="triangle"></div>
</div>
<div class="chat-icon">
<van-image :src="icon"/>
</div>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import Clipboard from "clipboard";
import {showNotify} from "vant";
const props = defineProps({
content: {
type: String,
default: '',
},
icon: {
type: String,
default: '/images/user-icon.png',
}
});
const contentRef = ref(null)
onMounted(() => {
const clipboard = new Clipboard(contentRef.value);
clipboard.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
})
</script>
<style lang="stylus">
.mobile-message-prompt {
display flex
justify-content: flex-end
.chat-icon {
margin-left 5px
.van-image {
width 32px
img {
border-radius 5px
}
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #98E165;
position: absolute;
right: 0;
top: 10px;
}
.content {
word-break break-word;
text-align left
padding: 5px 10px;
background-color: #98E165;
color #444444
font-size: 16px
border-radius: 5px
line-height 1.5
}
}
}
.van-theme-dark {
.mobile-message-prompt {
.chat-item {
.triangle {
border-left: 5px solid #223A34
}
.content {
background-color: #223A34
color #c1c1c1
}
}
}
}
</style>

View File

@@ -0,0 +1,145 @@
<template>
<div class="mobile-message-reply">
<div class="chat-icon">
<van-image :src="icon"/>
</div>
<div class="chat-item">
<div class="triangle"></div>
<div class="content-box">
<div ref="contentRef" :data-clipboard-text="orgContent" class="content" v-html="content"></div>
</div>
</div>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue"
import Clipboard from "clipboard";
import {showNotify} from "vant";
const props = defineProps({
content: {
type: String,
default: '',
},
orgContent: {
type: String,
default: '',
},
icon: {
type: String,
default: '/images/gpt-icon.png',
}
});
const contentRef = ref(null)
onMounted(() => {
const clipboard = new Clipboard(contentRef.value);
clipboard.on('success', () => {
showNotify({type: 'success', message: '复制成功', duration: 1000})
})
clipboard.on('error', () => {
showNotify({type: 'danger', message: '复制失败', duration: 2000})
})
})
</script>
<style lang="stylus">
.mobile-message-reply {
display flex
justify-content: flex-start;
.chat-icon {
margin-right 5px
.van-image {
width 32px
img {
border-radius 5px
}
}
}
.chat-item {
display: inline-block;
position: relative;
padding: 0 0 0 5px;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-right: 5px solid #fff;
position: absolute;
left: 0;
top: 13px;
}
.content-box {
display flex
flex-direction row
.content {
text-align left
width 100%
overflow-x auto
min-height 20px;
word-break break-word;
padding: 5px 10px;
color #444444
background-color: #ffffff;
font-size: 16px
border-radius: 5px;
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
p > code {
color #2b2b2b
background-color #c1c1c1
padding 2px 5px
border-radius 5px
}
}
}
}
}
.van-theme-dark {
.mobile-message-reply {
.chat-item {
.triangle {
border-right: 5px solid #404042;
}
.content-box {
.content {
color #c1c1c1
background-color: #404042;
p > code {
color #c1c1c1
background-color #2b2b2b
}
}
}
}
}
}
</style>

View File

@@ -0,0 +1,62 @@
<template>
<van-button size="small"
type="primary"
:disabled="!canSend"
:size="props.size"
@click="sendMsg">{{ btnText }}
</van-button>
</template>
<script setup>
// 发送短信验证码组件
import {ref} from "vue";
import {validateMobile} from "@/utils/validate";
import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http";
import {showNotify} from "vant";
const props = defineProps({
mobile: String,
size: String,
});
const btnText = ref('发送验证码')
const canSend = ref(true)
const sendMsg = () => {
if (!canSend.value) {
return
}
if (!validateMobile(props.mobile)) {
return showNotify({type: 'danger', message: '请输入合法的手机号'})
}
canSend.value = false
httpGet('/api/verify/token').then(res => {
httpPost('/api/verify/sms', {token: res.data, mobile: props.mobile}).then(() => {
showNotify({type: 'success', message: '短信发送成功'})
let time = 120
btnText.value = time
const handler = setInterval(() => {
time = time - 1
if (time <= 0) {
clearInterval(handler)
btnText.value = '重新发送'
canSend.value = true
} else {
btnText.value = time
}
}, 1000)
}).catch(e => {
canSend.value = true
showNotify({type: 'danger', message: '短信发送失败:' + e.message})
})
}).catch(e => {
console.log('failed to fetch token: ' + e.message)
})
}
</script>
<style scoped>
</style>

View File

@@ -1,112 +1,67 @@
import {createRouter, createWebHistory} from 'vue-router'
import {createApp} from 'vue' import {createApp} from 'vue'
import ElementPlus from "element-plus" import ElementPlus from "element-plus"
import "element-plus/dist/index.css" import "element-plus/dist/index.css"
import 'vant/lib/index.css';
import App from './App.vue' import App from './App.vue'
import ChatPlus from "@/views/ChatPlus.vue";
import NotFound from './views/404.vue'
import Home from "@/views/Home.vue";
import Login from "@/views/Login.vue"
import Register from "@/views/Register.vue";
import AdminLogin from "@/views/admin/Login.vue"
import {createPinia} from "pinia"; import {createPinia} from "pinia";
import {
const routes = [ Button,
{name: 'home', path: '/', component: Home, meta: {title: 'ChatGPT-Plus'}}, Cell,
{name: 'login', path: '/login', component: Login, meta: {title: '用户登录'}}, CellGroup,
{name: 'register', path: '/register', component: Register, meta: {title: '用户注册'}}, ConfigProvider,
{name: 'plus', path: '/chat', component: ChatPlus, meta: {title: 'ChatGPT-智能助手V3'}}, Dialog,
// {name: 'admin', path: '/admin', component: Admin, meta: {title: 'Chat-Plus 控制台'}}, DropdownItem,
{name: 'admin/login', path: '/admin/login', component: AdminLogin, meta: {title: 'Chat-Plus 控制台登录'}}, DropdownMenu,
{ Field,
name: 'admin', Form,
path: '/admin', Icon,
redirect: '/admin/welcome', Image,
component: () => import("@/views/admin/Home.vue"), List,
meta: {title: 'ChatGPT-Plus 管理后台'}, NavBar,
children: [ Notify,
{ Picker,
path: '/admin/welcome', Popup,
name: 'home', Search,
meta: {title: '系统首页'}, ShareSheet,
component: () => import('@/views/admin/Welcome.vue'), Sticky,
}, SwipeCell,
{ Switch,
path: '/admin/system', Tabbar,
name: 'system', TabbarItem,
meta: {title: '系统设置'}, Tag,
component: () => import('@/views/admin/SysConfig.vue'), TextEllipsis,
}, Uploader
{ } from "vant";
path: '/admin/user', import router from "@/router";
name: 'user',
meta: {title: '用户管理'},
component: () => import('@/views/admin/UserList.vue'),
},
{
path: '/admin/role',
name: 'role',
meta: {title: '角色管理'},
component: () => import('@/views/admin/RoleList.vue'),
},
{
path: '/admin/apikey',
name: 'apikey',
meta: {title: 'API-KEY 管理'},
component: () => import('@/views/admin/ApiKey.vue'),
},
{
path: '/admin/loginLog',
name: 'loginLog',
meta: {title: '登录日志'},
component: () => import('@/views/admin/LoginLog.vue'),
},
{
path: '/admin/demo/form',
name: 'form',
meta: {title: '表单页面'},
component: () => import('@/views/admin/demo/Form.vue'),
},
{
path: '/admin/demo/table',
name: 'table',
meta: {title: '数据列表'},
component: () => import('@/views/admin/demo/Table.vue'),
},
{
path: '/admin/demo/import',
name: 'import',
meta: {title: '导入数据'},
component: () => import('@/views/admin/demo/Import.vue'),
},
{
path: '/admin/demo/editor',
name: 'editor',
meta: {title: '富文本编辑器'},
component: () => import('@/views/admin/demo/Editor.vue'),
},
]
},
{name: 'test', path: '/test', component: () => import('@/views/Test.vue'), meta: {title: '测试页面'}},
{name: 'NotFound', path: '/:all(.*)', component: NotFound, meta: {title: '页面没有找到'}},
]
// console.log(MY_VARIABLE)
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
// dynamic change the title when router change
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = `${to.meta.title} | ChatGPT-PLUS`
}
next()
})
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(createPinia())
app.use(ConfigProvider);
app.use(Tabbar);
app.use(TabbarItem);
app.use(NavBar);
app.use(Search);
app.use(Cell)
app.use(Image)
app.use(TextEllipsis)
app.use(Notify)
app.use(Picker)
app.use(Popup)
app.use(List);
app.use(Form);
app.use(Field);
app.use(CellGroup);
app.use(Button);
app.use(DropdownMenu);
app.use(Icon);
app.use(DropdownItem);
app.use(Sticky);
app.use(SwipeCell);
app.use(Dialog);
app.use(ShareSheet);
app.use(Switch);
app.use(Uploader);
app.use(Tag);
app.use(router).use(ElementPlus).mount('#app') app.use(router).use(ElementPlus).mount('#app')

162
web/src/router.js Normal file
View File

@@ -0,0 +1,162 @@
import {createRouter, createWebHistory} from "vue-router";
const routes = [
{
name: 'home',
path: '/',
meta: {title: 'ChatGPT-Plus'},
component: () => import('@/views/Home.vue'),
},
{
name: 'login',
path: '/login',
meta: {title: '用户登录'},
component: () => import('@/views/Login.vue'),
},
{
name: 'register',
path: '/register',
meta: {title: '用户注册'},
component: () => import('@/views/Register.vue'),
},
{
name: 'plus',
path: '/chat',
meta: {title: 'ChatGPT-智能助手V3'},
component: () => import('@/views/ChatPlus.vue'),
},
{
path: '/admin/login',
name: 'admin-login',
meta: {title: 'Chat-Plus 控制台登录'},
component: () => import('@/views/admin/Login.vue'),
},
{
name: 'admin',
path: '/admin',
redirect: '/admin/welcome',
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/system',
name: 'admin-system',
meta: {title: '系统设置'},
component: () => import('@/views/admin/SysConfig.vue'),
},
{
path: '/admin/user',
name: 'admin-user',
meta: {title: '用户管理'},
component: () => import('@/views/admin/UserList.vue'),
},
{
path: '/admin/role',
name: 'admin-role',
meta: {title: '角色管理'},
component: () => import('@/views/admin/RoleList.vue'),
},
{
path: '/admin/apikey',
name: 'admin-apikey',
meta: {title: 'API-KEY 管理'},
component: () => import('@/views/admin/ApiKey.vue'),
},
{
path: '/admin/loginLog',
name: 'admin-loginLog',
meta: {title: '登录日志'},
component: () => import('@/views/admin/LoginLog.vue'),
},
{
path: '/admin/demo/form',
name: 'admin-form',
meta: {title: '表单页面'},
component: () => import('@/views/admin/demo/Form.vue'),
},
{
path: '/admin/demo/table',
name: 'admin-table',
meta: {title: '数据列表'},
component: () => import('@/views/admin/demo/Table.vue'),
},
{
path: '/admin/demo/import',
name: 'admin-import',
meta: {title: '导入数据'},
component: () => import('@/views/admin/demo/Import.vue'),
},
{
path: '/admin/demo/editor',
name: 'admin-editor',
meta: {title: '富文本编辑器'},
component: () => import('@/views/admin/demo/Editor.vue'),
},
]
},
{
path: '/mobile/chat/session',
name: 'mobile-chat-session',
component: () => import('@/views/mobile/ChatSession.vue'),
},
{
name: 'mobile',
path: '/mobile',
meta: {title: 'ChatGPT-智能助手V3'},
component: () => import('@/views/mobile/Home.vue'),
redirect: '/mobile/chat/list',
children: [
{
path: '/mobile/chat/list',
name: 'mobile-chat-list',
component: () => import('@/views/mobile/ChatList.vue'),
},
{
path: '/mobile/setting',
name: 'mobile-setting',
component: () => import('@/views/mobile/Setting.vue'),
},
{
path: '/mobile/profile',
name: 'mobile-profile',
component: () => import('@/views/mobile/Profile.vue'),
},
]
},
{
name: 'test',
path: '/test',
meta: {title: '测试页面'},
component: () => import('@/views/Test.vue'),
},
{
name: 'NotFound',
path: '/:all(.*)',
meta: {title: '页面没有找到'},
component: () => import('@/views/404.vue'),
},
]
// console.log(MY_VARIABLE)
const router = createRouter({
history: createWebHistory(),
routes: routes,
})
// dynamic change the title when router change
router.beforeEach((to, from, next) => {
if (to.meta.title) {
document.title = `${to.meta.title} | ChatGPT-PLUS`
}
next()
})
export default router;

11
web/src/store/chat.js Normal file
View File

@@ -0,0 +1,11 @@
import Storage from 'good-storage'
const CHAT_CONFIG_KEY = "chat_config"
export function getChatConfig() {
return Storage.get(CHAT_CONFIG_KEY)
}
export function setChatConfig(chatConfig) {
Storage.set(CHAT_CONFIG_KEY, chatConfig)
}

11
web/src/store/system.js Normal file
View File

@@ -0,0 +1,11 @@
import Storage from "good-storage";
const MOBILE_THEME = "MOBILE_THEME"
export function getMobileTheme() {
return Storage.get(MOBILE_THEME) ? Storage.get(MOBILE_THEME) : 'light'
}
export function setMobileTheme(theme) {
Storage.set(MOBILE_THEME, theme)
}

View File

@@ -1,5 +1,5 @@
import axios from 'axios' import axios from 'axios'
import {getSessionId} from "@/utils/storage"; import {getSessionId} from "@/store/session";
axios.defaults.timeout = 10000 axios.defaults.timeout = 10000
axios.defaults.baseURL = process.env.VUE_APP_API_HOST axios.defaults.baseURL = process.env.VUE_APP_API_HOST

11
web/src/utils/validate.js Normal file
View File

@@ -0,0 +1,11 @@
// 正则校验工具函数
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function validateMobile(mobile) {
const regex = /^1[345789]\d{9}$/;
return regex.test(mobile);
}

View File

@@ -1,17 +1,40 @@
<template> <template>
<div>{{ title }}</div> <div class="page-404" :style="{ height: winHeight + 'px' }">
<div class="inner">
<h1>404</h1>
<h2>饶了地球一圈还是没有找到您要的页面</h2>
</div>
</div>
</template> </template>
<script> <script setup>
import { defineComponent } from "vue" import {ref} from "vue"
export default defineComponent({ const winHeight = ref(window.innerHeight)
name: 'NotFound',
data () {
return {
title: "404 Page",
}
},
})
</script> </script>
<style lang="stylus" scoped>
.page-404 {
display: flex;
justify-content: center;
background-color: #282c34;
.inner {
text-align center
h1 {
color: #202020;
font-size: 120px;
font-weight: bold;
letter-spacing: 0.1em;
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
}
h2 {
color #ffffff;
font-weight: bold;
}
}
}
</style>

View File

@@ -40,10 +40,10 @@
</div> </div>
<div class="tool-box"> <div class="tool-box">
<el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="user"> <el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="isLogin">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<el-image :src="user['avatar']"/> <el-image :src="loginUser.avatar"/>
<span class="username">{{ user ? user['nickname'] : 'Chat-Plus-User' }}</span> <span class="username">{{ loginUser.nickname }}</span>
<el-icon><ArrowDown/></el-icon> <el-icon><ArrowDown/></el-icon>
</span> </span>
<template #dropdown> <template #dropdown>
@@ -60,6 +60,13 @@
<span>修改密码</span> <span>修改密码</span>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item @click="showBindMobileDialog = true">
<el-icon>
<Iphone/>
</el-icon>
<span>绑定手机号</span>
</el-dropdown-item>
<el-dropdown-item @click="clearAllChats"> <el-dropdown-item @click="clearAllChats">
<el-icon> <el-icon>
<Delete/> <Delete/>
@@ -187,6 +194,9 @@
@update-user="updateUser"/> @update-user="updateUser"/>
<password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false" <password-dialog v-if="isLogin" :show="showPasswordDialog" @hide="showPasswordDialog = false"
@logout="logout"/> @logout="logout"/>
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :mobile="loginUser.mobile"
@hide="showBindMobileDialog = false"/>
</div> </div>
@@ -200,7 +210,7 @@ import {
Check, Check,
Close, Close,
Delete, Delete,
Edit, Edit, Iphone,
Plus, Plus,
Promotion, Promotion,
RefreshRight, RefreshRight,
@@ -209,15 +219,17 @@ import {
VideoPause VideoPause
} from '@element-plus/icons-vue' } from '@element-plus/icons-vue'
import 'highlight.js/styles/a11y-dark.css' import 'highlight.js/styles/a11y-dark.css'
import {dateFormat, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs"; import {dateFormat, isMobile, randString, removeArrayItem, renderInputText, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import hl from "highlight.js"; import hl from "highlight.js";
import {getLoginUser, getSessionId, removeLoginUser} from "@/utils/storage"; import {getSessionId, removeLoginUser} from "@/store/session";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import ConfigDialog from "@/components/ConfigDialog.vue"; import ConfigDialog from "@/components/ConfigDialog.vue";
import PasswordDialog from "@/components/PasswordDialog.vue"; import PasswordDialog from "@/components/PasswordDialog.vue";
import {checkSession} from "@/action/session";
import BindMobile from "@/components/BindMobile.vue";
const title = ref('ChatGPT-智能助手'); const title = ref('ChatGPT-智能助手');
const logo = 'images/logo.png'; const logo = 'images/logo.png';
@@ -231,21 +243,30 @@ const mainWinHeight = ref(0); // 主窗口高度
const chatBoxHeight = ref(0); // 聊天内容框高度 const chatBoxHeight = ref(0); // 聊天内容框高度
const leftBoxHeight = ref(0); const leftBoxHeight = ref(0);
const loading = ref(true); const loading = ref(true);
const user = ref(getLoginUser()); const loginUser = ref(null);
const roles = ref([]); const roles = ref([]);
const roleId = ref(0) const roleId = ref(0)
const newChatItem = ref(null); const newChatItem = ref(null);
const router = useRouter(); const router = useRouter();
const showConfigDialog = ref(false); const showConfigDialog = ref(false);
const showPasswordDialog = ref(false); const showPasswordDialog = ref(false);
const showBindMobileDialog = ref(false);
const isLogin = ref(false) const isLogin = ref(false)
if (isMobile()) {
router.replace("/mobile")
}
onMounted(() => { onMounted(() => {
resizeElement(); resizeElement();
checkSession().then(() => { checkSession().then((user) => {
loginUser.value = user
isLogin.value = true isLogin.value = true
if (user.mobile === '') {
showBindMobileDialog.value = true
}
// 加载角色列表 // 加载角色列表
httpGet(`/api/role/list?user_id=${user.value.id}`).then((res) => { httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
roles.value = res.data; roles.value = res.data;
roleId.value = roles.value[0]['id']; roleId.value = roles.value[0]['id'];
// 获取会话列表 // 获取会话列表
@@ -264,7 +285,8 @@ onMounted(() => {
}).catch(e => { }).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message) ElMessage.error("加载系统配置失败: " + e.message)
}) })
}).catch(() => { }).catch((e) => {
console.log(e)
router.push('login') router.push('login')
}); });
@@ -278,19 +300,9 @@ onMounted(() => {
}) })
}); });
const checkSession = function () {
return new Promise((resolve, reject) => {
httpGet('/api/user/session').then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})
}
// 加载会话 // 加载会话
const loadChats = function () { const loadChats = function () {
httpGet("/api/chat/list?user_id=" + user.value.id).then((res) => { httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) { if (res.data) {
chatList.value = res.data; chatList.value = res.data;
allChats.value = res.data; allChats.value = res.data;
@@ -313,7 +325,7 @@ const getRoleById = function (rid) {
const resizeElement = function () { const resizeElement = function () {
chatBoxHeight.value = window.innerHeight - 51 - 82 - 38; chatBoxHeight.value = window.innerHeight - 51 - 82 - 38;
mainWinHeight.value = window.innerHeight - 51; mainWinHeight.value = window.innerHeight - 51;
leftBoxHeight.value = window.innerHeight - 51 - 100; leftBoxHeight.value = window.innerHeight - 43 - 47 - 45;
}; };
// 新建会话 // 新建会话
@@ -414,7 +426,6 @@ const removeChat = function (event, chat) {
// 创建 socket 连接 // 创建 socket 连接
const prompt = ref(''); const prompt = ref('');
// const replyIcon = 'images/avatar/gpt.png';// 回复信息的头像
const showStopGenerate = ref(false); // 停止生成 const showStopGenerate = ref(false); // 停止生成
const showReGenerate = ref(false); // 重新生成 const showReGenerate = ref(false); // 重新生成
const previousText = ref(''); // 上一次提问 const previousText = ref(''); // 上一次提问
@@ -507,7 +518,7 @@ const connect = function (chat_id, role_id) {
} else { } else {
lineBuffer.value += data.content; lineBuffer.value += data.content;
let md = require('markdown-it')(); const md = require('markdown-it')({breaks: true});
const reply = chatData.value[chatData.value.length - 1] const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value; reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(lineBuffer.value); reply['content'] = md.render(lineBuffer.value);
@@ -579,7 +590,7 @@ const sendMessage = function () {
chatData.value.push({ chatData.value.push({
type: "prompt", type: "prompt",
id: randString(32), id: randString(32),
icon: user.value.avatar, icon: loginUser.value.avatar,
content: renderInputText(prompt.value), content: renderInputText(prompt.value),
created_at: new Date().getTime(), created_at: new Date().getTime(),
}); });
@@ -645,7 +656,7 @@ const loadChatHistory = function (chatId) {
return return
} }
const md = require('markdown-it')(); const md = require('markdown-it')({breaks: true});
// md.use(require('markdown-it-copy')); // 代码复制功能 // md.use(require('markdown-it-copy')); // 代码复制功能
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") { if (data[i].type === "prompt") {
@@ -703,20 +714,21 @@ const chatName = ref('')
// 搜索会话 // 搜索会话
const searchChat = function () { const searchChat = function () {
if (chatName.value === '') { if (chatName.value === '') {
chatList.value = allChats.value
return return
} }
const roles = []; const items = [];
for (let i = 0; i < allChats.value.length; i++) { for (let i = 0; i < allChats.value.length; i++) {
if (allChats.value[i].title.indexOf(chatName.value) !== -1) { if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
roles.push(allChats.value[i]); items.push(allChats.value[i]);
} }
} }
chatList.value = roles; chatList.value = items;
} }
const updateUser = function (data) { const updateUser = function (data) {
user.value.avatar = data.avatar; loginUser.value.avatar = data.avatar;
user.value.nickname = data.nickname; loginUser.value.nickname = data.nickname;
} }
</script> </script>
@@ -791,9 +803,12 @@ $borderColor = #4676d0;
width: 100% width: 100%
justify-content: flex-start justify-content: flex-start
padding: 8px 12px padding: 8px 12px
border-bottom: 1px solid #3c3c3c //border-bottom: 1px solid #3c3c3c
cursor: pointer cursor: pointer
&:hover {
background-color #343540
}
.avatar { .avatar {
width: 28px; width: 28px;
@@ -835,7 +850,7 @@ $borderColor = #4676d0;
} }
.chat-list-item.active { .chat-list-item.active {
background-color: #363535; background-color: #343540;
.btn { .btn {
display inline display inline
@@ -849,7 +864,7 @@ $borderColor = #4676d0;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
padding 5px 20px; padding 0 20px 10px 20px;
border-top 1px solid #3c3c3c; border-top 1px solid #3c3c3c;
.user-info { .user-info {

View File

@@ -1,40 +1,23 @@
<template> <template>
<div class="home" :style="{ height: winHeight + 'px' }"> <h1>{{ title }}</h1>
<h1>{{ title }}</h1>
</div>
</template> </template>
<script setup> <script setup>
import {ref} from "vue" import {ref} from "vue"
import {useRouter} from "vue-router"; // 导入useRouter函数 import {useRouter} from "vue-router";
import {checkSession} from "@/action/session";
import {isMobile} from "@/utils/libs";
const title = ref("HI, ChatGPT PLUS!"); const title = ref("HI, ChatGPT PLUS!");
const winHeight = ref(window.innerHeight)
const router = useRouter(); const router = useRouter();
// onMounted(() => { checkSession().then(() => {
// if (isMobile()) { if (isMobile()) {
// router.push("mobile"); router.push("/mobile")
// } else { } else {
// router.push("chat"); router.push("/chat")
// } }
// }) }).catch(() => {
router.push("login")
})
</script> </script>
<style lang="stylus" scoped>
.home {
display: flex;
justify-content: center;
align-items: center;
color: #202020;
background-color: #282c34;
h1 {
font-size: 300%;
font-weight: bold;
letter-spacing: 0.1em;
text-shadow: -1px -1px 1px #111111, 2px 2px 1px #363636;
}
}
</style>

View File

@@ -4,7 +4,7 @@
<div class="main"> <div class="main">
<div class="contain"> <div class="contain">
<div class="logo"> <div class="logo">
<el-image src="images/logo.png" fit="cover" /> <el-image src="images/logo.png" fit="cover"/>
</div> </div>
<div class="header">{{ title }}</div> <div class="header">{{ title }}</div>
<div class="content"> <div class="content">
@@ -40,7 +40,7 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<footer-bar /> <footer-bar/>
</footer> </footer>
</div> </div>
</div> </div>
@@ -52,9 +52,10 @@ import {onMounted, ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue"; import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http"; import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {setLoginUser} from "@/utils/storage"; import {setLoginUser} from "@/store/session";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue"; import FooterBar from "@/components/FooterBar.vue";
import {isMobile} from "@/utils/libs";
const router = useRouter(); const router = useRouter();
const title = ref('ChatGPT-PLUS 用户登录'); const title = ref('ChatGPT-PLUS 用户登录');
@@ -79,8 +80,11 @@ const login = function () {
httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => { httpPost('/api/user/login', {username: username.value.trim(), password: password.value.trim()}).then((res) => {
setLoginUser(res.data) setLoginUser(res.data)
router.push("chat") if (isMobile()) {
router.push('/mobile')
} else {
router.push('chat')
}
}).catch((e) => { }).catch((e) => {
ElMessage.error('登录失败,' + e.message) ElMessage.error('登录失败,' + e.message)
}) })
@@ -118,6 +122,7 @@ const login = function () {
.logo { .logo {
text-align center text-align center
.el-image { .el-image {
width 120px; width 120px;
} }

View File

@@ -9,9 +9,9 @@
<div class="header">{{ title }}</div> <div class="header">{{ title }}</div>
<div class="content"> <div class="content">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="rules"> <el-form :model="formData" label-width="120px" ref="formRef">
<div class="block"> <div class="block">
<el-input placeholder="手机号/邮箱(4-30位)" <el-input placeholder="请输入用户名(4-30位)"
size="large" maxlength="30" size="large" maxlength="30"
v-model="formData.username" v-model="formData.username"
autocomplete="off"> autocomplete="off">
@@ -48,6 +48,39 @@
</el-input> </el-input>
</div> </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">
<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-row class="btn-row">
<el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button> <el-button class="login-btn" size="large" type="primary" @click="register">注册</el-button>
</el-row> </el-row>
@@ -70,17 +103,20 @@
<script setup> <script setup>
import {ref} from "vue"; import {ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue"; import {Checked, Iphone, Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http"; import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue"; import FooterBar from "@/components/FooterBar.vue";
import SendMsg from "@/components/SendMsg.vue";
const router = useRouter(); const router = useRouter();
const title = ref('ChatGPT-PLUS 用户注册'); const title = ref('ChatGPT-PLUS 用户注册');
const formData = ref({ const formData = ref({
username: '', username: '',
password: '', password: '',
mobile: '',
code: '',
repass: '', repass: '',
}) })
const formRef = ref(null) const formRef = ref(null)
@@ -89,9 +125,6 @@ const register = function () {
if (formData.value.username.length < 4) { if (formData.value.username.length < 4) {
return ElMessage.error('用户名的长度为4-30个字符'); return ElMessage.error('用户名的长度为4-30个字符');
} }
if (!validateEmail(formData.value.username) && !validateMobile(formData.value.username)) {
return ElMessage.error('用户名不合法,请输入手机号码或者邮箱地址');
}
if (formData.value.password.length < 8) { if (formData.value.password.length < 8) {
return ElMessage.error('密码的长度为8-16个字符'); return ElMessage.error('密码的长度为8-16个字符');
} }
@@ -106,15 +139,6 @@ const register = function () {
}) })
} }
const validateEmail = function (email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
const validateMobile = function (mobile) {
const regex = /^1[345789]\d{9}$/;
return regex.test(mobile);
}
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>

View File

@@ -1,165 +1,14 @@
<template> <template>
<div class="role-list"> <div>{{ title }}</div>
<el-form :model="form1" label-width="120px" ref="formRef" :rules="rules">
<el-form-item label="角色名称:" prop="name">
<el-input
v-model="form1.name"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="角色标志:" prop="key">
<el-input
v-model="form1.key"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="角色图标:" prop="icon">
<el-input
v-model="form1.icon"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="打招呼信息:" prop="hello_msg">
<el-input
v-model="form1.hello_msg"
autocomplete="off"
/>
</el-form-item>
<el-form-item label="上下文信息:" prop="context">
<template #default>
<el-table :data="form1.context" :border="childBorder" size="small">
<el-table-column label="对话角色" width="120">
<template #default="scope">
<el-input
v-model="scope.row.role"
autocomplete="off"
/>
</template>
</el-table-column>
<el-table-column label="对话内容">
<template #header>
<div class="context-msg-key">
<span>对话内容</span>
<span class="fr">
<el-button type="primary" @click="addContext" size="small">
<el-icon>
<Plus/>
</el-icon>
增加一行
</el-button>
</span>
</div>
</template>
<template #default="scope">
<div class="context-msg-content">
<el-input
v-model="scope.row.content"
autocomplete="off"
/>
<span><el-icon @click="removeContext(scope.$index)"><RemoveFilled/></el-icon></span>
</div>
</template>
</el-table-column>
</el-table>
</template>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form1.enable"/>
</el-form-item>
</el-form>
<span class="dialog-footer">
<el-button @click="showDialog = false">取消</el-button>
<el-button type="primary" @click="doUpdate">保存</el-button>
</span>
</div>
</template> </template>
<script setup> <script setup>
import {Plus, RemoveFilled} from "@element-plus/icons-vue"; import {ref} from "vue";
import {reactive, ref} from "vue";
import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
const showDialog = ref(false) const title = ref('Test Page')
const childBorder = ref(true)
const form1 = ref({context: []})
// const form2 = ref({context: []})
const formRef = ref(null)
const rules = reactive({
name: [{required: true, message: '请输入用户名', trigger: 'change',}],
key: [{required: true, message: '请输入角色标识', trigger: 'change',}],
icon: [{required: true, message: '请输入角色图标', trigger: 'change',}],
hello_msg: [{required: true, message: '请输入打招呼信息', trigger: 'change',}]
})
// 编辑
const doUpdate = function () {
formRef.value.validate((valid) => {
if (valid) {
showDialog.value = false
httpPost('/api/admin/chat-roles/set', form1.value).then(() => {
ElMessage.success('更新角色成功')
// 更新当前数据行
}).catch((e) => {
ElMessage.error('更新角色失败,' + e.message)
})
} else {
return false
}
})
}
const addContext = function () {
form1.value.context.push({role: '', content: ''})
}
const removeContext = function (index) {
form1.value.context.splice(index, 1);
}
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.role-list {
.opt-box {
padding-bottom: 10px;
.el-icon {
margin-right 5px;
}
}
.context-msg-key {
.fr {
float right
.el-icon {
margin-right 5px
}
}
}
.context-msg-content {
display flex
.el-icon {
font-size: 20px;
margin-top 5px;
margin-left 5px;
cursor pointer
}
}
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="admin-home"> <div class="admin-home" v-if="isLogin">
<admin-header/> <admin-header/>
<admin-sidebar/> <admin-sidebar/>
<div class="content-box" :class="{ 'content-collapse': sidebar.collapse }"> <div class="content-box" :class="{ 'content-collapse': sidebar.collapse }">
@@ -22,9 +22,21 @@ import {useTagsStore} from '@/store/tags';
import AdminHeader from "@/components/admin/AdminHeader.vue"; import AdminHeader from "@/components/admin/AdminHeader.vue";
import AdminSidebar from "@/components/admin/AdminSidebar.vue"; import AdminSidebar from "@/components/admin/AdminSidebar.vue";
import AdminTags from "@/components/admin/AdminTags.vue"; import AdminTags from "@/components/admin/AdminTags.vue";
import {useRouter} from "vue-router";
import {checkAdminSession} from "@/action/session";
import {ref} from "vue";
const sidebar = useSidebarStore(); const sidebar = useSidebarStore();
const tags = useTagsStore(); const tags = useTagsStore();
const isLogin = ref(false)
// 获取会话信息
const router = useRouter();
checkAdminSession().then(() => {
isLogin.value = true
}).catch(() => {
router.replace('/admin/login')
})
</script> </script>
<style scoped lang="stylus"> <style scoped lang="stylus">

View File

@@ -48,7 +48,7 @@ import {onMounted, ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue"; import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http"; import {httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {setLoginUser} from "@/utils/storage"; import {setLoginUser} from "@/store/session";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue"; import FooterBar from "@/components/FooterBar.vue";

View File

@@ -10,7 +10,7 @@
<el-input v-model="system['admin_title']"/> <el-input v-model="system['admin_title']"/>
</el-form-item> </el-form-item>
<el-form-item label="注册赠送次数" prop="init_calls"> <el-form-item label="注册赠送次数" prop="init_calls">
<el-input v-model.number="system['init_calls']" placeholder="新用户注册赠送对话次数"/> <el-input v-model.number="system['user_init_calls']" placeholder="新用户注册赠送对话次数"/>
</el-form-item> </el-form-item>
<el-alert type="info" show-icon :closable="false"> <el-alert type="info" show-icon :closable="false">
<p>在这里维护前端聊天页面可用的 GPT 模型列表</p> <p>在这里维护前端聊天页面可用的 GPT 模型列表</p>
@@ -84,7 +84,6 @@ const system = ref({models: []})
const chat = ref({}) const chat = ref({})
const loading = ref(true) const loading = ref(true)
const systemFormRef = ref(null) const systemFormRef = ref(null)
const tempModel = ref('')
const models = ref([]) const models = ref([])
onMounted(() => { onMounted(() => {

View File

@@ -38,8 +38,8 @@
<el-pagination v-if="users.total > 0" <el-pagination v-if="users.total > 0"
background background
layout="total, prev, pager, next" layout="total, prev, pager, next"
:current-page="users.page" v-model:current-page="users.page"
:page-size="users.page_size" v-model:page-size="users.page_size"
:total="users.total" :total="users.total"
@current-change="fetchUserList(users.page, users.page_size)" @current-change="fetchUserList(users.page, users.page_size)"
/> />
@@ -110,7 +110,7 @@ import {ElMessage, ElMessageBox} from "element-plus";
import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs"; import {dateFormat, disabledDate, removeArrayItem} from "@/utils/libs";
// 变量定义 // 变量定义
const users = ref({}) const users = ref({page: 1, page_size: 15})
const user = ref({chat_roles: []}) const user = ref({chat_roles: []})
const roles = ref([]) const roles = ref([])
@@ -128,7 +128,7 @@ const loading = ref(true)
const userEditFormRef = ref(null) const userEditFormRef = ref(null)
onMounted(() => { onMounted(() => {
fetchUserList(1, 10) fetchUserList(users.value.page, users.value.page_size)
// 获取角色列表 // 获取角色列表
httpGet('/api/admin/role/list').then((res) => { httpGet('/api/admin/role/list').then((res) => {
roles.value = res.data; roles.value = res.data;

View File

@@ -0,0 +1,283 @@
<template>
<div v-if="isLogin" class="container mobile-chat-list">
<van-nav-bar
:title="title"
left-text="新建会话"
@click-left="showPicker = true">
<template #right>
<van-icon name="delete-o" @click="clearAllChatHistory"/>
</template>
</van-nav-bar>
<div class="content">
<van-search
v-model="chatName"
input-align="center"
placeholder="请输入会话标题"
@input="search"
/>
<van-list
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败点击重新加载"
finished-text="没有更多了"
@load="onLoad"
>
<van-swipe-cell v-for="item in chats" :key="item.id">
<van-cell @click="changeChat(item)">
<div class="chat-list-item">
<van-image
:src="item.icon"
round
/>
<div class="van-ellipsis">{{ item.title }}</div>
</div>
</van-cell>
<template #right>
<van-button square text="修改" type="primary" @click="editChat(item)"/>
<van-button square text="删除" type="danger" @click="removeChat(item)"/>
</template>
</van-swipe-cell>
</van-list>
</div>
<van-popup v-model:show="showPicker" position="bottom">
<van-picker
:columns="columns"
title="选择模型和角色"
@cancel="showPicker = false"
@confirm="newChat"
>
<template #option="item">
<div class="picker-option">
<van-image
v-if="item.icon"
:src="item.icon"
fit="cover"
round
/>
<span>{{ item.text }}</span>
</div>
</template>
</van-picker>
</van-popup>
<bind-mobile v-if="isLogin" :show="showBindMobileDialog" :mobile="loginUser.mobile"
@hide="showBindMobileDialog = false"/>
</div>
</template>
<script setup>
import {ref} from "vue";
import {httpGet} from "@/utils/http";
import {showConfirmDialog, showFailToast, showSuccessToast, showToast} from "vant";
import {checkSession} from "@/action/session";
import router from "@/router";
import {setChatConfig} from "@/store/chat";
import {removeArrayItem} from "@/utils/libs";
import BindMobile from "@/components/mobile/BindMobile.vue";
const title = ref("会话列表")
const chatName = ref("")
const chats = ref([])
const allChats = ref([])
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const loginUser = ref(null)
const isLogin = ref(false)
const roles = ref([])
const models = ref([])
const showPicker = ref(false)
const columns = ref([roles.value, models.value])
const showBindMobileDialog = ref(false)
checkSession().then((user) => {
loginUser.value = user
isLogin.value = true
if (user.mobile === '') {
showBindMobileDialog.value = true
}
// 加载角色列表
httpGet(`/api/role/list?user_id=${user.id}`).then((res) => {
if (res.data) {
const items = res.data
for (let i = 0; i < items.length; i++) {
// console.log(items[i])
roles.value.push({
text: items[i].name,
value: items[i].id,
icon: items[i].icon,
helloMsg: items[i].hello_msg
})
}
}
}).catch(() => {
showFailToast("加载聊天角色失败")
})
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {
if (res.data) {
const items = res.data.models
for (let i = 0; i < items.length; i++) {
models.value.push({text: items[i].toUpperCase(), value: items[i]})
}
}
}).catch(() => {
showFailToast("加载系统配置失败")
})
}).catch(() => {
router.push("/login")
})
const onLoad = () => {
httpGet("/api/chat/list?user_id=" + loginUser.value.id).then((res) => {
if (res.data) {
chats.value = res.data;
allChats.value = res.data;
finished.value = true
}
loading.value = false;
}).catch(() => {
error.value = true
showFailToast("加载会话列表失败")
})
};
const search = () => {
if (chatName.value === '') {
chats.value = allChats.value
return
}
const items = [];
for (let i = 0; i < allChats.value.length; i++) {
if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
items.push(allChats.value[i]);
}
}
chats.value = items;
}
const clearAllChatHistory = () => {
showConfirmDialog({
title: '操作提示',
message: '确定要删除所有的会话记录吗?'
}).then(() => {
httpGet("/api/chat/clear").then(() => {
showSuccessToast('所有聊天记录已清空')
chats.value = [];
}).catch(e => {
showFailToast("操作失败:" + e.message)
})
}).catch(() => {
// on cancel
})
}
const newChat = (item) => {
showPicker.value = false
const options = item.selectedOptions
setChatConfig({
role: {
id: options[0].value,
name: options[0].text,
icon: options[0].icon,
helloMsg: options[0].helloMsg
},
model: options[1].value,
title: '新建会话',
chatId: 0
})
router.push('/mobile/chat/session')
}
const changeChat = (chat) => {
let role = {}
for (let i = 0; i < roles.value.length; i++) {
if (roles.value[i].value === chat.role_id) {
role = roles.value[i]
break
}
}
setChatConfig({
role: {
id: chat.role_id,
name: role.text,
icon: role.icon
},
model: chat.model,
title: chat.title,
chatId: chat.chat_id,
helloMsg: chat.hello_msg,
})
router.push('/mobile/chat/session')
}
const editChat = (item) => {
showToast('修改会话标题')
}
const removeChat = (item) => {
httpGet('/api/chat/remove?chat_id=' + item.chat_id).then(() => {
chats.value = removeArrayItem(chats.value, item, function (e1, e2) {
return e1.id === e2.id
})
}).catch(e => {
showFailToast('操作失败:' + e.message);
})
}
</script>
<style lang="stylus" scoped>
$fontSize = 16px;
.mobile-chat-list {
.content {
.van-cell__value {
.chat-list-item {
display flex
font-size $fontSize
.van-image {
min-width 32px
width 32px
height 32px
}
.van-ellipsis {
margin-top 5px;
margin-left 10px;
}
}
}
}
.van-picker-column {
.picker-option {
display flex
width 100%
padding 0 10px
.van-image {
width 20px;
height 20px;
margin-right 5px
}
}
}
.van-nav-bar {
.van-nav-bar__right {
.van-icon {
font-size 20px;
}
}
}
}
</style>

View File

@@ -0,0 +1,401 @@
<template>
<van-config-provider :theme="getMobileTheme()">
<div class="mobile-chat">
<van-sticky ref="navBarRef" :offset-top="0" position="top">
<van-nav-bar left-arrow left-text="返回" @click-left="router.back()">
<template #title>
<van-dropdown-menu>
<van-dropdown-item :title="title">
<van-cell center title="角色"> {{ role.name }}</van-cell>
<van-cell center title="模型">{{ model }}</van-cell>
</van-dropdown-item>
</van-dropdown-menu>
</template>
<template #right>
<van-icon name="share-o" @click="showShare = true"/>
</template>
</van-nav-bar>
</van-sticky>
<van-share-sheet
v-model:show="showShare"
title="立即分享给好友"
:options="shareOptions"
@select="shareChat"
/>
<div id="message-list-box" :style="{height: winHeight+'px'}" class="message-list-box">
<van-list
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败点击重新加载"
@load="onLoad"
>
<van-cell v-for="item in chatData" :key="item" :border="false" class="message-line">
<chat-prompt
v-if="item.type==='prompt'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:model="model"
:tokens="item['tokens']"/>
<chat-reply v-else-if="item.type==='reply'"
:content="item.content"
:created-at="dateFormat(item['created_at'])"
:icon="item.icon"
:org-content="item.orgContent"
:tokens="item['tokens']"/>
</van-cell>
</van-list>
</div>
<van-sticky ref="bottomBarRef" :offset-bottom="0" position="bottom">
<div class="chat-box">
<van-cell-group>
<van-field
v-model="prompt"
center
clearable
placeholder="输入你的问题"
>
<template #button>
<van-button size="small" type="primary" @click="sendMessage">发送</van-button>
</template>
<template #extra>
<div class="icon-box">
<van-icon v-if="showStopGenerate" name="stop-circle-o" @click="stopGenerate"/>
<van-icon v-if="showReGenerate" name="play-circle-o" @click="reGenerate"/>
</div>
</template>
</van-field>
</van-cell-group>
</div>
</van-sticky>
</div>
</van-config-provider>
</template>
<script setup>
import {nextTick, onMounted, ref} from "vue";
import {showToast, showDialog} from "vant";
import {useRouter} from "vue-router";
import {dateFormat, randString, renderInputText, UUID} from "@/utils/libs";
import {getChatConfig} from "@/store/chat";
import {httpGet} from "@/utils/http";
import hl from "highlight.js";
import 'highlight.js/styles/a11y-dark.css'
import ChatPrompt from "@/components/mobile/ChatPrompt.vue";
import ChatReply from "@/components/mobile/ChatReply.vue";
import {getSessionId} from "@/store/session";
import {checkSession} from "@/action/session";
import {getMobileTheme} from "@/store/system";
const winHeight = ref(0)
const navBarRef = ref(null)
const bottomBarRef = ref(null)
const router = useRouter()
const chatConfig = getChatConfig()
const role = chatConfig.role
const model = chatConfig.model
const title = chatConfig.title
const chatId = chatConfig.chatId
const loginUser = ref(null)
onMounted(() => {
winHeight.value = document.body.offsetHeight - navBarRef.value.$el.offsetHeight - bottomBarRef.value.$el.offsetHeight
})
const chatData = ref([])
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
checkSession().then(user => {
loginUser.value = user
}).catch(() => {
router.push('/login')
})
const onLoad = () => {
httpGet('/api/chat/history?chat_id=' + chatId).then(res => {
// 加载状态结束
loading.value = false;
finished.value = true;
const data = res.data
if (data && data.length > 0) {
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;
}
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("#message-list-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
})
}
// 连接会话
connect(chatId, role.id);
}).catch(() => {
error.value = true
})
};
// 创建 socket 连接
const prompt = ref('');
const showStopGenerate = ref(false); // 停止生成
const showReGenerate = ref(false); // 重新生成
const previousText = ref(''); // 上一次提问
const lineBuffer = ref(''); // 输出缓冲行
const socket = ref(null);
const activelyClose = ref(false); // 主动关闭
const canSend = ref(true);
const connect = function (chat_id, role_id) {
let isNewChat = false;
if (!chat_id) {
isNewChat = true;
chat_id = UUID();
}
// 先关闭已有连接
if (socket.value !== null) {
activelyClose.value = true;
socket.value.close();
}
// 初始化 WebSocket 对象
const _sessionId = getSessionId();
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
if (location.protocol === 'https:') {
host = 'wss://' + location.host;
} else {
host = 'ws://' + location.host;
}
}
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model=${model}`);
_socket.addEventListener('open', () => {
previousText.value = '';
canSend.value = true;
activelyClose.value = false;
if (isNewChat) { // 加载打招呼信息
loading.value = false;
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.icon,
content: role.helloMsg,
orgContent: role.helloMsg,
})
}
});
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
chatData.value.push({
type: "reply",
id: randString(32),
icon: role.icon,
content: ""
});
} else if (data.type === 'end') { // 消息接收完毕
canSend.value = true;
showReGenerate.value = true;
showStopGenerate.value = false;
lineBuffer.value = ''; // 清空缓冲
} else {
lineBuffer.value += data.content;
const md = require('markdown-it')({breaks: true});
const reply = chatData.value[chatData.value.length - 1]
reply['orgContent'] = lineBuffer.value;
reply['content'] = md.render(lineBuffer.value);
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const lines = document.querySelectorAll('.message-line');
const blocks = lines[lines.length - 1].querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
scrollListBox()
})
}
};
}
});
_socket.addEventListener('close', () => {
console.log(activelyClose.value)
if (activelyClose.value) { // 忽略主动关闭
return;
}
// 停止发送消息
canSend.value = true;
socket.value = null;
checkSession().then(() => {
connect(chat_id, role_id)
}).catch(() => {
showDialog({
title: '会话提示',
message: '当前会话已经失效,请重新登录!',
}).then(() => {
router.push('/login')
});
});
});
socket.value = _socket;
}
// 将聊天框的滚动条滑动到最底部
const scrollListBox = () => {
document.getElementById('message-list-box').scrollTo(0, document.getElementById('message-list-box').scrollHeight + 46)
}
const sendMessage = () => {
if (canSend.value === false) {
showToast("AI 正在作答中,请稍后...");
return
}
if (prompt.value.trim().length === 0 || canSend.value === false) {
return false;
}
// 追加消息
chatData.value.push({
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(prompt.value),
created_at: new Date().getTime(),
});
nextTick(() => {
scrollListBox()
})
canSend.value = false;
showStopGenerate.value = true;
showReGenerate.value = false;
socket.value.send(prompt.value);
previousText.value = prompt.value;
prompt.value = '';
return true;
}
const stopGenerate = () => {
showStopGenerate.value = false;
httpGet("/api/chat/stop?session_id=" + getSessionId()).then(() => {
canSend.value = true;
if (previousText.value !== '') {
showReGenerate.value = true;
}
})
}
const reGenerate = () => {
canSend.value = false;
showStopGenerate.value = true;
showReGenerate.value = false;
const text = '重新生成上述问题的答案:' + previousText.value;
// 追加消息
chatData.value.push({
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: renderInputText(text)
});
socket.value.send(text);
}
const showShare = ref(false)
const shareOptions = [
{name: '微信', icon: 'wechat'},
{name: '微博', icon: 'weibo'},
{name: '复制链接', icon: 'link'},
{name: '分享海报', icon: 'poster'},
]
const shareChat = () => {
showShare.value = false
showToast('功能待开发')
}
</script>
<style lang="stylus" scoped>
.mobile-chat {
.message-list-box {
padding-top 50px
padding-bottom 10px
overflow-x auto
background #F5F5F5;
.van-cell {
background none
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
}
.chat-box {
.icon-box {
.van-icon {
font-size 24px
margin-left 10px;
}
}
}
}
.van-theme-dark {
.mobile-chat {
.message-list-box {
background #232425;
}
}
}
</style>
<style lang="stylus">
.mobile-chat {
.van-nav-bar__title {
.van-dropdown-menu__title {
margin-right 10px
}
.van-cell__title {
text-align left
}
}
.van-nav-bar__right {
.van-icon {
font-size 20px
}
}
}
</style>

View File

@@ -0,0 +1,70 @@
<template>
<van-config-provider :theme="getMobileTheme()">
<div class="mobile-home">
<router-view/>
<van-tabbar route v-model="active" @change="onChange">
<van-tabbar-item to="/mobile/chat/list" name="home" icon="chat-o"></van-tabbar-item>
<van-tabbar-item to="/mobile/setting" name="setting" icon="setting-o"></van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o"></van-tabbar-item>
</van-tabbar>
</div>
</van-config-provider>
</template>
<script setup>
import {ref} from "vue";
import {getMobileTheme} from "@/store/system";
import {useRouter} from "vue-router";
import {isMobile} from "@/utils/libs";
import {checkSession} from "@/action/session";
const router = useRouter()
checkSession().then(() => {
if (!isMobile()) {
router.replace('/chat')
}
}).catch(() => {
router.push('/login')
})
const active = ref('home')
const onChange = (index) => {
console.log(index)
// showToast(`标签 ${index}`);
}
</script>
<style lang="stylus">
.mobile-home {
.container {
.van-nav-bar {
position fixed
width 100%
}
.content {
padding 46px 10px 0 10px;
}
}
}
// 黑色主题
.van-theme-dark body {
background #1c1c1e
}
.van-toast--fail {
background #fef0f0
color #f56c6c
}
.van-nav-bar {
position fixed
width 100%
}
</style>

View File

@@ -0,0 +1,130 @@
<template>
<div class="mobile-user-profile container">
<van-nav-bar :title="title"/>
<div class="content">
<van-form @submit="save">
<van-cell-group inset v-model="form">
<van-field
v-model="form.username"
name="用户名"
label="用户名"
readonly
disabled
placeholder="用户名"
/>
<van-field
v-model="form.mobile"
name="手机号"
label="手机号"
readonly
disabled
placeholder="手机号"
/>
<van-field
v-model="form.nickname"
name="昵称"
label="昵称"
placeholder="昵称"
:rules="[{ required: true, message: '请填写用户昵称' }]"
/>
<van-field label="头像">
<template #input>
<van-uploader v-model="fileList"
reupload max-count="1"
:deletable="false"
:after-read="afterRead"/>
</template>
</van-field>
<van-field label="剩余次数">
<template #input>
<van-tag type="success">{{ form.calls }}</van-tag>
</template>
</van-field>
<van-field label="消耗 Tokens">
<template #input>
<van-tag type="primary">{{ form.tokens }}</van-tag>
</template>
</van-field>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</div>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {showFailToast, showNotify, showSuccessToast} from "vant";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from 'compressorjs';
const title = ref('用户设置')
const form = ref({
username: '',
nickname: '',
mobile: '',
avatar: '',
calls: 0,
tokens: 0
})
const fileList = ref([
{
url: 'https://fastly.jsdelivr.net/npm/@vant/assets/leaf.jpeg',
message: '上传中...',
}
]);
onMounted(() => {
httpGet('/api/user/profile').then(res => {
form.value = res.data
fileList.value[0].url = form.value.avatar
}).catch((e) => {
console.log(e.message)
showFailToast('获取用户信息失败')
});
})
const afterRead = (file) => {
file.status = 'uploading';
file.message = '上传中...';
// 压缩图片并上传
new Compressor(file.file, {
quality: 0.6,
success(result) {
const formData = new FormData();
formData.append('file', result, result.name);
// 执行上传操作
httpPost('/api/upload', formData).then((res) => {
form.value.avatar = res.data
file.status = 'success'
showNotify({type: 'success', message: '上传成功'})
}).catch((e) => {
console.log(e.message)
showNotify({type: 'danger', message: '上传失败'})
})
},
error(err) {
console.log(err.message);
},
});
};
const save = () => {
httpPost('/api/user/profile/update', form.value).then(() => {
showSuccessToast('保存成功')
}).catch(() => {
showFailToast('保存失败')
})
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,128 @@
<template>
<div class="mobile-setting container">
<van-nav-bar :title="title"/>
<div class="content">
<van-form @submit="save" v-model="form">
<van-cell-group inset>
<van-field
v-model="form.chat_config.model"
readonly
label="默认模型"
placeholder=""
@click="showPicker = true"
/>
<van-field
v-model.number="form.chat_config.max_tokens"
type="number"
name="MaxTokens"
label="MaxTokens"
placeholder="每次请求最大 token 数量"
:rules="[{ required: true, message: '请填写 MaxTokens' }]"
/>
<van-field
v-model.number="form.chat_config.temperature"
type="number"
name="Temperature"
label="Temperature"
placeholder="模型温度"
:rules="[{ required: true, message: '请填写 Temperature' }]"
/>
<van-field name="switch" label="聊天记录">
<template #input>
<van-switch v-model="form.chat_config.enable_history"/>
</template>
</van-field>
<van-field name="switch" label="聊天上下文">
<template #input>
<van-switch v-model="form.chat_config.enable_context"/>
</template>
</van-field>
<van-field
v-model="form.chat_config.api_key"
name="API KEY"
label="API KEY"
placeholder="配置自己的 api key"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
提交
</van-button>
</div>
</van-form>
</div>
<van-popup v-model:show="showPicker" round position="bottom">
<van-picker
:columns="models"
@cancel="showPicker = false"
@confirm="selectModel"
/>
</van-popup>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {showFailToast, showSuccessToast} from "vant";
import {ElMessage} from "element-plus";
const title = ref('聊天设置')
const form = ref({
chat_config: {
model: '',
max_tokens: 0,
enable_context: false,
enable_history: false,
temperature: false,
api_key: ''
}
})
const showPicker = ref(false)
const models = ref([])
onMounted(() => {
// 获取最新用户信息
httpGet('/api/user/profile').then(res => {
form.value = res.data
}).catch(() => {
showFailToast('获取用户信息失败')
});
// 加载系统配置
httpGet('/api/admin/config/get?key=system').then(res => {
const mds = res.data.models;
mds.forEach(item => {
models.value.push({text: item, value: item})
})
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
})
const selectModel = (item) => {
showPicker.value = false
form.value.chat_config.model = item.selectedValues[0]
}
const save = () => {
httpPost('/api/user/profile/update', form.value).then(() => {
showSuccessToast('保存成功')
}).catch(() => {
showFailToast('保存失败')
})
}
</script>
<style scoped lang="stylus">
.mobile-setting {
.content {
padding-top 60px
}
}
</style>