mirror of
https://github.com/yangjian102621/geekai.git
synced 2025-10-29 21:33:43 +08:00
Compare commits
121 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51407abe44 | ||
|
|
8a470b1038 | ||
|
|
baddabaa16 | ||
|
|
427b434ce3 | ||
|
|
5f921965e6 | ||
|
|
1e705c8ed5 | ||
|
|
b8ae65bb30 | ||
|
|
321e2087ea | ||
|
|
aac60edce2 | ||
|
|
9dc9a6923e | ||
|
|
7ca4dfe09b | ||
|
|
c584b82ddb | ||
|
|
5f17ab2501 | ||
|
|
c84e912dd8 | ||
|
|
2ebff2623f | ||
|
|
72418ce4d7 | ||
|
|
e221b1eed4 | ||
|
|
696306f066 | ||
|
|
1807d5b5d4 | ||
|
|
85c12aa322 | ||
|
|
da9d0dc3bc | ||
|
|
daaca822ac | ||
|
|
59ced3f947 | ||
|
|
22ae7dd1f3 | ||
|
|
1816e9d5cf | ||
|
|
9be6755f65 | ||
|
|
e5fb986463 | ||
|
|
55d24e577e | ||
|
|
7f50fa3fcf | ||
|
|
fff8b78aba | ||
|
|
7ca1989d98 | ||
|
|
4595dcb7ed | ||
|
|
a688d3feb5 | ||
|
|
7d1d88a32f | ||
|
|
d95c048edd | ||
|
|
df2fc9d77c | ||
|
|
d7e815d2bb | ||
|
|
f58b0a65f0 | ||
|
|
b59ad521ca | ||
|
|
b47ff975b0 | ||
|
|
d043a87b30 | ||
|
|
4cae7525d9 | ||
|
|
76966d2ce7 | ||
|
|
5a740aecb0 | ||
|
|
1ae79331e7 | ||
|
|
8b14e141d0 | ||
|
|
9cbc6c91c4 | ||
|
|
21c3a419a5 | ||
|
|
287fac3a89 | ||
|
|
ba206bb387 | ||
|
|
4fc01f3f7b | ||
|
|
f5ed71bcc6 | ||
|
|
8fc26183e9 | ||
|
|
e8ae8fddb7 | ||
|
|
b876867297 | ||
|
|
91dfd59731 | ||
|
|
5fdff90a10 | ||
|
|
96c62619e6 | ||
|
|
083155413d | ||
|
|
d83019cbe4 | ||
|
|
cc7271aa73 | ||
|
|
f873d6b375 | ||
|
|
c86169022a | ||
|
|
db0a79da93 | ||
|
|
48393e0e83 | ||
|
|
7b4730271d | ||
|
|
9cbe36d4c6 | ||
|
|
b25bb2cc53 | ||
|
|
79ded6018b | ||
|
|
59f316b341 | ||
|
|
f307b8ba7a | ||
|
|
5034a20345 | ||
|
|
26944f9e39 | ||
|
|
e64946c3b6 | ||
|
|
e0a62d9b35 | ||
|
|
39dbffd8d0 | ||
|
|
952d6183ed | ||
|
|
3365a6008d | ||
|
|
2e13ddf405 | ||
|
|
1d3acc8ed3 | ||
|
|
fa341bab30 | ||
|
|
036a6e3e41 | ||
|
|
f4c6ca4554 | ||
|
|
327929243c | ||
|
|
f4349c7a8c | ||
|
|
4b46d847f0 | ||
|
|
c3f016eae8 | ||
|
|
ebd3ef842f | ||
|
|
18c033d57f | ||
|
|
b676f80110 | ||
|
|
f7fbaa534d | ||
|
|
ea93a22e14 | ||
|
|
9f7e6778c5 | ||
|
|
6c31a2bfa6 | ||
|
|
f943669e18 | ||
|
|
3b26735998 | ||
|
|
79d25769ee | ||
|
|
1dd6800987 | ||
|
|
5e673a9ee0 | ||
|
|
92eb67a2af | ||
|
|
b1bed59be2 | ||
|
|
0ac732a3a3 | ||
|
|
bf3f68fa19 | ||
|
|
46a551df16 | ||
|
|
20a12462b1 | ||
|
|
a49fb1940e | ||
|
|
32774d23c7 | ||
|
|
7ecd7eeba1 | ||
|
|
0cc9cf8b45 | ||
|
|
d06f94bddd | ||
|
|
b5955f08c9 | ||
|
|
c120569894 | ||
|
|
aa376f1737 | ||
|
|
0f8a0f89e3 | ||
|
|
68dc261b44 | ||
|
|
4cf3af0c7b | ||
|
|
b99b6735d9 | ||
|
|
52189b7880 | ||
|
|
3dbeb1ccb6 | ||
|
|
5a0f272fa8 | ||
|
|
6561b99f8f |
83
CHANGELOG.md
83
CHANGELOG.md
@@ -1,5 +1,88 @@
|
|||||||
# 更新日志
|
# 更新日志
|
||||||
|
|
||||||
|
## v3.1.8
|
||||||
|
|
||||||
|
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
|
||||||
|
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败问题
|
||||||
|
3. Bug修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
|
||||||
|
4. Bug修复:修复七牛云上传报错 expired token
|
||||||
|
5. Bug修复:修复高权重模型导致的对话次数为负数的漏洞
|
||||||
|
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
|
||||||
|
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
|
||||||
|
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
|
||||||
|
|
||||||
|
## v3.1.7
|
||||||
|
|
||||||
|
1. 功能新增:支持文心4.0 AI 模型
|
||||||
|
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
|
||||||
|
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如GPT4是GPT3.5的10倍
|
||||||
|
4. 功能新增:新增系统配置关闭 AI 模型的函数功能
|
||||||
|
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
|
||||||
|
|
||||||
|
## v3.1.6
|
||||||
|
|
||||||
|
1. 功能新增:新增AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
|
||||||
|
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
|
||||||
|
3. 功能优化:优化瀑布流组件的页面布局
|
||||||
|
4. 功能优化:新注册用户成功之后自动登录
|
||||||
|
5. 功能优化:优化更新对话标题的操作体验,绑定回车事件
|
||||||
|
|
||||||
|
## v3.1.5
|
||||||
|
|
||||||
|
1. 功能新增:新增百度文心一言大模型 API 接入支持
|
||||||
|
2. 功能新增:新增科大讯飞星火大模型 API 接入支持
|
||||||
|
3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中
|
||||||
|
4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能
|
||||||
|
5. Bug修复:修复管理后台更新 API Key 失败的 Bug
|
||||||
|
6. Bug修复:修复新建的对话无法更新对话标题的 Bug
|
||||||
|
7. 功能优化:其他一些小的体验优化工作
|
||||||
|
|
||||||
|
## v3.1.4
|
||||||
|
|
||||||
|
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio和阿里云 OSS 四种存储介质。
|
||||||
|
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
|
||||||
|
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
|
||||||
|
4. Bug修复:修复[用户注册报错BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
|
||||||
|
5. Bug修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
|
||||||
|
6. 功能优化:增加阿里云短信服务配置项 `Sign` 和 `CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
|
||||||
|
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
|
||||||
|
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
|
||||||
|
|
||||||
|
## v3.1.3
|
||||||
|
|
||||||
|
1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。
|
||||||
|
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
|
||||||
|
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
|
||||||
|
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
|
||||||
|
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
|
||||||
|
6. Bug修复:修复 JWT token 有效期计算错误的 Bug。
|
||||||
|
|
||||||
|
## v3.1.2
|
||||||
|
|
||||||
|
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
|
||||||
|
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
|
||||||
|
3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
|
||||||
|
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
|
||||||
|
5. 功能优化:取消导出聊天页面的授权验证
|
||||||
|
6. 功能优化:所有路由跳转都使用绝对路径
|
||||||
|
|
||||||
|
## v3.1.1
|
||||||
|
|
||||||
|
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug
|
||||||
|
|
||||||
|
## v3.1.0(大版本更新)
|
||||||
|
|
||||||
|
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及
|
||||||
|
ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
|
||||||
|
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
|
||||||
|
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。
|
||||||
|
4. 功能优化:更新头像自动删除旧的图片资源。
|
||||||
|
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
|
||||||
|
6. 功能新增:允许用户配置自己的 OPenAI,Azure 以及 ChatGLM API KEY。
|
||||||
|
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
|
||||||
|
8. 升级 gin, element-plus,redis 组件到最新版本。
|
||||||
|
9. Bug修复:修复若干已知的的 Bug
|
||||||
|
|
||||||
## v3.0.7
|
## v3.0.7
|
||||||
|
|
||||||
1. 聊天主界面:新增聊天引导页面,介绍产品功能
|
1. 聊天主界面:新增聊天引导页面,介绍产品功能
|
||||||
|
|||||||
228
README.md
228
README.md
@@ -1,48 +1,67 @@
|
|||||||
# ChatGPT-Plus
|
# ChatGPT-Plus
|
||||||
|
|
||||||
**ChatGPT-PLUS** 是基于 OpenAI API 实现的 ChatGPT 聊天系统。主要有如下特性:
|
**ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
|
||||||
|
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。主要有如下特性:
|
||||||
|
|
||||||
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
|
||||||
* 聊天体验跟 ChatGPT 官方版本完全一致。
|
* 基于 Websocket 实现,完美的打字机体验。
|
||||||
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
* 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
|
||||||
|
* 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
|
||||||
|
* 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
|
||||||
|
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
|
||||||
|
* 已集成支付宝支付功能,支持多种会员套餐和点卡购买功能。
|
||||||
|
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
|
||||||
|
绘画函数插件。
|
||||||
|
|
||||||
## 功能截图
|
## 功能截图
|
||||||
|
|
||||||
### PC 端聊天界面
|
### PC 端聊天界面
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 新版聊天界面
|
### AI 对话界面
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### MidJourney 专业绘画界面
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Stable-Diffusion 专业绘画页面
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
### 绘图作品展
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### AI应用列表
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 会员充值
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### 自动调用函数插件
|
### 自动调用函数插件
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
### 用户设置
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 登录页面
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### 管理后台
|
### 管理后台
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||

|

|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
### 移动端 Web 页面
|
### 移动端 Web 页面
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
### 7. 体验地址
|
### 7. 体验地址
|
||||||
|
|
||||||
@@ -93,14 +112,22 @@ ChatGPT 的服务。
|
|||||||
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
* Github 地址:https://github.com/yangjian102621/chatgpt-plus
|
||||||
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
* 码云地址:https://gitee.com/blackfox/chatgpt-plus
|
||||||
|
|
||||||
|
## 客户端下载
|
||||||
|
|
||||||
|
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/chatgpt-plus/releases/tag/v3.1.2
|
||||||
|
|
||||||
## TODOLIST
|
## TODOLIST
|
||||||
|
|
||||||
* [ ] 整合 Midjourney AI 绘画 API
|
* [x] 整合 Midjourney AI 绘画 API
|
||||||
* [ ] 开发移动端聊天页面
|
* [x] 开发移动端聊天页面
|
||||||
* [x] 接入微信支付功能
|
* [x] 接入微信收款功能
|
||||||
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
|
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
|
||||||
* [ ] 接入语音和 TTS API,支持语音聊天
|
* [x] 开发桌面版应用
|
||||||
* [ ] 开发手机 App 客户端
|
* [x] 开发手机 App 客户端
|
||||||
|
* [x] 支付宝支付功能
|
||||||
|
* [ ] 支持基于知识库的 AI 问答
|
||||||
|
* [ ] 会员推广功能
|
||||||
|
* [ ] 微信支付功能
|
||||||
|
|
||||||
## Docker 快速部署
|
## Docker 快速部署
|
||||||
|
|
||||||
@@ -119,7 +146,7 @@ cd docker/mysql
|
|||||||
# 创建 mysql 容器
|
# 创建 mysql 容器
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
# 导入数据库
|
# 导入数据库
|
||||||
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql
|
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.8.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
|
||||||
@@ -137,42 +164,100 @@ source database/chatgpt_plus.sql
|
|||||||
|
|
||||||
```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 = "" # 如 http://127.0.0.1:7777
|
||||||
#ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了
|
|
||||||
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/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"
|
||||||
|
StaticDir = "./static" # 静态资源的目录
|
||||||
|
StaticUrl = "/static" # 静态资源访问 URL
|
||||||
|
AesEncryptKey = ""
|
||||||
|
WeChatBot = false # 是否启动微信机器人
|
||||||
|
|
||||||
[Session]
|
[Session]
|
||||||
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80"
|
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
||||||
Name = "CHAT_SESSION_ID"
|
|
||||||
Path = "/"
|
|
||||||
Domain = ""
|
|
||||||
MaxAge = 86400
|
MaxAge = 86400
|
||||||
Secure = false
|
|
||||||
HttpOnly = false
|
|
||||||
SameSite = 2
|
|
||||||
|
|
||||||
[Manager]
|
[Manager]
|
||||||
Username = "admin"
|
Username = "admin"
|
||||||
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
||||||
|
|
||||||
|
[Redis] # redis 配置信息
|
||||||
|
Host = "localhost"
|
||||||
|
Port = 6379
|
||||||
|
Password = ""
|
||||||
|
DB = 0
|
||||||
|
|
||||||
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||||
ApiURL = "{URL}"
|
ApiURL = ""
|
||||||
AppId = "{APP_ID}"
|
AppId = ""
|
||||||
Token = "{TOKEN}"
|
Token = ""
|
||||||
|
|
||||||
[SmsConfig] # 阿里云短信服务配置
|
[SmsConfig] # 阿里云短信服务配置
|
||||||
AccessKey = "{YOUR_ACCESS_KEY}"
|
AccessKey = ""
|
||||||
AccessSecret = "{YOUR_SECRET_KEY}"
|
AccessSecret = ""
|
||||||
Product = "Dysmsapi"
|
Product = "Dysmsapi"
|
||||||
Domain = "dysmsapi.aliyuncs.com"
|
Domain = "dysmsapi.aliyuncs.com"
|
||||||
|
|
||||||
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
||||||
ApiURL = "插件扩展 API 地址"
|
ApiURL = "" # 插件扩展 API 地址
|
||||||
Token = "插件扩展 API Token" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
||||||
|
|
||||||
|
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||||
|
Active = "local" # 默认使用本地文件存储引擎
|
||||||
|
[OSS.Local]
|
||||||
|
BasePath = "./static/upload" # 本地文件上传根路径
|
||||||
|
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
||||||
|
[OSS.Minio]
|
||||||
|
Endpoint = "" # 如 172.22.11.200:9000
|
||||||
|
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
||||||
|
AccessSecret = ""
|
||||||
|
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
||||||
|
UseSSL = false
|
||||||
|
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
||||||
|
[OSS.QiNiu] # 七牛云 OSS 配置
|
||||||
|
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
||||||
|
AccessKey = ""
|
||||||
|
AccessSecret = ""
|
||||||
|
Bucket = ""
|
||||||
|
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
||||||
|
|
||||||
|
[MjConfig] # MidJourney AI 绘画配置
|
||||||
|
Enabled = false # 是否启动 MidJourney 机器人服务
|
||||||
|
UserToken = "" # 用户授权 Token
|
||||||
|
BotToken = "" # Discord 机器人 Token
|
||||||
|
GuildId = "" # 服务器 ID
|
||||||
|
ChanelId = "" # 频道 ID
|
||||||
|
|
||||||
|
[SdConfig]
|
||||||
|
Enabled = false # 是否启动 Stable Diffusion 机器人服务
|
||||||
|
ApiURL = "http://172.22.11.200:7860" # stable-diffusion-webui API 地址
|
||||||
|
ApiKey = "" # 如果开启了授权,这里需要配置授权的 ApiKey
|
||||||
|
Txt2ImgJsonPath = "res/text2img.json" # 文生图的 API 请求报文 json 模板,允许自定义请求json报文,因为不同版本的 API 绘图的参数以及 fn_index 会不同。
|
||||||
|
|
||||||
|
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||||
|
Enabled = false # 是否启用 XXL JOB 服务
|
||||||
|
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||||
|
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||||
|
ExecutorPort = "9999" # 执行器服务端口
|
||||||
|
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||||
|
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||||
|
|
||||||
|
[AlipayConfig]
|
||||||
|
Enabled = false # 启用支付宝支付通道
|
||||||
|
SandBox = false # 是否启用沙盒模式
|
||||||
|
UserId = "2088721020750581" # 商户ID
|
||||||
|
AppId = "9021000131658023" # App Id
|
||||||
|
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||||
|
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||||
|
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||||
|
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||||
|
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
||||||
```
|
```
|
||||||
|
|
||||||
> 如果要启用微信收款服务和 MidJourney
|
> 1. 如果你不知道如何获取 Discord 用户 Token 和 Bot Token
|
||||||
> 绘画功能,请先部署扩展服务项目 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts)。
|
请查参考 [Midjourney|如何集成到自己的平台](https://zhuanlan.zhihu.com/p/631079476)。
|
||||||
|
> 2. `Txt2ImgJsonPath`
|
||||||
|
的默认用的是使用最广泛的 [stable-diffusion-webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui) 项目的
|
||||||
|
API,如果你用的是其他版本,比如秋叶的懒人包部署的,那么请将对应的 text2img 的参数报文复制放在 `res/text2img.json`
|
||||||
|
文件中即可。
|
||||||
|
|
||||||
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
|
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
|
||||||
|
|
||||||
@@ -189,11 +274,52 @@ location /api/ {
|
|||||||
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://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
|
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
|
||||||
|
|
||||||
|
# 静态资源转发
|
||||||
|
location /static/ {
|
||||||
|
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 IP 地址
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 启动应用
|
### 3. 启动应用
|
||||||
|
|
||||||
|
先修改 `docker/docker-compose.yaml` 文件中的镜像地址,改成最新的版本:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
# 后端 API 镜像
|
||||||
|
chatgpt-plus-api:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:v3.1.8 #这里改成最新的 release 版本
|
||||||
|
container_name: chatgpt-plus-api
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- DEBUG=false
|
||||||
|
- LOG_LEVEL=info
|
||||||
|
- CONFIG_FILE=config.toml
|
||||||
|
ports:
|
||||||
|
- "5678:5678"
|
||||||
|
volumes:
|
||||||
|
- /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime
|
||||||
|
- ./conf/config.toml:/var/www/app/config.toml
|
||||||
|
- ./logs:/var/www/app/logs
|
||||||
|
- ./static:/var/www/app/static
|
||||||
|
|
||||||
|
# 前端应用镜像
|
||||||
|
chatgpt-plus-web:
|
||||||
|
image: registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:v3.1.8 #这里改成最新的 release 版本
|
||||||
|
container_name: chatgpt-plus-web
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "8080:8080" # 这边是对外的端口,支持 8080,80和443
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cd docker
|
cd docker
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
@@ -209,8 +335,8 @@ docker-compose up -d
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
|
最后进入前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
|
||||||
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
|
你可以注册新用户,也可以使用系统默认有个账号:`18575670125/12345678` 登录聊天。
|
||||||
|
|
||||||
祝你使用愉快!!!
|
祝你使用愉快!!!
|
||||||
|
|
||||||
@@ -311,6 +437,10 @@ make clean linux
|
|||||||
|
|
||||||
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
|
||||||
|
|
||||||
|

|
||||||

|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
api/.gitignore
vendored
1
api/.gitignore
vendored
@@ -18,3 +18,4 @@ data
|
|||||||
config.toml
|
config.toml
|
||||||
static/upload
|
static/upload
|
||||||
storage.json
|
storage.json
|
||||||
|
certs/alipay/*
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
SHELL=/usr/bin/env bash
|
SHELL=/usr/bin/env bash
|
||||||
NAME := chatgpt-v3
|
NAME := chatgpt-plus
|
||||||
all: window linux darwin
|
all: window linux darwin
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,89 @@
|
|||||||
Listen = "0.0.0.0:5678"
|
Listen = "0.0.0.0:5678"
|
||||||
ProxyURL = "http://172.22.11.200:7777"
|
ProxyURL = "" # 如 http://127.0.0.1:7777
|
||||||
MysqlDns = "root:mysql_pass@tcp(localhost:3306)/chatgpt_plus?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
|
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
|
||||||
StaticDir = "./static"
|
StaticDir = "./static" # 静态资源的目录
|
||||||
StaticUrl = "http://localhost:5678/static"
|
StaticUrl = "/static" # 静态资源访问 URL
|
||||||
AesEncryptKey = "{YOUR_AES_KEY}"
|
AesEncryptKey = ""
|
||||||
|
WeChatBot = false
|
||||||
|
|
||||||
[Session]
|
[Session]
|
||||||
Driver = "cookie"
|
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
|
||||||
SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5"
|
|
||||||
Name = "CHAT_SESSION_ID"
|
|
||||||
Path = "/"
|
|
||||||
Domain = "localhost"
|
|
||||||
MaxAge = 86400
|
MaxAge = 86400
|
||||||
Secure = false
|
|
||||||
HttpOnly = false
|
|
||||||
SameSite = 2
|
|
||||||
|
|
||||||
[Manager]
|
[Manager]
|
||||||
Username = "admin"
|
Username = "admin"
|
||||||
Password = "admin123"
|
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
|
||||||
|
|
||||||
[Redis]
|
[Redis] # redis 配置信息
|
||||||
Host = "localhost"
|
Host = "localhost"
|
||||||
Port = 6379
|
Port = 6379
|
||||||
Password = ""
|
Password = ""
|
||||||
|
DB = 0
|
||||||
|
|
||||||
[ApiConfig]
|
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
|
||||||
ApiURL = "{URL}"
|
ApiURL = ""
|
||||||
AppId = "{APP_ID}"
|
AppId = ""
|
||||||
Token = "{TOKEN}"
|
Token = ""
|
||||||
|
|
||||||
[SmsConfig]
|
[SmsConfig] # 阿里云短信服务配置
|
||||||
AccessKey = "{YOUR_ACCESS_KEY}"
|
AccessKey = ""
|
||||||
AccessSecret = "{YOUR_SECRET_KEY}"
|
AccessSecret = ""
|
||||||
Product = "Dysmsapi"
|
Product = "Dysmsapi"
|
||||||
Domain = "dysmsapi.aliyuncs.com"
|
Domain = "dysmsapi.aliyuncs.com"
|
||||||
|
Sign = ""
|
||||||
|
CodeTempId = ""
|
||||||
|
|
||||||
[ExtConfig]
|
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
|
||||||
ApiURL = "插件扩展 API 地址"
|
ApiURL = "" # 插件扩展 API 地址
|
||||||
Token = "插件扩展 API Token"
|
Token = "" # 这个 token 随便填,只要确保跟 chatgpt-plus-exts 项目的 token 一样就行
|
||||||
|
|
||||||
|
[OSS] # OSS 配置,用于存储 MJ 绘画图片
|
||||||
|
Active = "local" # 默认使用本地文件存储引擎
|
||||||
|
[OSS.Local]
|
||||||
|
BasePath = "./static/upload" # 本地文件上传根路径
|
||||||
|
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
|
||||||
|
[OSS.Minio]
|
||||||
|
Endpoint = "" # 如 172.22.11.200:9000
|
||||||
|
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
|
||||||
|
AccessSecret = ""
|
||||||
|
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
|
||||||
|
UseSSL = false
|
||||||
|
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
|
||||||
|
[OSS.QiNiu] # 七牛云 OSS 配置
|
||||||
|
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
|
||||||
|
AccessKey = ""
|
||||||
|
AccessSecret = ""
|
||||||
|
Bucket = ""
|
||||||
|
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
|
||||||
|
|
||||||
|
[MjConfig]
|
||||||
|
Enabled = false
|
||||||
|
UserToken = ""
|
||||||
|
BotToken = ""
|
||||||
|
GuildId = ""
|
||||||
|
ChanelId = ""
|
||||||
|
|
||||||
|
[SdConfig]
|
||||||
|
Enabled = false
|
||||||
|
ApiURL = "http://172.22.11.200:7860"
|
||||||
|
ApiKey = ""
|
||||||
|
Txt2ImgJsonPath = "res/text2img.json"
|
||||||
|
|
||||||
|
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
|
||||||
|
Enabled = false # 是否启用 XXL JOB 服务
|
||||||
|
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
|
||||||
|
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
|
||||||
|
ExecutorPort = "9999" # 执行器服务端口
|
||||||
|
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
|
||||||
|
RegistryKey = "chatgpt-plus" # 任务注册 key
|
||||||
|
|
||||||
|
[AlipayConfig]
|
||||||
|
Enabled = false # 启用支付宝支付通道
|
||||||
|
SandBox = false # 是否启用沙盒模式
|
||||||
|
UserId = "2088721020750581" # 商户ID
|
||||||
|
AppId = "9021000131658023" # App Id
|
||||||
|
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
|
||||||
|
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
|
||||||
|
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
|
||||||
|
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
|
||||||
|
NotifyURL = "http://r9it.com:6004/api/payment/alipay/notify" # 支付异步回调地址
|
||||||
@@ -2,21 +2,21 @@ package core
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
"chatplus/service/function"
|
"chatplus/service/fun"
|
||||||
"chatplus/store/model"
|
"chatplus/store/model"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"context"
|
"context"
|
||||||
"github.com/gin-contrib/sessions"
|
"fmt"
|
||||||
"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"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppServer struct {
|
type AppServer struct {
|
||||||
@@ -33,11 +33,10 @@ type AppServer struct {
|
|||||||
ChatSession *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
|
ChatSession *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
|
||||||
ChatClients *types.LMap[string, *types.WsClient] // map[sessionId]Websocket 连接集合
|
ChatClients *types.LMap[string, *types.WsClient] // map[sessionId]Websocket 连接集合
|
||||||
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
|
||||||
Functions map[string]function.Function
|
Functions map[string]fun.Function
|
||||||
MjTaskClients *types.LMap[string, *types.WsClient]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer {
|
func NewServer(appConfig *types.AppConfig, functions map[string]fun.Function) *AppServer {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
gin.DefaultWriter = io.Discard
|
gin.DefaultWriter = io.Discard
|
||||||
return &AppServer{
|
return &AppServer{
|
||||||
@@ -48,19 +47,17 @@ func NewServer(appConfig *types.AppConfig, functions map[string]function.Functio
|
|||||||
ChatSession: types.NewLMap[string, *types.ChatSession](),
|
ChatSession: types.NewLMap[string, *types.ChatSession](),
|
||||||
ChatClients: types.NewLMap[string, *types.WsClient](),
|
ChatClients: types.NewLMap[string, *types.WsClient](),
|
||||||
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
|
||||||
MjTaskClients: types.NewLMap[string, *types.WsClient](),
|
|
||||||
Functions: functions,
|
Functions: functions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AppServer) Init(debug bool) {
|
func (s *AppServer) Init(debug bool, client *redis.Client) {
|
||||||
if debug { // 调试模式允许跨域请求 API
|
if debug { // 调试模式允许跨域请求 API
|
||||||
s.Debug = debug
|
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.Config))
|
s.Engine.Use(authorizeMiddleware(s, client))
|
||||||
s.Engine.Use(authorizeMiddleware(s))
|
|
||||||
s.Engine.Use(errorHandler)
|
s.Engine.Use(errorHandler)
|
||||||
// 添加静态资源访问
|
// 添加静态资源访问
|
||||||
s.Engine.Static("/static", s.Config.StaticDir)
|
s.Engine.Static("/static", s.Config.StaticDir)
|
||||||
@@ -105,42 +102,6 @@ func errorHandler(c *gin.Context) {
|
|||||||
c.Next()
|
c.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 会话处理
|
|
||||||
func sessionMiddleware(config *types.AppConfig) gin.HandlerFunc {
|
|
||||||
// encrypt the cookie
|
|
||||||
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{
|
|
||||||
Path: config.Session.Path,
|
|
||||||
Domain: config.Session.Domain,
|
|
||||||
MaxAge: config.Session.MaxAge,
|
|
||||||
Secure: config.Session.Secure,
|
|
||||||
HttpOnly: config.Session.HttpOnly,
|
|
||||||
SameSite: config.Session.SameSite,
|
|
||||||
})
|
|
||||||
return sessions.Sessions(config.Session.Name, store)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 跨域中间件设置
|
// 跨域中间件设置
|
||||||
func corsMiddleware() gin.HandlerFunc {
|
func corsMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
@@ -151,7 +112,7 @@ func corsMiddleware() gin.HandlerFunc {
|
|||||||
c.Header("Access-Control-Allow-Origin", origin)
|
c.Header("Access-Control-Allow-Origin", origin)
|
||||||
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
|
||||||
//允许跨域设置可以返回其他子段,可以自定义字段
|
//允许跨域设置可以返回其他子段,可以自定义字段
|
||||||
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token")
|
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization")
|
||||||
// 允许浏览器(客户端)可以解析的头部 (重要)
|
// 允许浏览器(客户端)可以解析的头部 (重要)
|
||||||
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
|
||||||
//设置缓存时间
|
//设置缓存时间
|
||||||
@@ -175,44 +136,78 @@ func corsMiddleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 用户授权验证
|
// 用户授权验证
|
||||||
func authorizeMiddleware(s *AppServer) gin.HandlerFunc {
|
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
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" ||
|
||||||
c.Request.URL.Path == "/api/reward/notify" ||
|
c.Request.URL.Path == "/api/reward/notify" ||
|
||||||
c.Request.URL.Path == "/api/mj/notify" ||
|
c.Request.URL.Path == "/api/mj/notify" ||
|
||||||
|
c.Request.URL.Path == "/api/chat/history" ||
|
||||||
|
c.Request.URL.Path == "/api/chat/detail" ||
|
||||||
|
c.Request.URL.Path == "/api/role/list" ||
|
||||||
|
c.Request.URL.Path == "/api/mj/jobs" ||
|
||||||
|
c.Request.URL.Path == "/api/mj/proxy" ||
|
||||||
|
c.Request.URL.Path == "/api/sd/jobs" ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
|
||||||
|
strings.HasPrefix(c.Request.URL.Path, "/api/payment/") ||
|
||||||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket 连接请求验证
|
var tokenString string
|
||||||
if c.Request.URL.Path == "/api/chat" {
|
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
|
||||||
sessionId := c.Query("sessionId")
|
tokenString = c.GetHeader(types.AdminAuthHeader)
|
||||||
session := s.ChatSession.Get(sessionId)
|
} else if c.Request.URL.Path == "/api/chat/new" ||
|
||||||
if session.ClientIP == c.ClientIP() {
|
c.Request.URL.Path == "/api/mj/client" ||
|
||||||
c.Next()
|
c.Request.URL.Path == "/api/sd/client" {
|
||||||
} else {
|
tokenString = c.Query("token")
|
||||||
c.Abort()
|
} else {
|
||||||
}
|
tokenString = c.GetHeader(types.UserAuthHeader)
|
||||||
|
}
|
||||||
|
if tokenString == "" {
|
||||||
|
resp.ERROR(c, "You should put Authorization in request headers")
|
||||||
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
session := sessions.Default(c)
|
|
||||||
var value interface{}
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||||
value = session.Get(types.SessionAdmin)
|
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||||
} else {
|
}
|
||||||
value = session.Get(types.SessionUser)
|
|
||||||
}
|
return []byte(s.Config.Session.SecretKey), nil
|
||||||
if value != nil {
|
})
|
||||||
c.Next()
|
|
||||||
} else {
|
if err != nil {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(jwt.MapClaims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
resp.NotAuth(c, "Token is invalid")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
|
||||||
|
if expr > 0 && int64(expr) < time.Now().Unix() {
|
||||||
|
resp.NotAuth(c, "Token is expired")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf("users/%v", claims["user_id"])
|
||||||
|
if _, err := client.Get(context.Background(), key).Result(); err != nil {
|
||||||
|
resp.NotAuth(c, "Token is not found in redis")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Set(types.LoginUserID, claims["user_id"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
logger2 "chatplus/logger"
|
logger2 "chatplus/logger"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
@@ -23,18 +22,21 @@ func NewDefaultConfig() *types.AppConfig {
|
|||||||
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
|
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
|
||||||
AesEncryptKey: utils.RandString(24),
|
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",
|
|
||||||
Domain: "",
|
|
||||||
Path: "/",
|
|
||||||
MaxAge: 86400,
|
MaxAge: 86400,
|
||||||
Secure: true,
|
|
||||||
HttpOnly: false,
|
|
||||||
SameSite: http.SameSiteLaxMode,
|
|
||||||
},
|
},
|
||||||
ApiConfig: types.ChatPlusApiConfig{},
|
ApiConfig: types.ChatPlusApiConfig{},
|
||||||
ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)},
|
OSS: types.OSSConfig{
|
||||||
|
Active: "local",
|
||||||
|
Local: types.LocalStorageConfig{
|
||||||
|
BaseURL: "http://localhost/5678/static/upload",
|
||||||
|
BasePath: "./static/upload",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MjConfig: types.MidJourneyConfig{Enabled: false},
|
||||||
|
SdConfig: types.StableDiffusionConfig{Enabled: false, Txt2ImgJsonPath: "res/text2img.json"},
|
||||||
|
WeChatBot: false,
|
||||||
|
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ package types
|
|||||||
|
|
||||||
// ApiRequest API 请求实体
|
// ApiRequest API 请求实体
|
||||||
type ApiRequest struct {
|
type ApiRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model,omitempty"` // 兼容百度文心一言
|
||||||
Temperature float32 `json:"temperature"`
|
Temperature float32 `json:"temperature"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
|
||||||
Stream bool `json:"stream"`
|
Stream bool `json:"stream"`
|
||||||
Messages []interface{} `json:"messages"`
|
Messages []interface{} `json:"messages,omitempty"`
|
||||||
Functions []Function `json:"functions"`
|
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
|
||||||
|
Functions []Function `json:"functions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
@@ -34,21 +35,19 @@ type Delta struct {
|
|||||||
|
|
||||||
// ChatSession 聊天会话对象
|
// ChatSession 聊天会话对象
|
||||||
type ChatSession struct {
|
type ChatSession struct {
|
||||||
SessionId string `json:"session_id"`
|
SessionId string `json:"session_id"`
|
||||||
ClientIP string `json:"client_ip"` // 客户端 IP
|
ClientIP string `json:"client_ip"` // 客户端 IP
|
||||||
Username string `json:"username"` // 当前登录的 username
|
Username string `json:"username"` // 当前登录的 username
|
||||||
UserId uint `json:"user_id"` // 当前登录的 user ID
|
UserId uint `json:"user_id"` // 当前登录的 user ID
|
||||||
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
|
||||||
Model string `json:"model"` // GPT 模型
|
Model ChatModel `json:"model"` // GPT 模型
|
||||||
}
|
}
|
||||||
|
|
||||||
type MjTask struct {
|
type ChatModel struct {
|
||||||
ChatId string
|
Id uint `json:"id"`
|
||||||
MessageId string
|
Platform Platform `json:"platform"`
|
||||||
MessageHash string
|
Value string `json:"value"`
|
||||||
UserId uint
|
Weight int `json:"weight"`
|
||||||
RoleId uint
|
|
||||||
Icon string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ApiError struct {
|
type ApiError struct {
|
||||||
@@ -69,6 +68,10 @@ var ModelToTokens = map[string]int{
|
|||||||
"gpt-3.5-turbo-16k": 16384,
|
"gpt-3.5-turbo-16k": 16384,
|
||||||
"gpt-4": 8192,
|
"gpt-4": 8192,
|
||||||
"gpt-4-32k": 32768,
|
"gpt-4-32k": 32768,
|
||||||
|
"chatglm_pro": 32768, // 清华智普
|
||||||
|
"chatglm_std": 16384,
|
||||||
|
"chatglm_lite": 4096,
|
||||||
|
"ernie_bot_turbo": 8192, // 文心一言
|
||||||
|
"general": 8192, // 科大讯飞
|
||||||
|
"general2": 8192,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskStorePrefix = "/tasks/"
|
|
||||||
|
|||||||
@@ -36,6 +36,16 @@ func (wc *WsClient) Send(message []byte) error {
|
|||||||
return wc.Conn.WriteMessage(wc.mt, message)
|
return wc.Conn.WriteMessage(wc.mt, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (wc *WsClient) SendJson(value interface{}) error {
|
||||||
|
wc.lock.Lock()
|
||||||
|
defer wc.lock.Unlock()
|
||||||
|
|
||||||
|
if wc.Closed {
|
||||||
|
return ErrConClosed
|
||||||
|
}
|
||||||
|
return wc.Conn.WriteJSON(value)
|
||||||
|
}
|
||||||
|
|
||||||
func (wc *WsClient) Receive() (int, []byte, error) {
|
func (wc *WsClient) Receive() (int, []byte, error) {
|
||||||
if wc.Closed {
|
if wc.Closed {
|
||||||
return 0, nil, ErrConClosed
|
return 0, nil, ErrConClosed
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package types
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
@@ -17,8 +16,14 @@ type AppConfig struct {
|
|||||||
Redis RedisConfig // redis 连接信息
|
Redis RedisConfig // redis 连接信息
|
||||||
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
|
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
|
||||||
AesEncryptKey string
|
AesEncryptKey string
|
||||||
SmsConfig AliYunSmsConfig // AliYun send message service config
|
SmsConfig AliYunSmsConfig // AliYun send message service config
|
||||||
ExtConfig ChatPlusExtConfig // ChatPlus extensions callback api config
|
OSS OSSConfig // OSS config
|
||||||
|
MjConfig MidJourneyConfig // mj 绘画配置
|
||||||
|
WeChatBot bool // 是否启用微信机器人
|
||||||
|
SdConfig StableDiffusionConfig // sd 绘画配置
|
||||||
|
|
||||||
|
XXLConfig XXLConfig
|
||||||
|
AlipayConfig AlipayConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatPlusApiConfig struct {
|
type ChatPlusApiConfig struct {
|
||||||
@@ -27,9 +32,23 @@ type ChatPlusApiConfig struct {
|
|||||||
Token string
|
Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChatPlusExtConfig struct {
|
type MidJourneyConfig struct {
|
||||||
ApiURL string
|
Enabled bool
|
||||||
Token string
|
UserToken string
|
||||||
|
BotToken string
|
||||||
|
GuildId string // Server ID
|
||||||
|
ChanelId string // Chanel ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type WeChatConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type StableDiffusionConfig struct {
|
||||||
|
Enabled bool
|
||||||
|
ApiURL string
|
||||||
|
ApiKey string
|
||||||
|
Txt2ImgJsonPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AliYunSmsConfig struct {
|
type AliYunSmsConfig struct {
|
||||||
@@ -37,12 +56,36 @@ type AliYunSmsConfig struct {
|
|||||||
AccessSecret string
|
AccessSecret string
|
||||||
Product string
|
Product string
|
||||||
Domain string
|
Domain string
|
||||||
|
Sign string // 短信签名
|
||||||
|
CodeTempId string // 验证码短信模板 ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlipayConfig struct {
|
||||||
|
Enabled bool // 是否启用该服务
|
||||||
|
SandBox bool // 是否沙盒环境
|
||||||
|
AppId string // 应用 ID
|
||||||
|
UserId string // 支付宝用户 ID
|
||||||
|
PrivateKey string // 用户私钥文件路径
|
||||||
|
PublicKey string // 用户公钥文件路径
|
||||||
|
AlipayPublicKey string // 支付宝公钥文件路径
|
||||||
|
RootCert string // Root 秘钥路径
|
||||||
|
NotifyURL string // 异步通知回调
|
||||||
|
}
|
||||||
|
|
||||||
|
type XXLConfig struct { // XXL 任务调度配置
|
||||||
|
Enabled bool
|
||||||
|
ServerAddr string
|
||||||
|
ExecutorIp string
|
||||||
|
ExecutorPort string
|
||||||
|
AccessToken string
|
||||||
|
RegistryKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedisConfig struct {
|
type RedisConfig struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
Password string
|
Password string
|
||||||
|
DB int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c RedisConfig) Url() string {
|
func (c RedisConfig) Url() string {
|
||||||
@@ -55,45 +98,53 @@ 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
|
|
||||||
type Session struct {
|
|
||||||
Driver SessionDriver // session 存储驱动 mem|cookie|redis
|
|
||||||
SecretKey string // session encryption key
|
|
||||||
Name string
|
|
||||||
Path string
|
|
||||||
Domain string
|
|
||||||
MaxAge int
|
|
||||||
Secure bool
|
|
||||||
HttpOnly bool
|
|
||||||
SameSite http.SameSite
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChatConfig 系统默认的聊天配置
|
// ChatConfig 系统默认的聊天配置
|
||||||
type ChatConfig struct {
|
type ChatConfig struct {
|
||||||
ApiURL string `json:"api_url,omitempty"`
|
OpenAI ModelAPIConfig `json:"open_ai"`
|
||||||
Model string `json:"model"` // 默认模型
|
Azure ModelAPIConfig `json:"azure"`
|
||||||
Temperature float32 `json:"temperature"`
|
ChatGML ModelAPIConfig `json:"chat_gml"`
|
||||||
MaxTokens int `json:"max_tokens"`
|
Baidu ModelAPIConfig `json:"baidu"`
|
||||||
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
XunFei ModelAPIConfig `json:"xun_fei"`
|
||||||
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
|
||||||
ApiKey string `json:"api_key"`
|
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
|
||||||
ContextDeep int `json:"context_deep"` // 上下文深度
|
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
|
||||||
|
ContextDeep int `json:"context_deep"` // 上下文深度
|
||||||
|
}
|
||||||
|
|
||||||
|
type Platform string
|
||||||
|
|
||||||
|
const OpenAI = Platform("OpenAI")
|
||||||
|
const Azure = Platform("Azure")
|
||||||
|
const ChatGLM = Platform("ChatGLM")
|
||||||
|
const Baidu = Platform("Baidu")
|
||||||
|
const XunFei = Platform("XunFei")
|
||||||
|
|
||||||
|
// UserChatConfig 用户的聊天配置
|
||||||
|
type UserChatConfig struct {
|
||||||
|
ApiKeys map[Platform]string `json:"api_keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModelAPIConfig struct {
|
||||||
|
ApiURL string `json:"api_url,omitempty"`
|
||||||
|
Temperature float32 `json:"temperature"`
|
||||||
|
MaxTokens int `json:"max_tokens"`
|
||||||
|
ApiKey string `json:"api_key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemConfig struct {
|
type SystemConfig struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
AdminTitle string `json:"admin_title"`
|
AdminTitle string `json:"admin_title"`
|
||||||
Models []string `json:"models"`
|
Models []string `json:"models"`
|
||||||
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
|
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
|
||||||
InitImgCalls int `json:"init_img_calls"`
|
InitImgCalls int `json:"init_img_calls"`
|
||||||
EnabledRegister bool `json:"enabled_register"`
|
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
|
||||||
EnabledMsgService bool `json:"enabled_msg_service"`
|
EnabledRegister bool `json:"enabled_register"`
|
||||||
|
EnabledMsg bool `json:"enabled_msg"` // 启用短信验证码服务
|
||||||
|
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
|
||||||
|
RewardImg string `json:"reward_img"` // 众筹收款二维码地址
|
||||||
|
EnabledFunction bool `json:"enabled_function"` // 启用 API 函数功能
|
||||||
|
EnabledReward bool `json:"enabled_reward"` // 启用众筹功能
|
||||||
|
EnabledAlipay bool `json:"enabled_alipay"` // 是否启用支付宝支付通道
|
||||||
|
OrderPayTimeout int `json:"order_pay_timeout"` //订单支付超时时间
|
||||||
|
DefaultModels []string `json:"default_models"` // 默认开通的 AI 模型
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,15 +83,7 @@ var InnerFunctions = []Function{
|
|||||||
Properties: map[string]Property{
|
Properties: map[string]Property{
|
||||||
"prompt": {
|
"prompt": {
|
||||||
Type: "string",
|
Type: "string",
|
||||||
Description: "绘画内容描述,提示词,如果该参数中有中文的话,则需要翻译成英文",
|
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
|
||||||
},
|
|
||||||
"ar": {
|
|
||||||
Type: "string",
|
|
||||||
Description: "图片长宽比,如 --ar 4:3",
|
|
||||||
},
|
|
||||||
"niji": {
|
|
||||||
Type: "string",
|
|
||||||
Description: "动漫模型版本,例如 --niji 5",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Required: []string{},
|
Required: []string{},
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type MKey interface {
|
|||||||
string | int
|
string | int
|
||||||
}
|
}
|
||||||
type MValue interface {
|
type MValue interface {
|
||||||
*WsClient | *ChatSession | context.CancelFunc | []interface{} | MjTask
|
*WsClient | *ChatSession | context.CancelFunc | []interface{}
|
||||||
}
|
}
|
||||||
type LMap[K MKey, T MValue] struct {
|
type LMap[K MKey, T MValue] struct {
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
|
|||||||
17
api/core/types/order.go
Normal file
17
api/core/types/order.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type OrderStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OrderNotPaid = OrderStatus(0)
|
||||||
|
OrderScanned = OrderStatus(1) // 已扫码
|
||||||
|
OrderPaidSuccess = OrderStatus(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderRemark struct {
|
||||||
|
Days int `json:"days"` // 有效期
|
||||||
|
Calls int `json:"calls"` // 增加调用次数
|
||||||
|
Name string `json:"name"` // 产品名称
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
}
|
||||||
38
api/core/types/oss.go
Normal file
38
api/core/types/oss.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
type OSSConfig struct {
|
||||||
|
Active string
|
||||||
|
Local LocalStorageConfig
|
||||||
|
Minio MiniOssConfig
|
||||||
|
QiNiu QiNiuOssConfig
|
||||||
|
AliYun AliYunOssConfig
|
||||||
|
}
|
||||||
|
type MiniOssConfig struct {
|
||||||
|
Endpoint string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
Bucket string
|
||||||
|
UseSSL bool
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type QiNiuOssConfig struct {
|
||||||
|
Zone string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
Bucket string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type AliYunOssConfig struct {
|
||||||
|
Endpoint string
|
||||||
|
AccessKey string
|
||||||
|
AccessSecret string
|
||||||
|
Bucket string
|
||||||
|
Domain string
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalStorageConfig struct {
|
||||||
|
BasePath string
|
||||||
|
BaseURL string
|
||||||
|
}
|
||||||
@@ -1,6 +1,14 @@
|
|||||||
package types
|
package types
|
||||||
|
|
||||||
const SessionName = "ChatGPT-TOKEN"
|
const LoginUserID = "LOGIN_USER_ID"
|
||||||
const SessionUser = "SESSION_USER" // 存储用户信息的 session key
|
const LoginUserCache = "LOGIN_USER_CACHE"
|
||||||
const SessionAdmin = "SESSION_ADMIN" //存储管理员信息的 session key
|
|
||||||
const LoginUserCache = "LOGIN_USER_CACHE" // 已登录用户缓存
|
const UserAuthHeader = "Authorization"
|
||||||
|
const AdminAuthHeader = "Admin-Authorization"
|
||||||
|
const ChatTokenHeader = "Chat-Token"
|
||||||
|
|
||||||
|
// Session configs struct
|
||||||
|
type Session struct {
|
||||||
|
SecretKey string
|
||||||
|
MaxAge int
|
||||||
|
}
|
||||||
|
|||||||
69
api/core/types/task.go
Normal file
69
api/core/types/task.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package types
|
||||||
|
|
||||||
|
// TaskType 任务类别
|
||||||
|
type TaskType string
|
||||||
|
|
||||||
|
func (t TaskType) String() string {
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskImage = TaskType("image")
|
||||||
|
TaskUpscale = TaskType("upscale")
|
||||||
|
TaskVariation = TaskType("variation")
|
||||||
|
TaskTxt2Img = TaskType("text2img")
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskSrc 任务来源
|
||||||
|
type TaskSrc string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskSrcChat = TaskSrc("chat") // 来自聊天页面
|
||||||
|
TaskSrcImg = TaskSrc("img") // 专业绘画页面
|
||||||
|
)
|
||||||
|
|
||||||
|
// MjTask MidJourney 任务
|
||||||
|
type MjTask struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
SessionId string `json:"session_id"`
|
||||||
|
Src TaskSrc `json:"src"`
|
||||||
|
Type TaskType `json:"type"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
Prompt string `json:"prompt,omitempty"`
|
||||||
|
ChatId string `json:"chat_id,omitempty"`
|
||||||
|
RoleId int `json:"role_id,omitempty"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
|
Index int `json:"index,omitempty"`
|
||||||
|
MessageId string `json:"message_id,omitempty"`
|
||||||
|
MessageHash string `json:"message_hash,omitempty"`
|
||||||
|
RetryCount int `json:"retry_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SdTask struct {
|
||||||
|
Id int `json:"id"` // job 数据库ID
|
||||||
|
SessionId string `json:"session_id"`
|
||||||
|
Src TaskSrc `json:"src"`
|
||||||
|
Type TaskType `json:"type"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
Prompt string `json:"prompt,omitempty"`
|
||||||
|
Params SdTaskParams `json:"params"`
|
||||||
|
RetryCount int `json:"retry_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SdTaskParams struct {
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
Prompt string `json:"prompt"` // 提示词
|
||||||
|
NegativePrompt string `json:"negative_prompt"` // 反向提示词
|
||||||
|
Steps int `json:"steps"` // 迭代步数,默认20
|
||||||
|
Sampler string `json:"sampler"` // 采样器
|
||||||
|
FaceFix bool `json:"face_fix"` // 面部修复
|
||||||
|
CfgScale float32 `json:"cfg_scale"` //引导系数,默认 7
|
||||||
|
Seed int64 `json:"seed"` // 随机数种子
|
||||||
|
Height int `json:"height"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
HdFix bool `json:"hd_fix"` // 启用高清修复
|
||||||
|
HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度
|
||||||
|
HdScale int `json:"hd_scale"` // 放大倍数
|
||||||
|
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
|
||||||
|
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
|
||||||
|
}
|
||||||
@@ -34,4 +34,5 @@ const (
|
|||||||
OkMsg = "Success"
|
OkMsg = "Success"
|
||||||
ErrorMsg = "系统开小差了"
|
ErrorMsg = "系统开小差了"
|
||||||
InvalidArgs = "非法参数或参数解析失败"
|
InvalidArgs = "非法参数或参数解析失败"
|
||||||
|
NoData = "No Data"
|
||||||
)
|
)
|
||||||
|
|||||||
47
api/go.mod
47
api/go.mod
@@ -5,58 +5,80 @@ 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/aliyun/alibaba-cloud-sdk-go v1.62.405
|
||||||
github.com/gin-contrib/sessions v0.0.5
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
|
||||||
|
github.com/bwmarrin/discordgo v0.27.1
|
||||||
|
github.com/eatmoreapple/openwechat v1.2.1
|
||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.0.0
|
||||||
github.com/gorilla/websocket v1.5.0
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/imroc/req/v3 v3.37.2
|
github.com/imroc/req/v3 v3.37.2
|
||||||
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
|
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
|
||||||
|
github.com/minio/minio-go/v7 v7.0.62
|
||||||
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
|
||||||
|
github.com/qiniu/go-sdk/v7 v7.17.1
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
|
github.com/smartwalle/alipay/v3 v3.2.15
|
||||||
github.com/syndtr/goleveldb v1.0.0
|
github.com/syndtr/goleveldb v1.0.0
|
||||||
go.uber.org/zap v1.23.0
|
go.uber.org/zap v1.23.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/driver/mysql v1.4.7
|
gorm.io/driver/mysql v1.4.7
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require github.com/xxl-job/xxl-job-executor-go v1.2.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
github.com/dlclark/regexp2 v1.8.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
github.com/gaukas/godicttls v0.0.3 // indirect
|
github.com/gaukas/godicttls v0.0.3 // indirect
|
||||||
|
github.com/go-basic/ipv4 v1.0.0 // indirect
|
||||||
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
github.com/go-sql-driver/mysql v1.7.0 // indirect
|
||||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // indirect
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/gomodule/redigo v2.0.0+incompatible // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
github.com/hashicorp/go-multierror v1.1.1 // 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/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
|
||||||
github.com/klauspost/compress v1.15.15 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
github.com/onsi/ginkgo/v2 v2.10.0 // indirect
|
||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
|
|
||||||
github.com/quic-go/qpack v0.4.0 // indirect
|
github.com/quic-go/qpack v0.4.0 // indirect
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
|
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
|
||||||
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
|
||||||
github.com/quic-go/quic-go v0.35.1 // indirect
|
github.com/quic-go/quic-go v0.35.1 // indirect
|
||||||
github.com/refraction-networking/utls v1.3.2 // indirect
|
github.com/refraction-networking/utls v1.3.2 // indirect
|
||||||
|
github.com/rs/xid v1.5.0 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
|
github.com/smartwalle/ncrypto v1.0.2 // indirect
|
||||||
|
github.com/smartwalle/ngx v1.0.6 // indirect
|
||||||
|
github.com/smartwalle/nsign v1.0.8 // 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.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
|
||||||
golang.org/x/mod v0.11.0 // indirect
|
golang.org/x/mod v0.11.0 // indirect
|
||||||
golang.org/x/net v0.11.0 // indirect
|
golang.org/x/net v0.14.0 // indirect
|
||||||
golang.org/x/text v0.10.0 // indirect
|
golang.org/x/sync v0.3.0 // indirect
|
||||||
|
golang.org/x/text v0.12.0 // indirect
|
||||||
|
golang.org/x/time v0.3.0 // indirect
|
||||||
golang.org/x/tools v0.10.0 // indirect
|
golang.org/x/tools v0.10.0 // indirect
|
||||||
google.golang.org/protobuf v1.30.0 // indirect
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,9 +88,6 @@ require (
|
|||||||
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.14.0 // indirect
|
github.com/go-playground/validator/v10 v10.14.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/securecookie v1.1.1 // indirect
|
|
||||||
github.com/gorilla/sessions v1.2.1 // indirect
|
|
||||||
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.4 // indirect
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
@@ -78,7 +97,7 @@ require (
|
|||||||
go.uber.org/atomic v1.9.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.10.0
|
golang.org/x/crypto v0.12.0
|
||||||
golang.org/x/sys v0.9.0 // indirect
|
golang.org/x/sys v0.11.0 // indirect
|
||||||
gorm.io/gorm v1.25.1
|
gorm.io/gorm v1.25.1
|
||||||
)
|
)
|
||||||
|
|||||||
150
api/go.sum
150
api/go.sum
@@ -2,41 +2,61 @@ 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 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
|
||||||
|
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
|
||||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
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/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
|
||||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY=
|
||||||
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.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
|
||||||
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
|
||||||
|
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
|
||||||
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||||
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
|
|
||||||
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.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
|
||||||
|
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
|
||||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||||
|
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||||
|
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
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/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||||
|
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
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.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
|
||||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||||
|
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||||
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/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
@@ -44,6 +64,8 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
|
|||||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
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=
|
||||||
@@ -52,20 +74,14 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
|
|||||||
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/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/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
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/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||||
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||||
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
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/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=
|
||||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
@@ -73,7 +89,6 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
|||||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
|
||||||
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
|
||||||
@@ -87,31 +102,45 @@ github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht
|
|||||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
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/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw=
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4=
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
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/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
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/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
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.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
|
||||||
|
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
|
||||||
|
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
|
||||||
|
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
|
||||||
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/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
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.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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||||
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=
|
||||||
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/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
||||||
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
||||||
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=
|
||||||
@@ -120,14 +149,17 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
|
|||||||
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
|
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.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
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/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/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
|
||||||
|
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
|
||||||
|
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
|
||||||
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
|
||||||
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
|
||||||
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
|
||||||
@@ -138,6 +170,23 @@ github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62po
|
|||||||
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
|
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
|
||||||
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
|
||||||
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
|
github.com/smartwalle/alipay/v3 v3.2.15 h1:3fvFJnINKKAOXHR/Iv20k1Z7KJ+nOh3oK214lELPqG8=
|
||||||
|
github.com/smartwalle/alipay/v3 v3.2.15/go.mod h1:niTNB609KyUYuAx9Bex/MawEjv2yPx4XOjxSAkqmGjE=
|
||||||
|
github.com/smartwalle/ncrypto v1.0.2 h1:pTAhCqtPCMhpOwFXX+EcMdR6PNzruBNoGQrN2S1GbGI=
|
||||||
|
github.com/smartwalle/ncrypto v1.0.2/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk=
|
||||||
|
github.com/smartwalle/ngx v1.0.6 h1:JPNqNOIj+2nxxFtrSkJO+vKJfeNUSEQueck/Wworjps=
|
||||||
|
github.com/smartwalle/ngx v1.0.6/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0=
|
||||||
|
github.com/smartwalle/nsign v1.0.8 h1:78KWtwKPrdt4Xsn+tNEBVxaTLIJBX9YRX0ZSrMUeuHo=
|
||||||
|
github.com/smartwalle/nsign v1.0.8/go.mod h1:eY6I4CJlyNdVMP+t6z1H6Jpd4m5/V+8xi44ufSTxXgc=
|
||||||
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=
|
||||||
@@ -160,7 +209,10 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
|||||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
|
||||||
|
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
|
||||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
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 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
@@ -178,40 +230,66 @@ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
|||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
|
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||||
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
|
||||||
|
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||||
|
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
|
||||||
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
|
||||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
|
||||||
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
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.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||||
golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
|
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||||
|
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||||
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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||||
|
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-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-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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
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.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||||
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
|
||||||
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -222,12 +300,14 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
|
|||||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
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/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
|
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||||
|
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import (
|
|||||||
"chatplus/store/model"
|
"chatplus/store/model"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
|
"context"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -20,11 +22,12 @@ var logger = logger2.GetLogger()
|
|||||||
|
|
||||||
type ManagerHandler struct {
|
type ManagerHandler struct {
|
||||||
handler.BaseHandler
|
handler.BaseHandler
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
redis *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAdminHandler(app *core.AppServer, db *gorm.DB) *ManagerHandler {
|
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler {
|
||||||
h := ManagerHandler{db: db}
|
h := ManagerHandler{db: db, redis: client}
|
||||||
h.App = app
|
h.App = app
|
||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
@@ -38,13 +41,23 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
manager := h.App.Config.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)
|
// 创建 token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": manager.Username,
|
||||||
|
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||||
|
})
|
||||||
|
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, "Save session failed")
|
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
manager.Password = "" // 清空密码]
|
// 保存到 redis
|
||||||
resp.SUCCESS(c, manager)
|
key := "users/" + manager.Username
|
||||||
|
if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil {
|
||||||
|
resp.ERROR(c, "error with save token: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, tokenString)
|
||||||
} else {
|
} else {
|
||||||
resp.ERROR(c, "用户名或者密码错误")
|
resp.ERROR(c, "用户名或者密码错误")
|
||||||
}
|
}
|
||||||
@@ -52,11 +65,9 @@ func (h *ManagerHandler) Login(c *gin.Context) {
|
|||||||
|
|
||||||
// Logout 注销
|
// Logout 注销
|
||||||
func (h *ManagerHandler) Logout(c *gin.Context) {
|
func (h *ManagerHandler) Logout(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
key := h.GetUserKey(c)
|
||||||
session.Delete(types.SessionAdmin)
|
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||||
err := session.Save()
|
logger.Error("error with delete session: ", err)
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "Save session failed")
|
|
||||||
} else {
|
} else {
|
||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
}
|
}
|
||||||
@@ -64,9 +75,8 @@ func (h *ManagerHandler) Logout(c *gin.Context) {
|
|||||||
|
|
||||||
// Session 会话检测
|
// Session 会话检测
|
||||||
func (h *ManagerHandler) Session(c *gin.Context) {
|
func (h *ManagerHandler) Session(c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
token := c.GetHeader(types.AdminAuthHeader)
|
||||||
admin := session.Get(types.SessionAdmin)
|
if token == "" {
|
||||||
if admin == nil {
|
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
} else {
|
} else {
|
||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
"chatplus/store/vo"
|
"chatplus/store/vo"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -27,23 +25,22 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
|
|||||||
|
|
||||||
func (h *ApiKeyHandler) Save(c *gin.Context) {
|
func (h *ApiKeyHandler) Save(c *gin.Context) {
|
||||||
var data struct {
|
var data struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
UserId uint `json:"user_id"`
|
Platform string `json:"platform"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
LastUsedAt string `json:"last_used_at"`
|
|
||||||
CreatedAt int64 `json:"created_at"`
|
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := model.ApiKey{Value: data.Value, UserId: data.UserId, LastUsedAt: utils.Str2stamp(data.LastUsedAt)}
|
apiKey := model.ApiKey{}
|
||||||
apiKey.Id = data.Id
|
if data.Id > 0 {
|
||||||
if apiKey.Id > 0 {
|
h.db.Find(&apiKey, data.Id)
|
||||||
apiKey.CreatedAt = time.Unix(data.CreatedAt, 0)
|
|
||||||
}
|
}
|
||||||
res := h.db.Save(&apiKey)
|
apiKey.Platform = data.Platform
|
||||||
|
apiKey.Value = data.Value
|
||||||
|
res := h.db.Debug().Save(&apiKey)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
return
|
return
|
||||||
@@ -61,14 +58,9 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *ApiKeyHandler) List(c *gin.Context) {
|
func (h *ApiKeyHandler) List(c *gin.Context) {
|
||||||
userId := h.GetInt(c, "user_id", -1)
|
|
||||||
query := h.db.Session(&gorm.Session{})
|
|
||||||
if userId >= 0 {
|
|
||||||
query = query.Where("user_id", userId)
|
|
||||||
}
|
|
||||||
var items []model.ApiKey
|
var items []model.ApiKey
|
||||||
var keys = make([]vo.ApiKey, 0)
|
var keys = make([]vo.ApiKey, 0)
|
||||||
res := query.Find(&items)
|
res := h.db.Find(&items)
|
||||||
if res.Error == nil {
|
if res.Error == nil {
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
var key vo.ApiKey
|
var key vo.ApiKey
|
||||||
|
|||||||
144
api/handler/admin/chat_model_handler.go
Normal file
144
api/handler/admin/chat_model_handler.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatModelHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||||
|
h := ChatModelHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatModelHandler) Save(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
SortNum int `json:"sort_num"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled, SortNum: data.SortNum, Weight: data.Weight}
|
||||||
|
item.Id = data.Id
|
||||||
|
if item.Id > 0 {
|
||||||
|
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||||
|
}
|
||||||
|
res := h.db.Save(&item)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemVo vo.ChatModel
|
||||||
|
err := utils.CopyObject(item, &itemVo)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "数据拷贝失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemVo.Id = item.Id
|
||||||
|
itemVo.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
resp.SUCCESS(c, itemVo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 模型列表
|
||||||
|
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
enable := h.GetBool(c, "enable")
|
||||||
|
if enable {
|
||||||
|
session = session.Where("enabled", enable)
|
||||||
|
}
|
||||||
|
var items []model.ChatModel
|
||||||
|
var cms = make([]vo.ChatModel, 0)
|
||||||
|
res := session.Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var cm vo.ChatModel
|
||||||
|
err := utils.CopyObject(item, &cm)
|
||||||
|
if err == nil {
|
||||||
|
cm.Id = item.Id
|
||||||
|
cm.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
cms = append(cms, cm)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, cms)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatModelHandler) Enable(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatModelHandler) Sort(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Ids []uint `json:"ids"`
|
||||||
|
Sorts []int `json:"sorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, id := range data.Ids {
|
||||||
|
res := h.db.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatModelHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
res := h.db.Where("id = ?", id).Delete(&model.ChatModel{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
|
|||||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||||
var items []model.ChatRole
|
var items []model.ChatRole
|
||||||
var roles = make([]vo.ChatRole, 0)
|
var roles = make([]vo.ChatRole, 0)
|
||||||
res := h.db.Order("sort ASC").Find(&items)
|
res := h.db.Order("sort_num ASC").Find(&items)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "No data found")
|
resp.ERROR(c, "No data found")
|
||||||
return
|
return
|
||||||
@@ -75,24 +75,24 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
|||||||
resp.SUCCESS(c, roles)
|
resp.SUCCESS(c, roles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetSort 更新角色排序
|
// Sort 更新角色排序
|
||||||
func (h *ChatRoleHandler) SetSort(c *gin.Context) {
|
func (h *ChatRoleHandler) Sort(c *gin.Context) {
|
||||||
var data struct {
|
var data struct {
|
||||||
Id uint `json:"id"`
|
Ids []uint `json:"ids"`
|
||||||
Sort int `json:"sort"`
|
Sorts []int `json:"sorts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
if data.Id <= 0 {
|
|
||||||
resp.HACKER(c)
|
for index, id := range data.Ids {
|
||||||
return
|
res := h.db.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||||
}
|
if res.Error != nil {
|
||||||
res := h.db.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update("sort", data.Sort)
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
if res.Error != nil {
|
return
|
||||||
resp.ERROR(c, "更新数据库失败!")
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
|
|||||||
93
api/handler/admin/order_handler.go
Normal file
93
api/handler/admin/order_handler.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||||
|
h := OrderHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) List(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
PayTime []string `json:"pay_time"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
if data.OrderNo != "" {
|
||||||
|
session = session.Where("order_no", data.OrderNo)
|
||||||
|
}
|
||||||
|
if len(data.PayTime) == 2 {
|
||||||
|
start := utils.Str2stamp(data.PayTime[0] + " 00:00:00")
|
||||||
|
end := utils.Str2stamp(data.PayTime[1] + " 00:00:00")
|
||||||
|
session = session.Where("pay_time >= ? AND pay_time <= ?", start, end)
|
||||||
|
}
|
||||||
|
var total int64
|
||||||
|
session.Model(&model.Order{}).Count(&total)
|
||||||
|
var items []model.Order
|
||||||
|
var list = make([]vo.Order, 0)
|
||||||
|
offset := (data.Page - 1) * data.PageSize
|
||||||
|
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var order vo.Order
|
||||||
|
err := utils.CopyObject(item, &order)
|
||||||
|
if err == nil {
|
||||||
|
order.Id = item.Id
|
||||||
|
order.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, order)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
var item model.Order
|
||||||
|
res := h.db.First(&item, id)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "记录不存在!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.Status == types.OrderPaidSuccess {
|
||||||
|
resp.ERROR(c, "已支付订单不允许删除!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res = h.db.Where("id = ?", id).Delete(&model.Order{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
144
api/handler/admin/product_handler.go
Normal file
144
api/handler/admin/product_handler.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||||
|
h := ProductHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Save(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Days int `json:"days"`
|
||||||
|
Calls int `json:"calls"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item := model.Product{Name: data.Name, Price: data.Price, Discount: data.Discount, Days: data.Days, Calls: data.Calls, Enabled: data.Enabled}
|
||||||
|
item.Id = data.Id
|
||||||
|
if item.Id > 0 {
|
||||||
|
item.CreatedAt = time.Unix(data.CreatedAt, 0)
|
||||||
|
}
|
||||||
|
res := h.db.Save(&item)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var itemVo vo.Product
|
||||||
|
err := utils.CopyObject(item, &itemVo)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "数据拷贝失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
itemVo.Id = item.Id
|
||||||
|
itemVo.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
resp.SUCCESS(c, itemVo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 模型列表
|
||||||
|
func (h *ProductHandler) List(c *gin.Context) {
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
enable := h.GetBool(c, "enable")
|
||||||
|
if enable {
|
||||||
|
session = session.Where("enabled", enable)
|
||||||
|
}
|
||||||
|
var items []model.Product
|
||||||
|
var list = make([]vo.Product, 0)
|
||||||
|
res := session.Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var product vo.Product
|
||||||
|
err := utils.CopyObject(item, &product)
|
||||||
|
if err == nil {
|
||||||
|
product.Id = item.Id
|
||||||
|
product.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, product)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Enable(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.db.Model(&model.Product{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Sort(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Ids []uint `json:"ids"`
|
||||||
|
Sorts []int `json:"sorts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, id := range data.Ids {
|
||||||
|
res := h.db.Model(&model.Product{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ProductHandler) Remove(c *gin.Context) {
|
||||||
|
id := h.GetInt(c, "id", 0)
|
||||||
|
|
||||||
|
if id > 0 {
|
||||||
|
res := h.db.Where("id = ?", id).Delete(&model.Product{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
|
|||||||
|
|
||||||
func (h *RewardHandler) List(c *gin.Context) {
|
func (h *RewardHandler) List(c *gin.Context) {
|
||||||
var items []model.Reward
|
var items []model.Reward
|
||||||
res := h.db.Find(&items)
|
res := h.db.Order("id DESC").Find(&items)
|
||||||
var rewards = make([]vo.Reward, 0)
|
var rewards = make([]vo.Reward, 0)
|
||||||
if res.Error == nil {
|
if res.Error == nil {
|
||||||
userIds := make([]uint, 0)
|
userIds := make([]uint, 0)
|
||||||
@@ -46,7 +46,7 @@ func (h *RewardHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
r.Id = v.Id
|
r.Id = v.Id
|
||||||
r.Username = userMap[v.UserId].Username
|
r.Username = userMap[v.UserId].Mobile
|
||||||
r.CreatedAt = v.CreatedAt.Unix()
|
r.CreatedAt = v.CreatedAt.Unix()
|
||||||
r.UpdatedAt = v.UpdatedAt.Unix()
|
r.UpdatedAt = v.UpdatedAt.Unix()
|
||||||
rewards = append(rewards, r)
|
rewards = append(rewards, r)
|
||||||
|
|||||||
@@ -8,8 +8,6 @@ import (
|
|||||||
"chatplus/store/vo"
|
"chatplus/store/vo"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -30,7 +28,6 @@ func (h *UserHandler) List(c *gin.Context) {
|
|||||||
page := h.GetInt(c, "page", 1)
|
page := h.GetInt(c, "page", 1)
|
||||||
pageSize := h.GetInt(c, "page_size", 20)
|
pageSize := h.GetInt(c, "page_size", 20)
|
||||||
mobile := h.GetTrim(c, "mobile")
|
mobile := h.GetTrim(c, "mobile")
|
||||||
username := h.GetTrim(c, "username")
|
|
||||||
|
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
var items []model.User
|
var items []model.User
|
||||||
@@ -41,9 +38,6 @@ func (h *UserHandler) List(c *gin.Context) {
|
|||||||
if mobile != "" {
|
if mobile != "" {
|
||||||
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
|
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
|
||||||
}
|
}
|
||||||
if username != "" {
|
|
||||||
session = session.Where("username LIKE ?", "%"+username+"%")
|
|
||||||
}
|
|
||||||
|
|
||||||
session.Model(&model.User{}).Count(&total)
|
session.Model(&model.User{}).Count(&total)
|
||||||
res := session.Offset(offset).Limit(pageSize).Find(&items)
|
res := session.Offset(offset).Limit(pageSize).Find(&items)
|
||||||
@@ -68,13 +62,12 @@ func (h *UserHandler) List(c *gin.Context) {
|
|||||||
func (h *UserHandler) Save(c *gin.Context) {
|
func (h *UserHandler) Save(c *gin.Context) {
|
||||||
var data struct {
|
var data struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
Nickname string `json:"nickname"`
|
|
||||||
Calls int `json:"calls"`
|
Calls int `json:"calls"`
|
||||||
ImgCalls int `json:"img_calls"`
|
ImgCalls int `json:"img_calls"`
|
||||||
ChatRoles []string `json:"chat_roles"`
|
ChatRoles []string `json:"chat_roles"`
|
||||||
|
ChatModels []string `json:"chat_models"`
|
||||||
ExpiredTime string `json:"expired_time"`
|
ExpiredTime string `json:"expired_time"`
|
||||||
Status bool `json:"status"`
|
Status bool `json:"status"`
|
||||||
}
|
}
|
||||||
@@ -89,35 +82,34 @@ func (h *UserHandler) Save(c *gin.Context) {
|
|||||||
user.Id = data.Id
|
user.Id = data.Id
|
||||||
// 此处需要用 map 更新,用结构体无法更新 0 值
|
// 此处需要用 map 更新,用结构体无法更新 0 值
|
||||||
res = h.db.Model(&user).Updates(map[string]interface{}{
|
res = h.db.Model(&user).Updates(map[string]interface{}{
|
||||||
"nickname": data.Nickname,
|
"mobile": data.Mobile,
|
||||||
"mobile": data.Mobile,
|
"calls": data.Calls,
|
||||||
"calls": data.Calls,
|
"img_calls": data.ImgCalls,
|
||||||
"img_calls": data.ImgCalls,
|
"status": data.Status,
|
||||||
"status": data.Status,
|
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
|
||||||
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
|
"chat_models_json": utils.JsonEncode(data.ChatModels),
|
||||||
"expired_time": utils.Str2stamp(data.ExpiredTime),
|
"expired_time": utils.Str2stamp(data.ExpiredTime),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
salt := utils.RandString(8)
|
salt := utils.RandString(8)
|
||||||
u := model.User{
|
u := model.User{
|
||||||
Username: data.Username,
|
Mobile: data.Mobile,
|
||||||
Password: utils.GenPassword(data.Password, salt),
|
Password: utils.GenPassword(data.Password, salt),
|
||||||
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(data.ChatRoles),
|
ChatRoles: utils.JsonEncode(data.ChatRoles),
|
||||||
|
ChatModels: utils.JsonEncode(data.ChatModels),
|
||||||
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
|
||||||
ChatConfig: utils.JsonEncode(types.ChatConfig{
|
ChatConfig: utils.JsonEncode(types.UserChatConfig{
|
||||||
Temperature: h.App.ChatConfig.Temperature,
|
ApiKeys: map[types.Platform]string{
|
||||||
MaxTokens: h.App.ChatConfig.MaxTokens,
|
types.OpenAI: "",
|
||||||
EnableContext: h.App.ChatConfig.EnableContext,
|
types.Azure: "",
|
||||||
EnableHistory: true,
|
types.ChatGLM: "",
|
||||||
Model: h.App.ChatConfig.Model,
|
},
|
||||||
ApiKey: "",
|
|
||||||
}),
|
}),
|
||||||
Calls: h.App.SysConfig.UserInitCalls,
|
Calls: data.Calls,
|
||||||
|
ImgCalls: data.ImgCalls,
|
||||||
}
|
}
|
||||||
res = h.db.Create(&u)
|
res = h.db.Create(&u)
|
||||||
_ = utils.CopyObject(u, &userVo)
|
_ = utils.CopyObject(u, &userVo)
|
||||||
@@ -204,7 +196,7 @@ func (h *UserHandler) LoginLog(c *gin.Context) {
|
|||||||
h.db.Model(&model.UserLoginLog{}).Count(&total)
|
h.db.Model(&model.UserLoginLog{}).Count(&total)
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
var items []model.UserLoginLog
|
var items []model.UserLoginLog
|
||||||
res := h.db.Offset(offset).Limit(pageSize).Find(&items)
|
res := h.db.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "获取数据失败")
|
resp.ERROR(c, "获取数据失败")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core"
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
logger2 "chatplus/logger"
|
logger2 "chatplus/logger"
|
||||||
"strconv"
|
"chatplus/utils"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -20,47 +22,30 @@ func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
|
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
|
||||||
return intValue(c.PostForm(key), defaultValue)
|
return utils.IntValue(c.PostForm(key), defaultValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
|
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
|
||||||
return intValue(c.Query(key), defaultValue)
|
return utils.IntValue(c.Query(key), defaultValue)
|
||||||
}
|
|
||||||
|
|
||||||
func intValue(str string, defaultValue int) int {
|
|
||||||
value, err := strconv.Atoi(str)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
|
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
|
||||||
return floatValue(c.Query(key))
|
return utils.FloatValue(c.Query(key))
|
||||||
}
|
}
|
||||||
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
|
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
|
||||||
return floatValue(c.PostForm(key))
|
return utils.FloatValue(c.PostForm(key))
|
||||||
}
|
|
||||||
|
|
||||||
func floatValue(str string) float64 {
|
|
||||||
value, err := strconv.ParseFloat(str, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
|
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
|
||||||
return boolValue(c.Query(key))
|
return utils.BoolValue(c.Query(key))
|
||||||
}
|
}
|
||||||
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
|
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
|
||||||
return boolValue(c.PostForm(key))
|
return utils.BoolValue(c.PostForm(key))
|
||||||
}
|
}
|
||||||
|
func (h *BaseHandler) GetUserKey(c *gin.Context) string {
|
||||||
func boolValue(str string) bool {
|
userId, ok := c.Get(types.LoginUserID)
|
||||||
value, err := strconv.ParseBool(str)
|
if !ok {
|
||||||
if err != nil {
|
return ""
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
return value
|
return fmt.Sprintf("users/%v", userId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,606 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"chatplus/core"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
|
||||||
|
|
||||||
type ChatHandler struct {
|
|
||||||
BaseHandler
|
|
||||||
db *gorm.DB
|
|
||||||
leveldb *store.LevelDB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB) *ChatHandler {
|
|
||||||
handler := ChatHandler{db: db, leveldb: levelDB}
|
|
||||||
handler.App = app
|
|
||||||
return &handler
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatConfig types.ChatConfig
|
|
||||||
|
|
||||||
// ChatHandle 处理聊天 WebSocket 请求
|
|
||||||
func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
|
||||||
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionId := c.Query("session_id")
|
|
||||||
roleId := h.GetInt(c, "role_id", 0)
|
|
||||||
chatId := c.Query("chat_id")
|
|
||||||
chatModel := c.Query("model")
|
|
||||||
|
|
||||||
session := h.App.ChatSession.Get(sessionId)
|
|
||||||
if session == nil {
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
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
|
|
||||||
var chat model.ChatItem
|
|
||||||
res := h.db.Where("chat_id=?", chatId).First(&chat)
|
|
||||||
if res.Error == nil {
|
|
||||||
chatModel = chat.Model
|
|
||||||
roleId = int(chat.RoleId)
|
|
||||||
}
|
|
||||||
|
|
||||||
session.ChatId = chatId
|
|
||||||
session.Model = chatModel
|
|
||||||
logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username)
|
|
||||||
client := types.NewWsClient(ws)
|
|
||||||
var chatRole model.ChatRole
|
|
||||||
res = h.db.First(&chatRole, roleId)
|
|
||||||
if res.Error != nil || !chatRole.Enable {
|
|
||||||
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化聊天配置
|
|
||||||
var config model.Config
|
|
||||||
h.db.Where("marker", "chat").First(&config)
|
|
||||||
err = utils.JsonDecode(config.Config, &chatConfig)
|
|
||||||
if err != nil {
|
|
||||||
utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存会话连接
|
|
||||||
h.App.ChatClients.Put(sessionId, client)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
_, msg, err := client.Receive()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
client.Close()
|
|
||||||
h.App.ChatClients.Delete(sessionId)
|
|
||||||
h.App.ReqCancelFunc.Delete(sessionId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
message := string(msg)
|
|
||||||
logger.Info("Receive a message: ", message)
|
|
||||||
//utils.ReplyMessage(client, "这是一条测试消息!")
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
h.App.ReqCancelFunc.Put(sessionId, cancel)
|
|
||||||
// 回复消息
|
|
||||||
err = h.sendMessage(ctx, session, chatRole, message, client)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
} else {
|
|
||||||
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
|
||||||
logger.Info("回答完毕: " + string(message))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 将消息发送给 ChatGPT 并获取结果,通过 WebSocket 推送到客户端
|
|
||||||
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
|
||||||
promptCreatedAt := time.Now() // 记录提问时间
|
|
||||||
|
|
||||||
var user model.User
|
|
||||||
res := h.db.Model(&model.User{}).First(&user, session.UserId)
|
|
||||||
if res.Error != nil {
|
|
||||||
utils.ReplyMessage(ws, "非法用户,请联系管理员!")
|
|
||||||
return res.Error
|
|
||||||
}
|
|
||||||
var userVo vo.User
|
|
||||||
err := utils.CopyObject(user, &userVo)
|
|
||||||
userVo.Id = user.Id
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("User 对象转换失败," + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if userVo.Status == false {
|
|
||||||
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKey == "" {
|
|
||||||
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
|
||||||
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var req = types.ApiRequest{
|
|
||||||
Model: session.Model,
|
|
||||||
Temperature: userVo.ChatConfig.Temperature,
|
|
||||||
MaxTokens: userVo.ChatConfig.MaxTokens,
|
|
||||||
Stream: true,
|
|
||||||
Functions: types.InnerFunctions,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载聊天上下文
|
|
||||||
var chatCtx []interface{}
|
|
||||||
if userVo.ChatConfig.EnableContext {
|
|
||||||
if h.App.ChatContexts.Has(session.ChatId) {
|
|
||||||
chatCtx = h.App.ChatContexts.Get(session.ChatId)
|
|
||||||
} else {
|
|
||||||
// calculate the tokens of current request, to prevent to exceeding the max tokens num
|
|
||||||
tokens := req.MaxTokens
|
|
||||||
for _, f := range types.InnerFunctions {
|
|
||||||
tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
|
|
||||||
tokens += tks
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading the role context
|
|
||||||
var messages []types.Message
|
|
||||||
err := utils.JsonDecode(role.Context, &messages)
|
|
||||||
if err == nil {
|
|
||||||
for _, v := range messages {
|
|
||||||
tks, _ := utils.CalcTokens(v.Content, req.Model)
|
|
||||||
if tokens+tks >= types.ModelToTokens[req.Model] {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens += tks
|
|
||||||
chatCtx = append(chatCtx, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loading recent chat history as chat context
|
|
||||||
if chatConfig.ContextDeep > 0 {
|
|
||||||
var historyMessages []model.HistoryMessage
|
|
||||||
res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages)
|
|
||||||
if res.Error == nil {
|
|
||||||
for _, msg := range historyMessages {
|
|
||||||
if tokens+msg.Tokens >= types.ModelToTokens[session.Model] {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
tokens += msg.Tokens
|
|
||||||
ms := types.Message{Role: "user", Content: msg.Content}
|
|
||||||
if msg.Type == types.ReplyMsg {
|
|
||||||
ms.Role = "assistant"
|
|
||||||
}
|
|
||||||
chatCtx = append(chatCtx, ms)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.Debugf("聊天上下文:%+v", chatCtx)
|
|
||||||
}
|
|
||||||
reqMgs := make([]interface{}, 0)
|
|
||||||
for _, m := range chatCtx {
|
|
||||||
reqMgs = append(reqMgs, m)
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Messages = append(reqMgs, map[string]interface{}{
|
|
||||||
"role": "user",
|
|
||||||
"content": prompt,
|
|
||||||
})
|
|
||||||
var apiKey string
|
|
||||||
response, err := h.doRequest(ctx, userVo, &apiKey, req)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
return nil
|
|
||||||
} else if strings.Contains(err.Error(), "no available key") {
|
|
||||||
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY🔑,您可以导入自己的 API KEY🔑 继续使用!🙏🙏🙏")
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
defer response.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := response.Header.Get("Content-Type")
|
|
||||||
if strings.Contains(contentType, "text/event-stream") {
|
|
||||||
if true {
|
|
||||||
replyCreatedAt := time.Now()
|
|
||||||
// 循环读取 Chunk 消息
|
|
||||||
var message = types.Message{}
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var functionCall = false
|
|
||||||
var functionName string
|
|
||||||
var arguments = make([]string, 0)
|
|
||||||
reader := bufio.NewReader(response.Body)
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "context canceled") {
|
|
||||||
logger.Info("用户取消了请求:", prompt)
|
|
||||||
} else if err != io.EOF {
|
|
||||||
logger.Error("信息读取出错:", err)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !strings.Contains(line, "data:") || len(line) < 30 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseBody = types.ApiResponse{}
|
|
||||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
|
||||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
|
||||||
logger.Error(err, line)
|
|
||||||
utils.ReplyMessage(ws, ErrorMsg)
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
fun := responseBody.Choices[0].Delta.FunctionCall
|
|
||||||
if functionCall && fun.Name == "" {
|
|
||||||
arguments = append(arguments, fun.Arguments)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsEmptyValue(fun) {
|
|
||||||
functionCall = true
|
|
||||||
functionName = fun.Name
|
|
||||||
f := h.App.Functions[functionName]
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化 role
|
|
||||||
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
|
||||||
message.Role = responseBody.Choices[0].Delta.Role
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
|
||||||
continue
|
|
||||||
} else if responseBody.Choices[0].FinishReason != "" {
|
|
||||||
break // 输出完成或者输出中断了
|
|
||||||
} else {
|
|
||||||
content := responseBody.Choices[0].Delta.Content
|
|
||||||
contents = append(contents, utils.InterfaceToString(content))
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} // end for
|
|
||||||
|
|
||||||
if functionCall { // 调用函数完成任务
|
|
||||||
var params map[string]interface{}
|
|
||||||
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
|
||||||
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
|
||||||
|
|
||||||
// for creating image, check if the user's img_calls > 0
|
|
||||||
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
|
||||||
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
|
||||||
utils.ReplyMessage(ws, "")
|
|
||||||
} else {
|
|
||||||
f := h.App.Functions[functionName]
|
|
||||||
data, err := f.Invoke(params)
|
|
||||||
if err != nil {
|
|
||||||
msg := "调用函数出错:" + err.Error()
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: msg,
|
|
||||||
})
|
|
||||||
contents = append(contents, msg)
|
|
||||||
} else {
|
|
||||||
content := data
|
|
||||||
if functionName == types.FuncMidJourney {
|
|
||||||
key := utils.Sha256(data)
|
|
||||||
logger.Debug(data, ",", key)
|
|
||||||
// add task for MidJourney
|
|
||||||
h.App.MjTaskClients.Put(key, ws)
|
|
||||||
task := types.MjTask{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Icon: "/images/avatar/mid_journey.png",
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
}
|
|
||||||
err := h.leveldb.Put(types.TaskStorePrefix+key, task)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("error with store MidJourney task: ", err)
|
|
||||||
}
|
|
||||||
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
|
||||||
|
|
||||||
// update user's img_calls
|
|
||||||
h.db.Model(&user).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.ReplyChunkMessage(ws, types.WsMessage{
|
|
||||||
Type: types.WsMiddle,
|
|
||||||
Content: content,
|
|
||||||
})
|
|
||||||
contents = append(contents, content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息发送成功
|
|
||||||
if len(contents) > 0 {
|
|
||||||
// 更新用户的对话次数
|
|
||||||
if userVo.ChatConfig.ApiKey == "" { // 如果用户使用的是自己绑定的 API KEY 则不扣减对话次数
|
|
||||||
h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
|
|
||||||
}
|
|
||||||
|
|
||||||
if message.Role == "" {
|
|
||||||
message.Role = "assistant"
|
|
||||||
}
|
|
||||||
message.Content = strings.Join(contents, "")
|
|
||||||
useMsg := types.Message{Role: "user", Content: prompt}
|
|
||||||
|
|
||||||
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
|
||||||
if userVo.ChatConfig.EnableContext && functionCall == false {
|
|
||||||
chatCtx = append(chatCtx, useMsg) // 提问消息
|
|
||||||
chatCtx = append(chatCtx, message) // 回复消息
|
|
||||||
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 追加聊天记录
|
|
||||||
if userVo.ChatConfig.EnableHistory {
|
|
||||||
useContext := true
|
|
||||||
if functionCall {
|
|
||||||
useContext = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// for prompt
|
|
||||||
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
historyUserMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.PromptMsg,
|
|
||||||
Icon: user.Avatar,
|
|
||||||
Content: prompt,
|
|
||||||
Tokens: promptToken,
|
|
||||||
UseContext: useContext,
|
|
||||||
}
|
|
||||||
historyUserMsg.CreatedAt = promptCreatedAt
|
|
||||||
historyUserMsg.UpdatedAt = promptCreatedAt
|
|
||||||
res := h.db.Save(&historyUserMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save prompt history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// for reply
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
var replyToken = 0
|
|
||||||
if functionCall { // 函数名 + 参数 token
|
|
||||||
tokens, _ := utils.CalcTokens(functionName, req.Model)
|
|
||||||
replyToken += tokens
|
|
||||||
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
|
|
||||||
replyToken += tokens
|
|
||||||
} else {
|
|
||||||
replyToken, _ = utils.CalcTokens(message.Content, req.Model)
|
|
||||||
}
|
|
||||||
|
|
||||||
historyReplyMsg := model.HistoryMessage{
|
|
||||||
UserId: userVo.Id,
|
|
||||||
ChatId: session.ChatId,
|
|
||||||
RoleId: role.Id,
|
|
||||||
Type: types.ReplyMsg,
|
|
||||||
Icon: role.Icon,
|
|
||||||
Content: message.Content,
|
|
||||||
Tokens: replyToken,
|
|
||||||
UseContext: useContext,
|
|
||||||
}
|
|
||||||
historyReplyMsg.CreatedAt = replyCreatedAt
|
|
||||||
historyReplyMsg.UpdatedAt = replyCreatedAt
|
|
||||||
res = h.db.Create(&historyReplyMsg)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("failed to save reply history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算本次对话消耗的总 token 数量
|
|
||||||
var totalTokens = 0
|
|
||||||
if functionCall { // prompt + 函数名 + 参数 token
|
|
||||||
totalTokens = promptToken + replyToken
|
|
||||||
} else {
|
|
||||||
totalTokens = replyToken + getTotalTokens(req)
|
|
||||||
}
|
|
||||||
//utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("\n\n `本轮对话共消耗 Token 数量: %d`", totalTokens+11)})
|
|
||||||
if userVo.ChatConfig.ApiKey != "" { // 调用自己的 API KEY 不计算 token 消耗
|
|
||||||
h.db.Model(&user).UpdateColumn("tokens", gorm.Expr("tokens + ?",
|
|
||||||
totalTokens))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存当前会话
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res = h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
chatItem.ChatId = session.ChatId
|
|
||||||
chatItem.UserId = session.UserId
|
|
||||||
chatItem.RoleId = role.Id
|
|
||||||
chatItem.Model = session.Model
|
|
||||||
if utf8.RuneCountInString(prompt) > 30 {
|
|
||||||
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
|
||||||
} else {
|
|
||||||
chatItem.Title = prompt
|
|
||||||
}
|
|
||||||
h.db.Create(&chatItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body, err := io.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with reading response: %v", err)
|
|
||||||
}
|
|
||||||
var res types.ApiError
|
|
||||||
err = json.Unmarshal(body, &res)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error with decode response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenAI API 调用异常处理
|
|
||||||
// TODO: 是否考虑重发消息?
|
|
||||||
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
|
|
||||||
utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。")
|
|
||||||
// 移除当前 API key
|
|
||||||
h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{})
|
|
||||||
} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
|
|
||||||
utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
|
|
||||||
} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
|
|
||||||
logger.Error(res.Error.Message)
|
|
||||||
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
|
|
||||||
h.App.ChatContexts.Delete(session.ChatId)
|
|
||||||
return h.sendMessage(ctx, session, role, prompt, ws)
|
|
||||||
} else {
|
|
||||||
utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 发送请求到 OpenAI 服务器
|
|
||||||
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
|
||||||
func (h *ChatHandler) doRequest(ctx context.Context, user vo.User, apiKey *string, req types.ApiRequest) (*http.Response, error) {
|
|
||||||
var client *http.Client
|
|
||||||
requestBody, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 创建 HttpClient 请求对象
|
|
||||||
request, err := http.NewRequest(http.MethodPost, h.App.ChatConfig.ApiURL, bytes.NewBuffer(requestBody))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request = request.WithContext(ctx)
|
|
||||||
request.Header.Add("Content-Type", "application/json")
|
|
||||||
|
|
||||||
proxyURL := h.App.Config.ProxyURL
|
|
||||||
if proxyURL == "" {
|
|
||||||
client = &http.Client{}
|
|
||||||
} else { // 使用代理
|
|
||||||
proxy, _ := url.Parse(proxyURL)
|
|
||||||
client = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyURL(proxy),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 查询当前用户是否导入了自己的 API KEY
|
|
||||||
if user.ChatConfig.ApiKey != "" {
|
|
||||||
logger.Info("使用用户自己的 API KEY: ", user.ChatConfig.ApiKey)
|
|
||||||
*apiKey = user.ChatConfig.ApiKey
|
|
||||||
} else { // 获取系统的 API KEY
|
|
||||||
var key model.ApiKey
|
|
||||||
res := h.db.Where("user_id = ?", 0).Order("last_used_at ASC").First(&key)
|
|
||||||
if res.Error != nil {
|
|
||||||
return nil, errors.New("no available key, please import key")
|
|
||||||
}
|
|
||||||
*apiKey = key.Value
|
|
||||||
// 更新 API KEY 的最后使用时间
|
|
||||||
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("Sending OpenAI request, KEY: %s, PROXY: %s, Model: %s", *apiKey, proxyURL, req.Model)
|
|
||||||
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
|
|
||||||
return client.Do(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tokens 统计 token 数量
|
|
||||||
func (h *ChatHandler) Tokens(c *gin.Context) {
|
|
||||||
text := c.Query("text")
|
|
||||||
md := c.Query("model")
|
|
||||||
tokens, err := utils.CalcTokens(text, md)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, tokens)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getTotalTokens(req types.ApiRequest) int {
|
|
||||||
encode := utils.JsonEncode(req.Messages)
|
|
||||||
var items []map[string]interface{}
|
|
||||||
err := utils.JsonDecode(encode, &items)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
tokens := 0
|
|
||||||
for _, item := range items {
|
|
||||||
content, ok := item["content"]
|
|
||||||
if ok && !utils.IsEmptyValue(content) {
|
|
||||||
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
|
||||||
if err == nil {
|
|
||||||
tokens += t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopGenerate 停止生成
|
|
||||||
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
|
||||||
sessionId := c.Query("session_id")
|
|
||||||
if h.App.ReqCancelFunc.Has(sessionId) {
|
|
||||||
h.App.ReqCancelFunc.Get(sessionId)()
|
|
||||||
h.App.ReqCancelFunc.Delete(sessionId)
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Update 更新会话标题
|
|
||||||
func (h *ChatHandler) Update(c *gin.Context) {
|
|
||||||
var data struct {
|
|
||||||
Id uint `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var m = model.ChatItem{}
|
|
||||||
m.Id = data.Id
|
|
||||||
res := h.db.Model(&m).UpdateColumn("title", data.Title)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "Failed to update database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 删除会话
|
|
||||||
func (h *ChatHandler) Remove(c *gin.Context) {
|
|
||||||
chatId := h.GetTrim(c, "chat_id")
|
|
||||||
if chatId == "" {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "Failed to update database")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空会话上下文
|
|
||||||
h.App.ChatContexts.Delete(chatId)
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// History 获取聊天历史记录
|
|
||||||
func (h *ChatHandler) History(c *gin.Context) {
|
|
||||||
chatId := c.Query("chat_id") // 会话 ID
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var items []model.HistoryMessage
|
|
||||||
var messages = make([]vo.HistoryMessage, 0)
|
|
||||||
res := h.db.Where("chat_id = ? AND user_id = ?", chatId, user.Id).Find(&items)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No history message")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
for _, item := range items {
|
|
||||||
var v vo.HistoryMessage
|
|
||||||
err := utils.CopyObject(item, &v)
|
|
||||||
v.CreatedAt = item.CreatedAt.Unix()
|
|
||||||
v.UpdatedAt = item.UpdatedAt.Unix()
|
|
||||||
if err == nil {
|
|
||||||
messages = append(messages, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, messages)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear 清空所有聊天记录
|
|
||||||
func (h *ChatHandler) Clear(c *gin.Context) {
|
|
||||||
// 获取当前登录用户所有的聊天会话
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
|
||||||
if err != nil {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chats []model.ChatItem
|
|
||||||
res := h.db.Where("user_id = ?", user.Id).Find(&chats)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No chats found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// 清空聊天记录
|
|
||||||
for _, chat := range chats {
|
|
||||||
err := h.db.Where("chat_id = ? AND user_id = ?", chat.ChatId, user.Id).Delete(&model.HistoryMessage{})
|
|
||||||
if err != nil {
|
|
||||||
logger.Warnf("Failed to delele chat history for ChatID: %s", chat.ChatId)
|
|
||||||
}
|
|
||||||
// 清空会话上下文
|
|
||||||
h.App.ChatContexts.Delete(chat.ChatId)
|
|
||||||
}
|
|
||||||
// 删除所有的会话记录
|
|
||||||
res = h.db.Where("user_id = ?", user.Id).Delete(&model.ChatItem{})
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "Failed to remove chat from database.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, types.OkMsg)
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package handler
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"chatplus/utils/resp"
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
// List 获取会话列表
|
|
||||||
func (h *ChatHandler) List(c *gin.Context) {
|
|
||||||
userId := h.GetInt(c, "user_id", 0)
|
|
||||||
if userId == 0 {
|
|
||||||
resp.ERROR(c, "The parameter 'user_id' is needed.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var items = make([]vo.ChatItem, 0)
|
|
||||||
var chats []model.ChatItem
|
|
||||||
res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
|
|
||||||
if res.Error == nil {
|
|
||||||
var roleIds = make([]uint, 0)
|
|
||||||
for _, chat := range chats {
|
|
||||||
roleIds = append(roleIds, chat.RoleId)
|
|
||||||
}
|
|
||||||
var roles []model.ChatRole
|
|
||||||
res = h.db.Find(&roles, roleIds)
|
|
||||||
if res.Error == nil {
|
|
||||||
roleMap := make(map[uint]model.ChatRole)
|
|
||||||
for _, role := range roles {
|
|
||||||
roleMap[role.Id] = role
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, chat := range chats {
|
|
||||||
var item vo.ChatItem
|
|
||||||
err := utils.CopyObject(chat, &item)
|
|
||||||
if err == nil {
|
|
||||||
item.Id = chat.Id
|
|
||||||
item.Icon = roleMap[chat.RoleId].Icon
|
|
||||||
items = append(items, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c, items)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ChatHandler) Detail(c *gin.Context) {
|
|
||||||
chatId := h.GetTrim(c, "chat_id")
|
|
||||||
if utils.IsEmptyValue(chatId) {
|
|
||||||
resp.ERROR(c, "Invalid chatId")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatItem model.ChatItem
|
|
||||||
res := h.db.Where("chat_id = ?", chatId).First(&chatItem)
|
|
||||||
if res.Error != nil {
|
|
||||||
resp.ERROR(c, "No chat found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatItemVo vo.ChatItem
|
|
||||||
err := utils.CopyObject(chatItem, &chatItemVo)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.SUCCESS(c, chatItemVo)
|
|
||||||
}
|
|
||||||
58
api/handler/chat_model_handler.go
Normal file
58
api/handler/chat_model_handler.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChatModelHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
|
||||||
|
h := ChatModelHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 模型列表
|
||||||
|
func (h *ChatModelHandler) List(c *gin.Context) {
|
||||||
|
var items []model.ChatModel
|
||||||
|
var chatModels = make([]vo.ChatModel, 0)
|
||||||
|
// 只加载用户订阅的 AI 模型
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var models []string
|
||||||
|
err = utils.JsonDecode(user.ChatModels, &models)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "当前用户没有订阅任何模型")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.db.Where("enabled = ?", true).Where("value IN ?", models).Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var cm vo.ChatModel
|
||||||
|
err := utils.CopyObject(item, &cm)
|
||||||
|
if err == nil {
|
||||||
|
cm.Id = item.Id
|
||||||
|
cm.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
cm.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
chatModels = append(chatModels, cm)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, chatModels)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core"
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
"chatplus/store/model"
|
"chatplus/store/model"
|
||||||
"chatplus/store/vo"
|
"chatplus/store/vo"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
@@ -24,20 +25,39 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
|
|||||||
|
|
||||||
// List get user list
|
// List get user list
|
||||||
func (h *ChatRoleHandler) List(c *gin.Context) {
|
func (h *ChatRoleHandler) List(c *gin.Context) {
|
||||||
|
all := h.GetBool(c, "all")
|
||||||
var roles []model.ChatRole
|
var roles []model.ChatRole
|
||||||
res := h.db.Where("enable", true).Order("sort ASC").Find(&roles)
|
res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "No roles found,"+res.Error.Error())
|
resp.ERROR(c, "No roles found,"+res.Error.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := utils.GetLoginUser(c, h.db)
|
// 获取所有角色
|
||||||
if err != nil {
|
if all {
|
||||||
|
// 转成 vo
|
||||||
|
var roleVos = make([]vo.ChatRole, 0)
|
||||||
|
for _, r := range roles {
|
||||||
|
var v vo.ChatRole
|
||||||
|
err := utils.CopyObject(r, &v)
|
||||||
|
if err == nil {
|
||||||
|
v.Id = r.Id
|
||||||
|
roleVos = append(roleVos, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, roleVos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
|
if userId == 0 {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
var user model.User
|
||||||
|
h.db.First(&user, userId)
|
||||||
var roleKeys []string
|
var roleKeys []string
|
||||||
err = utils.JsonDecode(user.ChatRoles, &roleKeys)
|
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, "角色解析失败!")
|
resp.ERROR(c, "角色解析失败!")
|
||||||
return
|
return
|
||||||
@@ -57,3 +77,29 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
resp.SUCCESS(c, roleVos)
|
resp.SUCCESS(c, roleVos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateRole 更新用户聊天角色
|
||||||
|
func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Keys []string `json:"keys"`
|
||||||
|
}
|
||||||
|
if err = c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.db.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys))
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("添加应用失败:", err)
|
||||||
|
resp.ERROR(c, "更新数据库失败!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|||||||
286
api/handler/chatimpl/azure_handler.go
Normal file
286
api/handler/chatimpl/azure_handler.go
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 微软 Azure 模型消息发送实现
|
||||||
|
|
||||||
|
func (h *ChatHandler) sendAzureMessage(
|
||||||
|
chatCtx []interface{},
|
||||||
|
req types.ApiRequest,
|
||||||
|
userVo vo.User,
|
||||||
|
ctx context.Context,
|
||||||
|
session *types.ChatSession,
|
||||||
|
role model.ChatRole,
|
||||||
|
prompt string,
|
||||||
|
ws *types.WsClient) error {
|
||||||
|
promptCreatedAt := time.Now() // 记录提问时间
|
||||||
|
start := time.Now()
|
||||||
|
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||||
|
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||||
|
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
return nil
|
||||||
|
} else if strings.Contains(err.Error(), "no available key") {
|
||||||
|
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ReplyMessage(ws, ErrorMsg)
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := response.Header.Get("Content-Type")
|
||||||
|
if strings.Contains(contentType, "text/event-stream") {
|
||||||
|
replyCreatedAt := time.Now() // 记录回复时间
|
||||||
|
// 循环读取 Chunk 消息
|
||||||
|
var message = types.Message{}
|
||||||
|
var contents = make([]string, 0)
|
||||||
|
var functionCall = false
|
||||||
|
var functionName string
|
||||||
|
var arguments = make([]string, 0)
|
||||||
|
scanner := bufio.NewScanner(response.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseBody = types.ApiResponse{}
|
||||||
|
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||||
|
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
||||||
|
logger.Error(err, line)
|
||||||
|
utils.ReplyMessage(ws, ErrorMsg)
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fun := responseBody.Choices[0].Delta.FunctionCall
|
||||||
|
if functionCall && fun.Name == "" {
|
||||||
|
arguments = append(arguments, fun.Arguments)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsEmptyValue(fun) {
|
||||||
|
functionName = fun.Name
|
||||||
|
f := h.App.Functions[functionName]
|
||||||
|
if f != nil {
|
||||||
|
functionCall = true
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 role
|
||||||
|
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
||||||
|
message.Role = responseBody.Choices[0].Delta.Role
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
continue
|
||||||
|
} else if responseBody.Choices[0].FinishReason != "" {
|
||||||
|
break // 输出完成或者输出中断了
|
||||||
|
} else {
|
||||||
|
content := responseBody.Choices[0].Delta.Content
|
||||||
|
contents = append(contents, utils.InterfaceToString(content))
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
} else {
|
||||||
|
logger.Error("信息读取出错:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if functionCall { // 调用函数完成任务
|
||||||
|
var params map[string]interface{}
|
||||||
|
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||||
|
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
||||||
|
|
||||||
|
// for creating image, check if the user's img_calls > 0
|
||||||
|
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
||||||
|
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
} else {
|
||||||
|
f := h.App.Functions[functionName]
|
||||||
|
if functionName == types.FuncMidJourney {
|
||||||
|
params["user_id"] = userVo.Id
|
||||||
|
params["role_id"] = role.Id
|
||||||
|
params["chat_id"] = session.ChatId
|
||||||
|
params["icon"] = "/images/avatar/mid_journey.png"
|
||||||
|
params["session_id"] = session.SessionId
|
||||||
|
}
|
||||||
|
data, err := f.Invoke(params)
|
||||||
|
if err != nil {
|
||||||
|
msg := "调用函数出错:" + err.Error()
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: msg,
|
||||||
|
})
|
||||||
|
contents = append(contents, msg)
|
||||||
|
} else {
|
||||||
|
content := data
|
||||||
|
if functionName == types.FuncMidJourney {
|
||||||
|
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
||||||
|
h.mjService.ChatClients.Put(session.SessionId, ws)
|
||||||
|
// update user's img_calls
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
contents = append(contents, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息发送成功
|
||||||
|
if len(contents) > 0 {
|
||||||
|
// 更新用户的对话次数
|
||||||
|
h.subUserCalls(userVo, session)
|
||||||
|
|
||||||
|
if message.Role == "" {
|
||||||
|
message.Role = "assistant"
|
||||||
|
}
|
||||||
|
message.Content = strings.Join(contents, "")
|
||||||
|
useMsg := types.Message{Role: "user", Content: prompt}
|
||||||
|
|
||||||
|
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||||
|
if h.App.ChatConfig.EnableContext && functionCall == false {
|
||||||
|
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||||
|
chatCtx = append(chatCtx, message) // 回复消息
|
||||||
|
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加聊天记录
|
||||||
|
if h.App.ChatConfig.EnableHistory {
|
||||||
|
useContext := true
|
||||||
|
if functionCall {
|
||||||
|
useContext = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// for prompt
|
||||||
|
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
historyUserMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.PromptMsg,
|
||||||
|
Icon: userVo.Avatar,
|
||||||
|
Content: prompt,
|
||||||
|
Tokens: promptToken,
|
||||||
|
UseContext: useContext,
|
||||||
|
}
|
||||||
|
historyUserMsg.CreatedAt = promptCreatedAt
|
||||||
|
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||||
|
res := h.db.Save(&historyUserMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save prompt history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算本次对话消耗的总 token 数量
|
||||||
|
var totalTokens = 0
|
||||||
|
if functionCall { // prompt + 函数名 + 参数 token
|
||||||
|
tokens, _ := utils.CalcTokens(functionName, req.Model)
|
||||||
|
totalTokens += tokens
|
||||||
|
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
|
||||||
|
totalTokens += tokens
|
||||||
|
} else {
|
||||||
|
totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||||
|
}
|
||||||
|
totalTokens += getTotalTokens(req)
|
||||||
|
|
||||||
|
historyReplyMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.ReplyMsg,
|
||||||
|
Icon: role.Icon,
|
||||||
|
Content: message.Content,
|
||||||
|
Tokens: totalTokens,
|
||||||
|
UseContext: useContext,
|
||||||
|
}
|
||||||
|
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||||
|
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||||
|
res = h.db.Create(&historyReplyMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前会话
|
||||||
|
var chatItem model.ChatItem
|
||||||
|
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||||
|
if res.Error != nil {
|
||||||
|
chatItem.ChatId = session.ChatId
|
||||||
|
chatItem.UserId = session.UserId
|
||||||
|
chatItem.RoleId = role.Id
|
||||||
|
chatItem.ModelId = session.Model.Id
|
||||||
|
if utf8.RuneCountInString(prompt) > 30 {
|
||||||
|
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||||
|
} else {
|
||||||
|
chatItem.Title = prompt
|
||||||
|
}
|
||||||
|
h.db.Create(&chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with reading response: %v", err)
|
||||||
|
}
|
||||||
|
var res types.ApiError
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(res.Error.Message, "maximum context length") {
|
||||||
|
logger.Error(res.Error.Message)
|
||||||
|
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
|
||||||
|
h.App.ChatContexts.Delete(session.ChatId)
|
||||||
|
return h.sendMessage(ctx, session, role, prompt, ws)
|
||||||
|
} else {
|
||||||
|
utils.ReplyMessage(ws, "请求 Azure API 失败:"+res.Error.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
269
api/handler/chatimpl/baidu_handler.go
Normal file
269
api/handler/chatimpl/baidu_handler.go
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type baiduResp struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Object string `json:"object"`
|
||||||
|
Created int `json:"created"`
|
||||||
|
SentenceId int `json:"sentence_id"`
|
||||||
|
IsEnd bool `json:"is_end"`
|
||||||
|
IsTruncated bool `json:"is_truncated"`
|
||||||
|
Result string `json:"result"`
|
||||||
|
NeedClearHistory bool `json:"need_clear_history"`
|
||||||
|
Usage struct {
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
} `json:"usage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 百度文心一言消息发送实现
|
||||||
|
|
||||||
|
func (h *ChatHandler) sendBaiduMessage(
|
||||||
|
chatCtx []interface{},
|
||||||
|
req types.ApiRequest,
|
||||||
|
userVo vo.User,
|
||||||
|
ctx context.Context,
|
||||||
|
session *types.ChatSession,
|
||||||
|
role model.ChatRole,
|
||||||
|
prompt string,
|
||||||
|
ws *types.WsClient) error {
|
||||||
|
promptCreatedAt := time.Now() // 记录提问时间
|
||||||
|
start := time.Now()
|
||||||
|
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||||
|
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||||
|
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
return nil
|
||||||
|
} else if strings.Contains(err.Error(), "no available key") {
|
||||||
|
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ReplyMessage(ws, ErrorMsg)
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := response.Header.Get("Content-Type")
|
||||||
|
if strings.Contains(contentType, "text/event-stream") {
|
||||||
|
replyCreatedAt := time.Now() // 记录回复时间
|
||||||
|
// 循环读取 Chunk 消息
|
||||||
|
var message = types.Message{}
|
||||||
|
var contents = make([]string, 0)
|
||||||
|
var content string
|
||||||
|
scanner := bufio.NewScanner(response.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "data:") {
|
||||||
|
content = line[5:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp baiduResp
|
||||||
|
err := utils.JsonDecode(content, &resp)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with parse data line: ", err)
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(contents) == 0 {
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
}
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: utils.InterfaceToString(resp.Result),
|
||||||
|
})
|
||||||
|
contents = append(contents, resp.Result)
|
||||||
|
|
||||||
|
if resp.IsTruncated {
|
||||||
|
utils.ReplyMessage(ws, "AI 输出异常中断")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.IsEnd {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
} else {
|
||||||
|
logger.Error("信息读取出错:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息发送成功
|
||||||
|
if len(contents) > 0 {
|
||||||
|
// 更新用户的对话次数
|
||||||
|
h.subUserCalls(userVo, session)
|
||||||
|
|
||||||
|
if message.Role == "" {
|
||||||
|
message.Role = "assistant"
|
||||||
|
}
|
||||||
|
message.Content = strings.Join(contents, "")
|
||||||
|
useMsg := types.Message{Role: "user", Content: prompt}
|
||||||
|
|
||||||
|
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||||
|
if h.App.ChatConfig.EnableContext {
|
||||||
|
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||||
|
chatCtx = append(chatCtx, message) // 回复消息
|
||||||
|
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加聊天记录
|
||||||
|
if h.App.ChatConfig.EnableHistory {
|
||||||
|
// for prompt
|
||||||
|
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
historyUserMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.PromptMsg,
|
||||||
|
Icon: userVo.Avatar,
|
||||||
|
Content: prompt,
|
||||||
|
Tokens: promptToken,
|
||||||
|
UseContext: true,
|
||||||
|
}
|
||||||
|
historyUserMsg.CreatedAt = promptCreatedAt
|
||||||
|
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||||
|
res := h.db.Save(&historyUserMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save prompt history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for reply
|
||||||
|
// 计算本次对话消耗的总 token 数量
|
||||||
|
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
||||||
|
totalTokens := replyToken + getTotalTokens(req)
|
||||||
|
historyReplyMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.ReplyMsg,
|
||||||
|
Icon: role.Icon,
|
||||||
|
Content: message.Content,
|
||||||
|
Tokens: totalTokens,
|
||||||
|
UseContext: true,
|
||||||
|
}
|
||||||
|
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||||
|
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||||
|
res = h.db.Create(&historyReplyMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
|
}
|
||||||
|
// 更新用户信息
|
||||||
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前会话
|
||||||
|
var chatItem model.ChatItem
|
||||||
|
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||||
|
if res.Error != nil {
|
||||||
|
chatItem.ChatId = session.ChatId
|
||||||
|
chatItem.UserId = session.UserId
|
||||||
|
chatItem.RoleId = role.Id
|
||||||
|
chatItem.ModelId = session.Model.Id
|
||||||
|
if utf8.RuneCountInString(prompt) > 30 {
|
||||||
|
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||||
|
} else {
|
||||||
|
chatItem.Title = prompt
|
||||||
|
}
|
||||||
|
h.db.Create(&chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with reading response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res struct {
|
||||||
|
Code int `json:"error_code"`
|
||||||
|
Msg string `json:"error_msg"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with decode response: %v", err)
|
||||||
|
}
|
||||||
|
utils.ReplyMessage(ws, "请求百度文心大模型 API 失败:"+res.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatHandler) getBaiduToken(apiKey string) (string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
||||||
|
if err == nil {
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expr := time.Hour * 24 * 20 // access_token 有效期
|
||||||
|
key := strings.Split(apiKey, "|")
|
||||||
|
if len(key) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
||||||
|
}
|
||||||
|
url := fmt.Sprintf("https://aip.baidubce.com/oauth/2.0/token?client_id=%s&client_secret=%s&grant_type=client_credentials", key[0], key[1])
|
||||||
|
client := &http.Client{}
|
||||||
|
req, err := http.NewRequest("POST", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
req.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with send request: %w", err)
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with read response: %w", err)
|
||||||
|
}
|
||||||
|
var r map[string]interface{}
|
||||||
|
err = json.Unmarshal(body, &r)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r["error"] != nil {
|
||||||
|
return "", fmt.Errorf("error with api response: %s", r["error_description"])
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenString = fmt.Sprintf("%s", r["access_token"])
|
||||||
|
h.redis.Set(ctx, apiKey, tokenString, expr)
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
494
api/handler/chatimpl/chat_handler.go
Normal file
494
api/handler/chatimpl/chat_handler.go
Normal file
@@ -0,0 +1,494 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/handler"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"chatplus/service/mj"
|
||||||
|
"chatplus/store"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ErrorMsg = "抱歉,AI 助手开小差了,请稍后再试。"
|
||||||
|
const ErrImg = ""
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type ChatHandler struct {
|
||||||
|
handler.BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
leveldb *store.LevelDB
|
||||||
|
redis *redis.Client
|
||||||
|
mjService *mj.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client, service *mj.Service) *ChatHandler {
|
||||||
|
h := ChatHandler{
|
||||||
|
db: db,
|
||||||
|
leveldb: levelDB,
|
||||||
|
redis: redis,
|
||||||
|
mjService: service,
|
||||||
|
}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatConfig types.ChatConfig
|
||||||
|
|
||||||
|
// ChatHandle 处理聊天 WebSocket 请求
|
||||||
|
func (h *ChatHandler) ChatHandle(c *gin.Context) {
|
||||||
|
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := c.Query("session_id")
|
||||||
|
roleId := h.GetInt(c, "role_id", 0)
|
||||||
|
chatId := c.Query("chat_id")
|
||||||
|
modelId := h.GetInt(c, "model_id", 0)
|
||||||
|
|
||||||
|
client := types.NewWsClient(ws)
|
||||||
|
// get model info
|
||||||
|
var chatModel model.ChatModel
|
||||||
|
res := h.db.First(&chatModel, modelId)
|
||||||
|
if res.Error != nil || chatModel.Enabled == false {
|
||||||
|
utils.ReplyMessage(client, "当前AI模型暂未启用,连接已关闭!!!")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session := h.App.ChatSession.Get(sessionId)
|
||||||
|
if session == nil {
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
|
logger.Info("用户未登录")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session = &types.ChatSession{
|
||||||
|
SessionId: sessionId,
|
||||||
|
ClientIP: c.ClientIP(),
|
||||||
|
Username: user.Mobile,
|
||||||
|
UserId: user.Id,
|
||||||
|
}
|
||||||
|
h.App.ChatSession.Put(sessionId, session)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use old chat data override the chat model and role ID
|
||||||
|
var chat model.ChatItem
|
||||||
|
res = h.db.Where("chat_id=?", chatId).First(&chat)
|
||||||
|
if res.Error == nil {
|
||||||
|
chatModel.Id = chat.ModelId
|
||||||
|
roleId = int(chat.RoleId)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.ChatId = chatId
|
||||||
|
session.Model = types.ChatModel{
|
||||||
|
Id: chatModel.Id,
|
||||||
|
Value: chatModel.Value,
|
||||||
|
Weight: chatModel.Weight,
|
||||||
|
Platform: types.Platform(chatModel.Platform)}
|
||||||
|
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
|
||||||
|
var chatRole model.ChatRole
|
||||||
|
res = h.db.First(&chatRole, roleId)
|
||||||
|
if res.Error != nil || !chatRole.Enable {
|
||||||
|
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化聊天配置
|
||||||
|
var config model.Config
|
||||||
|
h.db.Where("marker", "chat").First(&config)
|
||||||
|
err = utils.JsonDecode(config.Config, &chatConfig)
|
||||||
|
if err != nil {
|
||||||
|
utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存会话连接
|
||||||
|
h.App.ChatClients.Put(sessionId, client)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
_, msg, err := client.Receive()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
client.Close()
|
||||||
|
h.App.ChatClients.Delete(sessionId)
|
||||||
|
cancelFunc := h.App.ReqCancelFunc.Get(sessionId)
|
||||||
|
if cancelFunc != nil {
|
||||||
|
cancelFunc()
|
||||||
|
h.App.ReqCancelFunc.Delete(sessionId)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := string(msg)
|
||||||
|
logger.Info("Receive a message: ", message)
|
||||||
|
//utils.ReplyMessage(client, "这是一条测试消息!")
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
h.App.ReqCancelFunc.Put(sessionId, cancel)
|
||||||
|
// 回复消息
|
||||||
|
err = h.sendMessage(ctx, session, chatRole, message, client)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||||
|
} else {
|
||||||
|
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
|
||||||
|
logger.Info("回答完毕: " + string(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
logger.Error("Recover message from error: ", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var user model.User
|
||||||
|
res := h.db.Model(&model.User{}).First(&user, session.UserId)
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.ReplyMessage(ws, "非法用户,请联系管理员!")
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
var userVo vo.User
|
||||||
|
err := utils.CopyObject(user, &userVo)
|
||||||
|
userVo.Id = user.Id
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("User 对象转换失败," + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if userVo.Status == false {
|
||||||
|
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if userVo.Calls < session.Model.Weight {
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("您当前剩余对话次数(%d)已不足以支付当前模型的单次对话需要消耗的对话额度(%d)!", userVo.Calls, session.Model.Weight))
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||||
|
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
|
||||||
|
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var req = types.ApiRequest{
|
||||||
|
Model: session.Model.Value,
|
||||||
|
Stream: true,
|
||||||
|
}
|
||||||
|
switch session.Model.Platform {
|
||||||
|
case types.Azure:
|
||||||
|
req.Temperature = h.App.ChatConfig.Azure.Temperature
|
||||||
|
req.MaxTokens = h.App.ChatConfig.Azure.MaxTokens
|
||||||
|
break
|
||||||
|
case types.ChatGLM:
|
||||||
|
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
|
||||||
|
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
|
||||||
|
break
|
||||||
|
case types.Baidu:
|
||||||
|
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
||||||
|
// TODO: 目前只支持 ERNIE-Bot-turbo 模型,如果是 ERNIE-Bot 模型则需要增加函数支持
|
||||||
|
case types.OpenAI:
|
||||||
|
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
|
||||||
|
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
|
||||||
|
// OpenAI 支持函数功能
|
||||||
|
if h.App.SysConfig.EnabledFunction {
|
||||||
|
var functions = make([]types.Function, 0)
|
||||||
|
for _, f := range types.InnerFunctions {
|
||||||
|
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
functions = append(functions, f)
|
||||||
|
}
|
||||||
|
req.Functions = functions
|
||||||
|
}
|
||||||
|
case types.XunFei:
|
||||||
|
req.Temperature = h.App.ChatConfig.XunFei.Temperature
|
||||||
|
req.MaxTokens = h.App.ChatConfig.XunFei.MaxTokens
|
||||||
|
default:
|
||||||
|
utils.ReplyMessage(ws, "不支持的平台:"+session.Model.Platform+",请联系管理员!")
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载聊天上下文
|
||||||
|
var chatCtx []interface{}
|
||||||
|
if h.App.ChatConfig.EnableContext {
|
||||||
|
if h.App.ChatContexts.Has(session.ChatId) {
|
||||||
|
chatCtx = h.App.ChatContexts.Get(session.ChatId)
|
||||||
|
} else {
|
||||||
|
// calculate the tokens of current request, to prevent to exceeding the max tokens num
|
||||||
|
tokens := req.MaxTokens
|
||||||
|
for _, f := range types.InnerFunctions {
|
||||||
|
tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
|
||||||
|
tokens += tks
|
||||||
|
}
|
||||||
|
|
||||||
|
// loading the role context
|
||||||
|
var messages []types.Message
|
||||||
|
if len(messages) > 0 {
|
||||||
|
err := utils.JsonDecode(role.Context, &messages)
|
||||||
|
if err == nil {
|
||||||
|
for _, v := range messages {
|
||||||
|
tks, _ := utils.CalcTokens(v.Content, req.Model)
|
||||||
|
if tokens+tks >= types.ModelToTokens[req.Model] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tokens += tks
|
||||||
|
chatCtx = append(chatCtx, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loading recent chat history as chat context
|
||||||
|
if chatConfig.ContextDeep > 0 {
|
||||||
|
var historyMessages []model.HistoryMessage
|
||||||
|
res := h.db.Debug().Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("id desc").Find(&historyMessages)
|
||||||
|
if res.Error == nil {
|
||||||
|
for i := len(historyMessages) - 1; i >= 0; i-- {
|
||||||
|
msg := historyMessages[i]
|
||||||
|
if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
tokens += msg.Tokens
|
||||||
|
ms := types.Message{Role: "user", Content: msg.Content}
|
||||||
|
if msg.Type == types.ReplyMsg {
|
||||||
|
ms.Role = "assistant"
|
||||||
|
}
|
||||||
|
chatCtx = append(chatCtx, ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.Debugf("聊天上下文:%+v", chatCtx)
|
||||||
|
}
|
||||||
|
reqMgs := make([]interface{}, 0)
|
||||||
|
for _, m := range chatCtx {
|
||||||
|
reqMgs = append(reqMgs, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Messages = append(reqMgs, map[string]interface{}{
|
||||||
|
"role": "user",
|
||||||
|
"content": prompt,
|
||||||
|
})
|
||||||
|
|
||||||
|
switch session.Model.Platform {
|
||||||
|
case types.Azure:
|
||||||
|
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||||
|
case types.OpenAI:
|
||||||
|
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||||
|
case types.ChatGLM:
|
||||||
|
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||||
|
case types.Baidu:
|
||||||
|
return h.sendBaiduMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||||
|
case types.XunFei:
|
||||||
|
return h.sendXunFeiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
|
||||||
|
|
||||||
|
}
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: fmt.Sprintf("Not supported platform: %s", session.Model.Platform),
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokens 统计 token 数量
|
||||||
|
func (h *ChatHandler) Tokens(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Text string `json:"text"`
|
||||||
|
Model string `json:"model"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
|
||||||
|
if data.Text == "" {
|
||||||
|
var item model.HistoryMessage
|
||||||
|
userId, _ := c.Get(types.LoginUserID)
|
||||||
|
res := h.db.Where("user_id = ?", userId).Last(&item)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, item.Tokens)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens, err := utils.CalcTokens(data.Text, data.Model)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTotalTokens(req types.ApiRequest) int {
|
||||||
|
encode := utils.JsonEncode(req.Messages)
|
||||||
|
var items []map[string]interface{}
|
||||||
|
err := utils.JsonDecode(encode, &items)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
tokens := 0
|
||||||
|
for _, item := range items {
|
||||||
|
content, ok := item["content"]
|
||||||
|
if ok && !utils.IsEmptyValue(content) {
|
||||||
|
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
|
||||||
|
if err == nil {
|
||||||
|
tokens += t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopGenerate 停止生成
|
||||||
|
func (h *ChatHandler) StopGenerate(c *gin.Context) {
|
||||||
|
sessionId := c.Query("session_id")
|
||||||
|
if h.App.ReqCancelFunc.Has(sessionId) {
|
||||||
|
h.App.ReqCancelFunc.Get(sessionId)()
|
||||||
|
h.App.ReqCancelFunc.Delete(sessionId)
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, types.OkMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求到 OpenAI 服务器
|
||||||
|
// useOwnApiKey: 是否使用了用户自己的 API KEY
|
||||||
|
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *string) (*http.Response, error) {
|
||||||
|
|
||||||
|
var apiURL string
|
||||||
|
switch platform {
|
||||||
|
case types.Azure:
|
||||||
|
md := strings.Replace(req.Model, ".", "", 1)
|
||||||
|
apiURL = strings.Replace(h.App.ChatConfig.Azure.ApiURL, "{model}", md, 1)
|
||||||
|
break
|
||||||
|
case types.ChatGLM:
|
||||||
|
apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
|
||||||
|
req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
|
||||||
|
req.Messages = nil
|
||||||
|
break
|
||||||
|
case types.Baidu:
|
||||||
|
apiURL = strings.Replace(h.App.ChatConfig.Baidu.ApiURL, "{model}", req.Model, 1)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
apiURL = h.App.ChatConfig.OpenAI.ApiURL
|
||||||
|
}
|
||||||
|
if *apiKey == "" {
|
||||||
|
var key model.ApiKey
|
||||||
|
res := h.db.Where("platform = ?", platform).Order("last_used_at ASC").First(&key)
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, errors.New("no available key, please import key")
|
||||||
|
}
|
||||||
|
// 更新 API KEY 的最后使用时间
|
||||||
|
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
||||||
|
*apiKey = key.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 百度文心,需要串接 access_token
|
||||||
|
if platform == types.Baidu {
|
||||||
|
token, err := h.getBaiduToken(*apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Info("百度文心 Access_Token:", token)
|
||||||
|
apiURL = fmt.Sprintf("%s?access_token=%s", apiURL, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 HttpClient 请求对象
|
||||||
|
var client *http.Client
|
||||||
|
requestBody, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request = request.WithContext(ctx)
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
proxyURL := h.App.Config.ProxyURL
|
||||||
|
if proxyURL != "" && platform == types.OpenAI { // 使用代理
|
||||||
|
proxy, _ := url.Parse(proxyURL)
|
||||||
|
client = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxy),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
client = http.DefaultClient
|
||||||
|
}
|
||||||
|
logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
|
||||||
|
switch platform {
|
||||||
|
case types.Azure:
|
||||||
|
request.Header.Set("api-key", *apiKey)
|
||||||
|
break
|
||||||
|
case types.ChatGLM:
|
||||||
|
token, err := h.getChatGLMToken(*apiKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
logger.Info(token)
|
||||||
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||||
|
break
|
||||||
|
case types.Baidu:
|
||||||
|
request.RequestURI = ""
|
||||||
|
case types.OpenAI:
|
||||||
|
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
|
||||||
|
}
|
||||||
|
return client.Do(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 扣减用户的对话次数
|
||||||
|
func (h *ChatHandler) subUserCalls(userVo vo.User, session *types.ChatSession) {
|
||||||
|
// 仅当用户没有导入自己的 API KEY 时才进行扣减
|
||||||
|
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
|
||||||
|
num := 1
|
||||||
|
if session.Model.Weight > 0 {
|
||||||
|
num = session.Model.Weight
|
||||||
|
}
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", num))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatHandler) incUserTokenFee(userId uint, tokens int) {
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userId).
|
||||||
|
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", tokens))
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userId).
|
||||||
|
UpdateColumn("tokens", gorm.Expr("tokens + ?", tokens))
|
||||||
|
}
|
||||||
196
api/handler/chatimpl/chat_item_handler.go
Normal file
196
api/handler/chatimpl/chat_item_handler.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// List 获取会话列表
|
||||||
|
func (h *ChatHandler) List(c *gin.Context) {
|
||||||
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
|
if userId == 0 {
|
||||||
|
resp.ERROR(c, "The parameter 'user_id' is needed.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var items = make([]vo.ChatItem, 0)
|
||||||
|
var chats []model.ChatItem
|
||||||
|
res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
|
||||||
|
if res.Error == nil {
|
||||||
|
var roleIds = make([]uint, 0)
|
||||||
|
for _, chat := range chats {
|
||||||
|
roleIds = append(roleIds, chat.RoleId)
|
||||||
|
}
|
||||||
|
var roles []model.ChatRole
|
||||||
|
res = h.db.Find(&roles, roleIds)
|
||||||
|
if res.Error == nil {
|
||||||
|
roleMap := make(map[uint]model.ChatRole)
|
||||||
|
for _, role := range roles {
|
||||||
|
roleMap[role.Id] = role
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chat := range chats {
|
||||||
|
var item vo.ChatItem
|
||||||
|
err := utils.CopyObject(chat, &item)
|
||||||
|
if err == nil {
|
||||||
|
item.Id = chat.Id
|
||||||
|
item.Icon = roleMap[chat.RoleId].Icon
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 更新会话标题
|
||||||
|
func (h *ChatHandler) Update(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
ChatId string `json:"chat_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res := h.db.Model(&model.ChatItem{}).Where("chat_id = ?", data.ChatId).UpdateColumn("title", data.Title)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Failed to update database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, types.OkMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空所有聊天记录
|
||||||
|
func (h *ChatHandler) Clear(c *gin.Context) {
|
||||||
|
// 获取当前登录用户所有的聊天会话
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var chats []model.ChatItem
|
||||||
|
res := h.db.Where("user_id = ?", user.Id).Find(&chats)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "No chats found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatIds = make([]string, 0)
|
||||||
|
for _, chat := range chats {
|
||||||
|
chatIds = append(chatIds, chat.ChatId)
|
||||||
|
// 清空会话上下文
|
||||||
|
h.App.ChatContexts.Delete(chat.ChatId)
|
||||||
|
}
|
||||||
|
err = h.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
res := h.db.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
|
||||||
|
if res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
res = h.db.Where("user_id = ? AND chat_id IN ?", user.Id, chatIds).Delete(&model.HistoryMessage{})
|
||||||
|
if res.Error != nil {
|
||||||
|
return res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("Error with delete chats: %+v", err)
|
||||||
|
resp.ERROR(c, "Failed to remove chat from database.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, types.OkMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// History 获取聊天历史记录
|
||||||
|
func (h *ChatHandler) History(c *gin.Context) {
|
||||||
|
chatId := c.Query("chat_id") // 会话 ID
|
||||||
|
var items []model.HistoryMessage
|
||||||
|
var messages = make([]vo.HistoryMessage, 0)
|
||||||
|
res := h.db.Where("chat_id = ?", chatId).Find(&items)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "No history message")
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
for _, item := range items {
|
||||||
|
var v vo.HistoryMessage
|
||||||
|
err := utils.CopyObject(item, &v)
|
||||||
|
v.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
v.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
if err == nil {
|
||||||
|
messages = append(messages, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除会话
|
||||||
|
func (h *ChatHandler) Remove(c *gin.Context) {
|
||||||
|
chatId := h.GetTrim(c, "chat_id")
|
||||||
|
if chatId == "" {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Failed to update database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除当前会话的聊天记录
|
||||||
|
res = h.db.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{})
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Failed to remove chat from database.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
|
||||||
|
|
||||||
|
// 清空会话上下文
|
||||||
|
h.App.ChatContexts.Delete(chatId)
|
||||||
|
resp.SUCCESS(c, types.OkMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail 对话详情,用户导出对话
|
||||||
|
func (h *ChatHandler) Detail(c *gin.Context) {
|
||||||
|
chatId := h.GetTrim(c, "chat_id")
|
||||||
|
if utils.IsEmptyValue(chatId) {
|
||||||
|
resp.ERROR(c, "Invalid chatId")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatItem model.ChatItem
|
||||||
|
res := h.db.Where("chat_id = ?", chatId).First(&chatItem)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "No chat found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var chatItemVo vo.ChatItem
|
||||||
|
err := utils.CopyObject(chatItem, &chatItemVo)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, chatItemVo)
|
||||||
|
}
|
||||||
232
api/handler/chatimpl/chatglm_handler.go
Normal file
232
api/handler/chatimpl/chatglm_handler.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 清华大学 ChatGML 消息发送实现
|
||||||
|
|
||||||
|
func (h *ChatHandler) sendChatGLMMessage(
|
||||||
|
chatCtx []interface{},
|
||||||
|
req types.ApiRequest,
|
||||||
|
userVo vo.User,
|
||||||
|
ctx context.Context,
|
||||||
|
session *types.ChatSession,
|
||||||
|
role model.ChatRole,
|
||||||
|
prompt string,
|
||||||
|
ws *types.WsClient) error {
|
||||||
|
promptCreatedAt := time.Now() // 记录提问时间
|
||||||
|
start := time.Now()
|
||||||
|
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||||
|
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||||
|
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
return nil
|
||||||
|
} else if strings.Contains(err.Error(), "no available key") {
|
||||||
|
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ReplyMessage(ws, ErrorMsg)
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := response.Header.Get("Content-Type")
|
||||||
|
if strings.Contains(contentType, "text/event-stream") {
|
||||||
|
replyCreatedAt := time.Now() // 记录回复时间
|
||||||
|
// 循环读取 Chunk 消息
|
||||||
|
var message = types.Message{}
|
||||||
|
var contents = make([]string, 0)
|
||||||
|
var event, content string
|
||||||
|
scanner := bufio.NewScanner(response.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if len(line) < 5 || strings.HasPrefix(line, "id:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "event:") {
|
||||||
|
event = line[6:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(line, "data:") {
|
||||||
|
content = line[5:]
|
||||||
|
}
|
||||||
|
switch event {
|
||||||
|
case "add":
|
||||||
|
if len(contents) == 0 {
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
}
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: utils.InterfaceToString(content),
|
||||||
|
})
|
||||||
|
contents = append(contents, content)
|
||||||
|
case "finish":
|
||||||
|
break
|
||||||
|
case "error":
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content))
|
||||||
|
break
|
||||||
|
case "interrupted":
|
||||||
|
utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**")
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
} else {
|
||||||
|
logger.Error("信息读取出错:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息发送成功
|
||||||
|
if len(contents) > 0 {
|
||||||
|
// 更新用户的对话次数
|
||||||
|
h.subUserCalls(userVo, session)
|
||||||
|
|
||||||
|
if message.Role == "" {
|
||||||
|
message.Role = "assistant"
|
||||||
|
}
|
||||||
|
message.Content = strings.Join(contents, "")
|
||||||
|
useMsg := types.Message{Role: "user", Content: prompt}
|
||||||
|
|
||||||
|
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||||
|
if h.App.ChatConfig.EnableContext {
|
||||||
|
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||||
|
chatCtx = append(chatCtx, message) // 回复消息
|
||||||
|
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加聊天记录
|
||||||
|
if h.App.ChatConfig.EnableHistory {
|
||||||
|
// for prompt
|
||||||
|
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
historyUserMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.PromptMsg,
|
||||||
|
Icon: userVo.Avatar,
|
||||||
|
Content: prompt,
|
||||||
|
Tokens: promptToken,
|
||||||
|
UseContext: true,
|
||||||
|
}
|
||||||
|
historyUserMsg.CreatedAt = promptCreatedAt
|
||||||
|
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||||
|
res := h.db.Save(&historyUserMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save prompt history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for reply
|
||||||
|
// 计算本次对话消耗的总 token 数量
|
||||||
|
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
||||||
|
totalTokens := replyToken + getTotalTokens(req)
|
||||||
|
historyReplyMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.ReplyMsg,
|
||||||
|
Icon: role.Icon,
|
||||||
|
Content: message.Content,
|
||||||
|
Tokens: totalTokens,
|
||||||
|
UseContext: true,
|
||||||
|
}
|
||||||
|
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||||
|
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||||
|
res = h.db.Create(&historyReplyMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
|
}
|
||||||
|
// 更新用户信息
|
||||||
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前会话
|
||||||
|
var chatItem model.ChatItem
|
||||||
|
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||||
|
if res.Error != nil {
|
||||||
|
chatItem.ChatId = session.ChatId
|
||||||
|
chatItem.UserId = session.UserId
|
||||||
|
chatItem.RoleId = role.Id
|
||||||
|
chatItem.ModelId = session.Model.Id
|
||||||
|
if utf8.RuneCountInString(prompt) > 30 {
|
||||||
|
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||||
|
} else {
|
||||||
|
chatItem.Title = prompt
|
||||||
|
}
|
||||||
|
h.db.Create(&chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with reading response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var res struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with decode response: %v", err)
|
||||||
|
}
|
||||||
|
if !res.Success {
|
||||||
|
utils.ReplyMessage(ws, "请求 ChatGLM 失败:"+res.Msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tokenString, err := h.redis.Get(ctx, apiKey).Result()
|
||||||
|
if err == nil {
|
||||||
|
return tokenString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expr := time.Hour * 2
|
||||||
|
key := strings.Split(apiKey, ".")
|
||||||
|
if len(key) != 2 {
|
||||||
|
return "", fmt.Errorf("invalid api key: %s", apiKey)
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"api_key": key[0],
|
||||||
|
"timestamp": time.Now().Unix(),
|
||||||
|
"exp": time.Now().Add(expr).Add(time.Second * 10).Unix(),
|
||||||
|
})
|
||||||
|
token.Header["alg"] = "HS256"
|
||||||
|
token.Header["sign_type"] = "SIGN"
|
||||||
|
delete(token.Header, "typ")
|
||||||
|
// Sign and get the complete encoded token as a string using the secret
|
||||||
|
tokenString, err = token.SignedString([]byte(key[1]))
|
||||||
|
h.redis.Set(ctx, apiKey, tokenString, expr)
|
||||||
|
return tokenString, err
|
||||||
|
}
|
||||||
292
api/handler/chatimpl/openai_handler.go
Normal file
292
api/handler/chatimpl/openai_handler.go
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OPenAI 消息发送实现
|
||||||
|
func (h *ChatHandler) sendOpenAiMessage(
|
||||||
|
chatCtx []interface{},
|
||||||
|
req types.ApiRequest,
|
||||||
|
userVo vo.User,
|
||||||
|
ctx context.Context,
|
||||||
|
session *types.ChatSession,
|
||||||
|
role model.ChatRole,
|
||||||
|
prompt string,
|
||||||
|
ws *types.WsClient) error {
|
||||||
|
promptCreatedAt := time.Now() // 记录提问时间
|
||||||
|
start := time.Now()
|
||||||
|
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||||
|
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
|
||||||
|
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
return nil
|
||||||
|
} else if strings.Contains(err.Error(), "no available key") {
|
||||||
|
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ReplyMessage(ws, ErrorMsg)
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
defer response.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType := response.Header.Get("Content-Type")
|
||||||
|
if strings.Contains(contentType, "text/event-stream") {
|
||||||
|
replyCreatedAt := time.Now() // 记录回复时间
|
||||||
|
// 循环读取 Chunk 消息
|
||||||
|
var message = types.Message{}
|
||||||
|
var contents = make([]string, 0)
|
||||||
|
var functionCall = false
|
||||||
|
var functionName string
|
||||||
|
var arguments = make([]string, 0)
|
||||||
|
scanner := bufio.NewScanner(response.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if !strings.Contains(line, "data:") || len(line) < 30 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseBody = types.ApiResponse{}
|
||||||
|
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
||||||
|
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
||||||
|
logger.Error(err, line)
|
||||||
|
utils.ReplyMessage(ws, ErrorMsg)
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
fun := responseBody.Choices[0].Delta.FunctionCall
|
||||||
|
if functionCall && fun.Name == "" {
|
||||||
|
arguments = append(arguments, fun.Arguments)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.IsEmptyValue(fun) {
|
||||||
|
functionName = fun.Name
|
||||||
|
f := h.App.Functions[functionName]
|
||||||
|
if f != nil {
|
||||||
|
functionCall = true
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化 role
|
||||||
|
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
|
||||||
|
message.Role = responseBody.Choices[0].Delta.Role
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
continue
|
||||||
|
} else if responseBody.Choices[0].FinishReason != "" {
|
||||||
|
break // 输出完成或者输出中断了
|
||||||
|
} else {
|
||||||
|
content := responseBody.Choices[0].Delta.Content
|
||||||
|
contents = append(contents, utils.InterfaceToString(content))
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} // end for
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
if strings.Contains(err.Error(), "context canceled") {
|
||||||
|
logger.Info("用户取消了请求:", prompt)
|
||||||
|
} else {
|
||||||
|
logger.Error("信息读取出错:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if functionCall { // 调用函数完成任务
|
||||||
|
var params map[string]interface{}
|
||||||
|
_ = utils.JsonDecode(strings.Join(arguments, ""), ¶ms)
|
||||||
|
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
|
||||||
|
|
||||||
|
// for creating image, check if the user's img_calls > 0
|
||||||
|
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
|
||||||
|
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
|
||||||
|
utils.ReplyMessage(ws, ErrImg)
|
||||||
|
} else {
|
||||||
|
f := h.App.Functions[functionName]
|
||||||
|
if functionName == types.FuncMidJourney {
|
||||||
|
params["user_id"] = userVo.Id
|
||||||
|
params["role_id"] = role.Id
|
||||||
|
params["chat_id"] = session.ChatId
|
||||||
|
params["icon"] = "/images/avatar/mid_journey.png"
|
||||||
|
params["session_id"] = session.SessionId
|
||||||
|
}
|
||||||
|
data, err := f.Invoke(params)
|
||||||
|
if err != nil {
|
||||||
|
msg := "调用函数出错:" + err.Error()
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: msg,
|
||||||
|
})
|
||||||
|
contents = append(contents, msg)
|
||||||
|
} else {
|
||||||
|
content := data
|
||||||
|
if functionName == types.FuncMidJourney {
|
||||||
|
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
|
||||||
|
h.mjService.ChatClients.Put(session.SessionId, ws)
|
||||||
|
// update user's img_calls
|
||||||
|
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: content,
|
||||||
|
})
|
||||||
|
contents = append(contents, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息发送成功
|
||||||
|
if len(contents) > 0 {
|
||||||
|
// 更新用户的对话次数
|
||||||
|
h.subUserCalls(userVo, session)
|
||||||
|
|
||||||
|
if message.Role == "" {
|
||||||
|
message.Role = "assistant"
|
||||||
|
}
|
||||||
|
message.Content = strings.Join(contents, "")
|
||||||
|
useMsg := types.Message{Role: "user", Content: prompt}
|
||||||
|
|
||||||
|
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||||
|
if h.App.ChatConfig.EnableContext && functionCall == false {
|
||||||
|
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||||
|
chatCtx = append(chatCtx, message) // 回复消息
|
||||||
|
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加聊天记录
|
||||||
|
if h.App.ChatConfig.EnableHistory {
|
||||||
|
useContext := true
|
||||||
|
if functionCall {
|
||||||
|
useContext = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// for prompt
|
||||||
|
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
historyUserMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.PromptMsg,
|
||||||
|
Icon: userVo.Avatar,
|
||||||
|
Content: prompt,
|
||||||
|
Tokens: promptToken,
|
||||||
|
UseContext: useContext,
|
||||||
|
}
|
||||||
|
historyUserMsg.CreatedAt = promptCreatedAt
|
||||||
|
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||||
|
res := h.db.Save(&historyUserMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save prompt history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算本次对话消耗的总 token 数量
|
||||||
|
var totalTokens = 0
|
||||||
|
if functionCall { // prompt + 函数名 + 参数 token
|
||||||
|
tokens, _ := utils.CalcTokens(functionName, req.Model)
|
||||||
|
totalTokens += tokens
|
||||||
|
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
|
||||||
|
totalTokens += tokens
|
||||||
|
} else {
|
||||||
|
totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
|
||||||
|
}
|
||||||
|
totalTokens += getTotalTokens(req)
|
||||||
|
|
||||||
|
historyReplyMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.ReplyMsg,
|
||||||
|
Icon: role.Icon,
|
||||||
|
Content: message.Content,
|
||||||
|
Tokens: totalTokens,
|
||||||
|
UseContext: useContext,
|
||||||
|
}
|
||||||
|
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||||
|
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||||
|
res = h.db.Create(&historyReplyMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前会话
|
||||||
|
var chatItem model.ChatItem
|
||||||
|
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||||
|
if res.Error != nil {
|
||||||
|
chatItem.ChatId = session.ChatId
|
||||||
|
chatItem.UserId = session.UserId
|
||||||
|
chatItem.RoleId = role.Id
|
||||||
|
chatItem.ModelId = session.Model.Id
|
||||||
|
if utf8.RuneCountInString(prompt) > 30 {
|
||||||
|
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||||
|
} else {
|
||||||
|
chatItem.Title = prompt
|
||||||
|
}
|
||||||
|
h.db.Create(&chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with reading response: %v", err)
|
||||||
|
}
|
||||||
|
var res types.ApiError
|
||||||
|
err = json.Unmarshal(body, &res)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with decode response: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAI API 调用异常处理
|
||||||
|
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
|
||||||
|
utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 所关联的账户被禁用。")
|
||||||
|
// 移除当前 API key
|
||||||
|
h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{})
|
||||||
|
} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
|
||||||
|
utils.ReplyMessage(ws, "请求 OpenAI API 失败:API KEY 触发并发限制,请稍后再试。")
|
||||||
|
} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
|
||||||
|
logger.Error(res.Error.Message)
|
||||||
|
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
|
||||||
|
h.App.ChatContexts.Delete(session.ChatId)
|
||||||
|
return h.sendMessage(ctx, session, role, prompt, ws)
|
||||||
|
} else {
|
||||||
|
utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
318
api/handler/chatimpl/xunfei_handler.go
Normal file
318
api/handler/chatimpl/xunfei_handler.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package chatimpl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type xunFeiResp struct {
|
||||||
|
Header struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Sid string `json:"sid"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"header"`
|
||||||
|
Payload struct {
|
||||||
|
Choices struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
Seq int `json:"seq"`
|
||||||
|
Text []struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Index int `json:"index"`
|
||||||
|
} `json:"text"`
|
||||||
|
} `json:"choices"`
|
||||||
|
Usage struct {
|
||||||
|
Text struct {
|
||||||
|
QuestionTokens int `json:"question_tokens"`
|
||||||
|
PromptTokens int `json:"prompt_tokens"`
|
||||||
|
CompletionTokens int `json:"completion_tokens"`
|
||||||
|
TotalTokens int `json:"total_tokens"`
|
||||||
|
} `json:"text"`
|
||||||
|
} `json:"usage"`
|
||||||
|
} `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 科大讯飞消息发送实现
|
||||||
|
|
||||||
|
func (h *ChatHandler) sendXunFeiMessage(
|
||||||
|
chatCtx []interface{},
|
||||||
|
req types.ApiRequest,
|
||||||
|
userVo vo.User,
|
||||||
|
ctx context.Context,
|
||||||
|
session *types.ChatSession,
|
||||||
|
role model.ChatRole,
|
||||||
|
prompt string,
|
||||||
|
ws *types.WsClient) error {
|
||||||
|
promptCreatedAt := time.Now() // 记录提问时间
|
||||||
|
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
|
||||||
|
if apiKey == "" {
|
||||||
|
var key model.ApiKey
|
||||||
|
res := h.db.Where("platform = ?", session.Model.Platform).Order("last_used_at ASC").First(&key)
|
||||||
|
if res.Error != nil {
|
||||||
|
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 更新 API KEY 的最后使用时间
|
||||||
|
h.db.Model(&key).UpdateColumn("last_used_at", time.Now().Unix())
|
||||||
|
apiKey = key.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
d := websocket.Dialer{
|
||||||
|
HandshakeTimeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
key := strings.Split(apiKey, "|")
|
||||||
|
if len(key) != 3 {
|
||||||
|
utils.ReplyMessage(ws, "非法的 API KEY!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var apiURL string
|
||||||
|
if req.Model == "generalv2" {
|
||||||
|
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v2.1", 1)
|
||||||
|
} else {
|
||||||
|
apiURL = strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", "v1.1", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
wsURL, err := assembleAuthUrl(apiURL, key[1], key[2])
|
||||||
|
//握手并建立websocket 连接
|
||||||
|
conn, resp, err := d.Dial(wsURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(readResp(resp) + err.Error())
|
||||||
|
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
||||||
|
return nil
|
||||||
|
} else if resp.StatusCode != 101 {
|
||||||
|
utils.ReplyMessage(ws, "请求讯飞星火模型 API 失败:"+readResp(resp)+err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := buildRequest(key[0], req)
|
||||||
|
fmt.Printf("%+v", data)
|
||||||
|
fmt.Println(apiURL)
|
||||||
|
err = conn.WriteJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
utils.ReplyMessage(ws, "发送消息失败:"+err.Error())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
replyCreatedAt := time.Now() // 记录回复时间
|
||||||
|
// 循环读取 Chunk 消息
|
||||||
|
var message = types.Message{}
|
||||||
|
var contents = make([]string, 0)
|
||||||
|
var content string
|
||||||
|
for {
|
||||||
|
_, msg, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with read message:", err)
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("**数据读取失败:%s**", err))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析数据
|
||||||
|
var result xunFeiResp
|
||||||
|
err = json.Unmarshal(msg, &result)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with parsing JSON:", err)
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Header.Code != 0 {
|
||||||
|
utils.ReplyMessage(ws, fmt.Sprintf("**请求 API 返回错误:%s**", result.Header.Message))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
content = result.Payload.Choices.Text[0].Content
|
||||||
|
contents = append(contents, content)
|
||||||
|
// 第一个结果
|
||||||
|
if result.Payload.Choices.Status == 0 {
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
|
||||||
|
}
|
||||||
|
utils.ReplyChunkMessage(ws, types.WsMessage{
|
||||||
|
Type: types.WsMiddle,
|
||||||
|
Content: utils.InterfaceToString(content),
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.Payload.Choices.Status == 2 { // 最终结果
|
||||||
|
_ = conn.Close() // 关闭连接
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
utils.ReplyMessage(ws, "**用户取消了生成指令!**")
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 消息发送成功
|
||||||
|
if len(contents) > 0 {
|
||||||
|
// 更新用户的对话次数
|
||||||
|
h.subUserCalls(userVo, session)
|
||||||
|
|
||||||
|
if message.Role == "" {
|
||||||
|
message.Role = "assistant"
|
||||||
|
}
|
||||||
|
message.Content = strings.Join(contents, "")
|
||||||
|
useMsg := types.Message{Role: "user", Content: prompt}
|
||||||
|
|
||||||
|
// 更新上下文消息,如果是调用函数则不需要更新上下文
|
||||||
|
if h.App.ChatConfig.EnableContext {
|
||||||
|
chatCtx = append(chatCtx, useMsg) // 提问消息
|
||||||
|
chatCtx = append(chatCtx, message) // 回复消息
|
||||||
|
h.App.ChatContexts.Put(session.ChatId, chatCtx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 追加聊天记录
|
||||||
|
if h.App.ChatConfig.EnableHistory {
|
||||||
|
// for prompt
|
||||||
|
promptToken, err := utils.CalcTokens(prompt, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
historyUserMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.PromptMsg,
|
||||||
|
Icon: userVo.Avatar,
|
||||||
|
Content: prompt,
|
||||||
|
Tokens: promptToken,
|
||||||
|
UseContext: true,
|
||||||
|
}
|
||||||
|
historyUserMsg.CreatedAt = promptCreatedAt
|
||||||
|
historyUserMsg.UpdatedAt = promptCreatedAt
|
||||||
|
res := h.db.Save(&historyUserMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save prompt history message: ", res.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// for reply
|
||||||
|
// 计算本次对话消耗的总 token 数量
|
||||||
|
replyToken, _ := utils.CalcTokens(message.Content, req.Model)
|
||||||
|
totalTokens := replyToken + getTotalTokens(req)
|
||||||
|
historyReplyMsg := model.HistoryMessage{
|
||||||
|
UserId: userVo.Id,
|
||||||
|
ChatId: session.ChatId,
|
||||||
|
RoleId: role.Id,
|
||||||
|
Type: types.ReplyMsg,
|
||||||
|
Icon: role.Icon,
|
||||||
|
Content: message.Content,
|
||||||
|
Tokens: totalTokens,
|
||||||
|
UseContext: true,
|
||||||
|
}
|
||||||
|
historyReplyMsg.CreatedAt = replyCreatedAt
|
||||||
|
historyReplyMsg.UpdatedAt = replyCreatedAt
|
||||||
|
res = h.db.Create(&historyReplyMsg)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("failed to save reply history message: ", res.Error)
|
||||||
|
}
|
||||||
|
// 更新用户信息
|
||||||
|
h.incUserTokenFee(userVo.Id, totalTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前会话
|
||||||
|
var chatItem model.ChatItem
|
||||||
|
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
|
||||||
|
if res.Error != nil {
|
||||||
|
chatItem.ChatId = session.ChatId
|
||||||
|
chatItem.UserId = session.UserId
|
||||||
|
chatItem.RoleId = role.Id
|
||||||
|
chatItem.ModelId = session.Model.Id
|
||||||
|
if utf8.RuneCountInString(prompt) > 30 {
|
||||||
|
chatItem.Title = string([]rune(prompt)[:30]) + "..."
|
||||||
|
} else {
|
||||||
|
chatItem.Title = prompt
|
||||||
|
}
|
||||||
|
h.db.Create(&chatItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建 websocket 请求实体
|
||||||
|
func buildRequest(appid string, req types.ApiRequest) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"header": map[string]interface{}{
|
||||||
|
"app_id": appid,
|
||||||
|
},
|
||||||
|
"parameter": map[string]interface{}{
|
||||||
|
"chat": map[string]interface{}{
|
||||||
|
"domain": req.Model,
|
||||||
|
"temperature": float64(req.Temperature),
|
||||||
|
"top_k": int64(6),
|
||||||
|
"max_tokens": int64(req.MaxTokens),
|
||||||
|
"auditing": "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"payload": map[string]interface{}{
|
||||||
|
"message": map[string]interface{}{
|
||||||
|
"text": req.Messages,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建鉴权 URL
|
||||||
|
func assembleAuthUrl(hostURL string, apiKey, apiSecret string) (string, error) {
|
||||||
|
ul, err := url.Parse(hostURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
date := time.Now().UTC().Format(time.RFC1123)
|
||||||
|
signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
|
||||||
|
//拼接签名字符串
|
||||||
|
signStr := strings.Join(signString, "\n")
|
||||||
|
sha := hmacWithSha256(signStr, apiSecret)
|
||||||
|
|
||||||
|
authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
|
||||||
|
"hmac-sha256", "host date request-line", sha)
|
||||||
|
//将请求参数使用base64编码
|
||||||
|
authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("host", ul.Host)
|
||||||
|
v.Add("date", date)
|
||||||
|
v.Add("authorization", authorization)
|
||||||
|
//将编码后的字符串url encode后添加到url后面
|
||||||
|
return hostURL + "?" + v.Encode(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 sha256 签名
|
||||||
|
func hmacWithSha256(data, key string) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(key))
|
||||||
|
mac.Write([]byte(data))
|
||||||
|
encodeData := mac.Sum(nil)
|
||||||
|
return base64.StdEncoding.EncodeToString(encodeData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
func readResp(resp *http.Response) string {
|
||||||
|
if resp == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("code=%d,body=%s", resp.StatusCode, string(b))
|
||||||
|
}
|
||||||
@@ -3,212 +3,364 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"chatplus/core"
|
"chatplus/core"
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
"chatplus/service/function"
|
"chatplus/service/mj"
|
||||||
"chatplus/store"
|
|
||||||
"chatplus/store/model"
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TaskStatus string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Start = TaskStatus("Started")
|
|
||||||
Running = TaskStatus("Running")
|
|
||||||
Stopped = TaskStatus("Stopped")
|
|
||||||
Finished = TaskStatus("Finished")
|
|
||||||
)
|
|
||||||
|
|
||||||
type Image struct {
|
|
||||||
URL string `json:"url"`
|
|
||||||
ProxyURL string `json:"proxy_url"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
Width int `json:"width"`
|
|
||||||
Height int `json:"height"`
|
|
||||||
Size int `json:"size"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MidJourneyHandler struct {
|
type MidJourneyHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
leveldb *store.LevelDB
|
redis *redis.Client
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
mjFunc function.FuncMidJourney
|
mjService *mj.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMidJourneyHandler(app *core.AppServer, leveldb *store.LevelDB, db *gorm.DB, functions map[string]function.Function) *MidJourneyHandler {
|
func NewMidJourneyHandler(
|
||||||
h := MidJourneyHandler{leveldb: leveldb, db: db, mjFunc: functions[types.FuncMidJourney].(function.FuncMidJourney)}
|
app *core.AppServer,
|
||||||
|
client *redis.Client,
|
||||||
|
db *gorm.DB,
|
||||||
|
mjService *mj.Service) *MidJourneyHandler {
|
||||||
|
h := MidJourneyHandler{
|
||||||
|
redis: client,
|
||||||
|
db: db,
|
||||||
|
mjService: mjService,
|
||||||
|
}
|
||||||
h.App = app
|
h.App = app
|
||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *MidJourneyHandler) Notify(c *gin.Context) {
|
// Client WebSocket 客户端,用于通知任务状态变更
|
||||||
token := c.GetHeader("Authorization")
|
func (h *MidJourneyHandler) Client(c *gin.Context) {
|
||||||
if token != h.App.Config.ExtConfig.Token {
|
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := c.Query("session_id")
|
||||||
|
client := types.NewWsClient(ws)
|
||||||
|
h.mjService.Clients.Put(sessionId, client)
|
||||||
|
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *MidJourneyHandler) checkLimits(c *gin.Context) bool {
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
resp.NotAuth(c)
|
resp.NotAuth(c)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.ImgCalls <= 0 {
|
||||||
|
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image 创建一个绘画任务
|
||||||
|
func (h *MidJourneyHandler) Image(c *gin.Context) {
|
||||||
|
if !h.App.Config.MjConfig.Enabled {
|
||||||
|
resp.ERROR(c, "MidJourney service is disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var data struct {
|
var data struct {
|
||||||
MessageId string `json:"message_id"`
|
SessionId string `json:"session_id"`
|
||||||
ReferenceId string `json:"reference_id"`
|
Prompt string `json:"prompt"`
|
||||||
Image Image `json:"image"`
|
Rate string `json:"rate"`
|
||||||
Content string `json:"content"`
|
Model string `json:"model"`
|
||||||
Prompt string `json:"prompt"`
|
Chaos int `json:"chaos"`
|
||||||
Status TaskStatus `json:"status"`
|
Raw bool `json:"raw"`
|
||||||
Key string `json:"key"`
|
Seed int64 `json:"seed"`
|
||||||
|
Stylize int `json:"stylize"`
|
||||||
|
Img string `json:"img"`
|
||||||
|
Weight float32 `json:"weight"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if !h.checkLimits(c) {
|
||||||
logger.Debugf("收到 MidJourney 回调请求:%+v", data)
|
|
||||||
|
|
||||||
// the job is saved
|
|
||||||
var job model.MidJourneyJob
|
|
||||||
res := h.db.Where("message_id = ?", data.MessageId).First(&job)
|
|
||||||
if res.Error == nil {
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data.Key = utils.Sha256(data.Prompt)
|
var prompt = data.Prompt
|
||||||
//logger.Info(data.Prompt, ",", key)
|
if data.Rate != "" && !strings.Contains(prompt, "--ar") {
|
||||||
if data.Status == Finished {
|
prompt += " --ar " + data.Rate
|
||||||
var task types.MjTask
|
}
|
||||||
err := h.leveldb.Get(types.TaskStorePrefix+data.Key, &task)
|
if data.Seed > 0 && !strings.Contains(prompt, "--seed") {
|
||||||
if err != nil {
|
prompt += fmt.Sprintf(" --seed %d", data.Seed)
|
||||||
logger.Error("error with get MidJourney task: ", err)
|
}
|
||||||
resp.SUCCESS(c)
|
if data.Stylize > 0 && !strings.Contains(prompt, "--s") && !strings.Contains(prompt, "--stylize") {
|
||||||
return
|
prompt += fmt.Sprintf(" --s %d", data.Stylize)
|
||||||
}
|
}
|
||||||
|
if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") {
|
||||||
// TODO: 是否需要把图片下载到本地服务器?
|
prompt += fmt.Sprintf(" --c %d", data.Chaos)
|
||||||
|
}
|
||||||
message := model.HistoryMessage{
|
if data.Img != "" {
|
||||||
UserId: task.UserId,
|
prompt = fmt.Sprintf("%s %s", data.Img, prompt)
|
||||||
ChatId: task.ChatId,
|
if data.Weight > 0 {
|
||||||
RoleId: task.RoleId,
|
prompt += fmt.Sprintf(" --iw %f", data.Weight)
|
||||||
Type: types.MjMsg,
|
|
||||||
Icon: task.Icon,
|
|
||||||
Content: utils.JsonEncode(data),
|
|
||||||
Tokens: 0,
|
|
||||||
UseContext: false,
|
|
||||||
}
|
|
||||||
res := h.db.Create(&message)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("error with save chat history message: ", res.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the job
|
|
||||||
job.UserId = task.UserId
|
|
||||||
job.ChatId = task.ChatId
|
|
||||||
job.MessageId = data.MessageId
|
|
||||||
job.ReferenceId = data.ReferenceId
|
|
||||||
job.Content = data.Content
|
|
||||||
job.Prompt = data.Prompt
|
|
||||||
job.Image = utils.JsonEncode(data.Image)
|
|
||||||
job.Hash = data.Image.Hash
|
|
||||||
job.CreatedAt = time.Now()
|
|
||||||
res = h.db.Create(&job)
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Error("error with save MidJourney Job: ", res.Error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if data.Raw {
|
||||||
|
prompt += " --style raw"
|
||||||
|
}
|
||||||
|
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
|
||||||
|
prompt += data.Model
|
||||||
|
}
|
||||||
|
|
||||||
// 推送消息到客户端
|
idValue, _ := c.Get(types.LoginUserID)
|
||||||
wsClient := h.App.MjTaskClients.Get(data.Key)
|
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||||
if wsClient == nil { // 客户端断线,则丢弃
|
job := model.MidJourneyJob{
|
||||||
logger.Errorf("Client is offline: %+v", data)
|
Type: types.TaskImage.String(),
|
||||||
resp.SUCCESS(c, "Client is offline")
|
UserId: userId,
|
||||||
|
Progress: 0,
|
||||||
|
Prompt: prompt,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if res := h.db.Create(&job); res.Error != nil {
|
||||||
|
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Status == Finished {
|
h.mjService.PushTask(types.MjTask{
|
||||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
Id: int(job.Id),
|
||||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
|
SessionId: data.SessionId,
|
||||||
// delete client
|
Src: types.TaskSrcImg,
|
||||||
h.App.MjTaskClients.Delete(data.Key)
|
Type: types.TaskImage,
|
||||||
} else {
|
Prompt: prompt,
|
||||||
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
UserId: userId,
|
||||||
|
})
|
||||||
|
|
||||||
|
var jobVo vo.MidJourneyJob
|
||||||
|
err := utils.CopyObject(job, &jobVo)
|
||||||
|
if err == nil {
|
||||||
|
// 推送任务到前端
|
||||||
|
client := h.mjService.Clients.Get(data.SessionId)
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, jobVo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resp.SUCCESS(c, "SUCCESS")
|
resp.SUCCESS(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
type reqVo struct {
|
type reqVo struct {
|
||||||
Index int32 `json:"index"`
|
Src string `json:"src"`
|
||||||
|
Index int `json:"index"`
|
||||||
MessageId string `json:"message_id"`
|
MessageId string `json:"message_id"`
|
||||||
MessageHash string `json:"message_hash"`
|
MessageHash string `json:"message_hash"`
|
||||||
SessionId string `json:"session_id"`
|
SessionId string `json:"session_id"`
|
||||||
Key string `json:"key"`
|
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
|
ChatId string `json:"chat_id"`
|
||||||
|
RoleId int `json:"role_id"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upscale send upscale command to MidJourney Bot
|
// Upscale send upscale command to MidJourney Bot
|
||||||
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
|
||||||
var data reqVo
|
var data reqVo
|
||||||
if err := c.ShouldBindJSON(&data); err != nil ||
|
if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" {
|
||||||
data.SessionId == "" ||
|
|
||||||
data.Key == "" {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wsClient := h.App.ChatClients.Get(data.SessionId)
|
|
||||||
if wsClient == nil {
|
if !h.checkLimits(c) {
|
||||||
resp.ERROR(c, "No Websocket client online")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.mjFunc.Upscale(function.MjUpscaleReq{
|
idValue, _ := c.Get(types.LoginUserID)
|
||||||
|
jobId := 0
|
||||||
|
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||||
|
src := types.TaskSrc(data.Src)
|
||||||
|
if src == types.TaskSrcImg {
|
||||||
|
job := model.MidJourneyJob{
|
||||||
|
Type: types.TaskUpscale.String(),
|
||||||
|
UserId: userId,
|
||||||
|
Hash: data.MessageHash,
|
||||||
|
Progress: 0,
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if res := h.db.Create(&job); res.Error == nil {
|
||||||
|
jobId = int(job.Id)
|
||||||
|
} else {
|
||||||
|
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobVo vo.MidJourneyJob
|
||||||
|
err := utils.CopyObject(job, &jobVo)
|
||||||
|
if err == nil {
|
||||||
|
// 推送任务到前端
|
||||||
|
client := h.mjService.Clients.Get(data.SessionId)
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, jobVo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mjService.PushTask(types.MjTask{
|
||||||
|
Id: jobId,
|
||||||
|
SessionId: data.SessionId,
|
||||||
|
Src: src,
|
||||||
|
Type: types.TaskUpscale,
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
UserId: userId,
|
||||||
|
RoleId: data.RoleId,
|
||||||
|
Icon: data.Icon,
|
||||||
|
ChatId: data.ChatId,
|
||||||
Index: data.Index,
|
Index: data.Index,
|
||||||
MessageId: data.MessageId,
|
MessageId: data.MessageId,
|
||||||
MessageHash: data.MessageHash,
|
MessageHash: data.MessageHash,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
content := fmt.Sprintf("**%s** 已推送 Upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
if src == types.TaskSrcChat {
|
||||||
utils.ReplyMessage(wsClient, content)
|
wsClient := h.App.ChatClients.Get(data.SessionId)
|
||||||
if h.App.MjTaskClients.Get(data.Key) == nil {
|
if wsClient != nil {
|
||||||
h.App.MjTaskClients.Put(data.Key, wsClient)
|
content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
||||||
|
utils.ReplyMessage(wsClient, content)
|
||||||
|
if h.mjService.ChatClients.Get(data.SessionId) == nil {
|
||||||
|
h.mjService.ChatClients.Put(data.SessionId, wsClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Variation send variation command to MidJourney Bot
|
||||||
func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
func (h *MidJourneyHandler) Variation(c *gin.Context) {
|
||||||
var data reqVo
|
var data reqVo
|
||||||
if err := c.ShouldBindJSON(&data); err != nil ||
|
if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" {
|
||||||
data.SessionId == "" ||
|
|
||||||
data.Key == "" {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wsClient := h.App.ChatClients.Get(data.SessionId)
|
|
||||||
if wsClient == nil {
|
if !h.checkLimits(c) {
|
||||||
resp.ERROR(c, "No Websocket client online")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.mjFunc.Variation(function.MjVariationReq{
|
idValue, _ := c.Get(types.LoginUserID)
|
||||||
|
jobId := 0
|
||||||
|
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||||
|
src := types.TaskSrc(data.Src)
|
||||||
|
if src == types.TaskSrcImg {
|
||||||
|
job := model.MidJourneyJob{
|
||||||
|
Type: types.TaskVariation.String(),
|
||||||
|
UserId: userId,
|
||||||
|
ImgURL: "",
|
||||||
|
Hash: data.MessageHash,
|
||||||
|
Progress: 0,
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
if res := h.db.Create(&job); res.Error == nil {
|
||||||
|
jobId = int(job.Id)
|
||||||
|
} else {
|
||||||
|
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobVo vo.MidJourneyJob
|
||||||
|
err := utils.CopyObject(job, &jobVo)
|
||||||
|
if err == nil {
|
||||||
|
// 推送任务到前端
|
||||||
|
client := h.mjService.Clients.Get(data.SessionId)
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, jobVo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.mjService.PushTask(types.MjTask{
|
||||||
|
Id: jobId,
|
||||||
|
SessionId: data.SessionId,
|
||||||
|
Src: src,
|
||||||
|
Type: types.TaskVariation,
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
UserId: userId,
|
||||||
|
RoleId: data.RoleId,
|
||||||
|
Icon: data.Icon,
|
||||||
|
ChatId: data.ChatId,
|
||||||
Index: data.Index,
|
Index: data.Index,
|
||||||
MessageId: data.MessageId,
|
MessageId: data.MessageId,
|
||||||
MessageHash: data.MessageHash,
|
MessageHash: data.MessageHash,
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, err.Error())
|
if src == types.TaskSrcChat {
|
||||||
return
|
// 从聊天窗口发送的请求,记录客户端信息
|
||||||
}
|
wsClient := h.mjService.ChatClients.Get(data.SessionId)
|
||||||
content := fmt.Sprintf("**%s** 已推送 Variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
if wsClient != nil {
|
||||||
utils.ReplyMessage(wsClient, content)
|
content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
|
||||||
if h.App.MjTaskClients.Get(data.Key) == nil {
|
utils.ReplyMessage(wsClient, content)
|
||||||
h.App.MjTaskClients.Put(data.Key, wsClient)
|
if h.mjService.Clients.Get(data.SessionId) == nil {
|
||||||
|
h.mjService.Clients.Put(data.SessionId, wsClient)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JobList 获取 MJ 任务列表
|
||||||
|
func (h *MidJourneyHandler) JobList(c *gin.Context) {
|
||||||
|
status := h.GetInt(c, "status", 0)
|
||||||
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
|
page := h.GetInt(c, "page", 0)
|
||||||
|
pageSize := h.GetInt(c, "page_size", 0)
|
||||||
|
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
if status == 1 {
|
||||||
|
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||||
|
} else {
|
||||||
|
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||||
|
}
|
||||||
|
if userId > 0 {
|
||||||
|
session = session.Where("user_id = ?", userId)
|
||||||
|
}
|
||||||
|
if page > 0 && pageSize > 0 {
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
session = session.Offset(offset).Limit(pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []model.MidJourneyJob
|
||||||
|
res := session.Find(&items)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, types.NoData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs = make([]vo.MidJourneyJob, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
var job vo.MidJourneyJob
|
||||||
|
err := utils.CopyObject(item, &job)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Progress < 100 {
|
||||||
|
// 30 分钟还没完成的任务直接删除
|
||||||
|
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
||||||
|
h.db.Delete(&item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.ImgURL != "" { // 正在运行中任务使用代理访问图片
|
||||||
|
image, err := utils.DownloadImage(item.ImgURL, h.App.Config.ProxyURL)
|
||||||
|
if err == nil {
|
||||||
|
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs = append(jobs, job)
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, jobs)
|
||||||
|
}
|
||||||
|
|||||||
57
api/handler/order_handler.go
Normal file
57
api/handler/order_handler.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
|
||||||
|
h := OrderHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *OrderHandler) List(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
Page int `json:"page"`
|
||||||
|
PageSize int `json:"page_size"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user, _ := utils.GetLoginUser(c, h.db)
|
||||||
|
session := h.db.Session(&gorm.Session{}).Where("user_id = ? AND status = ?", user.Id, types.OrderPaidSuccess)
|
||||||
|
var total int64
|
||||||
|
session.Model(&model.Order{}).Count(&total)
|
||||||
|
var items []model.Order
|
||||||
|
var list = make([]vo.Order, 0)
|
||||||
|
offset := (data.Page - 1) * data.PageSize
|
||||||
|
res := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var order vo.Order
|
||||||
|
err := utils.CopyObject(item, &order)
|
||||||
|
if err == nil {
|
||||||
|
order.Id = item.Id
|
||||||
|
order.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
order.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, order)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
|
||||||
|
}
|
||||||
273
api/handler/payment_handler.go
Normal file
273
api/handler/payment_handler.go
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/service"
|
||||||
|
"chatplus/service/payment"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
PayWayAlipay = "支付宝"
|
||||||
|
PayWayWechat = "微信支付"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PaymentHandler 支付服务回调 handler
|
||||||
|
type PaymentHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
alipayService *payment.AlipayService
|
||||||
|
snowflake *service.Snowflake
|
||||||
|
db *gorm.DB
|
||||||
|
fs embed.FS
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPaymentHandler(server *core.AppServer, alipayService *payment.AlipayService, snowflake *service.Snowflake, db *gorm.DB, fs embed.FS) *PaymentHandler {
|
||||||
|
h := PaymentHandler{lock: sync.Mutex{}}
|
||||||
|
h.App = server
|
||||||
|
h.alipayService = alipayService
|
||||||
|
h.snowflake = snowflake
|
||||||
|
h.db = db
|
||||||
|
h.fs = fs
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PaymentHandler) Alipay(c *gin.Context) {
|
||||||
|
orderNo := h.GetTrim(c, "order_no")
|
||||||
|
if orderNo == "" {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var order model.Order
|
||||||
|
res := h.db.Where("order_no = ?", orderNo).First(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Order not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新扫码状态
|
||||||
|
h.db.Model(&order).UpdateColumn("status", types.OrderScanned)
|
||||||
|
// 生成支付链接
|
||||||
|
notifyURL := h.App.Config.AlipayConfig.NotifyURL
|
||||||
|
returnURL := "" // 关闭同步回跳
|
||||||
|
amount := fmt.Sprintf("%.2f", order.Amount)
|
||||||
|
|
||||||
|
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, notifyURL, returnURL, amount, order.Subject)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "error with generate pay url: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(302, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderQuery 清单状态查询
|
||||||
|
func (h *PaymentHandler) OrderQuery(c *gin.Context) {
|
||||||
|
var data struct {
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var order model.Order
|
||||||
|
res := h.db.Where("order_no = ?", data.OrderNo).First(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Order not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if order.Status == types.OrderPaidSuccess {
|
||||||
|
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
for {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
var item model.Order
|
||||||
|
h.db.Where("order_no = ?", data.OrderNo).First(&item)
|
||||||
|
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
|
||||||
|
order.Status = item.Status
|
||||||
|
break
|
||||||
|
}
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.SUCCESS(c, gin.H{"status": order.Status})
|
||||||
|
}
|
||||||
|
|
||||||
|
// AlipayQrcode 生成支付宝支付 URL 二维码
|
||||||
|
func (h *PaymentHandler) AlipayQrcode(c *gin.Context) {
|
||||||
|
if !h.App.SysConfig.EnabledAlipay || h.alipayService == nil {
|
||||||
|
resp.ERROR(c, "当前支付通道已经关闭,请联系管理员开通!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var product model.Product
|
||||||
|
res := h.db.First(&product, data.ProductId)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Product not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderNo, err := h.snowflake.Next()
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "error with generate trade no: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var user model.User
|
||||||
|
res = h.db.First(&user, data.UserId)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "Invalid user ID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建订单
|
||||||
|
remark := types.OrderRemark{
|
||||||
|
Days: product.Days,
|
||||||
|
Calls: product.Calls,
|
||||||
|
Name: product.Name,
|
||||||
|
Price: product.Price,
|
||||||
|
Discount: product.Discount,
|
||||||
|
}
|
||||||
|
order := model.Order{
|
||||||
|
UserId: user.Id,
|
||||||
|
Mobile: user.Mobile,
|
||||||
|
ProductId: product.Id,
|
||||||
|
OrderNo: orderNo,
|
||||||
|
Subject: product.Name,
|
||||||
|
Amount: product.Price - product.Discount,
|
||||||
|
Status: types.OrderNotPaid,
|
||||||
|
PayWay: PayWayAlipay,
|
||||||
|
Remark: utils.JsonEncode(remark),
|
||||||
|
}
|
||||||
|
res = h.db.Create(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "error with create order: "+res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成二维码图片
|
||||||
|
file, err := h.fs.Open("res/img/alipay.jpg")
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parse, err := url.Parse(h.App.Config.AlipayConfig.NotifyURL)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
imageURL := fmt.Sprintf("%s://%s/api/payment/alipay?order_no=%s", parse.Scheme, parse.Host, orderNo)
|
||||||
|
imgData, err := utils.GenQrcode(imageURL, 400, file)
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
|
||||||
|
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
|
||||||
|
err := c.Request.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO:这里最好用支付宝的公钥签名签证一下交易真假
|
||||||
|
//res := h.alipayService.TradeVerify(c.Request.Form)
|
||||||
|
r := h.alipayService.TradeQuery(c.Request.Form.Get("out_trade_no"))
|
||||||
|
logger.Infof("验证支付结果:%+v", r)
|
||||||
|
if !r.Success() {
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.lock.Lock()
|
||||||
|
defer h.lock.Unlock()
|
||||||
|
|
||||||
|
var order model.Order
|
||||||
|
res := h.db.Where("order_no = ?", r.OutTradeNo).First(&order)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var user model.User
|
||||||
|
res = h.db.First(&user, order.UserId)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var remark types.OrderRemark
|
||||||
|
err = utils.JsonDecode(order.Remark, &remark)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 1. 点卡:days == 0, calls > 0
|
||||||
|
// 2. vip 套餐:days > 0, calls == 0
|
||||||
|
if remark.Days > 0 {
|
||||||
|
if user.ExpiredTime > time.Now().Unix() {
|
||||||
|
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
|
||||||
|
} else {
|
||||||
|
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
|
||||||
|
}
|
||||||
|
user.Vip = true
|
||||||
|
|
||||||
|
} else if !user.Vip { // 充值点卡的非 VIP 用户
|
||||||
|
user.ExpiredTime = time.Now().AddDate(0, 0, 30).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
if remark.Calls > 0 { // 充值点卡
|
||||||
|
user.Calls += remark.Calls
|
||||||
|
} else {
|
||||||
|
user.Calls += h.App.SysConfig.VipMonthCalls
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
res = h.db.Updates(&user)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error(res.Error)
|
||||||
|
c.String(http.StatusOK, "fail")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新订单状态
|
||||||
|
order.PayTime = time.Now().Unix()
|
||||||
|
order.Status = types.OrderPaidSuccess
|
||||||
|
h.db.Updates(&order)
|
||||||
|
|
||||||
|
// 更新产品销量
|
||||||
|
h.db.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
|
||||||
|
|
||||||
|
c.String(http.StatusOK, "success")
|
||||||
|
}
|
||||||
44
api/handler/product_handler.go
Normal file
44
api/handler/product_handler.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProductHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
|
||||||
|
h := ProductHandler{db: db}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
// List 模型列表
|
||||||
|
func (h *ProductHandler) List(c *gin.Context) {
|
||||||
|
var items []model.Product
|
||||||
|
var list = make([]vo.Product, 0)
|
||||||
|
res := h.db.Where("enabled", true).Order("sort_num ASC").Find(&items)
|
||||||
|
if res.Error == nil {
|
||||||
|
for _, item := range items {
|
||||||
|
var product vo.Product
|
||||||
|
err := utils.CopyObject(item, &product)
|
||||||
|
if err == nil {
|
||||||
|
product.Id = item.Id
|
||||||
|
product.CreatedAt = item.CreatedAt.Unix()
|
||||||
|
product.UpdatedAt = item.UpdatedAt.Unix()
|
||||||
|
list = append(list, product)
|
||||||
|
} else {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, list)
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RewardHandler struct {
|
type RewardHandler struct {
|
||||||
@@ -21,50 +22,6 @@ func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
|
|||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *RewardHandler) Notify(c *gin.Context) {
|
|
||||||
token := c.GetHeader("Authorization")
|
|
||||||
if token != h.App.Config.ExtConfig.Token {
|
|
||||||
resp.NotAuth(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var data struct {
|
|
||||||
TransId string `json:"trans_id"` // 微信转账交易 ID
|
|
||||||
Amount float64 `json:"amount"` // 微信转账交易金额
|
|
||||||
Remark string `json:"remark"` // 转账备注
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&data); err != nil {
|
|
||||||
resp.ERROR(c, types.InvalidArgs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.Amount <= 0 {
|
|
||||||
resp.ERROR(c, "Amount should not be 0")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("收到众筹收款信息: %+v", data)
|
|
||||||
var item model.Reward
|
|
||||||
res := h.db.Where("tx_id = ?", data.TransId).First(&item)
|
|
||||||
if res.Error == nil {
|
|
||||||
resp.ERROR(c, "当前交易 ID 己经存在!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
res = h.db.Create(&model.Reward{
|
|
||||||
TxId: data.TransId,
|
|
||||||
Amount: data.Amount,
|
|
||||||
Remark: data.Remark,
|
|
||||||
Status: false,
|
|
||||||
})
|
|
||||||
if res.Error != nil {
|
|
||||||
logger.Errorf("交易保存失败: %v", res.Error)
|
|
||||||
resp.ERROR(c, "交易保存失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
resp.SUCCESS(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify 打赏码核销
|
// Verify 打赏码核销
|
||||||
func (h *RewardHandler) Verify(c *gin.Context) {
|
func (h *RewardHandler) Verify(c *gin.Context) {
|
||||||
var data struct {
|
var data struct {
|
||||||
@@ -75,6 +32,9 @@ func (h *RewardHandler) Verify(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移除转账单号中间的空格,防止有人复制的时候多复制了空格
|
||||||
|
data.TxId = strings.ReplaceAll(data.TxId, " ", "")
|
||||||
|
|
||||||
var item model.Reward
|
var item model.Reward
|
||||||
res := h.db.Where("tx_id = ?", data.TxId).First(&item)
|
res := h.db.Where("tx_id = ?", data.TxId).First(&item)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
|
|||||||
207
api/handler/sd_handler.go
Normal file
207
api/handler/sd_handler.go
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/service/sd"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"chatplus/utils/resp"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SdJobHandler struct {
|
||||||
|
BaseHandler
|
||||||
|
redis *redis.Client
|
||||||
|
db *gorm.DB
|
||||||
|
service *sd.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSdJobHandler(app *core.AppServer, redisCli *redis.Client, db *gorm.DB, service *sd.Service) *SdJobHandler {
|
||||||
|
h := SdJobHandler{
|
||||||
|
redis: redisCli,
|
||||||
|
db: db,
|
||||||
|
service: service,
|
||||||
|
}
|
||||||
|
h.App = app
|
||||||
|
return &h
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client WebSocket 客户端,用于通知任务状态变更
|
||||||
|
func (h *SdJobHandler) Client(c *gin.Context) {
|
||||||
|
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionId := c.Query("session_id")
|
||||||
|
client := types.NewWsClient(ws)
|
||||||
|
// 删除旧的连接
|
||||||
|
h.service.Clients.Put(sessionId, client)
|
||||||
|
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *SdJobHandler) checkLimits(c *gin.Context) bool {
|
||||||
|
user, err := utils.GetLoginUser(c, h.db)
|
||||||
|
if err != nil {
|
||||||
|
resp.NotAuth(c)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.ImgCalls <= 0 {
|
||||||
|
resp.ERROR(c, "您的绘图次数不足,请联系管理员充值!")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Image 创建一个绘画任务
|
||||||
|
func (h *SdJobHandler) Image(c *gin.Context) {
|
||||||
|
if !h.App.Config.SdConfig.Enabled {
|
||||||
|
resp.ERROR(c, "Stable Diffusion service is disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h.checkLimits(c) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
SessionId string `json:"session_id"`
|
||||||
|
types.SdTaskParams
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
|
||||||
|
resp.ERROR(c, types.InvalidArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Width <= 0 {
|
||||||
|
data.Width = 512
|
||||||
|
}
|
||||||
|
if data.Height <= 0 {
|
||||||
|
data.Height = 512
|
||||||
|
}
|
||||||
|
if data.CfgScale <= 0 {
|
||||||
|
data.CfgScale = 7
|
||||||
|
}
|
||||||
|
if data.Seed == 0 {
|
||||||
|
data.Seed = -1
|
||||||
|
}
|
||||||
|
if data.Steps <= 0 {
|
||||||
|
data.Steps = 20
|
||||||
|
}
|
||||||
|
if data.Sampler == "" {
|
||||||
|
data.Sampler = "Euler a"
|
||||||
|
}
|
||||||
|
idValue, _ := c.Get(types.LoginUserID)
|
||||||
|
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
|
||||||
|
params := types.SdTaskParams{
|
||||||
|
TaskId: fmt.Sprintf("task(%s)", utils.RandString(15)),
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
NegativePrompt: data.NegativePrompt,
|
||||||
|
Steps: data.Steps,
|
||||||
|
Sampler: data.Sampler,
|
||||||
|
FaceFix: data.FaceFix,
|
||||||
|
CfgScale: data.CfgScale,
|
||||||
|
Seed: data.Seed,
|
||||||
|
Height: data.Height,
|
||||||
|
Width: data.Width,
|
||||||
|
HdFix: data.HdFix,
|
||||||
|
HdRedrawRate: data.HdRedrawRate,
|
||||||
|
HdScale: data.HdScale,
|
||||||
|
HdScaleAlg: data.HdScaleAlg,
|
||||||
|
HdSteps: data.HdSteps,
|
||||||
|
}
|
||||||
|
job := model.SdJob{
|
||||||
|
UserId: userId,
|
||||||
|
Type: types.TaskImage.String(),
|
||||||
|
TaskId: params.TaskId,
|
||||||
|
Params: utils.JsonEncode(params),
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
Progress: 0,
|
||||||
|
Started: false,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
res := h.db.Create(&job)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, "error with save job: "+res.Error.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.service.PushTask(types.SdTask{
|
||||||
|
Id: int(job.Id),
|
||||||
|
SessionId: data.SessionId,
|
||||||
|
Src: types.TaskSrcImg,
|
||||||
|
Type: types.TaskImage,
|
||||||
|
Prompt: data.Prompt,
|
||||||
|
Params: params,
|
||||||
|
UserId: userId,
|
||||||
|
})
|
||||||
|
var jobVo vo.SdJob
|
||||||
|
err := utils.CopyObject(job, &jobVo)
|
||||||
|
if err == nil {
|
||||||
|
// 推送任务到前端
|
||||||
|
client := h.service.Clients.Get(data.SessionId)
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, jobVo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JobList 获取 stable diffusion 任务列表
|
||||||
|
func (h *SdJobHandler) JobList(c *gin.Context) {
|
||||||
|
status := h.GetInt(c, "status", 0)
|
||||||
|
userId := h.GetInt(c, "user_id", 0)
|
||||||
|
page := h.GetInt(c, "page", 0)
|
||||||
|
pageSize := h.GetInt(c, "page_size", 0)
|
||||||
|
|
||||||
|
session := h.db.Session(&gorm.Session{})
|
||||||
|
if status == 1 {
|
||||||
|
session = session.Where("progress = ?", 100).Order("id DESC")
|
||||||
|
} else {
|
||||||
|
session = session.Where("progress < ?", 100).Order("id ASC")
|
||||||
|
}
|
||||||
|
if userId > 0 {
|
||||||
|
session = session.Where("user_id = ?", userId)
|
||||||
|
}
|
||||||
|
if page > 0 && pageSize > 0 {
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
session = session.Offset(offset).Limit(pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []model.SdJob
|
||||||
|
res := session.Find(&items)
|
||||||
|
if res.Error != nil {
|
||||||
|
resp.ERROR(c, types.NoData)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobs = make([]vo.SdJob, 0)
|
||||||
|
for _, item := range items {
|
||||||
|
var job vo.SdJob
|
||||||
|
err := utils.CopyObject(item, &job)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if item.Progress < 100 {
|
||||||
|
// 30 分钟还没完成的任务直接删除
|
||||||
|
if time.Now().Sub(item.CreatedAt) > time.Minute*30 {
|
||||||
|
h.db.Delete(&item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jobs = append(jobs, job)
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, jobs)
|
||||||
|
}
|
||||||
@@ -66,5 +66,5 @@ type statusVo struct {
|
|||||||
|
|
||||||
// Status check if the message service is enabled
|
// Status check if the message service is enabled
|
||||||
func (h *SmsHandler) Status(c *gin.Context) {
|
func (h *SmsHandler) Status(c *gin.Context) {
|
||||||
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister})
|
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsg, EnabledRegister: h.App.SysConfig.EnabledRegister})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,66 +2,30 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core"
|
"chatplus/core"
|
||||||
|
"chatplus/service/oss"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"fmt"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type UploadHandler struct {
|
type UploadHandler struct {
|
||||||
BaseHandler
|
BaseHandler
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
|
uploaderManager *oss.UploaderManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUploadHandler(app *core.AppServer, db *gorm.DB) *UploadHandler {
|
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
|
||||||
handler := &UploadHandler{db: db}
|
handler := &UploadHandler{db: db, uploaderManager: manager}
|
||||||
handler.App = app
|
handler.App = app
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UploadHandler) Upload(c *gin.Context) {
|
func (h *UploadHandler) Upload(c *gin.Context) {
|
||||||
file, err := c.FormFile("file")
|
fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resp.ERROR(c, fmt.Sprintf("文件上传失败: %s", err.Error()))
|
resp.ERROR(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath, err := h.genFilePath(file.Filename)
|
resp.SUCCESS(c, fileURL)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import (
|
|||||||
"chatplus/utils"
|
"chatplus/utils"
|
||||||
"chatplus/utils/resp"
|
"chatplus/utils/resp"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -23,10 +24,16 @@ type UserHandler struct {
|
|||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
searcher *xdb.Searcher
|
searcher *xdb.Searcher
|
||||||
leveldb *store.LevelDB
|
leveldb *store.LevelDB
|
||||||
|
redis *redis.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher, levelDB *store.LevelDB) *UserHandler {
|
func NewUserHandler(
|
||||||
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB}
|
app *core.AppServer,
|
||||||
|
db *gorm.DB,
|
||||||
|
searcher *xdb.Searcher,
|
||||||
|
levelDB *store.LevelDB,
|
||||||
|
client *redis.Client) *UserHandler {
|
||||||
|
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client}
|
||||||
handler.App = app
|
handler.App = app
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
@@ -35,20 +42,18 @@ func NewUserHandler(app *core.AppServer, db *gorm.DB, searcher *xdb.Searcher, le
|
|||||||
func (h *UserHandler) Register(c *gin.Context) {
|
func (h *UserHandler) Register(c *gin.Context) {
|
||||||
// parameters process
|
// parameters process
|
||||||
var data struct {
|
var data struct {
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Mobile string `json:"mobile"`
|
Mobile string `json:"mobile"`
|
||||||
|
Password string `json:"password"`
|
||||||
Code int `json:"code"`
|
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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.Username = strings.TrimSpace(data.Username)
|
|
||||||
data.Password = strings.TrimSpace(data.Password)
|
data.Password = strings.TrimSpace(data.Password)
|
||||||
|
|
||||||
if len(data.Username) < 5 {
|
if len(data.Mobile) < 10 {
|
||||||
resp.ERROR(c, "用户名长度不能少于5个字符")
|
resp.ERROR(c, "请输入合法的手机号")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(data.Password) < 8 {
|
if len(data.Password) < 8 {
|
||||||
@@ -58,11 +63,10 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
|
|
||||||
// 检查验证码
|
// 检查验证码
|
||||||
key := CodeStorePrefix + data.Mobile
|
key := CodeStorePrefix + data.Mobile
|
||||||
if h.App.SysConfig.EnabledMsgService {
|
if h.App.SysConfig.EnabledMsg {
|
||||||
var code int
|
var code int
|
||||||
err := h.leveldb.Get(key, &code)
|
err := h.leveldb.Get(key, &code)
|
||||||
if err != nil || code != data.Code {
|
if err != nil || code != data.Code {
|
||||||
logger.Info(code)
|
|
||||||
resp.ERROR(c, "短信验证码错误")
|
resp.ERROR(c, "短信验证码错误")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -70,43 +74,27 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
|
|
||||||
// check if the username is exists
|
// check if the username is exists
|
||||||
var item model.User
|
var item model.User
|
||||||
res := h.db.Where("username = ?", data.Username).First(&item)
|
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
|
||||||
if res.RowsAffected > 0 {
|
if res.RowsAffected > 0 {
|
||||||
resp.ERROR(c, "用户名已存在")
|
resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
res = h.db.Where("mobile = ?", data.Mobile).First(&item)
|
|
||||||
if res.RowsAffected > 0 {
|
|
||||||
resp.ERROR(c, "该手机号码以及被注册,请更换其他手机号")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认订阅所有角色
|
|
||||||
var chatRoles []model.ChatRole
|
|
||||||
h.db.Find(&chatRoles)
|
|
||||||
var roleKeys = make([]string, 0)
|
|
||||||
for _, r := range chatRoles {
|
|
||||||
roleKeys = append(roleKeys, r.Key)
|
|
||||||
}
|
|
||||||
|
|
||||||
salt := utils.RandString(8)
|
salt := utils.RandString(8)
|
||||||
user := model.User{
|
user := model.User{
|
||||||
Username: data.Username,
|
Password: utils.GenPassword(data.Password, salt),
|
||||||
Password: utils.GenPassword(data.Password, salt),
|
Avatar: "/images/avatar/user.png",
|
||||||
Nickname: fmt.Sprintf("极客学长@%d", utils.RandomNumber(5)),
|
Salt: salt,
|
||||||
Avatar: "/images/avatar/user.png",
|
Status: true,
|
||||||
Salt: salt,
|
Mobile: data.Mobile,
|
||||||
Status: true,
|
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
|
||||||
Mobile: data.Mobile,
|
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
|
||||||
ChatRoles: utils.JsonEncode(roleKeys),
|
ChatConfig: utils.JsonEncode(types.UserChatConfig{
|
||||||
ChatConfig: utils.JsonEncode(types.ChatConfig{
|
ApiKeys: map[types.Platform]string{
|
||||||
Temperature: h.App.ChatConfig.Temperature,
|
types.OpenAI: "",
|
||||||
MaxTokens: h.App.ChatConfig.MaxTokens,
|
types.Azure: "",
|
||||||
EnableContext: h.App.ChatConfig.EnableContext,
|
types.ChatGLM: "",
|
||||||
EnableHistory: true,
|
},
|
||||||
Model: h.App.ChatConfig.Model,
|
|
||||||
ApiKey: "",
|
|
||||||
}),
|
}),
|
||||||
Calls: h.App.SysConfig.UserInitCalls,
|
Calls: h.App.SysConfig.UserInitCalls,
|
||||||
ImgCalls: h.App.SysConfig.InitImgCalls,
|
ImgCalls: h.App.SysConfig.InitImgCalls,
|
||||||
@@ -118,24 +106,41 @@ func (h *UserHandler) Register(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.App.SysConfig.EnabledMsgService {
|
if h.App.SysConfig.EnabledMsg {
|
||||||
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
|
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
|
||||||
}
|
}
|
||||||
resp.SUCCESS(c, user)
|
|
||||||
|
// 自动登录创建 token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": user.Id,
|
||||||
|
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||||
|
})
|
||||||
|
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 保存到 redis
|
||||||
|
key = fmt.Sprintf("users/%d", user.Id)
|
||||||
|
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||||
|
resp.ERROR(c, "error with save token: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, tokenString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Login 用户登录
|
// Login 用户登录
|
||||||
func (h *UserHandler) Login(c *gin.Context) {
|
func (h *UserHandler) Login(c *gin.Context) {
|
||||||
var data struct {
|
var data struct {
|
||||||
Username string
|
Mobile string `json:"username"`
|
||||||
Password string
|
Password string `json:"password"`
|
||||||
}
|
}
|
||||||
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
|
||||||
}
|
}
|
||||||
var user model.User
|
var user model.User
|
||||||
res := h.db.Where("username = ? OR mobile = ?", data.Username, data.Username).First(&user)
|
res := h.db.Where("mobile = ?", data.Mobile).First(&user)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "用户名不存在")
|
resp.ERROR(c, "用户名不存在")
|
||||||
return
|
return
|
||||||
@@ -157,31 +162,38 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
user.LastLoginAt = time.Now().Unix()
|
user.LastLoginAt = time.Now().Unix()
|
||||||
h.db.Model(&user).Updates(user)
|
h.db.Model(&user).Updates(user)
|
||||||
|
|
||||||
err := utils.SetLoginUser(c, user)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "保存会话失败")
|
|
||||||
logger.Error("Error for save session: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
h.db.Create(&model.UserLoginLog{
|
h.db.Create(&model.UserLoginLog{
|
||||||
UserId: user.Id,
|
UserId: user.Id,
|
||||||
Username: user.Username,
|
Username: user.Mobile,
|
||||||
LoginIp: c.ClientIP(),
|
LoginIp: c.ClientIP(),
|
||||||
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
|
||||||
})
|
})
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
// 创建 token
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
|
"user_id": user.Id,
|
||||||
|
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
|
||||||
|
})
|
||||||
|
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
resp.ERROR(c, "Failed to generate token, "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 保存到 redis
|
||||||
|
key := fmt.Sprintf("users/%d", user.Id)
|
||||||
|
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
|
||||||
|
resp.ERROR(c, "error with save token: "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.SUCCESS(c, tokenString)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout 注 销
|
// Logout 注 销
|
||||||
func (h *UserHandler) Logout(c *gin.Context) {
|
func (h *UserHandler) Logout(c *gin.Context) {
|
||||||
sessionId := c.GetHeader(types.SessionName)
|
sessionId := c.GetHeader(types.ChatTokenHeader)
|
||||||
session := sessions.Default(c)
|
key := h.GetUserKey(c)
|
||||||
session.Delete(types.SessionUser)
|
if _, err := h.redis.Del(c, key).Result(); err != nil {
|
||||||
err := session.Save()
|
logger.Error("error with delete session: ", err)
|
||||||
if err != nil {
|
|
||||||
logger.Error("Error for save session: ", err)
|
|
||||||
}
|
}
|
||||||
// 删除 websocket 会话列表
|
// 删除 websocket 会话列表
|
||||||
h.App.ChatSession.Delete(sessionId)
|
h.App.ChatSession.Delete(sessionId)
|
||||||
@@ -211,15 +223,16 @@ func (h *UserHandler) Session(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type userProfile struct {
|
type userProfile struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Username string `json:"username"`
|
Mobile string `json:"mobile"`
|
||||||
Nickname string `json:"nickname"`
|
Avatar string `json:"avatar"`
|
||||||
Mobile string `json:"mobile"`
|
ChatConfig types.UserChatConfig `json:"chat_config"`
|
||||||
Avatar string `json:"avatar"`
|
Calls int `json:"calls"`
|
||||||
ChatConfig types.ChatConfig `json:"chat_config"`
|
ImgCalls int `json:"img_calls"`
|
||||||
Calls int `json:"calls"`
|
TotalTokens int64 `json:"total_tokens"`
|
||||||
ImgCalls int `json:"img_calls"`
|
Tokens int64 `json:"tokens"`
|
||||||
Tokens int64 `json:"tokens"`
|
ExpiredTime int64 `json:"expired_time"`
|
||||||
|
Vip bool `json:"vip"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *UserHandler) Profile(c *gin.Context) {
|
func (h *UserHandler) Profile(c *gin.Context) {
|
||||||
@@ -255,29 +268,14 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
h.db.First(&user, user.Id)
|
h.db.First(&user, user.Id)
|
||||||
user.Nickname = data.Nickname
|
|
||||||
user.Avatar = data.Avatar
|
user.Avatar = data.Avatar
|
||||||
|
user.ChatConfig = utils.JsonEncode(data.ChatConfig)
|
||||||
var chatConfig types.ChatConfig
|
|
||||||
err = utils.JsonDecode(user.ChatConfig, &chatConfig)
|
|
||||||
if err != nil {
|
|
||||||
resp.ERROR(c, "用户配置解析失败")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chatConfig.EnableHistory = data.ChatConfig.EnableHistory
|
|
||||||
chatConfig.EnableContext = data.ChatConfig.EnableContext
|
|
||||||
chatConfig.Model = data.ChatConfig.Model
|
|
||||||
chatConfig.MaxTokens = data.ChatConfig.MaxTokens
|
|
||||||
chatConfig.ApiKey = data.ChatConfig.ApiKey
|
|
||||||
chatConfig.Temperature = data.ChatConfig.Temperature
|
|
||||||
|
|
||||||
user.ChatConfig = utils.JsonEncode(chatConfig)
|
|
||||||
res := h.db.Updates(&user)
|
res := h.db.Updates(&user)
|
||||||
if res.Error != nil {
|
if res.Error != nil {
|
||||||
resp.ERROR(c, "更新用户信息失败")
|
resp.ERROR(c, "更新用户信息失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
resp.SUCCESS(c)
|
resp.SUCCESS(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
148
api/main.go
148
api/main.go
@@ -5,12 +5,19 @@ import (
|
|||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
"chatplus/handler"
|
"chatplus/handler"
|
||||||
"chatplus/handler/admin"
|
"chatplus/handler/admin"
|
||||||
|
"chatplus/handler/chatimpl"
|
||||||
logger2 "chatplus/logger"
|
logger2 "chatplus/logger"
|
||||||
"chatplus/service"
|
"chatplus/service"
|
||||||
"chatplus/service/function"
|
"chatplus/service/fun"
|
||||||
|
"chatplus/service/mj"
|
||||||
|
"chatplus/service/oss"
|
||||||
|
"chatplus/service/payment"
|
||||||
|
"chatplus/service/sd"
|
||||||
|
"chatplus/service/wx"
|
||||||
"chatplus/store"
|
"chatplus/store"
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,7 +33,7 @@ import (
|
|||||||
|
|
||||||
var logger = logger2.GetLogger()
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
//go:embed res/ip2region.xdb
|
//go:embed res
|
||||||
var xdbFS embed.FS
|
var xdbFS embed.FS
|
||||||
|
|
||||||
// AppLifecycle 应用程序生命周期
|
// AppLifecycle 应用程序生命周期
|
||||||
@@ -80,14 +87,19 @@ func main() {
|
|||||||
// 创建应用服务
|
// 创建应用服务
|
||||||
fx.Provide(core.NewServer),
|
fx.Provide(core.NewServer),
|
||||||
// 初始化
|
// 初始化
|
||||||
fx.Invoke(func(s *core.AppServer) {
|
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
|
||||||
s.Init(debug)
|
s.Init(debug, client)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 初始化数据库
|
// 初始化数据库
|
||||||
fx.Provide(store.NewGormConfig),
|
fx.Provide(store.NewGormConfig),
|
||||||
fx.Provide(store.NewMysql),
|
fx.Provide(store.NewMysql),
|
||||||
fx.Provide(store.NewLevelDB),
|
fx.Provide(store.NewLevelDB),
|
||||||
|
fx.Provide(store.NewRedisClient),
|
||||||
|
|
||||||
|
fx.Provide(func() embed.FS {
|
||||||
|
return xdbFS
|
||||||
|
}),
|
||||||
|
|
||||||
// 创建 Ip2Region 查询对象
|
// 创建 Ip2Region 查询对象
|
||||||
fx.Provide(func() (*xdb.Searcher, error) {
|
fx.Provide(func() (*xdb.Searcher, error) {
|
||||||
@@ -104,17 +116,22 @@ func main() {
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// 创建函数
|
// 创建函数
|
||||||
fx.Provide(function.NewFunctions),
|
fx.Provide(fun.NewFunctions),
|
||||||
|
|
||||||
// 创建控制器
|
// 创建控制器
|
||||||
fx.Provide(handler.NewChatRoleHandler),
|
fx.Provide(handler.NewChatRoleHandler),
|
||||||
fx.Provide(handler.NewUserHandler),
|
fx.Provide(handler.NewUserHandler),
|
||||||
fx.Provide(handler.NewChatHandler),
|
fx.Provide(chatimpl.NewChatHandler),
|
||||||
fx.Provide(handler.NewUploadHandler),
|
fx.Provide(handler.NewUploadHandler),
|
||||||
fx.Provide(handler.NewSmsHandler),
|
fx.Provide(handler.NewSmsHandler),
|
||||||
fx.Provide(handler.NewRewardHandler),
|
fx.Provide(handler.NewRewardHandler),
|
||||||
fx.Provide(handler.NewCaptchaHandler),
|
fx.Provide(handler.NewCaptchaHandler),
|
||||||
fx.Provide(handler.NewMidJourneyHandler),
|
fx.Provide(handler.NewMidJourneyHandler),
|
||||||
|
fx.Provide(handler.NewChatModelHandler),
|
||||||
|
fx.Provide(handler.NewSdJobHandler),
|
||||||
|
fx.Provide(handler.NewPaymentHandler),
|
||||||
|
fx.Provide(handler.NewOrderHandler),
|
||||||
|
fx.Provide(handler.NewProductHandler),
|
||||||
|
|
||||||
fx.Provide(admin.NewConfigHandler),
|
fx.Provide(admin.NewConfigHandler),
|
||||||
fx.Provide(admin.NewAdminHandler),
|
fx.Provide(admin.NewAdminHandler),
|
||||||
@@ -123,17 +140,74 @@ func main() {
|
|||||||
fx.Provide(admin.NewChatRoleHandler),
|
fx.Provide(admin.NewChatRoleHandler),
|
||||||
fx.Provide(admin.NewRewardHandler),
|
fx.Provide(admin.NewRewardHandler),
|
||||||
fx.Provide(admin.NewDashboardHandler),
|
fx.Provide(admin.NewDashboardHandler),
|
||||||
|
fx.Provide(admin.NewChatModelHandler),
|
||||||
|
fx.Provide(admin.NewProductHandler),
|
||||||
|
fx.Provide(admin.NewOrderHandler),
|
||||||
|
|
||||||
// 创建服务
|
// 创建服务
|
||||||
fx.Provide(service.NewAliYunSmsService),
|
fx.Provide(service.NewAliYunSmsService),
|
||||||
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
|
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
|
||||||
return service.NewCaptchaService(config.ApiConfig)
|
return service.NewCaptchaService(config.ApiConfig)
|
||||||
}),
|
}),
|
||||||
|
fx.Provide(oss.NewUploaderManager),
|
||||||
|
fx.Provide(mj.NewService),
|
||||||
|
|
||||||
|
// 微信机器人服务
|
||||||
|
fx.Provide(wx.NewWeChatBot),
|
||||||
|
fx.Invoke(func(config *types.AppConfig, bot *wx.Bot) {
|
||||||
|
if config.WeChatBot {
|
||||||
|
err := bot.Run()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("微信登录失败:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// MidJourney 机器人
|
||||||
|
fx.Provide(mj.NewBot),
|
||||||
|
fx.Provide(mj.NewClient),
|
||||||
|
fx.Invoke(func(config *types.AppConfig, bot *mj.Bot) {
|
||||||
|
if config.MjConfig.Enabled {
|
||||||
|
err := bot.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("MidJourney 服务启动失败:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(config *types.AppConfig, mjService *mj.Service) {
|
||||||
|
if config.MjConfig.Enabled {
|
||||||
|
go func() {
|
||||||
|
mjService.Run()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Stable Diffusion 机器人
|
||||||
|
fx.Provide(sd.NewService),
|
||||||
|
fx.Invoke(func(config *types.AppConfig, service *sd.Service) {
|
||||||
|
if config.SdConfig.Enabled {
|
||||||
|
go func() {
|
||||||
|
service.Run()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
fx.Provide(payment.NewAlipayService),
|
||||||
|
fx.Provide(service.NewSnowflake),
|
||||||
|
fx.Provide(service.NewXXLJobExecutor),
|
||||||
|
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
|
||||||
|
if config.XXLConfig.Enabled {
|
||||||
|
go func() {
|
||||||
|
log.Fatal(exec.Run())
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
// 注册路由
|
// 注册路由
|
||||||
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/")
|
||||||
group.GET("list", h.List)
|
group.GET("list", h.List)
|
||||||
|
group.POST("update", h.UpdateRole)
|
||||||
}),
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
|
||||||
group := s.Engine.Group("/api/user/")
|
group := s.Engine.Group("/api/user/")
|
||||||
@@ -146,7 +220,7 @@ func main() {
|
|||||||
group.POST("password", h.Password)
|
group.POST("password", h.Password)
|
||||||
group.POST("bind/mobile", h.BindMobile)
|
group.POST("bind/mobile", h.BindMobile)
|
||||||
}),
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
|
fx.Invoke(func(s *core.AppServer, h *chatimpl.ChatHandler) {
|
||||||
group := s.Engine.Group("/api/chat/")
|
group := s.Engine.Group("/api/chat/")
|
||||||
group.Any("new", h.ChatHandle)
|
group.Any("new", h.ChatHandle)
|
||||||
group.GET("list", h.List)
|
group.GET("list", h.List)
|
||||||
@@ -155,7 +229,7 @@ func main() {
|
|||||||
group.GET("remove", h.Remove)
|
group.GET("remove", h.Remove)
|
||||||
group.GET("history", h.History)
|
group.GET("history", h.History)
|
||||||
group.GET("clear", h.Clear)
|
group.GET("clear", h.Clear)
|
||||||
group.GET("tokens", h.Tokens)
|
group.POST("tokens", h.Tokens)
|
||||||
group.GET("stop", h.StopGenerate)
|
group.GET("stop", h.StopGenerate)
|
||||||
}),
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
|
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
|
||||||
@@ -173,13 +247,21 @@ func main() {
|
|||||||
}),
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
|
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
|
||||||
group := s.Engine.Group("/api/reward/")
|
group := s.Engine.Group("/api/reward/")
|
||||||
group.POST("notify", h.Notify)
|
|
||||||
group.POST("verify", h.Verify)
|
group.POST("verify", h.Verify)
|
||||||
}),
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
|
||||||
s.Engine.POST("/api/mj/notify", h.Notify)
|
group := s.Engine.Group("/api/mj/")
|
||||||
s.Engine.POST("/api/mj/upscale", h.Upscale)
|
group.POST("image", h.Image)
|
||||||
s.Engine.POST("/api/mj/variation", h.Variation)
|
group.POST("upscale", h.Upscale)
|
||||||
|
group.POST("variation", h.Variation)
|
||||||
|
group.GET("jobs", h.JobList)
|
||||||
|
group.Any("client", h.Client)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
|
||||||
|
group := s.Engine.Group("/api/sd")
|
||||||
|
group.POST("image", h.Image)
|
||||||
|
group.GET("jobs", h.JobList)
|
||||||
|
group.Any("client", h.Client)
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// 管理后台控制器
|
// 管理后台控制器
|
||||||
@@ -213,7 +295,7 @@ func main() {
|
|||||||
group := s.Engine.Group("/api/admin/role/")
|
group := s.Engine.Group("/api/admin/role/")
|
||||||
group.GET("list", h.List)
|
group.GET("list", h.List)
|
||||||
group.POST("save", h.Save)
|
group.POST("save", h.Save)
|
||||||
group.POST("sort", h.SetSort)
|
group.POST("sort", h.Sort)
|
||||||
group.GET("remove", h.Remove)
|
group.GET("remove", h.Remove)
|
||||||
}),
|
}),
|
||||||
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
|
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
|
||||||
@@ -224,6 +306,46 @@ func main() {
|
|||||||
group := s.Engine.Group("/api/admin/dashboard/")
|
group := s.Engine.Group("/api/admin/dashboard/")
|
||||||
group.GET("stats", h.Stats)
|
group.GET("stats", h.Stats)
|
||||||
}),
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
|
||||||
|
group := s.Engine.Group("/api/model/")
|
||||||
|
group.GET("list", h.List)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/model/")
|
||||||
|
group.POST("save", h.Save)
|
||||||
|
group.GET("list", h.List)
|
||||||
|
group.POST("enable", h.Enable)
|
||||||
|
group.POST("sort", h.Sort)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
|
||||||
|
group := s.Engine.Group("/api/payment/")
|
||||||
|
group.GET("alipay", h.Alipay)
|
||||||
|
group.POST("query", h.OrderQuery)
|
||||||
|
group.POST("alipay/qrcode", h.AlipayQrcode)
|
||||||
|
group.POST("alipay/notify", h.AlipayNotify)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/product/")
|
||||||
|
group.POST("save", h.Save)
|
||||||
|
group.GET("list", h.List)
|
||||||
|
group.POST("enable", h.Enable)
|
||||||
|
group.POST("sort", h.Sort)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
|
||||||
|
group := s.Engine.Group("/api/admin/order/")
|
||||||
|
group.POST("list", h.List)
|
||||||
|
group.GET("remove", h.Remove)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
|
||||||
|
group := s.Engine.Group("/api/order/")
|
||||||
|
group.POST("list", h.List)
|
||||||
|
}),
|
||||||
|
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
|
||||||
|
group := s.Engine.Group("/api/product/")
|
||||||
|
group.GET("list", h.List)
|
||||||
|
}),
|
||||||
|
|
||||||
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
|
||||||
err := s.Run(db)
|
err := s.Run(db)
|
||||||
|
|||||||
BIN
api/res/img/alipay.jpg
Normal file
BIN
api/res/img/alipay.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
67
api/res/text2img.json
Normal file
67
api/res/text2img.json
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"data": [
|
||||||
|
"task(m1wpaa4v60zedj8)",
|
||||||
|
"a cute cat",
|
||||||
|
"",
|
||||||
|
[],
|
||||||
|
20,
|
||||||
|
"DPM++ 2M Karras",
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
512,
|
||||||
|
384,
|
||||||
|
true,
|
||||||
|
0.7,
|
||||||
|
2,
|
||||||
|
"ESRGAN_4x",
|
||||||
|
10,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
"Use same checkpoint",
|
||||||
|
"Use same sampler",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
[],
|
||||||
|
"None",
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
0.8,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
-1,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"positive",
|
||||||
|
"comma",
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
"",
|
||||||
|
"Seed",
|
||||||
|
"",
|
||||||
|
[],
|
||||||
|
"Nothing",
|
||||||
|
"",
|
||||||
|
[],
|
||||||
|
"Nothing",
|
||||||
|
"",
|
||||||
|
[],
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
[],
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"event_data": null,
|
||||||
|
"fn_index": 96,
|
||||||
|
"session_hash": "kmb0ojjfhdj"
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AliYunSmsService struct {
|
type AliYunSmsService struct {
|
||||||
config *types.AppConfig
|
config *types.AliYunSmsConfig
|
||||||
db *store.LevelDB
|
db *store.LevelDB
|
||||||
client *dysmsapi.Client
|
client *dysmsapi.Client
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSms
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &AliYunSmsService{
|
return &AliYunSmsService{
|
||||||
config: config,
|
config: &config.SmsConfig,
|
||||||
db: db,
|
db: db,
|
||||||
client: client,
|
client: client,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -34,10 +34,10 @@ func (s *AliYunSmsService) SendVerifyCode(mobile string, code int) error {
|
|||||||
// 创建短信请求并设置参数
|
// 创建短信请求并设置参数
|
||||||
request := dysmsapi.CreateSendSmsRequest()
|
request := dysmsapi.CreateSendSmsRequest()
|
||||||
request.Scheme = "https"
|
request.Scheme = "https"
|
||||||
request.Domain = s.config.SmsConfig.Domain
|
request.Domain = s.config.Domain
|
||||||
request.PhoneNumbers = mobile
|
request.PhoneNumbers = mobile
|
||||||
request.SignName = "飞行的蜗牛"
|
request.SignName = s.config.Sign
|
||||||
request.TemplateCode = "SMS_281460317"
|
request.TemplateCode = s.config.CodeTempId
|
||||||
request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数
|
request.TemplateParam = fmt.Sprintf("{\"code\":\"%d\"}", code) // 短信模板中的参数
|
||||||
|
|
||||||
// 发送短信
|
// 发送短信
|
||||||
|
|||||||
42
api/service/fun/func_mj.go
Normal file
42
api/service/fun/func_mj.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package fun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/service/mj"
|
||||||
|
"chatplus/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AI 绘画函数
|
||||||
|
|
||||||
|
type FuncMidJourney struct {
|
||||||
|
name string
|
||||||
|
service *mj.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMidJourneyFunc(mjService *mj.Service) FuncMidJourney {
|
||||||
|
return FuncMidJourney{
|
||||||
|
name: "MidJourney AI 绘画",
|
||||||
|
service: mjService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
|
||||||
|
logger.Infof("MJ 绘画参数:%+v", params)
|
||||||
|
prompt := utils.InterfaceToString(params["prompt"])
|
||||||
|
f.service.PushTask(types.MjTask{
|
||||||
|
SessionId: utils.InterfaceToString(params["session_id"]),
|
||||||
|
Src: types.TaskSrcChat,
|
||||||
|
Type: types.TaskImage,
|
||||||
|
Prompt: prompt,
|
||||||
|
UserId: utils.IntValue(utils.InterfaceToString(params["user_id"]), 0),
|
||||||
|
RoleId: utils.IntValue(utils.InterfaceToString(params["role_id"]), 0),
|
||||||
|
Icon: utils.InterfaceToString(params["icon"]),
|
||||||
|
ChatId: utils.InterfaceToString(params["chat_id"]),
|
||||||
|
})
|
||||||
|
return prompt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FuncMidJourney) Name() string {
|
||||||
|
return f.name
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Function = &FuncMidJourney{}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package function
|
package fun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
logger2 "chatplus/logger"
|
logger2 "chatplus/logger"
|
||||||
|
"chatplus/service/mj"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Function interface {
|
type Function interface {
|
||||||
@@ -28,11 +29,11 @@ type dataItem struct {
|
|||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFunctions(config *types.AppConfig) map[string]Function {
|
func NewFunctions(config *types.AppConfig, mjService *mj.Service) map[string]Function {
|
||||||
return map[string]Function{
|
return map[string]Function{
|
||||||
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
|
||||||
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
|
||||||
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
|
||||||
types.FuncMidJourney: NewMidJourneyFunc(config.ExtConfig),
|
types.FuncMidJourney: NewMidJourneyFunc(mjService),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package function
|
package fun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package function
|
package fun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package function
|
package fun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"chatplus/core/types"
|
"chatplus/core/types"
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
package function
|
|
||||||
|
|
||||||
import (
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/utils"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/imroc/req/v3"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AI 绘画函数
|
|
||||||
|
|
||||||
type FuncMidJourney struct {
|
|
||||||
name string
|
|
||||||
config types.ChatPlusExtConfig
|
|
||||||
client *req.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMidJourneyFunc(config types.ChatPlusExtConfig) FuncMidJourney {
|
|
||||||
return FuncMidJourney{
|
|
||||||
name: "MidJourney AI 绘画",
|
|
||||||
config: config,
|
|
||||||
client: req.C().SetTimeout(30 * time.Second)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
|
|
||||||
if f.config.Token == "" {
|
|
||||||
return "", errors.New("无效的 API Token")
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Infof("MJ 绘画参数:%+v", params)
|
|
||||||
prompt := utils.InterfaceToString(params["prompt"])
|
|
||||||
if !utils.IsEmptyValue(params["ar"]) {
|
|
||||||
prompt = fmt.Sprintf("%s --ar %s", prompt, params["ar"])
|
|
||||||
delete(params, "--ar")
|
|
||||||
}
|
|
||||||
if !utils.IsEmptyValue(params["niji"]) {
|
|
||||||
prompt = fmt.Sprintf("%s --niji %s", prompt, params["niji"])
|
|
||||||
delete(params, "niji")
|
|
||||||
} else {
|
|
||||||
prompt = prompt + " --v 5.2"
|
|
||||||
}
|
|
||||||
params["prompt"] = prompt
|
|
||||||
url := fmt.Sprintf("%s/api/mj/image", f.config.ApiURL)
|
|
||||||
var res types.BizVo
|
|
||||||
r, err := f.client.R().
|
|
||||||
SetHeader("Authorization", f.config.Token).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(params).
|
|
||||||
SetSuccessResult(&res).Post(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return "", fmt.Errorf("%v%v", r.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return "", errors.New(res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return prompt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MjUpscaleReq struct {
|
|
||||||
Index int32 `json:"index"`
|
|
||||||
MessageId string `json:"message_id"`
|
|
||||||
MessageHash string `json:"message_hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncMidJourney) Upscale(upReq MjUpscaleReq) error {
|
|
||||||
url := fmt.Sprintf("%s/api/mj/upscale", f.config.ApiURL)
|
|
||||||
var res types.BizVo
|
|
||||||
r, err := f.client.R().
|
|
||||||
SetHeader("Authorization", f.config.Token).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(upReq).
|
|
||||||
SetSuccessResult(&res).Post(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return fmt.Errorf("%v%v", r.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return errors.New(res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MjVariationReq struct {
|
|
||||||
Index int32 `json:"index"`
|
|
||||||
MessageId string `json:"message_id"`
|
|
||||||
MessageHash string `json:"message_hash"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncMidJourney) Variation(upReq MjVariationReq) error {
|
|
||||||
url := fmt.Sprintf("%s/api/mj/variation", f.config.ApiURL)
|
|
||||||
var res types.BizVo
|
|
||||||
r, err := f.client.R().
|
|
||||||
SetHeader("Authorization", f.config.Token).
|
|
||||||
SetHeader("Content-Type", "application/json").
|
|
||||||
SetBody(upReq).
|
|
||||||
SetSuccessResult(&res).Post(url)
|
|
||||||
if err != nil || r.IsErrorState() {
|
|
||||||
return fmt.Errorf("%v%v", r.String(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Code != types.Success {
|
|
||||||
return errors.New(res.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f FuncMidJourney) Name() string {
|
|
||||||
return f.name
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Function = &FuncMidJourney{}
|
|
||||||
213
api/service/mj/bot.go
Normal file
213
api/service/mj/bot.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package mj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"chatplus/utils"
|
||||||
|
"github.com/bwmarrin/discordgo"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MidJourney 机器人
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type Bot struct {
|
||||||
|
config *types.MidJourneyConfig
|
||||||
|
bot *discordgo.Session
|
||||||
|
service *Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBot(config *types.AppConfig, service *Service) (*Bot, error) {
|
||||||
|
discord, err := discordgo.New("Bot " + config.MjConfig.BotToken)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ProxyURL != "" {
|
||||||
|
proxy, _ := url.Parse(config.ProxyURL)
|
||||||
|
discord.Client = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyURL(proxy),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
discord.Dialer = &websocket.Dialer{
|
||||||
|
Proxy: http.ProxyURL(proxy),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Bot{
|
||||||
|
config: &config.MjConfig,
|
||||||
|
bot: discord,
|
||||||
|
service: service,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) Run() error {
|
||||||
|
b.bot.Identify.Intents = discordgo.IntentsAllWithoutPrivileged | discordgo.IntentsGuildMessages | discordgo.IntentMessageContent
|
||||||
|
b.bot.AddHandler(b.messageCreate)
|
||||||
|
b.bot.AddHandler(b.messageUpdate)
|
||||||
|
|
||||||
|
logger.Info("Starting MidJourney Bot...")
|
||||||
|
err := b.bot.Open()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Error opening Discord connection:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("Starting MidJourney Bot successfully!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TaskStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Start = TaskStatus("Started")
|
||||||
|
Running = TaskStatus("Running")
|
||||||
|
Stopped = TaskStatus("Stopped")
|
||||||
|
Finished = TaskStatus("Finished")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
URL string `json:"url"`
|
||||||
|
ProxyURL string `json:"proxy_url"`
|
||||||
|
Filename string `json:"filename"`
|
||||||
|
Width int `json:"width"`
|
||||||
|
Height int `json:"height"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||||
|
// ignore messages for other channels
|
||||||
|
if m.GuildID != b.config.GuildId || m.ChannelID != b.config.ChanelId {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ignore messages for self
|
||||||
|
if m.Author.ID == s.State.User.ID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("CREATE: %s", utils.JsonEncode(m))
|
||||||
|
var referenceId = ""
|
||||||
|
if m.ReferencedMessage != nil {
|
||||||
|
referenceId = m.ReferencedMessage.ID
|
||||||
|
}
|
||||||
|
if strings.Contains(m.Content, "(Waiting to start)") && !strings.Contains(m.Content, "Rerolling **") {
|
||||||
|
// parse content
|
||||||
|
req := CBReq{
|
||||||
|
MessageId: m.ID,
|
||||||
|
ReferenceId: referenceId,
|
||||||
|
Prompt: extractPrompt(m.Content),
|
||||||
|
Content: m.Content,
|
||||||
|
Progress: 0,
|
||||||
|
Status: Start}
|
||||||
|
b.service.Notify(req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.addAttachment(m.ID, referenceId, m.Content, m.Attachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) {
|
||||||
|
// ignore messages for other channels
|
||||||
|
if m.GuildID != b.config.GuildId || m.ChannelID != b.config.ChanelId {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// ignore messages for self
|
||||||
|
if m.Author.ID == s.State.User.ID {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debugf("UPDATE: %s", utils.JsonEncode(m))
|
||||||
|
|
||||||
|
var referenceId = ""
|
||||||
|
if m.ReferencedMessage != nil {
|
||||||
|
referenceId = m.ReferencedMessage.ID
|
||||||
|
}
|
||||||
|
if strings.Contains(m.Content, "(Stopped)") {
|
||||||
|
req := CBReq{
|
||||||
|
MessageId: m.ID,
|
||||||
|
ReferenceId: referenceId,
|
||||||
|
Prompt: extractPrompt(m.Content),
|
||||||
|
Content: m.Content,
|
||||||
|
Progress: extractProgress(m.Content),
|
||||||
|
Status: Stopped}
|
||||||
|
b.service.Notify(req)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b.addAttachment(m.ID, referenceId, m.Content, m.Attachments)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) addAttachment(messageId string, referenceId string, content string, attachments []*discordgo.MessageAttachment) {
|
||||||
|
progress := extractProgress(content)
|
||||||
|
var status TaskStatus
|
||||||
|
if progress == 100 {
|
||||||
|
status = Finished
|
||||||
|
} else {
|
||||||
|
status = Running
|
||||||
|
}
|
||||||
|
for _, attachment := range attachments {
|
||||||
|
if attachment.Width == 0 || attachment.Height == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
image := Image{
|
||||||
|
URL: attachment.URL,
|
||||||
|
Height: attachment.Height,
|
||||||
|
ProxyURL: attachment.ProxyURL,
|
||||||
|
Width: attachment.Width,
|
||||||
|
Size: attachment.Size,
|
||||||
|
Filename: attachment.Filename,
|
||||||
|
Hash: extractHashFromFilename(attachment.Filename),
|
||||||
|
}
|
||||||
|
req := CBReq{
|
||||||
|
MessageId: messageId,
|
||||||
|
ReferenceId: referenceId,
|
||||||
|
Image: image,
|
||||||
|
Prompt: extractPrompt(content),
|
||||||
|
Content: content,
|
||||||
|
Progress: progress,
|
||||||
|
Status: status,
|
||||||
|
}
|
||||||
|
b.service.Notify(req)
|
||||||
|
break // only get one image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract prompt from string
|
||||||
|
func extractPrompt(input string) string {
|
||||||
|
pattern := `\*\*(.*?)\*\*`
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
matches := re.FindStringSubmatch(input)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return strings.TrimSpace(matches[1])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractProgress(input string) int {
|
||||||
|
pattern := `\((\d+)\%\)`
|
||||||
|
re := regexp.MustCompile(pattern)
|
||||||
|
matches := re.FindStringSubmatch(input)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return utils.IntValue(matches[1], 0)
|
||||||
|
}
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractHashFromFilename(filename string) string {
|
||||||
|
if !strings.HasSuffix(filename, ".png") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
index := strings.LastIndex(filename, "_")
|
||||||
|
if index != -1 {
|
||||||
|
return filename[index+1 : len(filename)-4]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
144
api/service/mj/client.go
Normal file
144
api/service/mj/client.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package mj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"fmt"
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MidJourney client
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
client *req.Client
|
||||||
|
config *types.MidJourneyConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(config *types.AppConfig) *Client {
|
||||||
|
client := req.C().SetTimeout(10 * time.Second)
|
||||||
|
// set proxy URL
|
||||||
|
if config.ProxyURL != "" {
|
||||||
|
client.SetProxyURL(config.ProxyURL)
|
||||||
|
}
|
||||||
|
return &Client{client: client, config: &config.MjConfig}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) Imagine(prompt string) error {
|
||||||
|
interactionsReq := &InteractionsRequest{
|
||||||
|
Type: 2,
|
||||||
|
ApplicationID: ApplicationID,
|
||||||
|
GuildID: c.config.GuildId,
|
||||||
|
ChannelID: c.config.ChanelId,
|
||||||
|
SessionID: SessionID,
|
||||||
|
Data: map[string]any{
|
||||||
|
"version": "1166847114203123795",
|
||||||
|
"id": "938956540159881230",
|
||||||
|
"name": "imagine",
|
||||||
|
"type": "1",
|
||||||
|
"options": []map[string]any{
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"name": "prompt",
|
||||||
|
"value": prompt,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"application_command": map[string]any{
|
||||||
|
"id": "938956540159881230",
|
||||||
|
"application_id": ApplicationID,
|
||||||
|
"version": "1118961510123847772",
|
||||||
|
"default_permission": true,
|
||||||
|
"default_member_permissions": nil,
|
||||||
|
"type": 1,
|
||||||
|
"nsfw": false,
|
||||||
|
"name": "imagine",
|
||||||
|
"description": "Create images with Midjourney",
|
||||||
|
"dm_permission": true,
|
||||||
|
"options": []map[string]any{
|
||||||
|
{
|
||||||
|
"type": 3,
|
||||||
|
"name": "prompt",
|
||||||
|
"description": "The prompt to imagine",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"attachments": []any{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://discord.com/api/v9/interactions"
|
||||||
|
r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(interactionsReq).
|
||||||
|
Post(url)
|
||||||
|
|
||||||
|
if err != nil || r.IsErrorState() {
|
||||||
|
return fmt.Errorf("error with http request: %w%v", err, r.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upscale 放大指定的图片
|
||||||
|
func (c *Client) Upscale(index int, messageId string, hash string) error {
|
||||||
|
flags := 0
|
||||||
|
interactionsReq := &InteractionsRequest{
|
||||||
|
Type: 3,
|
||||||
|
ApplicationID: ApplicationID,
|
||||||
|
GuildID: c.config.GuildId,
|
||||||
|
ChannelID: c.config.ChanelId,
|
||||||
|
MessageFlags: &flags,
|
||||||
|
MessageID: &messageId,
|
||||||
|
SessionID: SessionID,
|
||||||
|
Data: map[string]any{
|
||||||
|
"component_type": 2,
|
||||||
|
"custom_id": fmt.Sprintf("MJ::JOB::upsample::%d::%s", index, hash),
|
||||||
|
},
|
||||||
|
Nonce: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://discord.com/api/v9/interactions"
|
||||||
|
var res InteractionsResult
|
||||||
|
r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(interactionsReq).
|
||||||
|
SetErrorResult(&res).
|
||||||
|
Post(url)
|
||||||
|
if err != nil || r.IsErrorState() {
|
||||||
|
return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variation 以指定的图片的视角进行变换再创作,注意需要在对应的频道中关闭 Remix 变换,否则 Variation 指令将不会生效
|
||||||
|
func (c *Client) Variation(index int, messageId string, hash string) error {
|
||||||
|
flags := 0
|
||||||
|
interactionsReq := &InteractionsRequest{
|
||||||
|
Type: 3,
|
||||||
|
ApplicationID: ApplicationID,
|
||||||
|
GuildID: c.config.GuildId,
|
||||||
|
ChannelID: c.config.ChanelId,
|
||||||
|
MessageFlags: &flags,
|
||||||
|
MessageID: &messageId,
|
||||||
|
SessionID: SessionID,
|
||||||
|
Data: map[string]any{
|
||||||
|
"component_type": 2,
|
||||||
|
"custom_id": fmt.Sprintf("MJ::JOB::variation::%d::%s", index, hash),
|
||||||
|
},
|
||||||
|
Nonce: fmt.Sprintf("%d", time.Now().UnixNano()),
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "https://discord.com/api/v9/interactions"
|
||||||
|
var res InteractionsResult
|
||||||
|
r, err := c.client.R().SetHeader("Authorization", c.config.UserToken).
|
||||||
|
SetHeader("Content-Type", "application/json").
|
||||||
|
SetBody(interactionsReq).
|
||||||
|
SetErrorResult(&res).
|
||||||
|
Post(url)
|
||||||
|
if err != nil || r.IsErrorState() {
|
||||||
|
return fmt.Errorf("error with http request: %v%v%v", err, r.Err, res.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
249
api/service/mj/service.go
Normal file
249
api/service/mj/service.go
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
package mj
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/service/oss"
|
||||||
|
"chatplus/store"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MJ 绘画服务
|
||||||
|
|
||||||
|
const RunningJobKey = "MidJourney_Running_Job"
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
client *Client // MJ 客户端
|
||||||
|
taskQueue *store.RedisQueue
|
||||||
|
redis *redis.Client
|
||||||
|
db *gorm.DB
|
||||||
|
uploadManager *oss.UploaderManager
|
||||||
|
Clients *types.LMap[string, *types.WsClient] // MJ 绘画页面 websocket 连接池,用户推送绘画消息
|
||||||
|
ChatClients *types.LMap[string, *types.WsClient] // 聊天页面 websocket 连接池,用于推送绘画消息
|
||||||
|
proxyURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(redisCli *redis.Client, db *gorm.DB, client *Client, manager *oss.UploaderManager, config *types.AppConfig) *Service {
|
||||||
|
return &Service{
|
||||||
|
redis: redisCli,
|
||||||
|
db: db,
|
||||||
|
taskQueue: store.NewRedisQueue("MidJourney_Task_Queue", redisCli),
|
||||||
|
client: client,
|
||||||
|
uploadManager: manager,
|
||||||
|
Clients: types.NewLMap[string, *types.WsClient](),
|
||||||
|
ChatClients: types.NewLMap[string, *types.WsClient](),
|
||||||
|
proxyURL: config.ProxyURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Run() {
|
||||||
|
logger.Info("Starting MidJourney job consumer.")
|
||||||
|
ctx := context.Background()
|
||||||
|
for {
|
||||||
|
_, err := s.redis.Get(ctx, RunningJobKey).Result()
|
||||||
|
if err == nil { // 队列串行执行
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var task types.MjTask
|
||||||
|
err = s.taskQueue.LPop(&task)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("taking task with error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("Consuming Task: %+v", task)
|
||||||
|
switch task.Type {
|
||||||
|
case types.TaskImage:
|
||||||
|
err = s.client.Imagine(task.Prompt)
|
||||||
|
break
|
||||||
|
case types.TaskUpscale:
|
||||||
|
err = s.client.Upscale(task.Index, task.MessageId, task.MessageHash)
|
||||||
|
|
||||||
|
break
|
||||||
|
case types.TaskVariation:
|
||||||
|
err = s.client.Variation(task.Index, task.MessageId, task.MessageHash)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("绘画任务执行失败:", err)
|
||||||
|
if task.RetryCount <= 5 {
|
||||||
|
s.taskQueue.RPush(task)
|
||||||
|
}
|
||||||
|
task.RetryCount += 1
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务的执行状态
|
||||||
|
s.db.Model(&model.MidJourneyJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
|
||||||
|
// 锁定任务执行通道,直到任务超时(5分钟)
|
||||||
|
s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PushTask(task types.MjTask) {
|
||||||
|
logger.Infof("add a new MidJourney Task: %+v", task)
|
||||||
|
s.taskQueue.RPush(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Notify(data CBReq) {
|
||||||
|
taskString, err := s.redis.Get(context.Background(), RunningJobKey).Result()
|
||||||
|
if err != nil { // 过期任务,丢弃
|
||||||
|
logger.Warn("任务已过期:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var task types.MjTask
|
||||||
|
err = utils.JsonDecode(taskString, &task)
|
||||||
|
if err != nil { // 非标准任务,丢弃
|
||||||
|
logger.Warn("任务解析失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var job model.MidJourneyJob
|
||||||
|
res := s.db.Where("message_id = ?", data.MessageId).First(&job)
|
||||||
|
if res.Error == nil && data.Status == Finished {
|
||||||
|
logger.Warn("重复消息:", data.MessageId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if task.Src == types.TaskSrcImg { // 绘画任务
|
||||||
|
var job model.MidJourneyJob
|
||||||
|
res := s.db.Where("id = ?", task.Id).First(&job)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Warn("非法任务:", res.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job.MessageId = data.MessageId
|
||||||
|
job.ReferenceId = data.ReferenceId
|
||||||
|
job.Progress = data.Progress
|
||||||
|
job.Prompt = data.Prompt
|
||||||
|
job.Hash = data.Image.Hash
|
||||||
|
|
||||||
|
// 任务完成,将最终的图片下载下来
|
||||||
|
if data.Progress == 100 {
|
||||||
|
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with download img: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job.ImgURL = imgURL
|
||||||
|
} else {
|
||||||
|
// 临时图片直接保存,访问的时候使用代理进行转发
|
||||||
|
job.ImgURL = data.Image.URL
|
||||||
|
}
|
||||||
|
res = s.db.Updates(&job)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("error with update job: ", res.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobVo vo.MidJourneyJob
|
||||||
|
err := utils.CopyObject(job, &jobVo)
|
||||||
|
if err == nil {
|
||||||
|
if data.Progress < 100 {
|
||||||
|
image, err := utils.DownloadImage(jobVo.ImgURL, s.proxyURL)
|
||||||
|
if err == nil {
|
||||||
|
jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推送任务到前端
|
||||||
|
client := s.Clients.Get(task.SessionId)
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, jobVo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if task.Src == types.TaskSrcChat { // 聊天任务
|
||||||
|
wsClient := s.ChatClients.Get(task.SessionId)
|
||||||
|
if data.Status == Finished {
|
||||||
|
if wsClient != nil && data.ReferenceId != "" {
|
||||||
|
content := fmt.Sprintf("**%s** 任务执行成功,正在从 MidJourney 服务器下载图片,请稍后...", data.Prompt)
|
||||||
|
utils.ReplyMessage(wsClient, content)
|
||||||
|
}
|
||||||
|
// download image
|
||||||
|
imgURL, err := s.uploadManager.GetUploadHandler().PutImg(data.Image.URL, true)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with download image: ", err)
|
||||||
|
if wsClient != nil && data.ReferenceId != "" {
|
||||||
|
content := fmt.Sprintf("**%s** 图片下载失败:%s", data.Prompt, err.Error())
|
||||||
|
utils.ReplyMessage(wsClient, content)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx := s.db.Begin()
|
||||||
|
data.Image.URL = imgURL
|
||||||
|
message := model.HistoryMessage{
|
||||||
|
UserId: uint(task.UserId),
|
||||||
|
ChatId: task.ChatId,
|
||||||
|
RoleId: uint(task.RoleId),
|
||||||
|
Type: types.MjMsg,
|
||||||
|
Icon: task.Icon,
|
||||||
|
Content: utils.JsonEncode(data),
|
||||||
|
Tokens: 0,
|
||||||
|
UseContext: false,
|
||||||
|
}
|
||||||
|
res = tx.Create(&message)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("error with update database: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the job
|
||||||
|
job.UserId = task.UserId
|
||||||
|
job.Type = task.Type.String()
|
||||||
|
job.MessageId = data.MessageId
|
||||||
|
job.ReferenceId = data.ReferenceId
|
||||||
|
job.Prompt = data.Prompt
|
||||||
|
job.ImgURL = imgURL
|
||||||
|
job.Progress = data.Progress
|
||||||
|
job.Hash = data.Image.Hash
|
||||||
|
job.CreatedAt = time.Now()
|
||||||
|
res = tx.Create(&job)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("error with update database: ", err)
|
||||||
|
tx.Rollback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
if wsClient == nil { // 客户端断线,则丢弃
|
||||||
|
logger.Errorf("Client is offline: %+v", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Status == Finished {
|
||||||
|
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
||||||
|
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
|
||||||
|
// 本次绘画完毕,移除客户端
|
||||||
|
s.ChatClients.Delete(task.SessionId)
|
||||||
|
} else {
|
||||||
|
// 使用代理临时转发图片
|
||||||
|
if data.Image.URL != "" {
|
||||||
|
image, err := utils.DownloadImage(data.Image.URL, s.proxyURL)
|
||||||
|
if err == nil {
|
||||||
|
data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户剩余绘图次数
|
||||||
|
// TODO: 放大图片是否需要消耗绘图次数?
|
||||||
|
if data.Status == Finished {
|
||||||
|
s.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||||
|
// 解除任务锁定
|
||||||
|
s.redis.Del(context.Background(), RunningJobKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
34
api/service/mj/types.go
Normal file
34
api/service/mj/types.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package mj
|
||||||
|
|
||||||
|
const (
|
||||||
|
ApplicationID string = "936929561302675456"
|
||||||
|
SessionID string = "ea8816d857ba9ae2f74c59ae1a953afe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InteractionsRequest struct {
|
||||||
|
Type int `json:"type"`
|
||||||
|
ApplicationID string `json:"application_id"`
|
||||||
|
MessageFlags *int `json:"message_flags,omitempty"`
|
||||||
|
MessageID *string `json:"message_id,omitempty"`
|
||||||
|
GuildID string `json:"guild_id"`
|
||||||
|
ChannelID string `json:"channel_id"`
|
||||||
|
SessionID string `json:"session_id"`
|
||||||
|
Data map[string]any `json:"data"`
|
||||||
|
Nonce string `json:"nonce,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InteractionsResult struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string
|
||||||
|
Error map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
type CBReq struct {
|
||||||
|
MessageId string `json:"message_id"`
|
||||||
|
ReferenceId string `json:"reference_id"`
|
||||||
|
Image Image `json:"image"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
Status TaskStatus `json:"status"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
}
|
||||||
97
api/service/oss/aliyun_oss.go
Normal file
97
api/service/oss/aliyun_oss.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/utils"
|
||||||
|
"fmt"
|
||||||
|
"github.com/aliyun/aliyun-oss-go-sdk/oss"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AliYunOss struct {
|
||||||
|
config *types.AliYunOssConfig
|
||||||
|
bucket *oss.Bucket
|
||||||
|
proxyURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAliYunOss(appConfig *types.AppConfig) (*AliYunOss, error) {
|
||||||
|
config := &appConfig.OSS.AliYun
|
||||||
|
// 创建 OSS 客户端
|
||||||
|
client, err := oss.New(config.Endpoint, config.AccessKey, config.AccessSecret)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取存储空间
|
||||||
|
bucket, err := client.Bucket(config.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AliYunOss{
|
||||||
|
config: config,
|
||||||
|
bucket: bucket,
|
||||||
|
proxyURL: appConfig.ProxyURL,
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AliYunOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||||
|
// 解析表单
|
||||||
|
file, err := ctx.FormFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 打开上传文件
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
fileExt := filepath.Ext(file.Filename)
|
||||||
|
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||||
|
// 上传文件
|
||||||
|
err = s.bucket.PutObject(objectKey, src)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AliYunOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||||
|
var imageData []byte
|
||||||
|
var err error
|
||||||
|
if useProxy {
|
||||||
|
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
||||||
|
} else {
|
||||||
|
imageData, err = utils.DownloadImage(imageURL, "")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
|
}
|
||||||
|
parse, err := url.Parse(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||||
|
}
|
||||||
|
fileExt := filepath.Ext(parse.Path)
|
||||||
|
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||||
|
// 上传文件字节数据
|
||||||
|
err = s.bucket.PutObject(objectKey, bytes.NewReader(imageData))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("https://%s.%s/%s", s.config.Bucket, s.config.Endpoint, objectKey), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AliYunOss) Delete(fileURL string) error {
|
||||||
|
objectName := filepath.Base(fileURL)
|
||||||
|
return s.bucket.DeleteObject(objectName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Uploader = AliYunOss{}
|
||||||
73
api/service/oss/localstorage.go
Normal file
73
api/service/oss/localstorage.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/utils"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LocalStorage struct {
|
||||||
|
config *types.LocalStorageConfig
|
||||||
|
proxyURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLocalStorage(config *types.AppConfig) LocalStorage {
|
||||||
|
return LocalStorage{
|
||||||
|
config: &config.OSS.Local,
|
||||||
|
proxyURL: config.ProxyURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s LocalStorage) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||||
|
file, err := ctx.FormFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with get form: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with generate filename: %s", err.Error())
|
||||||
|
}
|
||||||
|
// 将文件保存到指定路径
|
||||||
|
err = ctx.SaveUploadedFile(file, filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with save upload file: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||||
|
parse, err := url.Parse(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||||
|
}
|
||||||
|
filename := filepath.Base(parse.Path)
|
||||||
|
filePath, err := utils.GenUploadPath(s.config.BasePath, filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with generate image dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if useProxy {
|
||||||
|
err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
|
||||||
|
} else {
|
||||||
|
err = utils.DownloadFile(imageURL, filePath, "")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s LocalStorage) Delete(fileURL string) error {
|
||||||
|
filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
|
||||||
|
return os.Remove(filePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Uploader = LocalStorage{}
|
||||||
94
api/service/oss/minio_oss.go
Normal file
94
api/service/oss/minio_oss.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiniOss struct {
|
||||||
|
config *types.MiniOssConfig
|
||||||
|
client *minio.Client
|
||||||
|
proxyURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
|
||||||
|
config := &appConfig.OSS.Minio
|
||||||
|
minioClient, err := minio.New(config.Endpoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
|
||||||
|
Secure: config.UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return MiniOss{}, err
|
||||||
|
}
|
||||||
|
return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||||
|
var imageData []byte
|
||||||
|
var err error
|
||||||
|
if useProxy {
|
||||||
|
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
||||||
|
} else {
|
||||||
|
imageData, err = utils.DownloadImage(imageURL, "")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
|
}
|
||||||
|
parse, err := url.Parse(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||||
|
}
|
||||||
|
fileExt := filepath.Ext(parse.Path)
|
||||||
|
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||||
|
info, err := s.client.PutObject(
|
||||||
|
context.Background(),
|
||||||
|
s.config.Bucket,
|
||||||
|
filename,
|
||||||
|
strings.NewReader(string(imageData)),
|
||||||
|
int64(len(imageData)),
|
||||||
|
minio.PutObjectOptions{ContentType: "image/png"})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MiniOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||||
|
file, err := ctx.FormFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with get form: %v", err)
|
||||||
|
}
|
||||||
|
// Open the uploaded file
|
||||||
|
fileReader, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error opening file: %v", err)
|
||||||
|
}
|
||||||
|
defer fileReader.Close()
|
||||||
|
|
||||||
|
fileExt := filepath.Ext(file.Filename)
|
||||||
|
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
|
||||||
|
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
|
||||||
|
ContentType: file.Header.Get("Content-Type"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error uploading to MinIO: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s MiniOss) Delete(fileURL string) error {
|
||||||
|
objectName := filepath.Base(fileURL)
|
||||||
|
return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Uploader = MiniOss{}
|
||||||
111
api/service/oss/qiniu_oss.go
Normal file
111
api/service/oss/qiniu_oss.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/qiniu/go-sdk/v7/auth/qbox"
|
||||||
|
"github.com/qiniu/go-sdk/v7/storage"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type QinNiuOss struct {
|
||||||
|
config *types.QiNiuOssConfig
|
||||||
|
mac *qbox.Mac
|
||||||
|
putPolicy storage.PutPolicy
|
||||||
|
uploader *storage.FormUploader
|
||||||
|
manager *storage.BucketManager
|
||||||
|
proxyURL string
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
|
||||||
|
config := &appConfig.OSS.QiNiu
|
||||||
|
// build storage uploader
|
||||||
|
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
|
||||||
|
if !ok {
|
||||||
|
zone = storage.ZoneHuanan
|
||||||
|
}
|
||||||
|
storeConfig := storage.Config{Zone: &zone}
|
||||||
|
formUploader := storage.NewFormUploader(&storeConfig)
|
||||||
|
// generate token
|
||||||
|
mac := qbox.NewMac(config.AccessKey, config.AccessSecret)
|
||||||
|
putPolicy := storage.PutPolicy{
|
||||||
|
Scope: config.Bucket,
|
||||||
|
}
|
||||||
|
return QinNiuOss{
|
||||||
|
config: config,
|
||||||
|
mac: mac,
|
||||||
|
putPolicy: putPolicy,
|
||||||
|
uploader: formUploader,
|
||||||
|
manager: storage.NewBucketManager(mac, &storeConfig),
|
||||||
|
proxyURL: appConfig.ProxyURL,
|
||||||
|
dir: "chatgpt-plus",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
|
||||||
|
// 解析表单
|
||||||
|
file, err := ctx.FormFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
// 打开上传文件
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
fileExt := filepath.Ext(file.Filename)
|
||||||
|
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
|
||||||
|
// 上传文件
|
||||||
|
ret := storage.PutRet{}
|
||||||
|
extra := storage.PutExtra{}
|
||||||
|
err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
|
||||||
|
var imageData []byte
|
||||||
|
var err error
|
||||||
|
if useProxy {
|
||||||
|
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
|
||||||
|
} else {
|
||||||
|
imageData, err = utils.DownloadImage(imageURL, "")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with download image: %v", err)
|
||||||
|
}
|
||||||
|
parse, err := url.Parse(imageURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error with parse image URL: %v", err)
|
||||||
|
}
|
||||||
|
fileExt := filepath.Ext(parse.Path)
|
||||||
|
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
|
||||||
|
ret := storage.PutRet{}
|
||||||
|
extra := storage.PutExtra{}
|
||||||
|
// 上传文件字节数据
|
||||||
|
err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s QinNiuOss) Delete(fileURL string) error {
|
||||||
|
objectName := filepath.Base(fileURL)
|
||||||
|
key := fmt.Sprintf("%s/%s", s.dir, objectName)
|
||||||
|
return s.manager.Delete(s.config.Bucket, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Uploader = QinNiuOss{}
|
||||||
9
api/service/oss/uploader.go
Normal file
9
api/service/oss/uploader.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
type Uploader interface {
|
||||||
|
PutFile(ctx *gin.Context, name string) (string, error)
|
||||||
|
PutImg(imageURL string, useProxy bool) (string, error)
|
||||||
|
Delete(fileURL string) error
|
||||||
|
}
|
||||||
51
api/service/oss/uploader_manager.go
Normal file
51
api/service/oss/uploader_manager.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package oss
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UploaderManager struct {
|
||||||
|
handler Uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
const Local = "LOCAL"
|
||||||
|
const Minio = "MINIO"
|
||||||
|
const QiNiu = "QINIU"
|
||||||
|
const AliYun = "ALIYUN"
|
||||||
|
|
||||||
|
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
|
||||||
|
active := Local
|
||||||
|
if config.OSS.Active != "" {
|
||||||
|
active = strings.ToUpper(config.OSS.Active)
|
||||||
|
}
|
||||||
|
var handler Uploader
|
||||||
|
switch active {
|
||||||
|
case Local:
|
||||||
|
handler = NewLocalStorage(config)
|
||||||
|
break
|
||||||
|
case Minio:
|
||||||
|
client, err := NewMiniOss(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handler = client
|
||||||
|
break
|
||||||
|
case QiNiu:
|
||||||
|
handler = NewQiNiuOss(config)
|
||||||
|
break
|
||||||
|
case AliYun:
|
||||||
|
client, err := NewAliYunOss(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
handler = client
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UploaderManager{handler: handler}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UploaderManager) GetUploadHandler() Uploader {
|
||||||
|
return m.handler
|
||||||
|
}
|
||||||
142
api/service/payment/alipay_service.go
Normal file
142
api/service/payment/alipay_service.go
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package payment
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"fmt"
|
||||||
|
"github.com/smartwalle/alipay/v3"
|
||||||
|
"log"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlipayService struct {
|
||||||
|
config *types.AlipayConfig
|
||||||
|
client *alipay.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
func NewAlipayService(appConfig *types.AppConfig) (*AlipayService, error) {
|
||||||
|
config := appConfig.AlipayConfig
|
||||||
|
if !config.Enabled {
|
||||||
|
logger.Info("Disabled Alipay service")
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
priKey, err := readKey(config.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error with read App Private key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
xClient, err := alipay.New(config.AppId, priKey, !config.SandBox)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error with initialize alipay service: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = xClient.LoadAppCertPublicKeyFromFile(config.PublicKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("error with loading App PublicKey: %v", err)
|
||||||
|
}
|
||||||
|
if err = xClient.LoadAliPayRootCertFromFile(config.RootCert); err != nil {
|
||||||
|
return nil, fmt.Errorf("error with loading alipay RootCert: %v", err)
|
||||||
|
}
|
||||||
|
if err = xClient.LoadAlipayCertPublicKeyFromFile(config.AlipayPublicKey); err != nil {
|
||||||
|
return nil, fmt.Errorf("error with loading Alipay PublicKey: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AlipayService{config: &config, client: xClient}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlipayService) PayUrlMobile(outTradeNo string, notifyURL string, returnURL string, Amount string, subject string) (string, error) {
|
||||||
|
var p = alipay.TradeWapPay{}
|
||||||
|
p.NotifyURL = notifyURL
|
||||||
|
p.ReturnURL = returnURL
|
||||||
|
p.Subject = subject
|
||||||
|
p.OutTradeNo = outTradeNo
|
||||||
|
p.TotalAmount = Amount
|
||||||
|
p.ProductCode = "QUICK_WAP_WAY"
|
||||||
|
res, err := s.client.TradeWapPay(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlipayService) PayUrlPc(outTradeNo string, notifyURL string, returnURL string, amount string, subject string) (string, error) {
|
||||||
|
var p = alipay.TradePagePay{}
|
||||||
|
p.NotifyURL = notifyURL
|
||||||
|
p.ReturnURL = returnURL
|
||||||
|
p.Subject = subject
|
||||||
|
p.OutTradeNo = outTradeNo
|
||||||
|
p.TotalAmount = amount
|
||||||
|
p.ProductCode = "FAST_INSTANT_TRADE_PAY"
|
||||||
|
res, err := s.client.TradePagePay(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TradeVerify 交易验证
|
||||||
|
func (s *AlipayService) TradeVerify(reqForm url.Values) NotifyVo {
|
||||||
|
err := s.client.VerifySign(reqForm)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("异步通知验证签名发生错误", err)
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 0,
|
||||||
|
Message: "异步通知验证签名发生错误",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.TradeQuery(reqForm.Get("out_trade_no"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *AlipayService) TradeQuery(outTradeNo string) NotifyVo {
|
||||||
|
var p = alipay.TradeQuery{}
|
||||||
|
p.OutTradeNo = outTradeNo
|
||||||
|
rsp, err := s.client.TradeQuery(p)
|
||||||
|
if err != nil {
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 0,
|
||||||
|
Message: "异步查询验证订单信息发生错误" + outTradeNo + err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rsp.IsSuccess() == true && rsp.TradeStatus == "TRADE_SUCCESS" {
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 1,
|
||||||
|
OutTradeNo: rsp.OutTradeNo,
|
||||||
|
TradeNo: rsp.TradeNo,
|
||||||
|
Amount: rsp.TotalAmount,
|
||||||
|
Subject: rsp.Subject,
|
||||||
|
Message: "OK",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return NotifyVo{
|
||||||
|
Status: 0,
|
||||||
|
Message: "异步查询验证订单信息发生错误" + outTradeNo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readKey(filename string) (string, error) {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifyVo struct {
|
||||||
|
Status int
|
||||||
|
OutTradeNo string
|
||||||
|
TradeNo string
|
||||||
|
Amount string
|
||||||
|
Message string
|
||||||
|
Subject string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v NotifyVo) Success() bool {
|
||||||
|
return v.Status == 1
|
||||||
|
}
|
||||||
309
api/service/sd/service.go
Normal file
309
api/service/sd/service.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package sd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"chatplus/service/oss"
|
||||||
|
"chatplus/store"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/store/vo"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
"github.com/imroc/req/v3"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SD 绘画服务
|
||||||
|
|
||||||
|
const RunningJobKey = "StableDiffusion_Running_Job"
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
httpClient *req.Client
|
||||||
|
config *types.StableDiffusionConfig
|
||||||
|
taskQueue *store.RedisQueue
|
||||||
|
redis *redis.Client
|
||||||
|
db *gorm.DB
|
||||||
|
uploadManager *oss.UploaderManager
|
||||||
|
Clients *types.LMap[string, *types.WsClient] // SD 绘画页面 websocket 连接池
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(config *types.AppConfig, redisCli *redis.Client, db *gorm.DB, manager *oss.UploaderManager) *Service {
|
||||||
|
return &Service{
|
||||||
|
config: &config.SdConfig,
|
||||||
|
httpClient: req.C(),
|
||||||
|
redis: redisCli,
|
||||||
|
db: db,
|
||||||
|
uploadManager: manager,
|
||||||
|
Clients: types.NewLMap[string, *types.WsClient](),
|
||||||
|
taskQueue: store.NewRedisQueue("stable_diffusion_task_queue", redisCli),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Run() {
|
||||||
|
logger.Info("Starting StableDiffusion job consumer.")
|
||||||
|
ctx := context.Background()
|
||||||
|
for {
|
||||||
|
_, err := s.redis.Get(ctx, RunningJobKey).Result()
|
||||||
|
if err == nil { // 队列串行执行
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var task types.SdTask
|
||||||
|
err = s.taskQueue.LPop(&task)
|
||||||
|
if err != nil {
|
||||||
|
logger.Errorf("taking task with error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
logger.Infof("Consuming Task: %+v", task)
|
||||||
|
err = s.Txt2Img(task)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("绘画任务执行失败:", err)
|
||||||
|
if task.RetryCount <= 5 {
|
||||||
|
s.taskQueue.RPush(task)
|
||||||
|
}
|
||||||
|
task.RetryCount += 1
|
||||||
|
time.Sleep(time.Second * 3)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新任务的执行状态
|
||||||
|
s.db.Model(&model.SdJob{}).Where("id = ?", task.Id).UpdateColumn("started", true)
|
||||||
|
// 锁定任务执行通道,直到任务超时(5分钟)
|
||||||
|
s.redis.Set(ctx, RunningJobKey, utils.JsonEncode(task), time.Minute*5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushTask 推送任务到队列
|
||||||
|
func (s *Service) PushTask(task types.SdTask) {
|
||||||
|
logger.Infof("add a new Stable Diffusion Task: %+v", task)
|
||||||
|
s.taskQueue.RPush(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Txt2Img 文生图 API
|
||||||
|
func (s *Service) Txt2Img(task types.SdTask) error {
|
||||||
|
var taskInfo TaskInfo
|
||||||
|
bytes, err := os.ReadFile(s.config.Txt2ImgJsonPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with load text2img json template file: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(bytes, &taskInfo)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error with decode json params: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
data := taskInfo.Data
|
||||||
|
params := task.Params
|
||||||
|
data[ParamKeys["task_id"]] = params.TaskId
|
||||||
|
data[ParamKeys["prompt"]] = params.Prompt
|
||||||
|
data[ParamKeys["negative_prompt"]] = params.NegativePrompt
|
||||||
|
data[ParamKeys["steps"]] = params.Steps
|
||||||
|
data[ParamKeys["sampler"]] = params.Sampler
|
||||||
|
// @fix bug: 有些 stable diffusion 没有面部修复功能
|
||||||
|
//data[ParamKeys["face_fix"]] = params.FaceFix
|
||||||
|
data[ParamKeys["cfg_scale"]] = params.CfgScale
|
||||||
|
data[ParamKeys["seed"]] = params.Seed
|
||||||
|
data[ParamKeys["height"]] = params.Height
|
||||||
|
data[ParamKeys["width"]] = params.Width
|
||||||
|
data[ParamKeys["hd_fix"]] = params.HdFix
|
||||||
|
data[ParamKeys["hd_redraw_rate"]] = params.HdRedrawRate
|
||||||
|
data[ParamKeys["hd_scale"]] = params.HdScale
|
||||||
|
data[ParamKeys["hd_scale_alg"]] = params.HdScaleAlg
|
||||||
|
data[ParamKeys["hd_sample_num"]] = params.HdSteps
|
||||||
|
|
||||||
|
taskInfo.SessionId = task.SessionId
|
||||||
|
taskInfo.TaskId = params.TaskId
|
||||||
|
taskInfo.Data = data
|
||||||
|
taskInfo.JobId = task.Id
|
||||||
|
go func() {
|
||||||
|
s.runTask(taskInfo, s.httpClient)
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行任务
|
||||||
|
func (s *Service) runTask(taskInfo TaskInfo, client *req.Client) {
|
||||||
|
body := map[string]any{
|
||||||
|
"data": taskInfo.Data,
|
||||||
|
"event_data": taskInfo.EventData,
|
||||||
|
"fn_index": taskInfo.FnIndex,
|
||||||
|
"session_hash": taskInfo.SessionHash,
|
||||||
|
}
|
||||||
|
logger.Debug(utils.JsonEncode(body))
|
||||||
|
var result = make(chan CBReq)
|
||||||
|
go func() {
|
||||||
|
var res struct {
|
||||||
|
Data []interface{} `json:"data"`
|
||||||
|
IsGenerating bool `json:"is_generating"`
|
||||||
|
Duration float64 `json:"duration"`
|
||||||
|
AverageDuration float64 `json:"average_duration"`
|
||||||
|
}
|
||||||
|
var cbReq = CBReq{TaskId: taskInfo.TaskId, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
|
||||||
|
response, err := client.R().SetBody(body).SetSuccessResult(&res).Post(s.config.ApiURL + "/run/predict")
|
||||||
|
if err != nil {
|
||||||
|
cbReq.Message = "error with send request: " + err.Error()
|
||||||
|
cbReq.Success = false
|
||||||
|
result <- cbReq
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsErrorState() {
|
||||||
|
bytes, _ := io.ReadAll(response.Body)
|
||||||
|
cbReq.Message = "error http status code: " + string(bytes)
|
||||||
|
cbReq.Success = false
|
||||||
|
result <- cbReq
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var images []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
|
IsFile bool `json:"is_file"`
|
||||||
|
}
|
||||||
|
err = utils.ForceCovert(res.Data[0], &images)
|
||||||
|
if err != nil {
|
||||||
|
cbReq.Message = "error with decode image:" + err.Error()
|
||||||
|
cbReq.Success = false
|
||||||
|
result <- cbReq
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var info map[string]any
|
||||||
|
err = utils.JsonDecode(utils.InterfaceToString(res.Data[1]), &info)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(res.Data)
|
||||||
|
cbReq.Message = "error with decode image url:" + err.Error()
|
||||||
|
cbReq.Success = false
|
||||||
|
result <- cbReq
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取真实的 seed 值
|
||||||
|
cbReq.ImageName = images[0].Name
|
||||||
|
seed, _ := strconv.ParseInt(utils.InterfaceToString(info["seed"]), 10, 64)
|
||||||
|
cbReq.Seed = seed
|
||||||
|
cbReq.Success = true
|
||||||
|
cbReq.Progress = 100
|
||||||
|
result <- cbReq
|
||||||
|
close(result)
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case value := <-result:
|
||||||
|
s.callback(value)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
var progressReq = map[string]any{
|
||||||
|
"id_task": taskInfo.TaskId,
|
||||||
|
"id_live_preview": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
var progressRes struct {
|
||||||
|
Active bool `json:"active"`
|
||||||
|
Queued bool `json:"queued"`
|
||||||
|
Completed bool `json:"completed"`
|
||||||
|
Progress float64 `json:"progress"`
|
||||||
|
Eta float64 `json:"eta"`
|
||||||
|
LivePreview string `json:"live_preview"`
|
||||||
|
IDLivePreview int `json:"id_live_preview"`
|
||||||
|
TextInfo interface{} `json:"textinfo"`
|
||||||
|
}
|
||||||
|
response, err := client.R().SetBody(progressReq).SetSuccessResult(&progressRes).Post(s.config.ApiURL + "/internal/progress")
|
||||||
|
var cbReq = CBReq{TaskId: taskInfo.TaskId, Success: true, JobId: taskInfo.JobId, SessionId: taskInfo.SessionId}
|
||||||
|
if err != nil { // TODO: 这里可以考虑设置失败重试次数
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsErrorState() {
|
||||||
|
bytes, _ := io.ReadAll(response.Body)
|
||||||
|
logger.Error(string(bytes))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cbReq.ImageData = progressRes.LivePreview
|
||||||
|
cbReq.Progress = int(progressRes.Progress * 100)
|
||||||
|
logger.Debug(cbReq)
|
||||||
|
s.callback(cbReq)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) callback(data CBReq) {
|
||||||
|
// 释放任务锁
|
||||||
|
s.redis.Del(context.Background(), RunningJobKey)
|
||||||
|
client := s.Clients.Get(data.SessionId)
|
||||||
|
if data.Success { // 任务成功
|
||||||
|
var job model.SdJob
|
||||||
|
res := s.db.Where("id = ?", data.JobId).First(&job)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Warn("非法任务:", res.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 更新任务进度
|
||||||
|
job.Progress = data.Progress
|
||||||
|
// 更新任务 seed
|
||||||
|
var params types.SdTaskParams
|
||||||
|
err := utils.JsonDecode(job.Params, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("任务解析失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Seed = data.Seed
|
||||||
|
if data.ImageName != "" { // 下载图片
|
||||||
|
imageURL := fmt.Sprintf("%s/file=%s", s.config.ApiURL, data.ImageName)
|
||||||
|
imageURL, err := s.uploadManager.GetUploadHandler().PutImg(imageURL, false)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with download img: ", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
job.ImgURL = imageURL
|
||||||
|
}
|
||||||
|
|
||||||
|
job.Params = utils.JsonEncode(params)
|
||||||
|
res = s.db.Updates(&job)
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Error("error with update job: ", res.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobVo vo.SdJob
|
||||||
|
err = utils.CopyObject(job, &jobVo)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("error with copy object: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Progress < 100 && data.ImageData != "" {
|
||||||
|
jobVo.ImgURL = data.ImageData
|
||||||
|
}
|
||||||
|
// 扣减绘图次数
|
||||||
|
s.db.Where("id = ?", jobVo.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
|
||||||
|
// 推送任务到前端
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, jobVo)
|
||||||
|
}
|
||||||
|
} else { // 任务失败
|
||||||
|
logger.Error("任务执行失败:", data.Message)
|
||||||
|
// 删除任务
|
||||||
|
s.db.Delete(&model.SdJob{Id: uint(data.JobId)})
|
||||||
|
// 推送消息到前端
|
||||||
|
if client != nil {
|
||||||
|
utils.ReplyChunkMessage(client, vo.SdJob{
|
||||||
|
Id: uint(data.JobId),
|
||||||
|
Progress: -1,
|
||||||
|
TaskId: data.TaskId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
api/service/sd/types.go
Normal file
45
api/service/sd/types.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package sd
|
||||||
|
|
||||||
|
import logger2 "chatplus/logger"
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type TaskInfo struct {
|
||||||
|
SessionId string `json:"session_id"`
|
||||||
|
JobId int `json:"job_id"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
Data []interface{} `json:"data"`
|
||||||
|
EventData interface{} `json:"event_data"`
|
||||||
|
FnIndex int `json:"fn_index"`
|
||||||
|
SessionHash string `json:"session_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CBReq struct {
|
||||||
|
SessionId string
|
||||||
|
JobId int
|
||||||
|
TaskId string
|
||||||
|
ImageName string
|
||||||
|
ImageData string
|
||||||
|
Progress int
|
||||||
|
Seed int64
|
||||||
|
Success bool
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
var ParamKeys = map[string]int{
|
||||||
|
"task_id": 0,
|
||||||
|
"prompt": 1,
|
||||||
|
"negative_prompt": 2,
|
||||||
|
"steps": 4,
|
||||||
|
"sampler": 5,
|
||||||
|
"face_fix": 6, // 面部修复
|
||||||
|
"cfg_scale": 8,
|
||||||
|
"seed": 27,
|
||||||
|
"height": 9,
|
||||||
|
"width": 10,
|
||||||
|
"hd_fix": 11,
|
||||||
|
"hd_redraw_rate": 12, //高清修复重绘幅度
|
||||||
|
"hd_scale": 13, // 高清修复放大倍数
|
||||||
|
"hd_scale_alg": 14, // 高清修复放大算法
|
||||||
|
"hd_sample_num": 15, // 高清修复采样次数
|
||||||
|
}
|
||||||
56
api/service/snowflake.go
Normal file
56
api/service/snowflake.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Snowflake 雪花算法实现
|
||||||
|
type Snowflake struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
lastTimestamp int64
|
||||||
|
workerID int
|
||||||
|
sequence int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSnowflake() *Snowflake {
|
||||||
|
return &Snowflake{
|
||||||
|
lastTimestamp: -1,
|
||||||
|
workerID: 0, // TODO: 增加 WorkID 参数
|
||||||
|
sequence: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next 生成一个新的唯一ID
|
||||||
|
func (s *Snowflake) Next() (string, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
timestamp := time.Now().UnixNano() / 1000000 // 转换为毫秒
|
||||||
|
if timestamp < s.lastTimestamp {
|
||||||
|
return "", fmt.Errorf("clock moved backwards. Refusing to generate id for %d milliseconds", s.lastTimestamp-timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
if timestamp == s.lastTimestamp {
|
||||||
|
s.sequence = (s.sequence + 1) & 4095
|
||||||
|
if s.sequence == 0 {
|
||||||
|
timestamp = s.waitNextMillis()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s.sequence = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s.lastTimestamp = timestamp
|
||||||
|
id := (timestamp << 22) | (int64(s.workerID) << 10) | int64(s.sequence)
|
||||||
|
now := time.Now()
|
||||||
|
return fmt.Sprintf("%d%02d%02d%d", now.Year(), now.Month(), now.Day(), id), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snowflake) waitNextMillis() int64 {
|
||||||
|
timestamp := time.Now().UnixNano() / 1000000
|
||||||
|
for timestamp <= s.lastTimestamp {
|
||||||
|
timestamp = time.Now().UnixNano() / 1000000
|
||||||
|
}
|
||||||
|
return timestamp
|
||||||
|
}
|
||||||
87
api/service/wx/bot.go
Normal file
87
api/service/wx/bot.go
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
package wx
|
||||||
|
|
||||||
|
import (
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"github.com/eatmoreapple/openwechat"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 微信收款机器人
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type Bot struct {
|
||||||
|
bot *openwechat.Bot
|
||||||
|
token string
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWeChatBot(db *gorm.DB) *Bot {
|
||||||
|
bot := openwechat.DefaultBot(openwechat.Desktop)
|
||||||
|
return &Bot{
|
||||||
|
bot: bot,
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) Run() error {
|
||||||
|
logger.Info("Starting WeChat Bot...")
|
||||||
|
|
||||||
|
// set message handler
|
||||||
|
b.bot.MessageHandler = func(msg *openwechat.Message) {
|
||||||
|
b.messageHandler(msg)
|
||||||
|
}
|
||||||
|
// scan code login callback
|
||||||
|
b.bot.UUIDCallback = b.qrCodeCallBack
|
||||||
|
|
||||||
|
err := b.bot.Login()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("微信登录成功!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// message handler
|
||||||
|
func (b *Bot) messageHandler(msg *openwechat.Message) {
|
||||||
|
sender, err := msg.Sender()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理微信支付的推送消息
|
||||||
|
if sender.NickName == "微信支付" ||
|
||||||
|
msg.MsgType == openwechat.MsgTypeApp ||
|
||||||
|
msg.AppMsgType == openwechat.AppMsgTypeUrl {
|
||||||
|
// 解析支付金额
|
||||||
|
message, err := parseTransactionMessage(msg.Content)
|
||||||
|
if err == nil {
|
||||||
|
transaction := extractTransaction(message)
|
||||||
|
logger.Infof("解析到收款信息:%+v", transaction)
|
||||||
|
var item model.Reward
|
||||||
|
res := b.db.Where("tx_id = ?", transaction.TransId).First(&item)
|
||||||
|
if res.Error == nil {
|
||||||
|
logger.Error("当前交易 ID 己经存在!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res = b.db.Create(&model.Reward{
|
||||||
|
TxId: transaction.TransId,
|
||||||
|
Amount: transaction.Amount,
|
||||||
|
Remark: transaction.Remark,
|
||||||
|
Status: false,
|
||||||
|
})
|
||||||
|
if res.Error != nil {
|
||||||
|
logger.Errorf("交易保存失败: %v", res.Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Bot) qrCodeCallBack(uuid string) {
|
||||||
|
logger.Info("请使用微信扫描下面二维码登录")
|
||||||
|
q, _ := qrcode.New("https://login.weixin.qq.com/l/"+uuid, qrcode.Medium)
|
||||||
|
logger.Info(q.ToString(true))
|
||||||
|
}
|
||||||
68
api/service/wx/tranaction.go
Normal file
68
api/service/wx/tranaction.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package wx
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Message 转账消息
|
||||||
|
type Message struct {
|
||||||
|
XMLName xml.Name `xml:"msg"`
|
||||||
|
AppMsg struct {
|
||||||
|
Des string `xml:"des"`
|
||||||
|
Url string `xml:"url"`
|
||||||
|
} `xml:"appmsg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transaction 解析后的交易信息
|
||||||
|
type Transaction struct {
|
||||||
|
TransId string `json:"trans_id"` // 微信转账交易 ID
|
||||||
|
Amount float64 `json:"amount"` // 微信转账交易金额
|
||||||
|
Remark string `json:"remark"` // 转账备注
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析微信转账消息
|
||||||
|
func parseTransactionMessage(xmlData string) (*Message, error) {
|
||||||
|
var msg Message
|
||||||
|
if err := xml.Unmarshal([]byte(xmlData), &msg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出交易信息
|
||||||
|
func extractTransaction(message *Message) Transaction {
|
||||||
|
var tx = Transaction{}
|
||||||
|
// 导出交易金额和备注
|
||||||
|
lines := strings.Split(message.AppMsg.Des, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if len(line) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 解析收款金额
|
||||||
|
prefix := "收款金额¥"
|
||||||
|
if strings.HasPrefix(line, prefix) {
|
||||||
|
if value, err := strconv.ParseFloat(line[len(prefix):], 64); err == nil {
|
||||||
|
tx.Amount = value
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 解析收款备注
|
||||||
|
prefix = "付款方备注"
|
||||||
|
if strings.HasPrefix(line, prefix) {
|
||||||
|
tx.Remark = line[len(prefix):]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析交易 ID
|
||||||
|
index := strings.Index(message.AppMsg.Url, "trans_id=")
|
||||||
|
if index != -1 {
|
||||||
|
end := strings.LastIndex(message.AppMsg.Url, "&")
|
||||||
|
tx.TransId = strings.TrimSpace(message.AppMsg.Url[index+9 : end])
|
||||||
|
}
|
||||||
|
return tx
|
||||||
|
}
|
||||||
144
api/service/xxl_job_service.go
Normal file
144
api/service/xxl_job_service.go
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
logger2 "chatplus/logger"
|
||||||
|
"chatplus/store/model"
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"github.com/xxl-job/xxl-job-executor-go"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var logger = logger2.GetLogger()
|
||||||
|
|
||||||
|
type XXLJobExecutor struct {
|
||||||
|
executor xxl.Executor
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewXXLJobExecutor(config *types.AppConfig, db *gorm.DB) *XXLJobExecutor {
|
||||||
|
if !config.XXLConfig.Enabled {
|
||||||
|
logger.Info("XXL-JOB service is disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exec := xxl.NewExecutor(
|
||||||
|
xxl.ServerAddr(config.XXLConfig.ServerAddr),
|
||||||
|
xxl.AccessToken(config.XXLConfig.AccessToken), //请求令牌(默认为空)
|
||||||
|
xxl.ExecutorIp(config.XXLConfig.ExecutorIp), //可自动获取
|
||||||
|
xxl.ExecutorPort(config.XXLConfig.ExecutorPort), //默认9999(非必填)
|
||||||
|
xxl.RegistryKey(config.XXLConfig.RegistryKey), //执行器名称
|
||||||
|
xxl.SetLogger(&customLogger{}), //自定义日志
|
||||||
|
)
|
||||||
|
exec.Init()
|
||||||
|
return &XXLJobExecutor{executor: exec, db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *XXLJobExecutor) Run() error {
|
||||||
|
e.executor.RegTask("ClearOrder", e.ClearOrder)
|
||||||
|
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
|
||||||
|
return e.executor.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearOrder 清理未支付的订单,如果没有抛出异常则表示执行成功
|
||||||
|
func (e *XXLJobExecutor) ClearOrder(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||||
|
logger.Debug("执行清理未支付订单...")
|
||||||
|
var sysConfig model.Config
|
||||||
|
res := e.db.Where("marker", "system").First(&sysConfig)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "error with get system config: " + res.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var config types.SystemConfig
|
||||||
|
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||||
|
if err != nil {
|
||||||
|
return "error with decode system config: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.OrderPayTimeout == 0 { // 默认未支付订单的生命周期为 30 分钟
|
||||||
|
config.OrderPayTimeout = 1800
|
||||||
|
}
|
||||||
|
timeout := time.Now().Unix() - int64(config.OrderPayTimeout)
|
||||||
|
start := utils.Stamp2str(timeout)
|
||||||
|
res = e.db.Where("status != ? AND created_at < ?", types.OrderPaidSuccess, start).Delete(&model.Order{})
|
||||||
|
return fmt.Sprintf("Clear order successfully, affect rows: %d", res.RowsAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetVipCalls 清理过期的 VIP 会员
|
||||||
|
func (e *XXLJobExecutor) ResetVipCalls(cxt context.Context, param *xxl.RunReq) (msg string) {
|
||||||
|
logger.Info("开始进行月底账号盘点...")
|
||||||
|
var users []model.User
|
||||||
|
res := e.db.Where("vip = ?", 1).Find(&users)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "No vip users found"
|
||||||
|
}
|
||||||
|
|
||||||
|
var sysConfig model.Config
|
||||||
|
res = e.db.Where("marker", "system").First(&sysConfig)
|
||||||
|
if res.Error != nil {
|
||||||
|
return "error with get system config: " + res.Error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
var config types.SystemConfig
|
||||||
|
err := utils.JsonDecode(sysConfig.Config, &config)
|
||||||
|
if err != nil {
|
||||||
|
return "error with decode system config: " + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本月月初时间
|
||||||
|
currentTime := time.Now()
|
||||||
|
year, month, _ := currentTime.Date()
|
||||||
|
firstOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, currentTime.Location()).Unix()
|
||||||
|
for _, u := range users {
|
||||||
|
// 账号到期,直接清零
|
||||||
|
if u.ExpiredTime <= currentTime.Unix() {
|
||||||
|
logger.Info("账号过期:", u.Mobile)
|
||||||
|
u.Calls = 0
|
||||||
|
u.Vip = false
|
||||||
|
} else {
|
||||||
|
if u.Calls <= 0 {
|
||||||
|
u.Calls = config.VipMonthCalls
|
||||||
|
} else {
|
||||||
|
// 如果该用户当月有充值点卡,则将点卡中未用完的点数结余到下个月
|
||||||
|
var orders []model.Order
|
||||||
|
e.db.Debug().Where("user_id = ? AND pay_time > ?", u.Id, firstOfMonth).Find(&orders)
|
||||||
|
var calls = 0
|
||||||
|
for _, o := range orders {
|
||||||
|
var remark types.OrderRemark
|
||||||
|
err = utils.JsonDecode(o.Remark, &remark)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if remark.Days > 0 { // 会员续费
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
calls += remark.Calls
|
||||||
|
}
|
||||||
|
if u.Calls > calls { // 本月套餐没有用完
|
||||||
|
u.Calls = calls + config.VipMonthCalls
|
||||||
|
} else {
|
||||||
|
u.Calls = u.Calls + config.VipMonthCalls
|
||||||
|
}
|
||||||
|
logger.Infof("%s 点卡结余:%d", u.Mobile, calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
u.Tokens = 0
|
||||||
|
// update user
|
||||||
|
e.db.Updates(&u)
|
||||||
|
}
|
||||||
|
logger.Info("月底盘点完成!")
|
||||||
|
return "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
type customLogger struct{}
|
||||||
|
|
||||||
|
func (l *customLogger) Info(format string, a ...interface{}) {
|
||||||
|
logger.Debugf(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *customLogger) Error(format string, a ...interface{}) {
|
||||||
|
logger.Errorf(format, a...)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package model
|
|||||||
// ApiKey OpenAI API 模型
|
// ApiKey OpenAI API 模型
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
UserId uint //用户ID,系统添加的用户 ID 为 0
|
Platform string
|
||||||
Value string // API Key 的值
|
Value string // API Key 的值
|
||||||
LastUsedAt int64 // 最后使用时间
|
LastUsedAt int64 // 最后使用时间
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
type HistoryMessage struct {
|
type HistoryMessage struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
ChatId string // 会话 ID
|
ChatId string // 会话 ID
|
||||||
@@ -10,6 +12,7 @@ type HistoryMessage struct {
|
|||||||
Tokens int
|
Tokens int
|
||||||
Content string
|
Content string
|
||||||
UseContext bool // 是否可以作为聊天上下文
|
UseContext bool // 是否可以作为聊天上下文
|
||||||
|
DeletedAt gorm.DeletedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
func (HistoryMessage) TableName() string {
|
func (HistoryMessage) TableName() string {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "gorm.io/gorm"
|
||||||
|
|
||||||
type ChatItem struct {
|
type ChatItem struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
ChatId string `gorm:"column:chat_id;unique"` // 会话 ID
|
ChatId string `gorm:"column:chat_id;unique"` // 会话 ID
|
||||||
UserId uint // 用户 ID
|
UserId uint // 用户 ID
|
||||||
RoleId uint // 角色 ID
|
RoleId uint // 角色 ID
|
||||||
Model string // 会话模型
|
ModelId uint // 会话模型
|
||||||
Title string // 会话标题
|
Title string // 会话标题
|
||||||
|
DeletedAt gorm.DeletedAt
|
||||||
}
|
}
|
||||||
|
|||||||
11
api/store/model/chat_model.go
Normal file
11
api/store/model/chat_model.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type ChatModel struct {
|
||||||
|
BaseModel
|
||||||
|
Platform string
|
||||||
|
Name string
|
||||||
|
Value string // API Key 的值
|
||||||
|
SortNum int
|
||||||
|
Enabled bool
|
||||||
|
Weight int // 对话权重,每次对话扣减多少次对话额度
|
||||||
|
}
|
||||||
@@ -8,5 +8,5 @@ type ChatRole struct {
|
|||||||
HelloMsg string // 打招呼的消息
|
HelloMsg string // 打招呼的消息
|
||||||
Icon string // 角色聊天图标
|
Icon string // 角色聊天图标
|
||||||
Enable bool // 是否启用被启用
|
Enable bool // 是否启用被启用
|
||||||
Sort int //排序数字
|
SortNum int //排序数字
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import "time"
|
|||||||
|
|
||||||
type MidJourneyJob struct {
|
type MidJourneyJob struct {
|
||||||
Id uint `gorm:"primarykey;column:id"`
|
Id uint `gorm:"primarykey;column:id"`
|
||||||
UserId uint
|
Type string
|
||||||
ChatId string
|
UserId int
|
||||||
MessageId string
|
MessageId string
|
||||||
ReferenceId string
|
ReferenceId string
|
||||||
Hash string
|
ImgURL string
|
||||||
Content string
|
Hash string // message hash
|
||||||
|
Progress int
|
||||||
Prompt string
|
Prompt string
|
||||||
Image string
|
Started bool
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
api/store/model/order.go
Normal file
22
api/store/model/order.go
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Order 充值订单
|
||||||
|
type Order struct {
|
||||||
|
BaseModel
|
||||||
|
UserId uint
|
||||||
|
ProductId uint
|
||||||
|
Mobile string
|
||||||
|
OrderNo string
|
||||||
|
Subject string
|
||||||
|
Amount float64
|
||||||
|
Status types.OrderStatus
|
||||||
|
Remark string
|
||||||
|
PayTime int64
|
||||||
|
PayWay string // 支付方式
|
||||||
|
DeletedAt gorm.DeletedAt
|
||||||
|
}
|
||||||
14
api/store/model/product.go
Normal file
14
api/store/model/product.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Product 充值产品
|
||||||
|
type Product struct {
|
||||||
|
BaseModel
|
||||||
|
Name string
|
||||||
|
Price float64
|
||||||
|
Discount float64
|
||||||
|
Days int
|
||||||
|
Calls int
|
||||||
|
Enabled bool
|
||||||
|
Sales int
|
||||||
|
SortNum int
|
||||||
|
}
|
||||||
20
api/store/model/sd_job.go
Normal file
20
api/store/model/sd_job.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type SdJob struct {
|
||||||
|
Id uint `gorm:"primarykey;column:id"`
|
||||||
|
Type string
|
||||||
|
UserId int
|
||||||
|
TaskId string
|
||||||
|
ImgURL string
|
||||||
|
Progress int
|
||||||
|
Prompt string
|
||||||
|
Params string
|
||||||
|
Started bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (SdJob) TableName() string {
|
||||||
|
return "chatgpt_sd_jobs"
|
||||||
|
}
|
||||||
@@ -2,19 +2,20 @@ package model
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Username string `gorm:"index:username,unique"`
|
|
||||||
Mobile string
|
Mobile string
|
||||||
Password string
|
Password string
|
||||||
Nickname string
|
|
||||||
Avatar string
|
Avatar string
|
||||||
Salt string // 密码盐
|
Salt string // 密码盐
|
||||||
Tokens int64 // 剩余tokens
|
TotalTokens int64 // 总消耗 tokens
|
||||||
Calls int // 剩余对话次数
|
Calls int // 剩余对话次数
|
||||||
ImgCalls int // 剩余绘图次数
|
ImgCalls int // 剩余绘图次数
|
||||||
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
|
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
|
||||||
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
|
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
|
||||||
|
ChatModels string `gorm:"column:chat_models_json"` // AI 模型,不同的用户拥有不同的聊天模型
|
||||||
ExpiredTime int64 // 账户到期时间
|
ExpiredTime int64 // 账户到期时间
|
||||||
Status bool `gorm:"default:true"` // 当前状态
|
Status bool `gorm:"default:true"` // 当前状态
|
||||||
LastLoginAt int64 // 最后登录时间
|
LastLoginAt int64 // 最后登录时间
|
||||||
LastLoginIp string // 最后登录 IP
|
LastLoginIp string // 最后登录 IP
|
||||||
|
Vip bool // 是否 VIP 会员
|
||||||
|
Tokens int
|
||||||
}
|
}
|
||||||
|
|||||||
20
api/store/redis.go
Normal file
20
api/store/redis.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"context"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRedisClient(config *types.AppConfig) (*redis.Client, error) {
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: config.Redis.Url(),
|
||||||
|
Password: config.Redis.Password,
|
||||||
|
DB: config.Redis.DB,
|
||||||
|
})
|
||||||
|
_, err := client.Ping(context.Background()).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
41
api/store/redis_queue.go
Normal file
41
api/store/redis_queue.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package store
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/utils"
|
||||||
|
"context"
|
||||||
|
"github.com/go-redis/redis/v8"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RedisQueue struct {
|
||||||
|
name string
|
||||||
|
client *redis.Client
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRedisQueue(name string, client *redis.Client) *RedisQueue {
|
||||||
|
return &RedisQueue{name: name, client: client, ctx: context.Background()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *RedisQueue) RPush(value interface{}) {
|
||||||
|
q.client.RPush(q.ctx, q.name, utils.JsonEncode(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *RedisQueue) LPush(value interface{}) {
|
||||||
|
q.client.LPush(q.ctx, q.name, utils.JsonEncode(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *RedisQueue) LPop(value interface{}) error {
|
||||||
|
result, err := q.client.BLPop(q.ctx, 0, q.name).Result()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.JsonDecode(result[1], value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *RedisQueue) RPop(value interface{}) error {
|
||||||
|
result, err := q.client.BRPop(q.ctx, 0, q.name).Result()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.JsonDecode(result[1], value)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ package vo
|
|||||||
// ApiKey OpenAI API 模型
|
// ApiKey OpenAI API 模型
|
||||||
type ApiKey struct {
|
type ApiKey struct {
|
||||||
BaseVo
|
BaseVo
|
||||||
UserId uint `json:"user_id"` //用户ID,系统添加的用户 ID 为 0
|
Platform string `json:"platform"`
|
||||||
Value string `json:"value"` // API Key 的值
|
Value string `json:"value"` // API Key 的值
|
||||||
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
|
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,3 @@ type HistoryMessage struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
UseContext bool `json:"use_context"`
|
UseContext bool `json:"use_context"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (HistoryMessage) TableName() string {
|
|
||||||
return "chatgpt_chat_history"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package vo
|
|||||||
|
|
||||||
type ChatItem struct {
|
type ChatItem struct {
|
||||||
BaseVo
|
BaseVo
|
||||||
UserId uint `json:"user_id"`
|
UserId uint `json:"user_id"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
RoleId uint `json:"role_id"`
|
RoleId uint `json:"role_id"`
|
||||||
ChatId string `json:"chat_id"`
|
ChatId string `json:"chat_id"`
|
||||||
Model string `json:"model"`
|
ModelId uint `json:"model_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
}
|
}
|
||||||
|
|||||||
11
api/store/vo/chat_model.go
Normal file
11
api/store/vo/chat_model.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
type ChatModel struct {
|
||||||
|
BaseVo
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
SortNum int `json:"sort_num"`
|
||||||
|
Weight int `json:"weight"`
|
||||||
|
}
|
||||||
@@ -10,5 +10,5 @@ type ChatRole struct {
|
|||||||
HelloMsg string `json:"hello_msg"` // 打招呼的消息
|
HelloMsg string `json:"hello_msg"` // 打招呼的消息
|
||||||
Icon string `json:"icon"` // 角色聊天图标
|
Icon string `json:"icon"` // 角色聊天图标
|
||||||
Enable bool `json:"enable"` // 是否启用被启用
|
Enable bool `json:"enable"` // 是否启用被启用
|
||||||
Sort int `json:"sort"` // 排序
|
SortNum int `json:"sort"` // 排序
|
||||||
}
|
}
|
||||||
|
|||||||
17
api/store/vo/mj_job.go
Normal file
17
api/store/vo/mj_job.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type MidJourneyJob struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
MessageId string `json:"message_id"`
|
||||||
|
ReferenceId string `json:"reference_id"`
|
||||||
|
ImgURL string `json:"img_url"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Started bool `json:"started"`
|
||||||
|
}
|
||||||
19
api/store/vo/order.go
Normal file
19
api/store/vo/order.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Order struct {
|
||||||
|
BaseVo
|
||||||
|
UserId uint `json:"user_id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
Mobile string `json:"mobile"`
|
||||||
|
OrderNo string `json:"order_no"`
|
||||||
|
Subject string `json:"subject"`
|
||||||
|
Amount float64 `json:"amount"`
|
||||||
|
Status types.OrderStatus `json:"status"`
|
||||||
|
PayTime int64 `json:"pay_time"`
|
||||||
|
PayWay string `json:"pay_way"`
|
||||||
|
Remark types.OrderRemark `json:"remark"`
|
||||||
|
}
|
||||||
13
api/store/vo/product.go
Normal file
13
api/store/vo/product.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
type Product struct {
|
||||||
|
BaseVo
|
||||||
|
Name string `json:"name"`
|
||||||
|
Price float64 `json:"price"`
|
||||||
|
Discount float64 `json:"discount"`
|
||||||
|
Days int `json:"days"`
|
||||||
|
Calls int `json:"calls"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Sales int `json:"sales"`
|
||||||
|
SortNum int `json:"sort_num"`
|
||||||
|
}
|
||||||
19
api/store/vo/sd_job.go
Normal file
19
api/store/vo/sd_job.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package vo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"chatplus/core/types"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SdJob struct {
|
||||||
|
Id uint `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
UserId int `json:"user_id"`
|
||||||
|
TaskId string `json:"task_id"`
|
||||||
|
ImgURL string `json:"img_url"`
|
||||||
|
Params types.SdTaskParams `json:"params"`
|
||||||
|
Progress int `json:"progress"`
|
||||||
|
Prompt string `json:"prompt"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Started bool `json:"started"`
|
||||||
|
}
|
||||||
@@ -4,18 +4,19 @@ import "chatplus/core/types"
|
|||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
BaseVo
|
BaseVo
|
||||||
Username string `json:"username"`
|
Mobile string `json:"mobile"`
|
||||||
Mobile string `json:"mobile"`
|
Avatar string `json:"avatar"`
|
||||||
Nickname string `json:"nickname"`
|
Salt string `json:"salt"` // 密码盐
|
||||||
Avatar string `json:"avatar"`
|
TotalTokens int64 `json:"total_tokens"` // 总消耗tokens
|
||||||
Salt string `json:"salt"` // 密码盐
|
Calls int `json:"calls"` // 剩余对话次数
|
||||||
Tokens int64 `json:"tokens"` // 剩余tokens
|
ImgCalls int `json:"img_calls"`
|
||||||
Calls int `json:"calls"` // 剩余对话次数
|
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
|
||||||
ImgCalls int `json:"img_calls"`
|
ChatRoles []string `json:"chat_roles"` // 聊天角色集合
|
||||||
ChatConfig types.ChatConfig `json:"chat_config"` // 聊天配置
|
ChatModels []string `json:"chat_models"` // AI模型集合
|
||||||
ChatRoles []string `json:"chat_roles"` // 聊天角色集合
|
ExpiredTime int64 `json:"expired_time"` // 账户到期时间
|
||||||
ExpiredTime int64 `json:"expired_time"` // 账户到期时间
|
Status bool `json:"status"` // 当前状态
|
||||||
Status bool `json:"status"` // 当前状态
|
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
||||||
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
|
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
||||||
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
|
Vip bool `json:"vip"`
|
||||||
|
Tokens int `json:"token"` // 当月消耗的 fee
|
||||||
}
|
}
|
||||||
|
|||||||
198
api/test/test.go
198
api/test/test.go
@@ -1,203 +1,5 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"chatplus/core/types"
|
|
||||||
"chatplus/store/model"
|
|
||||||
"chatplus/store/vo"
|
|
||||||
"chatplus/utils"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
|
||||||
"github.com/pkoukk/tiktoken-go"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Println(utils.RandString(32))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Http client 取消操作
|
|
||||||
func testHttpClient(ctx context.Context) {
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", "http://localhost:2345", nil)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req = req.WithContext(ctx)
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func(Body io.ReadCloser) {
|
|
||||||
err := Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
}
|
|
||||||
}(resp.Body)
|
|
||||||
_, err = io.ReadAll(resp.Body)
|
|
||||||
for {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
fmt.Println(time.Now())
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
fmt.Println("取消退出")
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDate() {
|
|
||||||
fmt.Println(time.Unix(1683336167, 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testIp2Region() {
|
|
||||||
dbPath := "res/ip2region.xdb"
|
|
||||||
// 1、从 dbPath 加载整个 xdb 到内存
|
|
||||||
cBuff, err := xdb.LoadContentFromFile(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to load content from `%s`: %s\n", dbPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2、用全局的 cBuff 创建完全基于内存的查询对象。
|
|
||||||
searcher, err := xdb.NewWithBuffer(cBuff)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("failed to create searcher with content: %s\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
str, err := searcher.SearchByStr("103.88.46.85")
|
|
||||||
fmt.Println(str)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
arr := strings.Split(str, "|")
|
|
||||||
fmt.Println(arr[2], arr[3], arr[4])
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func testJson() {
|
|
||||||
|
|
||||||
var role = model.ChatRole{
|
|
||||||
Key: "programmer",
|
|
||||||
Name: "程序员",
|
|
||||||
Context: "[{\"role\":\"user\",\"content\":\"现在开始你扮演一位程序员,你是一名优秀的程序员,具有很强的逻辑思维能力,总能高效的解决问题。你热爱编程,熟悉多种编程语言,尤其精通 Go 语言,注重代码质量,有创新意识,持续学习,良好的沟通协作。\"},{\"role\"\n:\"assistant\",\"content\":\"好的,现在我将扮演一位程序员,非常感谢您对我的评价。作为一名优秀的程序员,我非常热爱编程,并且注重代码质量。我熟悉多种编程语言,尤其是 Go 语言,可以使用它来高效地解决各种问题。\"}]",
|
|
||||||
HelloMsg: "Talk is cheap, i will show code!",
|
|
||||||
Icon: "images/avatar/programmer.jpg",
|
|
||||||
Enable: true,
|
|
||||||
Sort: 1,
|
|
||||||
}
|
|
||||||
role.Id = 1
|
|
||||||
var v vo.ChatRole
|
|
||||||
|
|
||||||
err := utils.CopyObject(role, &v)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("%+v\n", v.Id)
|
|
||||||
|
|
||||||
//var v2 = model.ChatRoles{}
|
|
||||||
//err = utils.CopyObject(v, &v2)
|
|
||||||
//if err != nil {
|
|
||||||
// log.Fatal(err)
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//fmt.Printf("%+v\n", v2.Id)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func calTokens() {
|
|
||||||
text := "须知少年凌云志,曾许人间第一流"
|
|
||||||
encoding := "cl100k_base"
|
|
||||||
|
|
||||||
tke, err := tiktoken.GetEncoding(encoding)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("getEncoding: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// encode
|
|
||||||
token := tke.Encode(text, nil, nil)
|
|
||||||
|
|
||||||
//tokens
|
|
||||||
fmt.Println(token)
|
|
||||||
// num_tokens
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFunction() error {
|
|
||||||
open, err := os.Open("res/data.txt")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
reader := bufio.NewReader(open)
|
|
||||||
var contents = make([]string, 0)
|
|
||||||
var functionCall = false
|
|
||||||
var functionName string
|
|
||||||
for {
|
|
||||||
line, err := reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !strings.Contains(line, "data:") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseBody = types.ApiResponse{}
|
|
||||||
err = json.Unmarshal([]byte(line[6:]), &responseBody)
|
|
||||||
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
function := responseBody.Choices[0].Delta.FunctionCall
|
|
||||||
if functionCall && function.Name == "" {
|
|
||||||
contents = append(contents, function.Arguments)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !utils.IsEmptyValue(function) {
|
|
||||||
functionCall = true
|
|
||||||
functionName = function.Name
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("函数名称: ", functionName)
|
|
||||||
fmt.Println(strings.Join(contents, ""))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"image/jpeg"
|
||||||
|
"io"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||||
@@ -35,7 +44,7 @@ func CopyObject(src interface{}, dst interface{}) error {
|
|||||||
pType := reflect.New(value.Type())
|
pType := reflect.New(value.Type())
|
||||||
v2 := pType.Interface()
|
v2 := pType.Interface()
|
||||||
err := json.Unmarshal([]byte(v.String()), &v2)
|
err := json.Unmarshal([]byte(v.String()), &v2)
|
||||||
if err == nil {
|
if err == nil && v2 != nil {
|
||||||
value.Set(reflect.ValueOf(v2).Elem())
|
value.Set(reflect.ValueOf(v2).Elem())
|
||||||
}
|
}
|
||||||
// map, struct, slice to string
|
// map, struct, slice to string
|
||||||
@@ -92,7 +101,7 @@ func IsEmptyValue(obj interface{}) bool {
|
|||||||
if obj == nil {
|
if obj == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
v := reflect.ValueOf(obj)
|
v := reflect.ValueOf(obj)
|
||||||
switch v.Kind() {
|
switch v.Kind() {
|
||||||
case reflect.Ptr, reflect.Interface:
|
case reflect.Ptr, reflect.Interface:
|
||||||
@@ -113,3 +122,83 @@ func IsEmptyValue(obj interface{}) bool {
|
|||||||
return reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
|
return reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BoolValue(str string) bool {
|
||||||
|
value, err := strconv.ParseBool(str)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func FloatValue(str string) float64 {
|
||||||
|
value, err := strconv.ParseFloat(str, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func IntValue(str string, defaultValue int) int {
|
||||||
|
value, err := strconv.Atoi(str)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func ForceCovert(src any, dst interface{}) error {
|
||||||
|
b, err := json.Marshal(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(b, dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenQrcode(text string, size int, logo io.Reader) ([]byte, error) {
|
||||||
|
qr, err := qrcode.New(text, qrcode.Medium)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
qr.BackgroundColor = color.White
|
||||||
|
qr.ForegroundColor = color.Black
|
||||||
|
if logo == nil {
|
||||||
|
return qr.PNG(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成带Logo的二维码图像
|
||||||
|
logoImage, _, err := image.Decode(logo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缩放 Logo
|
||||||
|
scaledLogo := resize.Resize(uint(size/9), uint(size/9), logoImage, resize.Lanczos3)
|
||||||
|
// 将Logo叠加到二维码图像上
|
||||||
|
qrWithLogo := overlayLogo(qr.Image(size), scaledLogo)
|
||||||
|
|
||||||
|
// 将带Logo的二维码图像以JPEG格式编码为图片数据
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = jpeg.Encode(&buf, qrWithLogo, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 叠加Logo到图片上
|
||||||
|
func overlayLogo(qrImage, logoImage image.Image) image.Image {
|
||||||
|
offsetX := (qrImage.Bounds().Dx() - logoImage.Bounds().Dx()) / 2
|
||||||
|
offsetY := (qrImage.Bounds().Dy() - logoImage.Bounds().Dy()) / 2
|
||||||
|
|
||||||
|
combinedImage := image.NewRGBA(qrImage.Bounds())
|
||||||
|
draw.Draw(combinedImage, qrImage.Bounds(), qrImage, image.Point{}, draw.Over)
|
||||||
|
draw.Draw(combinedImage, logoImage.Bounds().Add(image.Pt(offsetX, offsetY)), logoImage, image.Point{}, draw.Over)
|
||||||
|
|
||||||
|
return combinedImage
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user