Compare commits

..

136 Commits

Author SHA1 Message Date
RockYang
63c7041e1f docs: update change logs 2023-11-28 15:33:30 +08:00
RockYang
b1263ddc69 docs: 添加一键部署脚本 2023-11-28 15:25:51 +08:00
RockYang
7e50e17aaf opt: 缩略图生成算法 2023-11-28 14:50:19 +08:00
RockYang
a7265c4251 feat: 为大图片生成缩略图,加快前端图片加载速度 2023-11-28 12:04:02 +08:00
RockYang
6f39f639bd fix: fix bug for oss image domain 2023-11-28 07:27:18 +08:00
RockYang
a7db123437 docs: update build script 2023-11-27 18:24:52 +08:00
RockYang
241c714a8b Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-27 12:03:23 +08:00
RockYang
67ac3cfe32 feat: merge mysql and redis docker service to docker-compose.yaml file 2023-11-27 10:56:18 +08:00
RockYang
c926e0afcc Merge pull request #55 from openjst/main
修改code显示颜色样式
2023-11-27 10:27:13 +08:00
RockYang
5bc07e6d57 docs: make a full docker-compose.yaml 2023-11-27 07:21:37 +08:00
openjst
c3666a9a71 修改code显示颜色样式 2023-11-26 23:26:08 +08:00
RockYang
23b5ffa97d feat: implements image function replace Mj with DALL-E-3 2023-11-26 20:37:48 +08:00
RockYang
a2c7a75705 feat: add type field for api key 2023-11-24 18:05:59 +08:00
RockYang
d68f2ef12c feat: add support for registing use force use invite code 2023-11-24 12:02:28 +08:00
RockYang
67d30353f0 opt: optimize image preview for MidJourney image list page, only preview current image not for all images 2023-11-23 17:55:12 +08:00
RockYang
4813163eac opt: 增加中间件自动对HTTP请求的参数去掉首尾空格 2023-11-23 17:50:55 +08:00
RockYang
5c5210625e opt: optimize styles for invitation page 2023-11-23 17:40:15 +08:00
RockYang
a4a1eec30b feat: add invitation and promotion functions 2023-11-23 16:30:15 +08:00
RockYang
d35164506a docs: add database sql file for v3.1.9 2023-11-23 09:58:01 +08:00
RockYang
1ed08f01ea docs: update change log 2023-11-23 09:46:49 +08:00
RockYang
eca07ab830 docs: update change log and readme file 2023-11-23 09:14:42 +08:00
RockYang
3512715704 feat: 支持讯飞大模型 v3.0 2023-11-23 07:11:13 +08:00
RockYang
6d07881141 feat: reset password function is ready 2023-11-22 18:00:45 +08:00
RockYang
251fe626f2 feat: add copy code btn in chat page, fixed bug for code wrap in model of ChatGLM and XunFei 2023-11-22 17:00:43 +08:00
RockYang
5fee3a9288 feat: add feekback orcode in chat page 2023-11-22 12:05:58 +08:00
RockYang
9b68d8101e fix: fixed bug for enable user vip in admin console not work 2023-11-22 11:30:58 +08:00
RockYang
cfe6f27d48 option: replace leveldb with redis in storing message code 2023-11-22 10:57:24 +08:00
RockYang
b314dd0900 Merge pull request #52 from KunMingStar/fix_migrate
fix: remove dead code
2023-11-22 08:23:01 +08:00
tongkunming
950fab6374 fix: remove dead code 2023-11-21 22:18:53 +08:00
RockYang
9d1f5c42ce chore: remove dead code 2023-11-21 19:04:53 +08:00
RockYang
a84046390b feat: add wechat id card for closing register 2023-11-20 16:38:04 +08:00
RockYang
aa29323a8a docs: update docs installation docs url 2023-11-17 18:42:13 +08:00
RockYang
d5617b7c3a docs: update readme and config sample files 2023-11-16 17:20:38 +08:00
RockYang
1ef60a9e5e fix: fix bug with missing chat context 2023-11-15 18:20:34 +08:00
RockYang
fb6e395ad8 fix: unbind event for login page when the component is unmount 2023-11-14 18:19:40 +08:00
RockYang
d9216060bc feat: add link on logo 2023-11-14 13:50:44 +08:00
RockYang
bcaa9a92e5 fix: fix member page flex styles 2023-11-14 11:21:16 +08:00
RockYang
576adc9036 fix: deducating the user's img call quota after stable diffusion callback 2023-11-14 08:59:39 +08:00
RockYang
00de18be9a feat: 更改 MJ 和 SD 菜单图标 2023-11-13 11:27:42 +08:00
RockYang
c61d32816a Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-13 10:11:45 +08:00
RockYang
f3fbb0b89c feat: 更改 MJ 和 SD 菜单图标 2023-11-13 10:11:21 +08:00
RockYang
e311a39632 chore: change xxl-job name 2023-11-12 15:39:12 +08:00
RockYang
51407abe44 opt: 优化 ItemList 组件样式,调整支付页面布局 2023-11-11 22:11:04 +08:00
RockYang
8a470b1038 docs: update change log file 2023-11-11 12:58:57 +08:00
RockYang
baddabaa16 fix: fix bug for issue 49, stable diffusion service not decrease img_calls 2023-11-11 11:00:26 +08:00
RockYang
427b434ce3 opt: 优化 ItemList 组件样式 2023-11-11 10:53:33 +08:00
RockYang
5f921965e6 feat: add order list compponent 2023-11-10 18:06:32 +08:00
RockYang
1e705c8ed5 Merge branch 'alipay' 2023-11-10 16:51:00 +08:00
RockYang
b8ae65bb30 docs: export database, update readme doc, remove useless configs for alipay 2023-11-10 16:49:07 +08:00
RockYang
321e2087ea feat: add switch to disable xxl-job service, update readme 2023-11-10 15:22:35 +08:00
RockYang
aac60edce2 feat: 增加订单倒计时组件,自动清理过期未支付订单 2023-11-10 14:39:27 +08:00
RockYang
9dc9a6923e feat: add system configration switch option for order pay service, support sandbox env for alipay 2023-11-09 18:28:56 +08:00
RockYang
7ca4dfe09b feat: check if the user's chat quota is gt than current chat model required before starting a conversation 2023-11-09 16:56:44 +08:00
RockYang
c584b82ddb Merge pull request #47 from KunMingStar/fix/sendMessage
fix: 添加messages为空校验
2023-11-09 08:21:24 +08:00
RockYang
5f17ab2501 fix: fix bug for token expired with QiNiu oss upload file 2023-11-08 21:14:09 +08:00
RockYang
c84e912dd8 feat: adjust layout for user profile page 2023-11-08 18:33:26 +08:00
RockYang
2ebff2623f feat: order payment function is ready 2023-11-08 17:48:07 +08:00
tongkunming
72418ce4d7 fix: 添加messages为空校验 2023-11-08 16:51:36 +08:00
RockYang
e221b1eed4 feat: update stable diffusion api version 2023-11-08 14:54:10 +08:00
RockYang
696306f066 feat: finish payment page layout 2023-11-07 18:10:28 +08:00
RockYang
1807d5b5d4 feat: adjust table styles for markdown 2023-11-07 12:02:16 +08:00
RockYang
85c12aa322 feat: integrated Alipay payment module 2023-11-06 17:55:46 +08:00
RockYang
da9d0dc3bc Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-06 15:04:11 +08:00
RockYang
daaca822ac opt:update mj api version, wrapper chat api error message as a constant 2023-11-06 15:03:56 +08:00
RockYang
59ced3f947 chore: remove invalid filepath for windows 2023-11-02 19:16:02 +08:00
RockYang
22ae7dd1f3 feat: different AI model consuming different amounts of use_calls 2023-10-26 14:38:06 +08:00
RockYang
1816e9d5cf docs: add database full backup 2023-10-26 14:00:06 +08:00
RockYang
9be6755f65 feat: 支持文心4.0模型,不同的用户可以订阅不同的AI模型 2023-10-26 13:41:49 +08:00
RockYang
e5fb986463 feat: add system configration item to close rewarding function 2023-10-19 11:19:29 +08:00
RockYang
55d24e577e docs: add alipay reward qrcode 2023-10-17 17:58:03 +08:00
RockYang
7f50fa3fcf chore: set app list page as the index page 2023-10-16 14:20:44 +08:00
RockYang
fff8b78aba update changelog file 2023-10-16 13:40:44 +08:00
RockYang
7ca1989d98 opt: adjust styles for ItemList component, cut string for chat role's hello message 2023-10-16 10:46:10 +08:00
RockYang
4595dcb7ed feat: finish adding chat role to user function 2023-10-16 06:56:42 +08:00
RockYang
a688d3feb5 opt: adjust ItemList component styles 2023-10-15 15:47:42 +08:00
RockYang
7d1d88a32f opt: optimize imgae loading for image wall page, loading thumb image replace source image 2023-10-13 23:07:41 +08:00
RockYang
d95c048edd feat: create chat app list page, build the layout of page 2023-10-13 18:05:40 +08:00
RockYang
df2fc9d77c feat: auto login when register successfully 2023-10-13 16:27:40 +08:00
RockYang
d7e815d2bb docs: update images for readme 2023-10-13 15:55:30 +08:00
RockYang
f58b0a65f0 Merge branch 'main' into image-wall 2023-10-13 15:39:34 +08:00
RockYang
b59ad521ca feat: image wall stable diffusion image list component is ready 2023-10-13 15:16:40 +08:00
RockYang
b47ff975b0 feat: optimize the midjourney image list styles 2023-10-13 11:14:39 +08:00
RockYang
d043a87b30 docs: update readme, database files 2023-10-13 09:59:05 +08:00
RockYang
4cae7525d9 docs: update readme file 2023-10-13 06:49:21 +08:00
RockYang
76966d2ce7 docs: update change log 2023-10-13 06:39:44 +08:00
RockYang
5a740aecb0 style: fix style for update chat title input element 2023-10-13 06:29:25 +08:00
RockYang
1ae79331e7 feat: image wall page is ready 2023-10-12 18:09:50 +08:00
RockYang
8b14e141d0 opt: close websocket connection when finish a chat call for XunFei API 2023-10-12 10:02:12 +08:00
RockYang
9cbc6c91c4 feat: XunFei ai mode api implements is ready 2023-10-11 18:17:03 +08:00
RockYang
21c3a419a5 feat: adjust package struct, put chat code the seperate 'chatimpl' package, fix bug: baidu api chat context number must be even number 2023-10-11 15:46:40 +08:00
RockYang
287fac3a89 feat: add system configration for enable/disable funciton in chat session 2023-10-11 14:35:47 +08:00
RockYang
ba206bb387 finish baidu ai model api implementation 2023-10-11 14:21:16 +08:00
RockYang
4fc01f3f7b add baidu ai model api configrations 2023-10-10 18:19:56 +08:00
RockYang
f5ed71bcc6 docs: update readme 2023-10-09 12:13:21 +08:00
RockYang
8fc26183e9 docs: add 3.1.4 release version change logs 2023-10-09 12:05:51 +08:00
RockYang
e8ae8fddb7 doc: update database sql file 2023-10-08 20:44:55 +08:00
RockYang
b876867297 docs: udpate readme and config.toml template 2023-10-08 18:09:46 +08:00
RockYang
91dfd59731 feat: add system config item for reward image, add app config item to use custom text2img param json file 2023-10-08 17:48:50 +08:00
RockYang
5fdff90a10 feat: add configuration handler for AliYun SMS signature and template ID 2023-10-08 12:01:09 +08:00
RockYang
96c62619e6 optimze the style for stable diffusion image dialog 2023-10-08 11:39:55 +08:00
RockYang
083155413d feat: stable diffusion page function is ready 2023-10-06 22:25:37 +08:00
RockYang
d83019cbe4 fix: fixed bug for generating the upload file path 2023-10-05 18:09:42 +08:00
RockYang
cc7271aa73 feat: stable diffusion page is ready 2023-09-28 18:09:45 +08:00
RockYang
f873d6b375 feat: migrate the chatgpt-plus-ext project code to this project 2023-09-27 18:14:07 +08:00
RockYang
c86169022a feat: add implements for stable diffusion service 2023-09-26 18:16:51 +08:00
RockYang
db0a79da93 fix: fixed bug for register error with parse args 2023-09-26 09:30:18 +08:00
RockYang
48393e0e83 fix: fixed bug for add user in admin console page that calls and img_calls parameter not work 2023-09-22 18:15:32 +08:00
RockYang
7b4730271d feat: the upload handler for AliYun OSS is ready 2023-09-22 09:56:45 +08:00
RockYang
9cbe36d4c6 docs: adjust ui styles for mj image page 2023-09-20 14:33:15 +08:00
RockYang
b25bb2cc53 docs: update docker-compose.yaml, change image version to v3.1.3 2023-09-20 11:42:23 +08:00
RockYang
79ded6018b some page ui optimization, add release v3.1.3 2023-09-20 11:39:11 +08:00
RockYang
59f316b341 fix socket connect for mj task notify 2023-09-20 06:59:30 +08:00
RockYang
f307b8ba7a opt: add sessionId for mj task 2023-09-19 18:15:08 +08:00
RockYang
5034a20345 feat: mj advance drawing page function is ready, use better task scheduling argorithm 2023-09-17 18:03:45 +08:00
RockYang
26944f9e39 feat: add task type params for add new mj task 2023-09-16 14:01:53 +08:00
RockYang
e64946c3b6 feat: adjust UI for task list page 2023-09-15 18:16:59 +08:00
RockYang
e0a62d9b35 feat: midjourney page task and image list component is ready 2023-09-15 17:40:39 +08:00
RockYang
39dbffd8d0 feat: midjourney page parameter ui is ready 2023-09-14 18:28:24 +08:00
RockYang
952d6183ed feat: finish mj model layout 2023-09-14 06:55:03 +08:00
RockYang
3365a6008d feat: add ImageMj page view 2023-09-13 18:04:31 +08:00
RockYang
2e13ddf405 feat: optimize mj notidy api, use job queue to send ai drawing request 2023-09-13 15:50:00 +08:00
RockYang
1d3acc8ed3 feat: chat chrawing function is refactored 2023-09-13 06:57:25 +08:00
RockYang
fa341bab30 feat: refactor MidJourney service for conpatible drawing in chat and draw in app 2023-09-12 18:01:24 +08:00
RockYang
036a6e3e41 fix: fixed bug for jwt token expire caculation 2023-09-12 10:49:55 +08:00
RockYang
f4c6ca4554 feat: new WebUI for the main page, add MJ and SD drawing function pages 2023-09-11 16:22:11 +08:00
RockYang
327929243c opt: optimize the styles of chat page; caculate all tokens of context as chat history's token 2023-09-11 13:34:20 +08:00
RockYang
f4349c7a8c chore: update gpt avatar 2023-09-10 12:02:37 +08:00
RockYang
4b46d847f0 opt: refactor the web page's router and layout 2023-09-08 22:14:58 +08:00
RockYang
c3f016eae8 opt: return error when download mj image failed 2023-09-08 21:02:21 +08:00
RockYang
ebd3ef842f opt: add lock for mj task callback 2023-09-08 18:12:18 +08:00
RockYang
18c033d57f opt: add lock for mj task callback 2023-09-08 17:23:32 +08:00
RockYang
b676f80110 opt: Only create actived upload service 2023-09-08 06:46:24 +08:00
RockYang
f7fbaa534d update desktop package deps 2023-09-07 18:05:13 +08:00
RockYang
ea93a22e14 fix: remove username and nickname field for updating user info 2023-09-07 15:41:01 +08:00
RockYang
9f7e6778c5 Update README.md 2023-09-07 15:23:38 +08:00
RockYang
6c31a2bfa6 adjust home page styles 2023-09-07 15:12:43 +08:00
225 changed files with 21130 additions and 2916 deletions

View File

@@ -1,6 +1,90 @@
# 更新日志 # 更新日志
## v3.2.0
* 功能新增:新增邀请注册功能
* 功能优化增加中间件自动对HTTP请求的参数去掉首尾空格
* 功能优化:增加中间件自动为大图片生成缩略图
* 功能优化MidJourney 页面图片加载优化,实现图片预览懒加载
* 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
* Bug修复修复阿里云 OSS 域名设置不起做用的bug
* Bug修复修复MidJourney绘图失败后重复添加到队列的问题
## v3.1.9
* 功能新增:增加讯飞星火大模型 v3.0 支持
* 功能新增:新增找回密码功能
* 功能新增:支持 Markdown 代码复制功能
* Bug修复: xxl-job 任务调度失败的 Bug
* 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
* Bug修复Stable-Diffusion 绘画成功之后没有扣减用户画图次数
* 功能优化:优化会员充值页面 ItemList 组件
* 功能优化:给首页 Logo 增加链接
* Bug修复[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
* Bug修复聊天上下文失效问题
* 功能优化:关闭注册时显示联系管理员二维码
* 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
* Bug修复后台启用用户 VIP 不生效问题
* 功能优化:充值支付页面的支付说明文字可以后台配置
* Bug修复ChatGLM百度文心科大讯飞模型输出代码不换行问题
## 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 ## v3.1.2
1. 功能新增:新增七牛云 OSS 实现目前已支持三种文件上传服务Local, Minio, QiNiu OSS。 1. 功能新增:新增七牛云 OSS 实现目前已支持三种文件上传服务Local, Minio, QiNiu OSS。
2. 功能新增:新增桌面版,使用 electron 套壳网页版。 2. 功能新增:新增桌面版,使用 electron 套壳网页版。
3. Bug修复自动去除众筹核销时候转账单号中的空格防止复制的时候多复制了空格。 3. Bug修复自动去除众筹核销时候转账单号中的空格防止复制的时候多复制了空格。
@@ -9,17 +93,20 @@
6. 功能优化:所有路由跳转都使用绝对路径 6. 功能优化:所有路由跳转都使用绝对路径
## v3.1.1 ## v3.1.1
紧急修复版本采用弹窗的方式显示验证码解决验证码在低分辨率下被掩盖的Bug 紧急修复版本采用弹窗的方式显示验证码解决验证码在低分辨率下被掩盖的Bug
## v3.1.0(大版本更新) ## v3.1.0(大版本更新)
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAIAzure 以及 ChatGLM用户可以在这两个平台的模型中随意切换体验不同的模型聊天。
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAIAzure 以及
ChatGLM用户可以在这两个平台的模型中随意切换体验不同的模型聊天。
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。 2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
3. 功能重构重构文件夹上传服务支持多种文件上传存储handler目前已经实现本地存储和 minio oss 存储。 3. 功能重构重构文件夹上传服务支持多种文件上传存储handler目前已经实现本地存储和 minio oss 存储。
4. 功能优化:更新头像自动删除旧的图片资源。 4. 功能优化:更新头像自动删除旧的图片资源。
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。 5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
6. 功能新增:允许用户配置自己的 OPenAIAzure 以及 ChatGLM API KEY。 6. 功能新增:允许用户配置自己的 OPenAIAzure 以及 ChatGLM API KEY。
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug 7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
8. 升级 gin, element-plusredis 组件到最新版本。 8. 升级 gin, element-plusredis 组件到最新版本。
9. Bug修复修复若干已知的的 Bug 9. Bug修复修复若干已知的的 Bug
## v3.0.7 ## v3.0.7

319
README.md
View File

@@ -1,51 +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 实现,完美的打字机体验
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。 * 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
* 支持 MidJourney AI 绘画集成,开箱即用 * 支持 OPenAIAzure文心一言讯飞星火清华 ChatGLM等多个大语言模型
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持) * 支持 MidJourney / Stable Diffusion AI 绘画集成,开箱即用。
* 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件 * 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道
* 已集成支付宝支付功能,支持多种会员套餐和点卡购买功能。
* 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
## 功能截图 ## 功能截图
### PC 端聊天界面 ### PC 端聊天界面
![ChatGPT Chat Page](docs/imgs/gpt.gif) ![ChatGPT Chat Page](/docs/imgs/gpt.gif)
### 新版聊天界面 ### AI 对话界面
![ChatGPT new Chat Page](docs/imgs/chat-new.png) ![ChatGPT new Chat Page](/docs/imgs/chat-new.png)
### MidJourney 专业绘画界面
![mid-journey](/docs/imgs/mj_image.jpg)
### Stable-Diffusion 专业绘画页面
![Stable-Diffusion](/docs/imgs/sd_image.jpg)
![Stable-Diffusion](/docs/imgs/sd_image_detail.jpg)
### 绘图作品展
![ChatGPT image_list](/docs/imgs/image-list.png)
### AI应用列表
![ChatGPT-app-list](/docs/imgs/app-list.jpg)
### 会员充值
![会员充值](/docs/imgs/member.png)
### 自动调用函数插件 ### 自动调用函数插件
![ChatGPT function plugin](docs/imgs/plugin.png) ![ChatGPT function plugin](/docs/imgs/plugin.png)
![ChatGPT function plugin](/docs/imgs/mj.jpg)
![ChatGPT function plugin](docs/imgs/mj.jpg)
### 用户设置
![ChatGPT user profle](docs/imgs/user_profile.png)
### 登录页面
![ChatGPT Login](docs/imgs/login.png)
### 管理后台 ### 管理后台
![ChatGPT admin](docs/imgs/admin_dashboard.png) ![ChatGPT admin](/docs/imgs/admin_dashboard.png)
![ChatGPT admin](/docs/imgs/admin_config.jpg)
![ChatGPT admin](docs/imgs/admin_config.png) ![ChatGPT admin](/docs/imgs/admin_models.jpg)
![ChatGPT admin](/docs/imgs/admin_user.png)
![ChatGPT admin](docs/imgs/admin_user.png)
### 移动端 Web 页面 ### 移动端 Web 页面
![Mobile chat list](/docs/imgs/mobile_chat_list.png) ![Mobile chat list](/docs/imgs/mobile_chat_list.png)
![Mobile chat session](/docs/imgs/mobile_chat_session.png) ![Mobile chat session](/docs/imgs/mobile_chat_session.png)
![Mobile chat setting](/docs/imgs/mobile_chat_setting.png) ![Mobile chat setting](/docs/imgs/mobile_user_profile.png)
![Mobile chat setting](/docs/imgs/mobile_pay.png)
### 7. 体验地址 ### 7. 体验地址
@@ -57,252 +73,30 @@
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。 1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。 2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
## 项目介绍
这一套完整的系统,包括前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供
ChatGPT 的服务。
### 项目的技术架构
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端后期会涉及到很多业务数据查询统计leveldb 已经完全不够用了。
> Gin + fx + MySQL
3.0 版本之后会陆续添加其他语言的 API 实现,比如 PHPJava 等。考虑到作者精力有限api 目录已经添加了,有兴趣的同学自主去认领各自擅长的语言去实现。
前端的框架还是:
> Vue3 + Element-Plus
前后台的页面风格已经全部变了,几乎所有页面样式代码都重写了。逻辑代码还是沿用之前的,毕竟功能没有太大的变化。
此次重构改版主要是为了后面功能的扩展准备了。
新版本已经实现的功能如下:
1. 引入用户体系,新增用户注册和登录功能。
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
3. 创建会话的时候可以选择聊天角色和模型。
4. 新增聊天设置功能,用户可以导入自己的 API KEY
5. 保存聊天记录,支持聊天上下文。
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
7. 引入 ip2region 组件记录用户的登录IP和地址。
8. 支持会话搜索过滤。
9. 支持微信支付充值
## 项目地址 ## 项目地址
* 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
* [x] 整合 Midjourney AI 绘画 API
* [x] 开发移动端聊天页面
* [x] 接入微信支付功能
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
* [ ] 支持基于知识库的 AI 问答 * [ ] 支持基于知识库的 AI 问答
* [ ] 开发桌面版应用 * [ ] 会员邀请注册推广功能
* [ ] 开发手机 App 客户端 * [ ] 微信支付功能
## Docker 快速部署 ## 项目文档
>
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
### 1. 导入数据库
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
chatgpt-plus v3.2.0 一键部署脚本来了,真的只需运行一条命令,就可以完成部署:
```shell ```shell
cd docker/mysql bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.0-24e4849229.sh)"
# 创建 mysql 容器
docker-compose up -d
# 导入数据库
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus.sql
``` ```
目前只支持 Ubuntu 系统,推荐 Ubuntu 22.04 LTS
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可 详细部署文档请参考 [ChatGPT-Plus 文档](https://ai.r9it.com/docs/)
```shell
# 连接数据库
mysql -u username -p password
# 导入数据库
source database/chatgpt_plus.sql
```
### 2. 修改配置文档
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
```toml
Listen = "0.0.0.0:5678"
ProxyURL = ["YOUR_PROXY_URL"] # 替换成你本地代理http://127.0.0.1:7777
#ProxyURL = "" 如果你的服务器本身就在墙外,那么你直接留空就好了
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
MaxAge = 86400
[Manager]
Username = "admin"
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
ApiURL = "{URL}"
AppId = "{APP_ID}"
Token = "{TOKEN}"
[SmsConfig] # 阿里云短信服务配置
AccessKey = "{YOUR_ACCESS_KEY}"
AccessSecret = "{YOUR_SECRET_KEY}"
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
ApiURL = "插件扩展 API 地址"
Token = "插件扩展 API 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 = "IP:端口" # 如 172.22.11.200:9000
AccessKey = "minio oss access key" # 自己去 Minio 控制台去创建一个 Access Key
AccessSecret = "minio oss access secret"
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
UseSSL = false
Domain = "minio 文件公开访问地址" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
[OSS.QiNiu] # 七牛云 OSS 配置
Zone = "z2" # 区域z0华东z1: 华北na0北美as0新加坡
AccessKey = "七牛云 OSS AccessKey"
AccessSecret = "七牛云 OSS AccessSecret"
Bucket = "七牛云 OSS Bucket"
Domain = "OSS Bucket 所绑定的域名,如 https://img.r9it.com"
```
> 如果要启用微信收款服务和 MidJourney
> 绘画功能,请先部署扩展服务项目 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts)。
修改 nginx 配置文档 `docker/conf/nginx/conf.d/chatgpt-plus.conf`,把后端转发的地址改成当前主机的内网 IP 地址。
```shell
# 这里配置后端 API 的转发
location /api/ {
proxy_http_version 1.1;
proxy_connect_timeout 300s;
proxy_read_timeout 300s;
proxy_send_timeout 12s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://172.28.173.76:6789; # 这里改成后端服务的内网 IP 地址
}
```
### 3. 启动应用
```shell
cd docker
docker-compose up -d
```
* 前端访问地址http://localhost:8080/chat
* 后台管理地址http://localhost:8080/admin
* 移动端地址http://localhost:8080/mobile
> 注意:你得访问后台管理系统 http://localhost:8080/admin
> 输入你前面配置文档中设置的管理员用户名和密码登录。
> 然后进入 `API KEY 管理` 菜单,添加一个 OpenAI 的 API KEY 才可以正常开启 AI 对话。
![add API Key](docs/imgs/apikey_add.png)
最后登录前端聊天页面 [http://localhost:8080/chat](http://localhost:8080/chat)
你可以注册新用户,也可以使用系统默认有个账号:`geekmaster/12345678` 登录聊天。
祝你使用愉快!!!
## 本地开发调试
本地开发同样要分别运行前端和后端程序。
### 运行后端程序
1. 同样你首先要 [导入数据库](#1-导入数据库)
2. 然后 [修改配置文档](#2-修改配置文档)
3. 运行后端程序:
```shell
cd api
# 1. 先下载依赖
go mod tidy
# 2. 运行程序
go run main.go
# 如果你安装了 fresh 可以使用 fresh 实现热启动
fresh -c fresh.conf
```
### 运行前端程序
同样先拷贝配置文档:
```shell
cd web
cp .env.production .env.development
```
编辑 `.env.development` 文件,修改后端 API 的访问路径:
```ini
VUE_APP_API_HOST=http://localhost:5678
VUE_APP_WS_HOST=ws://localhost:5678
```
配置好了之后就可以运行前端应用了:
```
# 安装依赖
npm install
# 运行
npm run dev
```
* 前端页面http://localhost:8888/chat
* 后台管理页面http://localhost:8888/admin
## 项目打包
由于本项目是采用异构开发的方式,所项目打包分成两步:首先编译后端程序,然后再打包前端应用。
### 打包前端
```shell
cd web
npm run build
```
### 打包后端
你可以根据个人需求将项目打包成 windows/linux/darwin 平台项目。
```shell
cd api
# for all platforms
make clean all
# for linux only
make clean linux
```
打包后的可执行文件在 `bin` 目录下。
## 参与贡献 ## 参与贡献
@@ -312,7 +106,7 @@ make clean linux
![微信名片](docs/imgs/wx.png) ![微信名片](docs/imgs/wx.png)
#### 特此声明:不接受在微信或者微信群给开发者提 Bug有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合 #### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合
### Commit 类型 ### Commit 类型
@@ -328,6 +122,9 @@ make clean linux
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~ 如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
![微信打赏](docs/imgs/wechat-pay.png) ![打赏](docs/imgs/donate.png)
![Star History Chart](https://api.star-history.com/svg?repos=yangjian102621/chatgpt-plus&type=Date)

1
api/.gitignore vendored
View File

@@ -18,3 +18,4 @@ data
config.toml config.toml
static/upload static/upload
storage.json storage.json
certs/alipay/*

View File

@@ -1,53 +1,85 @@
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]
SecretKey = "m0cjm3gsuw9jk73np1ni7r42koilybjcndlycjdmq7za3pbqn7w12fyok5pqh6q5" SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
MaxAge = 86400 MaxAge = 86400
[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] [OSS] # OSS 配置,用于存储 MJ 绘画图片
ApiURL = "插件扩展 API 地址" Active = "local" # 默认使用本地文件存储引擎
Token = "插件扩展 API Token"
[OSS]
Active = "local"
[OSS.Local] [OSS.Local]
BasePath = "./static/upload" BasePath = "./static/upload" # 本地文件上传根路径
BaseURL = "http://localhost:5678/static/upload" BaseURL = "http://localhost:5678/static/upload" # 本地上传文件根 URL 如果是线上,则直接设置为 /static/upload 即可
[OSS.Minio] [OSS.Minio]
Endpoint = "IP:端口" Endpoint = "" # 如 172.22.11.200:9000
AccessKey = "minio oss access key" AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
AccessSecret = "minio oss access secret" AccessSecret = ""
Bucket = "minio oss bucket" Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
UseSSL = false UseSSL = false
Domain = "minio 文件公开访问地址" Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
[OSS.QiNiu] # 七牛云 OSS 配置 [OSS.QiNiu] # 七牛云 OSS 配置
Zone = "z2" # 区域z0华东z1: 华北na0北美as0新加坡 Zone = "z2" # 区域z0华东z1: 华北na0北美as0新加坡
AccessKey = "七牛云 OSS AccessKey" AccessKey = ""
AccessSecret = "七牛云 OSS AccessSecret" AccessSecret = ""
Bucket = "七牛云 OSS Bucket" Bucket = ""
Domain = "OSS Bucket 所绑定的域名,如 https://img.r9it.com" 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" # 支付异步回调地址

View File

@@ -1,8 +1,9 @@
package core package core
import ( import (
"bytes"
"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"
@@ -11,9 +12,14 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/nfnt/resize"
"gorm.io/gorm" "gorm.io/gorm"
"image"
"image/jpeg"
"io" "io"
"log"
"net/http" "net/http"
"os"
"runtime/debug" "runtime/debug"
"strings" "strings"
"time" "time"
@@ -33,11 +39,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,7 +53,6 @@ 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,
} }
} }
@@ -59,7 +63,9 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
logger.Info("Enabled debug mode") logger.Info("Enabled debug mode")
} }
s.Engine.Use(corsMiddleware()) s.Engine.Use(corsMiddleware())
s.Engine.Use(staticResourceMiddleware())
s.Engine.Use(authorizeMiddleware(s, client)) s.Engine.Use(authorizeMiddleware(s, client))
s.Engine.Use(parameterHandlerMiddleware())
s.Engine.Use(errorHandler) s.Engine.Use(errorHandler)
// 添加静态资源访问 // 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir) s.Engine.Static("/static", s.Config.StaticDir)
@@ -141,14 +147,18 @@ func corsMiddleware() gin.HandlerFunc {
func authorizeMiddleware(s *AppServer, client *redis.Client) 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/user/resetPass" ||
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/mj/notify" ||
c.Request.URL.Path == "/api/chat/history" || c.Request.URL.Path == "/api/chat/history" ||
c.Request.URL.Path == "/api/chat/detail" || 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/invite/hits" ||
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()
@@ -158,7 +168,9 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
var tokenString string var tokenString string
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
tokenString = c.GetHeader(types.AdminAuthHeader) tokenString = c.GetHeader(types.AdminAuthHeader)
} else if c.Request.URL.Path == "/api/chat/new" { } else if c.Request.URL.Path == "/api/chat/new" ||
c.Request.URL.Path == "/api/mj/client" ||
c.Request.URL.Path == "/api/sd/client" {
tokenString = c.Query("token") tokenString = c.Query("token")
} else { } else {
tokenString = c.GetHeader(types.UserAuthHeader) tokenString = c.GetHeader(types.UserAuthHeader)
@@ -206,3 +218,120 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
c.Set(types.LoginUserID, claims["user_id"]) c.Set(types.LoginUserID, claims["user_id"])
} }
} }
// 统一参数处理
func parameterHandlerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// GET 参数处理
params := c.Request.URL.Query()
for key, values := range params {
for i, value := range values {
params[key][i] = strings.TrimSpace(value)
}
}
// 更新参数
c.Request.URL.RawQuery = params.Encode()
// POST JSON 参数处理
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Next()
return
}
// 还原请求体
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 将请求体解析为 JSON
var jsonData map[string]interface{}
if err := c.ShouldBindJSON(&jsonData); err != nil {
c.Next()
return
}
// 对 JSON 数据中的字符串值去除两端空格
trimJSONStrings(jsonData)
// 更新请求体
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
c.Next()
}
}
// 递归对 JSON 数据中的字符串值去除两端空格
func trimJSONStrings(data interface{}) {
switch v := data.(type) {
case map[string]interface{}:
for key, value := range v {
switch valueType := value.(type) {
case string:
v[key] = strings.TrimSpace(valueType)
case map[string]interface{}, []interface{}:
trimJSONStrings(value)
}
}
case []interface{}:
for i, value := range v {
switch valueType := value.(type) {
case string:
v[i] = strings.TrimSpace(valueType)
case map[string]interface{}, []interface{}:
trimJSONStrings(value)
}
}
}
}
// 静态资源中间件
func staticResourceMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
url := c.Request.URL.String()
// 拦截生成缩略图请求
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
r := strings.SplitAfter(url, "imageView2")
size := strings.Split(r[1], "/")
if len(size) != 8 {
c.String(http.StatusNotFound, "invalid thumb args")
return
}
with := utils.IntValue(size[3], 0)
height := utils.IntValue(size[5], 0)
quality := utils.IntValue(size[7], 75)
// 打开图片文件
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
file, err := os.Open(filePath)
if err != nil {
c.String(http.StatusNotFound, "Image not found")
return
}
defer file.Close()
// 解码图片
img, _, err := image.Decode(file)
if err != nil {
c.String(http.StatusInternalServerError, "Error decoding image")
return
}
var newImg image.Image
if height == 0 || with == 0 {
// 固定宽度,高度自适应
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
} else {
// 生成缩略图
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
}
var buffer bytes.Buffer
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
if err != nil {
log.Fatal(err)
}
// 直接输出图像数据流
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
return
}
c.Next()
}
}

View File

@@ -26,7 +26,6 @@ func NewDefaultConfig() *types.AppConfig {
MaxAge: 86400, MaxAge: 86400,
}, },
ApiConfig: types.ChatPlusApiConfig{}, ApiConfig: types.ChatPlusApiConfig{},
ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)},
OSS: types.OSSConfig{ OSS: types.OSSConfig{
Active: "local", Active: "local",
Local: types.LocalStorageConfig{ Local: types.LocalStorageConfig{
@@ -34,6 +33,10 @@ func NewDefaultConfig() *types.AppConfig {
BasePath: "./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},
} }
} }

View File

@@ -2,9 +2,9 @@ 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,omitempty"` Messages []interface{} `json:"messages,omitempty"`
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
@@ -47,15 +47,7 @@ type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform Platform `json:"platform"` Platform Platform `json:"platform"`
Value string `json:"value"` Value string `json:"value"`
} Weight int `json:"weight"`
type MjTask struct {
ChatId string
MessageId string
MessageHash string
UserId uint
RoleId uint
Icon string
} }
type ApiError struct { type ApiError struct {
@@ -76,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/"

View File

@@ -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

View File

@@ -16,10 +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 绘画配置
OSS OSSConfig // OSS config XXLConfig XXLConfig
AlipayConfig AlipayConfig
} }
type ChatPlusApiConfig struct { type ChatPlusApiConfig struct {
@@ -28,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 {
@@ -38,34 +56,29 @@ type AliYunSmsConfig struct {
AccessSecret string AccessSecret string
Product string Product string
Domain string Domain string
Sign string // 短信签名
CodeTempId string // 验证码短信模板 ID
} }
type OSSConfig struct { type AlipayConfig struct {
Active string Enabled bool // 是否启用该服务
Local LocalStorageConfig SandBox bool // 是否沙盒环境
Minio MinioConfig AppId string // 应用 ID
QiNiu QiNiuConfig UserId string // 支付宝用户 ID
} PrivateKey string // 用户私钥文件路径
type MinioConfig struct { PublicKey string // 用户公钥文件路径
Endpoint string AlipayPublicKey string // 支付宝公钥文件路径
AccessKey string RootCert string // Root 秘钥路径
AccessSecret string NotifyURL string // 异步通知回调
Bucket string
UseSSL bool
Domain string
} }
type QiNiuConfig struct { type XXLConfig struct { // XXL 任务调度配置
Zone string Enabled bool
AccessKey string ServerAddr string
AccessSecret string ExecutorIp string
Bucket string ExecutorPort string
Domain string AccessToken string
} RegistryKey string
type LocalStorageConfig struct {
BasePath string
BaseURL string
} }
type RedisConfig struct { type RedisConfig struct {
@@ -90,6 +103,8 @@ type ChatConfig struct {
OpenAI ModelAPIConfig `json:"open_ai"` OpenAI ModelAPIConfig `json:"open_ai"`
Azure ModelAPIConfig `json:"azure"` Azure ModelAPIConfig `json:"azure"`
ChatGML ModelAPIConfig `json:"chat_gml"` ChatGML ModelAPIConfig `json:"chat_gml"`
Baidu ModelAPIConfig `json:"baidu"`
XunFei ModelAPIConfig `json:"xun_fei"`
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文 EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录 EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
@@ -101,12 +116,19 @@ type Platform string
const OpenAI = Platform("OpenAI") const OpenAI = Platform("OpenAI")
const Azure = Platform("Azure") const Azure = Platform("Azure")
const ChatGLM = Platform("ChatGLM") const ChatGLM = Platform("ChatGLM")
const Baidu = Platform("Baidu")
const XunFei = Platform("XunFei")
// UserChatConfig 用户的聊天配置 // UserChatConfig 用户的聊天配置
type UserChatConfig struct { type UserChatConfig struct {
ApiKeys map[Platform]string `json:"api_keys"` ApiKeys map[Platform]string `json:"api_keys"`
} }
type InviteReward struct {
ChatCalls int `json:"chat_calls"`
ImgCalls int `json:"img_calls"`
}
type ModelAPIConfig struct { type ModelAPIConfig struct {
ApiURL string `json:"api_url,omitempty"` ApiURL string `json:"api_url,omitempty"`
Temperature float32 `json:"temperature"` Temperature float32 `json:"temperature"`
@@ -115,13 +137,22 @@ type ModelAPIConfig struct {
} }
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"` // 新用户注册默认总送多少次调用 InitChatCalls int `json:"init_chat_calls"` // 新用户注册赠送对话次数
InitImgCalls int `json:"init_img_calls"` InitImgCalls int `json:"init_img_calls"` // 新用户注册赠送绘图次数
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数 VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
EnabledRegister bool `json:"enabled_register"` EnabledRegister bool `json:"enabled_register"` // 是否启用注册功能,关闭注册功能之后将无法注册
EnabledMsgService bool `json:"enabled_msg_service"` 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 模型
OrderPayInfoText string `json:"order_pay_info_text"` // 订单支付页面说明文字
InviteChatCalls int `json:"invite_chat_calls"` // 邀请用户注册奖励对话次数
InviteImgCalls int `json:"invite_img_calls"` // 邀请用户注册奖励绘图次数
ForceInvite bool `json:"force_invite"` // 是否强制必须使用邀请码才能注册
} }

View File

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

View File

@@ -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
View 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
View 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
}

69
api/core/types/task.go Normal file
View 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"` // 高清修复迭代步数
}

View File

@@ -34,4 +34,5 @@ const (
OkMsg = "Success" OkMsg = "Success"
ErrorMsg = "系统开小差了" ErrorMsg = "系统开小差了"
InvalidArgs = "非法参数或参数解析失败" InvalidArgs = "非法参数或参数解析失败"
NoData = "No Data"
) )

View File

@@ -5,6 +5,9 @@ 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/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/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v5 v5.0.0 github.com/golang-jwt/jwt/v5 v5.0.0
@@ -14,12 +17,15 @@ require (
github.com/minio/minio-go/v7 v7.0.62 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/qiniu/go-sdk/v7 v7.17.1
github.com/syndtr/goleveldb v1.0.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/smartwalle/alipay/v3 v3.2.15
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/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect
@@ -30,6 +36,7 @@ require (
github.com/dustin/go-humanize v1.0.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
@@ -45,6 +52,7 @@ require (
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // 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
@@ -55,6 +63,9 @@ require (
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/rs/xid v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // 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
@@ -63,6 +74,7 @@ require (
golang.org/x/net v0.14.0 // indirect golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.12.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.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
@@ -74,7 +86,6 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/golang/snappy v0.0.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

View File

@@ -2,9 +2,13 @@ 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/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY=
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=
@@ -23,7 +27,8 @@ 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 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.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 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=
@@ -33,6 +38,8 @@ 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.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=
@@ -60,12 +67,8 @@ github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJ
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 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.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 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.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
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=
@@ -73,6 +76,7 @@ github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9S
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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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=
@@ -80,7 +84,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/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=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -126,13 +129,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
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/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.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 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.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
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=
@@ -166,6 +168,16 @@ github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 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 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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=
@@ -178,8 +190,6 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o= github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
@@ -188,6 +198,8 @@ 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= 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=
@@ -207,6 +219,7 @@ 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.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 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.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.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
@@ -218,7 +231,6 @@ 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.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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -227,13 +239,11 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 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 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= 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-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.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 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-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=
@@ -260,6 +270,8 @@ 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.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 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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=
@@ -278,15 +290,12 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 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/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
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 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 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=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -5,13 +5,10 @@ import (
"chatplus/core/types" "chatplus/core/types"
"chatplus/handler" "chatplus/handler"
logger2 "chatplus/logger" logger2 "chatplus/logger"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"context" "context"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -44,7 +41,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
// 创建 token // 创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": manager.Username, "user_id": manager.Username,
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)), "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
}) })
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil { if err != nil {
@@ -52,7 +49,8 @@ func (h *ManagerHandler) Login(c *gin.Context) {
return return
} }
// 保存到 redis // 保存到 redis
if _, err := h.redis.Set(context.Background(), "users/"+manager.Username, tokenString, 0).Result(); err != nil { 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()) resp.ERROR(c, "error with save token: "+err.Error())
return return
} }
@@ -64,8 +62,8 @@ func (h *ManagerHandler) Login(c *gin.Context) {
// Logout 注销 // Logout 注销
func (h *ManagerHandler) Logout(c *gin.Context) { func (h *ManagerHandler) Logout(c *gin.Context) {
token := c.GetHeader(types.AdminAuthHeader) key := h.GetUserKey(c)
if _, err := h.redis.Del(c, token).Result(); err != nil { if _, err := h.redis.Del(c, key).Result(); err != nil {
logger.Error("error with delete session: ", err) logger.Error("error with delete session: ", err)
} else { } else {
resp.SUCCESS(c) resp.SUCCESS(c)
@@ -81,67 +79,3 @@ func (h *ManagerHandler) Session(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
} }
// Migrate 数据修正
func (h *ManagerHandler) Migrate(c *gin.Context) {
opt := c.Query("opt")
switch opt {
case "user":
// 将用户订阅角色的数据结构从 map 改成数组
var users []model.User
h.db.Find(&users)
for _, u := range users {
var m map[string]int
var roleKeys = make([]string, 0)
err := utils.JsonDecode(u.ChatRoles, &m)
if err != nil {
continue
}
for k := range m {
roleKeys = append(roleKeys, k)
}
u.ChatRoles = utils.JsonEncode(roleKeys)
h.db.Updates(&u)
}
break
case "role":
// 修改角色图片,改成绝对路径
var roles []model.ChatRole
h.db.Find(&roles)
for _, r := range roles {
if !strings.HasPrefix(r.Icon, "/") {
r.Icon = "/" + r.Icon
h.db.Updates(&r)
}
}
break
case "history":
// 修改角色图片,改成绝对路径
var message []model.HistoryMessage
h.db.Find(&message)
for _, r := range message {
if !strings.HasPrefix(r.Icon, "/") {
r.Icon = "/" + r.Icon
h.db.Updates(&r)
}
}
break
case "avatar":
// 更新用户的头像地址
var users []model.User
h.db.Find(&users)
for _, u := range users {
if !strings.HasPrefix(u.Avatar, "/") {
u.Avatar = "/" + u.Avatar
h.db.Updates(&u)
}
}
break
}
resp.SUCCESS(c, "SUCCESS")
}

View File

@@ -27,6 +27,7 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform string `json:"platform"` Platform string `json:"platform"`
Type string `json:"type"`
Value string `json:"value"` Value string `json:"value"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@@ -36,10 +37,11 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
apiKey := model.ApiKey{} apiKey := model.ApiKey{}
if data.Id > 0 { if data.Id > 0 {
h.db.Find(&apiKey) h.db.Find(&apiKey, data.Id)
} }
apiKey.Platform = data.Platform apiKey.Platform = data.Platform
apiKey.Value = data.Value apiKey.Value = data.Value
apiKey.Type = data.Type
res := h.db.Save(&apiKey) res := h.db.Save(&apiKey)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")

View File

@@ -31,7 +31,9 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
Value string `json:"value"` Value string `json:"value"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"` SortNum int `json:"sort_num"`
Open bool `json:"open"`
Platform string `json:"platform"` Platform string `json:"platform"`
Weight int `json:"weight"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@@ -39,7 +41,14 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
return return
} }
item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled} item := model.ChatModel{
Platform: data.Platform,
Name: data.Name,
Value: data.Value,
Enabled: data.Enabled,
SortNum: data.SortNum,
Open: data.Open,
Weight: data.Weight}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0) item.CreatedAt = time.Unix(data.CreatedAt, 0)
@@ -88,10 +97,11 @@ func (h *ChatModelHandler) List(c *gin.Context) {
resp.SUCCESS(c, cms) resp.SUCCESS(c, cms)
} }
func (h *ChatModelHandler) Enable(c *gin.Context) { func (h *ChatModelHandler) Set(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
Enabled bool `json:"enabled"` Filed string `json:"filed"`
Value interface{} `json:"value"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
@@ -99,7 +109,7 @@ func (h *ChatModelHandler) Enable(c *gin.Context) {
return return
} }
res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled) res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update(data.Filed, data.Value)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "更新数据库失败!") resp.ERROR(c, "更新数据库失败!")
return return

View File

@@ -2,6 +2,7 @@ package admin
import ( import (
"chatplus/core" "chatplus/core"
"chatplus/core/types"
"chatplus/handler" "chatplus/handler"
"chatplus/store/model" "chatplus/store/model"
"chatplus/utils/resp" "chatplus/utils/resp"
@@ -22,10 +23,10 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
} }
type statsVo struct { type statsVo struct {
Users int64 `json:"users"` Users int64 `json:"users"`
Chats int64 `json:"chats"` Chats int64 `json:"chats"`
Tokens int64 `json:"tokens"` Tokens int `json:"tokens"`
Rewards float64 `json:"rewards"` Income float64 `json:"income"`
} }
func (h *DashboardHandler) Stats(c *gin.Context) { func (h *DashboardHandler) Stats(c *gin.Context) {
@@ -47,17 +48,24 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
} }
// tokens took stats // tokens took stats
var tokenCount int64 var historyMessages []model.HistoryMessage
res = h.db.Model(&model.HistoryMessage{}).Select("sum(tokens) as total").Where("created_at > ?", zeroTime).Scan(&tokenCount) res = h.db.Where("created_at > ?", zeroTime).Find(&historyMessages)
if res.Error == nil { for _, item := range historyMessages {
stats.Tokens = tokenCount stats.Tokens += item.Tokens
} }
// reward revenue // 众筹收入
var amount float64 var rewards []model.Reward
res = h.db.Model(&model.Reward{}).Select("sum(amount) as total").Where("created_at > ?", zeroTime).Scan(&amount) res = h.db.Where("created_at > ?", zeroTime).Find(&rewards)
if res.Error == nil { for _, item := range rewards {
stats.Rewards = amount stats.Income += item.Amount
}
// 订单收入
var orders []model.Order
res = h.db.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
for _, item := range orders {
stats.Income += item.Amount
} }
resp.SUCCESS(c, stats) resp.SUCCESS(c, stats)
} }

View 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)
}

View 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)
}

View File

@@ -62,15 +62,15 @@ 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"`
Vip bool `json:"vip"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -83,13 +83,14 @@ 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, "vip": data.Vip,
"chat_roles_json": utils.JsonEncode(data.ChatRoles), "chat_roles_json": utils.JsonEncode(data.ChatRoles),
"expired_time": utils.Str2stamp(data.ExpiredTime), "chat_models_json": utils.JsonEncode(data.ChatModels),
"expired_time": utils.Str2stamp(data.ExpiredTime),
}) })
} else { } else {
salt := utils.RandString(8) salt := utils.RandString(8)
@@ -100,6 +101,7 @@ func (h *UserHandler) Save(c *gin.Context) {
Salt: salt, Salt: salt,
Status: true, Status: true,
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.UserChatConfig{ ChatConfig: utils.JsonEncode(types.UserChatConfig{
ApiKeys: map[types.Platform]string{ ApiKeys: map[types.Platform]string{
@@ -108,7 +110,8 @@ func (h *UserHandler) Save(c *gin.Context) {
types.ChatGLM: "", types.ChatGLM: "",
}, },
}), }),
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)

View File

@@ -2,8 +2,10 @@ package handler
import ( import (
"chatplus/core" "chatplus/core"
"chatplus/core/types"
logger2 "chatplus/logger" logger2 "chatplus/logger"
"chatplus/utils" "chatplus/utils"
"fmt"
"strings" "strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -40,3 +42,18 @@ func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool { func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
return utils.BoolValue(c.PostForm(key)) return utils.BoolValue(c.PostForm(key))
} }
func (h *BaseHandler) GetUserKey(c *gin.Context) string {
userId, ok := c.Get(types.LoginUserID)
if !ok {
return ""
}
return fmt.Sprintf("users/%v", userId)
}
func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
userId, ok := c.Get(types.LoginUserID)
if !ok {
return 0
}
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
}

View File

@@ -1,103 +0,0 @@
package handler
import (
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"gorm.io/gorm"
"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)
}
// 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)
}
// 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)
}

View File

@@ -24,8 +24,25 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
// List 模型列表 // List 模型列表
func (h *ChatModelHandler) List(c *gin.Context) { func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel var items []model.ChatModel
var cms = make([]vo.ChatModel, 0) var chatModels = make([]vo.ChatModel, 0)
res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items) // 只加载用户订阅的 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(
h.db.Where("value IN ?", models).Or("open =?", true),
).Order("sort_num ASC").Find(&items)
if res.Error == nil { if res.Error == nil {
for _, item := range items { for _, item := range items {
var cm vo.ChatModel var cm vo.ChatModel
@@ -34,11 +51,11 @@ func (h *ChatModelHandler) List(c *gin.Context) {
cm.Id = item.Id cm.Id = item.Id
cm.CreatedAt = item.CreatedAt.Unix() cm.CreatedAt = item.CreatedAt.Unix()
cm.UpdatedAt = item.UpdatedAt.Unix() cm.UpdatedAt = item.UpdatedAt.Unix()
cms = append(cms, cm) chatModels = append(chatModels, cm)
} else { } else {
logger.Error(err) logger.Error(err)
} }
} }
} }
resp.SUCCESS(c, cms) resp.SUCCESS(c, chatModels)
} }

View File

@@ -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,6 +25,7 @@ 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_num ASC").Find(&roles) res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles)
if res.Error != nil { if res.Error != nil {
@@ -31,13 +33,31 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
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)
}

View File

@@ -1,4 +1,4 @@
package handler package chatimpl
import ( import (
"bufio" "bufio"
@@ -16,7 +16,8 @@ import (
"unicode/utf8" "unicode/utf8"
) )
// 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端 // 微软 Azure 模型消息发送实现
func (h *ChatHandler) sendAzureMessage( func (h *ChatHandler) sendAzureMessage(
chatCtx []interface{}, chatCtx []interface{},
req types.ApiRequest, req types.ApiRequest,
@@ -43,7 +44,7 @@ func (h *ChatHandler) sendAzureMessage(
} }
utils.ReplyMessage(ws, ErrorMsg) utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
return err return err
} else { } else {
defer response.Body.Close() defer response.Body.Close()
@@ -70,7 +71,7 @@ func (h *ChatHandler) sendAzureMessage(
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
logger.Error(err, line) logger.Error(err, line)
utils.ReplyMessage(ws, ErrorMsg) utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
break break
} }
@@ -126,11 +127,18 @@ func (h *ChatHandler) sendAzureMessage(
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params) logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0 // for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 { if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**") utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
} else { } else {
f := h.App.Functions[functionName] f := h.App.Functions[functionName]
if functionName == types.FuncImage {
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) data, err := f.Invoke(params)
if err != nil { if err != nil {
msg := "调用函数出错:" + err.Error() msg := "调用函数出错:" + err.Error()
@@ -141,23 +149,8 @@ func (h *ChatHandler) sendAzureMessage(
contents = append(contents, msg) contents = append(contents, msg)
} else { } else {
content := data content := data
if functionName == types.FuncMidJourney { if functionName == types.FuncImage {
key := utils.Sha256(data) content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景", params["prompt"])
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 // update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
} }
@@ -174,9 +167,7 @@ func (h *ChatHandler) sendAzureMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数 // 更新用户的对话次数
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { h.subUserCalls(userVo, session)
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
@@ -220,17 +211,17 @@ func (h *ChatHandler) sendAzureMessage(
logger.Error("failed to save prompt history message: ", res.Error) logger.Error("failed to save prompt history message: ", res.Error)
} }
// for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
var replyToken = 0 var totalTokens = 0
if functionCall { // 函数名 + 参数 token if functionCall { // prompt + 函数名 + 参数 token
tokens, _ := utils.CalcTokens(functionName, req.Model) tokens, _ := utils.CalcTokens(functionName, req.Model)
replyToken += tokens totalTokens += tokens
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
replyToken += tokens totalTokens += tokens
} else { } else {
replyToken, _ = utils.CalcTokens(message.Content, req.Model) totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
} }
totalTokens += getTotalTokens(req)
historyReplyMsg := model.HistoryMessage{ historyReplyMsg := model.HistoryMessage{
UserId: userVo.Id, UserId: userVo.Id,
@@ -239,7 +230,7 @@ func (h *ChatHandler) sendAzureMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: message.Content, Content: message.Content,
Tokens: replyToken, Tokens: totalTokens,
UseContext: useContext, UseContext: useContext,
} }
historyReplyMsg.CreatedAt = replyCreatedAt historyReplyMsg.CreatedAt = replyCreatedAt
@@ -249,15 +240,8 @@ func (h *ChatHandler) sendAzureMessage(
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 计算本次对话消耗的总 token 数量 // 更新用户信息
var totalTokens = 0 h.incUserTokenFee(userVo.Id, totalTokens)
if functionCall { // prompt + 函数名 + 参数 token
totalTokens = promptToken + replyToken
} else {
totalTokens = replyToken + getTotalTokens(req)
}
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
} }
// 保存当前会话 // 保存当前会话

View File

@@ -0,0 +1,274 @@
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:]
}
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
var resp baiduResp
err := utils.JsonDecode(content, &resp)
if err != nil {
logger.Error("error with parse data line: ", err)
utils.ReplyMessage(ws, fmt.Sprintf("**解析数据行失败:%s**", err))
break
}
if len(contents) == 0 {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(resp.Result),
})
contents = append(contents, resp.Result)
if resp.IsTruncated {
utils.ReplyMessage(ws, "AI 输出异常中断")
break
}
if resp.IsEnd {
break
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
// 消息发送成功
if len(contents) > 0 {
// 更新用户的对话次数
h.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
}

View File

@@ -1,10 +1,12 @@
package handler package chatimpl
import ( import (
"bytes" "bytes"
"chatplus/core" "chatplus/core"
"chatplus/core/types" "chatplus/core/types"
"chatplus/store" "chatplus/handler"
logger2 "chatplus/logger"
"chatplus/service/mj"
"chatplus/store/model" "chatplus/store/model"
"chatplus/store/vo" "chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
@@ -24,18 +26,25 @@ import (
) )
const ErrorMsg = "抱歉AI 助手开小差了,请稍后再试。" const ErrorMsg = "抱歉AI 助手开小差了,请稍后再试。"
const ErrImg = "![](/images/wx.png)"
var logger = logger2.GetLogger()
type ChatHandler struct { type ChatHandler struct {
BaseHandler handler.BaseHandler
db *gorm.DB db *gorm.DB
leveldb *store.LevelDB redis *redis.Client
redis *redis.Client mjService *mj.Service
} }
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client) *ChatHandler { func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, service *mj.Service) *ChatHandler {
handler := ChatHandler{db: db, leveldb: levelDB, redis: redis} h := ChatHandler{
handler.App = app db: db,
return &handler redis: redis,
mjService: service,
}
h.App = app
return &h
} }
var chatConfig types.ChatConfig var chatConfig types.ChatConfig
@@ -92,8 +101,9 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session.Model = types.ChatModel{ session.Model = types.ChatModel{
Id: chatModel.Id, Id: chatModel.Id,
Value: chatModel.Value, Value: chatModel.Value,
Weight: chatModel.Weight,
Platform: types.Platform(chatModel.Platform)} Platform: types.Platform(chatModel.Platform)}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.Request.RemoteAddr, session.Username) logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
var chatRole model.ChatRole var chatRole model.ChatRole
res = h.db.First(&chatRole, roleId) res = h.db.First(&chatRole, roleId)
if res.Error != nil || !chatRole.Enable { if res.Error != nil || !chatRole.Enable {
@@ -121,7 +131,11 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
logger.Error(err) logger.Error(err)
client.Close() client.Close()
h.App.ChatClients.Delete(sessionId) h.App.ChatClients.Delete(sessionId)
h.App.ReqCancelFunc.Delete(sessionId) cancelFunc := h.App.ReqCancelFunc.Get(sessionId)
if cancelFunc != nil {
cancelFunc()
h.App.ReqCancelFunc.Delete(sessionId)
}
return return
} }
@@ -134,6 +148,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
err = h.sendMessage(ctx, session, chatRole, message, client) err = h.sendMessage(ctx, session, chatRole, message, client)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
} else { } else {
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd}) utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
logger.Info("回答完毕: " + string(message)) logger.Info("回答完毕: " + string(message))
@@ -165,19 +180,25 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
if userVo.Status == false { if userVo.Status == false {
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!") utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
utils.ReplyMessage(ws, "![](/images/wx.png)") 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 return nil
} }
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者点击左下角菜单加入众筹获得100次对话!") utils.ReplyMessage(ws, "您的对话次数已经用尽,请联系管理员或者充值点卡继续对话!")
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
return nil return nil
} }
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() { if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!") utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
return nil return nil
} }
var req = types.ApiRequest{ var req = types.ApiRequest{
@@ -193,17 +214,27 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
req.Temperature = h.App.ChatConfig.ChatGML.Temperature req.Temperature = h.App.ChatConfig.ChatGML.Temperature
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
break break
default: 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.Temperature = h.App.ChatConfig.OpenAI.Temperature
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
var functions = make([]types.Function, 0) // OpenAI 支持函数功能
for _, f := range types.InnerFunctions { if h.App.SysConfig.EnabledFunction {
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney { var functions = make([]types.Function, 0)
continue for _, f := range types.InnerFunctions {
functions = append(functions, f)
} }
functions = append(functions, f) req.Functions = functions
} }
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
} }
// 加载聊天上下文 // 加载聊天上下文
@@ -236,9 +267,10 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
// loading recent chat history as chat context // loading recent chat history as chat context
if chatConfig.ContextDeep > 0 { if chatConfig.ContextDeep > 0 {
var historyMessages []model.HistoryMessage 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) 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 { if res.Error == nil {
for _, msg := range historyMessages { for i := len(historyMessages) - 1; i >= 0; i-- {
msg := historyMessages[i]
if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] { if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
break break
} }
@@ -271,8 +303,17 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.ChatGLM: case types.ChatGLM:
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws) 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)
} }
return fmt.Errorf("not supported platform: %s", session.Model.Platform) utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: fmt.Sprintf("Not supported platform: %s", session.Model.Platform),
})
return nil
} }
// Tokens 统计 token 数量 // Tokens 统计 token 数量
@@ -286,6 +327,19 @@ func (h *ChatHandler) Tokens(c *gin.Context) {
return 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) tokens, err := utils.CalcTokens(data.Text, data.Model)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
@@ -337,12 +391,36 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
break break
case types.ChatGLM: case types.ChatGLM:
apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1) apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
req.Prompt = req.Messages req.Prompt = req.Messages // 使用 prompt 字段替代 message 字段
req.Messages = nil req.Messages = nil
break break
case types.Baidu:
apiURL = strings.Replace(h.App.ChatConfig.Baidu.ApiURL, "{model}", req.Model, 1)
break
default: default:
apiURL = h.App.ChatConfig.OpenAI.ApiURL apiURL = h.App.ChatConfig.OpenAI.ApiURL
} }
if *apiKey == "" {
var key model.ApiKey
res := h.db.Where("platform = ? AND type = ?", platform, "chat").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 请求对象 // 创建 HttpClient 请求对象
var client *http.Client var client *http.Client
requestBody, err := json.Marshal(req) requestBody, err := json.Marshal(req)
@@ -367,17 +445,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
} else { } else {
client = http.DefaultClient client = http.DefaultClient
} }
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
}
logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model) logger.Infof("Sending %s request, KEY: %s, PROXY: %s, Model: %s", platform, *apiKey, proxyURL, req.Model)
switch platform { switch platform {
case types.Azure: case types.Azure:
@@ -391,8 +458,29 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
logger.Info(token) logger.Info(token)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
break break
default: case types.Baidu:
request.RequestURI = ""
case types.OpenAI:
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey)) request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
} }
return client.Do(request) 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))
}

View File

@@ -1,4 +1,4 @@
package handler package chatimpl
import ( import (
"chatplus/core/types" "chatplus/core/types"
@@ -7,6 +7,7 @@ import (
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
) )
// List 获取会话列表 // List 获取会话列表
@@ -47,6 +48,95 @@ func (h *ChatHandler) List(c *gin.Context) {
resp.SUCCESS(c, items) 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 删除会话 // Remove 删除会话
func (h *ChatHandler) Remove(c *gin.Context) { func (h *ChatHandler) Remove(c *gin.Context) {
chatId := h.GetTrim(c, "chat_id") chatId := h.GetTrim(c, "chat_id")
@@ -80,6 +170,7 @@ func (h *ChatHandler) Remove(c *gin.Context) {
resp.SUCCESS(c, types.OkMsg) resp.SUCCESS(c, types.OkMsg)
} }
// Detail 对话详情,用户导出对话
func (h *ChatHandler) Detail(c *gin.Context) { func (h *ChatHandler) Detail(c *gin.Context) {
chatId := h.GetTrim(c, "chat_id") chatId := h.GetTrim(c, "chat_id")
if utils.IsEmptyValue(chatId) { if utils.IsEmptyValue(chatId) {

View File

@@ -1,4 +1,4 @@
package handler package chatimpl
import ( import (
"bufio" "bufio"
@@ -10,14 +10,14 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
"io" "io"
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
) )
// 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端 // 清华大学 ChatGML 消息发送实现
func (h *ChatHandler) sendChatGLMMessage( func (h *ChatHandler) sendChatGLMMessage(
chatCtx []interface{}, chatCtx []interface{},
req types.ApiRequest, req types.ApiRequest,
@@ -44,7 +44,7 @@ func (h *ChatHandler) sendChatGLMMessage(
} }
utils.ReplyMessage(ws, ErrorMsg) utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
return err return err
} else { } else {
defer response.Body.Close() defer response.Body.Close()
@@ -71,6 +71,10 @@ func (h *ChatHandler) sendChatGLMMessage(
if strings.HasPrefix(line, "data:") { if strings.HasPrefix(line, "data:") {
content = line[5:] content = line[5:]
} }
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
switch event { switch event {
case "add": case "add":
if len(contents) == 0 { if len(contents) == 0 {
@@ -103,9 +107,7 @@ func (h *ChatHandler) sendChatGLMMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数 // 更新用户的对话次数
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { h.subUserCalls(userVo, session)
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
@@ -146,9 +148,8 @@ func (h *ChatHandler) sendChatGLMMessage(
// for reply // for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
var replyToken = 0 replyToken, _ := utils.CalcTokens(message.Content, req.Model)
replyToken, _ = utils.CalcTokens(message.Content, req.Model) totalTokens := replyToken + getTotalTokens(req)
historyReplyMsg := model.HistoryMessage{ historyReplyMsg := model.HistoryMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: session.ChatId,
@@ -156,7 +157,7 @@ func (h *ChatHandler) sendChatGLMMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: message.Content, Content: message.Content,
Tokens: replyToken, Tokens: totalTokens,
UseContext: true, UseContext: true,
} }
historyReplyMsg.CreatedAt = replyCreatedAt historyReplyMsg.CreatedAt = replyCreatedAt
@@ -165,12 +166,8 @@ func (h *ChatHandler) sendChatGLMMessage(
if res.Error != nil { if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 更新用户信息
// 计算本次对话消耗的总 token 数量 h.incUserTokenFee(userVo.Id, totalTokens)
var totalTokens = 0
totalTokens = replyToken + getTotalTokens(req)
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
} }
// 保存当前会话 // 保存当前会话

View File

@@ -1,4 +1,4 @@
package handler package chatimpl
import ( import (
"bufio" "bufio"
@@ -16,7 +16,7 @@ import (
"unicode/utf8" "unicode/utf8"
) )
// 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端 // OPenAI 消息发送实现
func (h *ChatHandler) sendOpenAiMessage( func (h *ChatHandler) sendOpenAiMessage(
chatCtx []interface{}, chatCtx []interface{},
req types.ApiRequest, req types.ApiRequest,
@@ -43,7 +43,7 @@ func (h *ChatHandler) sendOpenAiMessage(
} }
utils.ReplyMessage(ws, ErrorMsg) utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
return err return err
} else { } else {
defer response.Body.Close() defer response.Body.Close()
@@ -70,7 +70,7 @@ func (h *ChatHandler) sendOpenAiMessage(
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错 if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
logger.Error(err, line) logger.Error(err, line)
utils.ReplyMessage(ws, ErrorMsg) utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
break break
} }
@@ -126,11 +126,18 @@ func (h *ChatHandler) sendOpenAiMessage(
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params) logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0 // for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 { if functionName == types.FuncImage && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**") utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, "![](/images/wx.png)") utils.ReplyMessage(ws, ErrImg)
} else { } else {
f := h.App.Functions[functionName] f := h.App.Functions[functionName]
if functionName == types.FuncImage {
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) data, err := f.Invoke(params)
if err != nil { if err != nil {
msg := "调用函数出错:" + err.Error() msg := "调用函数出错:" + err.Error()
@@ -141,23 +148,8 @@ func (h *ChatHandler) sendOpenAiMessage(
contents = append(contents, msg) contents = append(contents, msg)
} else { } else {
content := data content := data
if functionName == types.FuncMidJourney { if functionName == types.FuncImage {
key := utils.Sha256(data) content = fmt.Sprintf("下面是根据您的描述创作的图片,他们描绘了 【%s】 的场景。%s", params["prompt"], 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 // update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1)) h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
} }
@@ -174,9 +166,7 @@ func (h *ChatHandler) sendOpenAiMessage(
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
// 更新用户的对话次数 // 更新用户的对话次数
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" { h.subUserCalls(userVo, session)
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" { if message.Role == "" {
message.Role = "assistant" message.Role = "assistant"
@@ -220,17 +210,17 @@ func (h *ChatHandler) sendOpenAiMessage(
logger.Error("failed to save prompt history message: ", res.Error) logger.Error("failed to save prompt history message: ", res.Error)
} }
// for reply
// 计算本次对话消耗的总 token 数量 // 计算本次对话消耗的总 token 数量
var replyToken = 0 var totalTokens = 0
if functionCall { // 函数名 + 参数 token if functionCall { // prompt + 函数名 + 参数 token
tokens, _ := utils.CalcTokens(functionName, req.Model) tokens, _ := utils.CalcTokens(functionName, req.Model)
replyToken += tokens totalTokens += tokens
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model) tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
replyToken += tokens totalTokens += tokens
} else { } else {
replyToken, _ = utils.CalcTokens(message.Content, req.Model) totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
} }
totalTokens += getTotalTokens(req)
historyReplyMsg := model.HistoryMessage{ historyReplyMsg := model.HistoryMessage{
UserId: userVo.Id, UserId: userVo.Id,
@@ -239,7 +229,7 @@ func (h *ChatHandler) sendOpenAiMessage(
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: role.Icon,
Content: message.Content, Content: message.Content,
Tokens: replyToken, Tokens: totalTokens,
UseContext: useContext, UseContext: useContext,
} }
historyReplyMsg.CreatedAt = replyCreatedAt historyReplyMsg.CreatedAt = replyCreatedAt
@@ -249,15 +239,8 @@ func (h *ChatHandler) sendOpenAiMessage(
logger.Error("failed to save reply history message: ", res.Error) logger.Error("failed to save reply history message: ", res.Error)
} }
// 计算本次对话消耗的总 token 数量 // 更新用户信息
var totalTokens = 0 h.incUserTokenFee(userVo.Id, totalTokens)
if functionCall { // prompt + 函数名 + 参数 token
totalTokens = promptToken + replyToken
} else {
totalTokens = replyToken + getTotalTokens(req)
}
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
} }
// 保存当前会话 // 保存当前会话

View File

@@ -0,0 +1,322 @@
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"`
}
var Model2URL = map[string]string{
"generalv1": "1.1",
"generalv2": "v2.1",
"generalv3": "v3.1",
}
// 科大讯飞消息发送实现
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 = ? AND type = ?", session.Model.Platform, "chat").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
}
apiURL := strings.Replace(h.App.ChatConfig.XunFei.ApiURL, "{version}", Model2URL[req.Model], 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
// 处理代码换行
if len(content) == 0 {
content = "\n"
}
contents = append(contents, content)
// 第一个结果
if result.Payload.Choices.Status == 0 {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(content),
})
if result.Payload.Choices.Status == 2 { // 最终结果
_ = conn.Close() // 关闭连接
break
}
select {
case <-ctx.Done():
utils.ReplyMessage(ws, "**用户取消了生成指令!**")
return nil
default:
continue
}
}
// 消息发送成功
if len(contents) > 0 {
// 更新用户的对话次数
h.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))
}

View File

@@ -0,0 +1,96 @@
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"
"strings"
)
// InviteHandler 用户邀请
type InviteHandler struct {
BaseHandler
db *gorm.DB
}
func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
h := InviteHandler{db: db}
h.App = app
return &h
}
// Code 获取当前用户邀请码
func (h *InviteHandler) Code(c *gin.Context) {
userId := h.GetLoginUserId(c)
var inviteCode model.InviteCode
res := h.db.Where("user_id = ?", userId).First(&inviteCode)
// 如果邀请码不存在,则创建一个
if res.Error != nil {
code := strings.ToUpper(utils.RandString(8))
for {
res = h.db.Where("code = ?", code).First(&inviteCode)
if res.Error != nil { // 不存在相同的邀请码则退出
break
}
}
inviteCode.UserId = userId
inviteCode.Code = code
h.db.Create(&inviteCode)
}
var codeVo vo.InviteCode
err := utils.CopyObject(inviteCode, &codeVo)
if err != nil {
resp.ERROR(c, "拷贝对象失败")
return
}
resp.SUCCESS(c, codeVo)
}
// List Log 用户邀请记录
func (h *InviteHandler) 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
}
userId := h.GetLoginUserId(c)
session := h.db.Session(&gorm.Session{}).Where("inviter_id = ?", userId)
var total int64
session.Model(&model.InviteLog{}).Count(&total)
var items []model.InviteLog
var list = make([]vo.InviteLog, 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 v vo.InviteLog
err := utils.CopyObject(item, &v)
if err == nil {
v.Id = item.Id
v.CreatedAt = item.CreatedAt.Unix()
list = append(list, v)
} else {
logger.Error(err)
}
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
}
// Hits 访问邀请码
func (h *InviteHandler) Hits(c *gin.Context) {
code := c.Query("code")
h.db.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
resp.SUCCESS(c)
}

View File

@@ -3,237 +3,364 @@ package handler
import ( import (
"chatplus/core" "chatplus/core"
"chatplus/core/types" "chatplus/core/types"
"chatplus/service/function" "chatplus/service/mj"
"chatplus/service/oss"
"chatplus/store"
"chatplus/store/model" "chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"encoding/base64" "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
uploaderManager *oss.UploaderManager
} }
func NewMidJourneyHandler( func NewMidJourneyHandler(
app *core.AppServer, app *core.AppServer,
leveldb *store.LevelDB, client *redis.Client,
db *gorm.DB, db *gorm.DB,
manager *oss.UploaderManager, mjService *mj.Service) *MidJourneyHandler {
functions map[string]function.Function) *MidJourneyHandler {
h := MidJourneyHandler{ h := MidJourneyHandler{
leveldb: leveldb, redis: client,
db: db, db: db,
uploaderManager: manager, mjService: mjService,
mjFunc: functions[types.FuncMidJourney].(function.FuncMidJourney)} }
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)
} }
// download image if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") {
imgURL, err := h.uploaderManager.GetActiveService().PutImg(data.Image.URL) prompt += fmt.Sprintf(" --c %d", data.Chaos)
if err != nil { }
logger.Error("error with download image: ", err) if data.Img != "" {
resp.SUCCESS(c) prompt = fmt.Sprintf("%s %s", data.Img, prompt)
return if data.Weight > 0 {
} prompt += fmt.Sprintf(" --iw %f", data.Weight)
data.Image.URL = imgURL
message := model.HistoryMessage{
UserId: task.UserId,
ChatId: task.ChatId,
RoleId: task.RoleId,
Type: types.MjMsg,
Icon: task.Icon,
Content: utils.JsonEncode(data),
Tokens: 0,
UseContext: false,
}
res := h.db.Create(&message)
if res.Error != nil {
logger.Error("error with save chat history message: ", res.Error)
}
// save the job
job.UserId = task.UserId
job.ChatId = task.ChatId
job.MessageId = data.MessageId
job.ReferenceId = data.ReferenceId
job.Content = data.Content
job.Prompt = data.Prompt
job.Image = utils.JsonEncode(data.Image)
job.Hash = data.Image.Hash
job.CreatedAt = time.Now()
res = h.db.Create(&job)
if res.Error != nil {
logger.Error("error with save MidJourney Job: ", res.Error)
} }
} }
if data.Raw {
prompt += " --style raw"
}
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
prompt += fmt.Sprintf(" %s", 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,
// 使用代理临时转发图片 UserId: userId,
if data.Image.URL != "" { })
image, err := utils.DownloadImage(data.Image.URL, h.App.Config.ProxyURL)
if err == nil { var jobVo vo.MidJourneyJob
data.Image.URL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image) err := utils.CopyObject(job, &jobVo)
} if err == nil {
// 推送任务到前端
client := h.mjService.Clients.Get(data.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, jobVo)
} }
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
} }
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 {
// 10 分钟还没完成的任务直接删除
if time.Now().Sub(item.CreatedAt) > time.Minute*10 {
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)
}

View 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))
}

View 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")
}

View 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)
}

View File

@@ -22,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 {

207
api/handler/sd_handler.go Normal file
View 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)
}

View File

@@ -4,23 +4,23 @@ import (
"chatplus/core" "chatplus/core"
"chatplus/core/types" "chatplus/core/types"
"chatplus/service" "chatplus/service"
"chatplus/store"
"chatplus/utils" "chatplus/utils"
"chatplus/utils/resp" "chatplus/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
) )
const CodeStorePrefix = "/verify/codes/" const CodeStorePrefix = "/verify/codes/"
type SmsHandler struct { type SmsHandler struct {
BaseHandler BaseHandler
leveldb *store.LevelDB redis *redis.Client
sms *service.AliYunSmsService sms *service.AliYunSmsService
captcha *service.CaptchaService captcha *service.CaptchaService
} }
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler { func NewSmsHandler(app *core.AppServer, client *redis.Client, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha} handler := &SmsHandler{redis: client, sms: sms, captcha: captcha}
handler.App = app handler.App = app
return handler return handler
} }
@@ -50,7 +50,7 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
} }
// 存储验证码,等待后面注册验证 // 存储验证码,等待后面注册验证
err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code) _, err = h.redis.Set(c, CodeStorePrefix+data.Mobile, code, 0).Result()
if err != nil { if err != nil {
resp.ERROR(c, "验证码保存失败") resp.ERROR(c, "验证码保存失败")
return return
@@ -58,13 +58,3 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
type statusVo struct {
EnabledMsgService bool `json:"enabled_msg_service"`
EnabledRegister bool `json:"enabled_register"`
}
// Status check if the message service is enabled
func (h *SmsHandler) Status(c *gin.Context) {
resp.SUCCESS(c, statusVo{EnabledMsgService: h.App.SysConfig.EnabledMsgService, EnabledRegister: h.App.SysConfig.EnabledRegister})
}

View File

@@ -21,11 +21,11 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan
} }
func (h *UploadHandler) Upload(c *gin.Context) { func (h *UploadHandler) Upload(c *gin.Context) {
fileURL, err := h.uploaderManager.GetActiveService().PutFile(c, "file") fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
resp.SUCCESS(c, fileURL) resp.SUCCESS(c, fileURL)
} }

View File

@@ -3,7 +3,6 @@ package handler
import ( import (
"chatplus/core" "chatplus/core"
"chatplus/core/types" "chatplus/core/types"
"chatplus/store"
"chatplus/store/model" "chatplus/store/model"
"chatplus/store/vo" "chatplus/store/vo"
"chatplus/utils" "chatplus/utils"
@@ -23,7 +22,6 @@ type UserHandler struct {
BaseHandler BaseHandler
db *gorm.DB db *gorm.DB
searcher *xdb.Searcher searcher *xdb.Searcher
leveldb *store.LevelDB
redis *redis.Client redis *redis.Client
} }
@@ -31,9 +29,8 @@ func NewUserHandler(
app *core.AppServer, app *core.AppServer,
db *gorm.DB, db *gorm.DB,
searcher *xdb.Searcher, searcher *xdb.Searcher,
levelDB *store.LevelDB,
client *redis.Client) *UserHandler { client *redis.Client) *UserHandler {
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client} handler := &UserHandler{db: db, searcher: searcher, redis: client}
handler.App = app handler.App = app
return handler return handler
} }
@@ -42,9 +39,10 @@ func NewUserHandler(
func (h *UserHandler) Register(c *gin.Context) { func (h *UserHandler) Register(c *gin.Context) {
// parameters process // parameters process
var data struct { var data struct {
Mobile string `json:"mobile"` Mobile string `json:"mobile"`
Password string `json:"password"` Password string `json:"password"`
Code int `json:"code"` Code string `json:"code"`
InviteCode string `json:"invite_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)
@@ -63,15 +61,29 @@ 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 code, err := h.redis.Get(c, key).Result()
err := h.leveldb.Get(key, &code)
if err != nil || code != data.Code { if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误") resp.ERROR(c, "短信验证码错误")
return return
} }
} }
// 验证邀请码
inviteCode := model.InviteCode{}
if data.InviteCode == "" {
if h.App.SysConfig.ForceInvite {
resp.ERROR(c, "当前系统设定必须使用邀请码才能注册")
return
}
} else {
res := h.db.Where("code = ?", data.InviteCode).First(&inviteCode)
if res.Error != nil {
resp.ERROR(c, "无效的邀请码")
return
}
}
// check if the username is exists // check if the username is exists
var item model.User var item model.User
res := h.db.Where("mobile = ?", data.Mobile).First(&item) res := h.db.Where("mobile = ?", data.Mobile).First(&item)
@@ -80,22 +92,15 @@ func (h *UserHandler) Register(c *gin.Context) {
return 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{
Password: utils.GenPassword(data.Password, salt), Password: utils.GenPassword(data.Password, salt),
Avatar: "/images/avatar/user.png", Avatar: "/images/avatar/user.png",
Salt: salt, Salt: salt,
Status: true, Status: true,
Mobile: data.Mobile, Mobile: data.Mobile,
ChatRoles: utils.JsonEncode(roleKeys), ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
ChatConfig: utils.JsonEncode(types.UserChatConfig{ ChatConfig: utils.JsonEncode(types.UserChatConfig{
ApiKeys: map[types.Platform]string{ ApiKeys: map[types.Platform]string{
types.OpenAI: "", types.OpenAI: "",
@@ -103,7 +108,7 @@ func (h *UserHandler) Register(c *gin.Context) {
types.ChatGLM: "", types.ChatGLM: "",
}, },
}), }),
Calls: h.App.SysConfig.UserInitCalls, Calls: h.App.SysConfig.InitChatCalls,
ImgCalls: h.App.SysConfig.InitImgCalls, ImgCalls: h.App.SysConfig.InitImgCalls,
} }
res = h.db.Create(&user) res = h.db.Create(&user)
@@ -113,10 +118,47 @@ func (h *UserHandler) Register(c *gin.Context) {
return return
} }
if h.App.SysConfig.EnabledMsgService { // 记录邀请关系
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码 if data.InviteCode != "" {
// 增加邀请数量
h.db.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.InviteChatCalls > 0 {
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("calls", gorm.Expr("calls + ?", h.App.SysConfig.InviteChatCalls))
}
if h.App.SysConfig.InviteImgCalls > 0 {
h.db.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls + ?", h.App.SysConfig.InviteImgCalls))
}
// 添加邀请记录
h.db.Create(&model.InviteLog{
InviterId: inviteCode.UserId,
UserId: user.Id,
Username: user.Mobile,
InviteCode: inviteCode.Code,
Reward: utils.JsonEncode(types.InviteReward{ChatCalls: h.App.SysConfig.InviteChatCalls, ImgCalls: h.App.SysConfig.InviteImgCalls}),
})
} }
resp.SUCCESS(c, user) if h.App.SysConfig.EnabledMsg {
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
}
// 自动登录创建 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 用户登录
@@ -160,10 +202,9 @@ func (h *UserHandler) Login(c *gin.Context) {
}) })
// 创建 token // 创建 token
expired := time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge))
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id, "user_id": user.Id,
"expired": expired, "expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
}) })
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil { if err != nil {
@@ -182,8 +223,8 @@ func (h *UserHandler) Login(c *gin.Context) {
// Logout 注 销 // Logout 注 销
func (h *UserHandler) Logout(c *gin.Context) { func (h *UserHandler) Logout(c *gin.Context) {
sessionId := c.GetHeader(types.ChatTokenHeader) sessionId := c.GetHeader(types.ChatTokenHeader)
token := c.GetHeader(types.UserAuthHeader) key := h.GetUserKey(c)
if _, err := h.redis.Del(c, token).Result(); err != nil { if _, err := h.redis.Del(c, key).Result(); err != nil {
logger.Error("error with delete session: ", err) logger.Error("error with delete session: ", err)
} }
// 删除 websocket 会话列表 // 删除 websocket 会话列表
@@ -221,6 +262,9 @@ type userProfile struct {
Calls int `json:"calls"` Calls int `json:"calls"`
ImgCalls int `json:"img_calls"` ImgCalls int `json:"img_calls"`
TotalTokens int64 `json:"total_tokens"` TotalTokens int64 `json:"total_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) {
@@ -267,8 +311,8 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// Password 更新密码 // UpdatePass 更新密码
func (h *UserHandler) Password(c *gin.Context) { func (h *UserHandler) UpdatePass(c *gin.Context) {
var data struct { var data struct {
OldPass string `json:"old_pass"` OldPass string `json:"old_pass"`
Password string `json:"password"` Password string `json:"password"`
@@ -307,17 +351,65 @@ func (h *UserHandler) Password(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// ResetPass 重置密码
func (h *UserHandler) ResetPass(c *gin.Context) {
var data struct {
Mobile string
Code string // 验证码
Password string // 新密码
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.User
res := h.db.Where("mobile", data.Mobile).First(&user)
if res.Error != nil {
resp.ERROR(c, "用户不存在!")
return
}
// 检查验证码
key := CodeStorePrefix + data.Mobile
if h.App.SysConfig.EnabledMsg {
code, err := h.redis.Get(c, key).Result()
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
}
password := utils.GenPassword(data.Password, user.Salt)
user.Password = password
res = h.db.Updates(&user)
if res.Error != nil {
resp.ERROR(c)
} else {
h.redis.Del(c, key)
resp.SUCCESS(c)
}
}
// BindMobile 绑定手机号 // BindMobile 绑定手机号
func (h *UserHandler) BindMobile(c *gin.Context) { func (h *UserHandler) BindMobile(c *gin.Context) {
var data struct { var data struct {
Mobile string `json:"mobile"` Mobile string `json:"mobile"`
Code int `json:"code"` Code string `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
} }
// 检查验证码
key := CodeStorePrefix + data.Mobile
code, err := h.redis.Get(c, key).Result()
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
// 检查手机号是否被其他账号绑定 // 检查手机号是否被其他账号绑定
var item model.User var item model.User
res := h.db.Where("mobile = ?", data.Mobile).First(&item) res := h.db.Where("mobile = ?", data.Mobile).First(&item)
@@ -326,15 +418,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
return return
} }
// 检查验证码
key := CodeStorePrefix + data.Mobile
var code int
err := h.leveldb.Get(key, &code)
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
user, err := utils.GetLoginUser(c, h.db) user, err := utils.GetLoginUser(c, h.db)
if err != nil { if err != nil {
resp.NotAuth(c) resp.NotAuth(c)
@@ -347,6 +430,6 @@ func (h *UserHandler) BindMobile(c *gin.Context) {
return return
} }
_ = h.leveldb.Delete(key) // 删除短信验证码 _ = h.redis.Del(c, key) // 删除短信验证码
resp.SUCCESS(c) resp.SUCCESS(c)
} }

View File

@@ -5,10 +5,15 @@ 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/oss"
"chatplus/service/payment"
"chatplus/service/sd"
"chatplus/service/wx"
"chatplus/store" "chatplus/store"
"context" "context"
"embed" "embed"
@@ -28,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 应用程序生命周期
@@ -89,9 +94,12 @@ func main() {
// 初始化数据库 // 初始化数据库
fx.Provide(store.NewGormConfig), fx.Provide(store.NewGormConfig),
fx.Provide(store.NewMysql), fx.Provide(store.NewMysql),
fx.Provide(store.NewLevelDB),
fx.Provide(store.NewRedisClient), 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) {
file, err := xdbFS.Open("res/ip2region.xdb") file, err := xdbFS.Open("res/ip2region.xdb")
@@ -107,18 +115,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.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),
@@ -128,6 +140,8 @@ func main() {
fx.Provide(admin.NewRewardHandler), fx.Provide(admin.NewRewardHandler),
fx.Provide(admin.NewDashboardHandler), fx.Provide(admin.NewDashboardHandler),
fx.Provide(admin.NewChatModelHandler), fx.Provide(admin.NewChatModelHandler),
fx.Provide(admin.NewProductHandler),
fx.Provide(admin.NewOrderHandler),
// 创建服务 // 创建服务
fx.Provide(service.NewAliYunSmsService), fx.Provide(service.NewAliYunSmsService),
@@ -135,11 +149,64 @@ func main() {
return service.NewCaptchaService(config.ApiConfig) return service.NewCaptchaService(config.ApiConfig)
}), }),
fx.Provide(oss.NewUploaderManager), 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/")
@@ -149,10 +216,11 @@ func main() {
group.GET("session", h.Session) group.GET("session", h.Session)
group.GET("profile", h.Profile) group.GET("profile", h.Profile)
group.POST("profile/update", h.ProfileUpdate) group.POST("profile/update", h.ProfileUpdate)
group.POST("password", h.Password) group.POST("password", h.UpdatePass)
group.POST("bind/mobile", h.BindMobile) group.POST("bind/mobile", h.BindMobile)
group.POST("resetPass", h.ResetPass)
}), }),
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)
@@ -161,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) {
@@ -169,7 +237,6 @@ func main() {
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) { fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
group := s.Engine.Group("/api/sms/") group := s.Engine.Group("/api/sms/")
group.GET("status", h.Status)
group.POST("code", h.SendCode) group.POST("code", h.SendCode)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) { fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
@@ -179,13 +246,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)
}), }),
// 管理后台控制器 // 管理后台控制器
@@ -199,7 +274,6 @@ func main() {
group.POST("login", h.Login) group.POST("login", h.Login)
group.GET("logout", h.Logout) group.GET("logout", h.Logout)
group.GET("session", h.Session) group.GET("session", h.Session)
group.GET("migrate", h.Migrate)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
group := s.Engine.Group("/api/admin/apikey/") group := s.Engine.Group("/api/admin/apikey/")
@@ -238,10 +312,46 @@ func main() {
group := s.Engine.Group("/api/admin/model/") group := s.Engine.Group("/api/admin/model/")
group.POST("save", h.Save) group.POST("save", h.Save)
group.GET("list", h.List) group.GET("list", h.List)
group.POST("set", h.Set)
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("enable", h.Enable)
group.POST("sort", h.Sort) group.POST("sort", h.Sort)
group.GET("remove", h.Remove) 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.Provide(handler.NewInviteHandler),
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
group := s.Engine.Group("/api/invite/")
group.GET("code", h.Code)
group.POST("list", h.List)
group.GET("hits", h.Hits)
}),
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

67
api/res/text2img.json Normal file
View 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"
}

View File

@@ -2,18 +2,16 @@ package service
import ( import (
"chatplus/core/types" "chatplus/core/types"
"chatplus/store"
"fmt" "fmt"
"github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi" "github.com/aliyun/alibaba-cloud-sdk-go/services/dysmsapi"
) )
type AliYunSmsService struct { type AliYunSmsService struct {
config *types.AppConfig config *types.AliYunSmsConfig
db *store.LevelDB
client *dysmsapi.Client client *dysmsapi.Client
} }
func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSmsService, error) { func NewAliYunSmsService(config *types.AppConfig) (*AliYunSmsService, error) {
// 创建阿里云短信客户端 // 创建阿里云短信客户端
client, err := dysmsapi.NewClientWithAccessKey( client, err := dysmsapi.NewClientWithAccessKey(
"cn-hangzhou", "cn-hangzhou",
@@ -24,8 +22,7 @@ func NewAliYunSmsService(config *types.AppConfig, db *store.LevelDB) (*AliYunSms
} }
return &AliYunSmsService{ return &AliYunSmsService{
config: config, config: &config.SmsConfig,
db: db,
client: client, client: client,
}, nil }, nil
} }
@@ -34,10 +31,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) // 短信模板中的参数
// 发送短信 // 发送短信

View File

@@ -0,0 +1,92 @@
package fun
import (
"chatplus/core/types"
"chatplus/service/oss"
"chatplus/store/model"
"chatplus/utils"
"fmt"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
// AI 绘画函数
type FuncImage struct {
name string
apiURL string
db *gorm.DB
uploadManager *oss.UploaderManager
proxyURL string
}
func NewImageFunc(db *gorm.DB, manager *oss.UploaderManager, config *types.AppConfig) FuncImage {
return FuncImage{
db: db,
name: "DALL-E3 绘画",
uploadManager: manager,
proxyURL: config.ProxyURL,
apiURL: "https://api.openai.com/v1/images/generations",
}
}
type imgReq struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
N int `json:"n"`
Size string `json:"size"`
}
type imgRes struct {
Created int64 `json:"created"`
Data []struct {
RevisedPrompt string `json:"revised_prompt"`
Url string `json:"url"`
} `json:"data"`
}
type ErrRes struct {
Error struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Param interface{} `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
func (f FuncImage) Invoke(params map[string]interface{}) (string, error) {
logger.Infof("绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
// 获取绘图 API KEY
var apiKey model.ApiKey
f.db.Where("platform = ? AND type = ?", types.OpenAI, "img").Order("last_used_at ASC").First(&apiKey)
var res imgRes
var errRes ErrRes
r, err := req.C().SetProxyURL(f.proxyURL).R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(imgReq{
Model: "dall-e-3",
Prompt: prompt,
N: 1,
Size: "1024x1024",
}).
SetErrorResult(&errRes).
SetSuccessResult(&res).Post(f.apiURL)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("error with http request: %v%v%s", err, r.Err, errRes.Error.Message)
}
// 存储图片
imgURL, err := f.uploadManager.GetUploadHandler().PutImg(res.Data[0].Url, false)
if err != nil {
return "", fmt.Errorf("下载图片失败: %s", err.Error())
}
logger.Info(imgURL)
return fmt.Sprintf("\n\n![](%s)\n", imgURL), nil
}
func (f FuncImage) Name() string {
return f.name
}
var _ Function = &FuncImage{}

View File

@@ -1,8 +1,10 @@
package function package fun
import ( import (
"chatplus/core/types" "chatplus/core/types"
logger2 "chatplus/logger" logger2 "chatplus/logger"
"chatplus/service/oss"
"gorm.io/gorm"
) )
type Function interface { type Function interface {
@@ -28,11 +30,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, db *gorm.DB, manager *oss.UploaderManager) 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.FuncImage: NewImageFunc(db, manager, config),
} }
} }

View File

@@ -1,4 +1,4 @@
package function package fun
import ( import (
"chatplus/core/types" "chatplus/core/types"

View File

@@ -1,4 +1,4 @@
package function package fun
import ( import (
"chatplus/core/types" "chatplus/core/types"

View File

@@ -1,4 +1,4 @@
package function package fun
import ( import (
"chatplus/core/types" "chatplus/core/types"

View File

@@ -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
View 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
View 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
}

257
api/service/mj/service.go Normal file
View File

@@ -0,0 +1,257 @@
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)
// 删除任务
s.db.Delete(&model.MidJourneyJob{Id: uint(task.Id)})
// 推送任务到前端
client := s.Clients.Get(task.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, vo.MidJourneyJob{
Type: task.Type.String(),
UserId: task.UserId,
MessageId: task.MessageId,
Progress: -1,
Prompt: task.Prompt,
})
}
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
View 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"`
}

View 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.Domain, 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.Domain, objectKey), nil
}
func (s AliYunOss) Delete(fileURL string) error {
objectName := filepath.Base(fileURL)
return s.bucket.DeleteObject(objectName)
}
var _ Uploader = AliYunOss{}

View File

@@ -5,24 +5,25 @@ import (
"chatplus/utils" "chatplus/utils"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
) )
type LocalStorageService struct { type LocalStorage struct {
config *types.LocalStorageConfig config *types.LocalStorageConfig
proxyURL string proxyURL string
} }
func NewLocalStorageService(config *types.AppConfig) LocalStorageService { func NewLocalStorage(config *types.AppConfig) LocalStorage {
return LocalStorageService{ return LocalStorage{
config: &config.OSS.Local, config: &config.OSS.Local,
proxyURL: config.ProxyURL, proxyURL: config.ProxyURL,
} }
} }
func (s LocalStorageService) PutFile(ctx *gin.Context, name string) (string, error) { func (s LocalStorage) PutFile(ctx *gin.Context, name string) (string, error) {
file, err := ctx.FormFile(name) file, err := ctx.FormFile(name)
if err != nil { if err != nil {
return "", fmt.Errorf("error with get form: %v", err) return "", fmt.Errorf("error with get form: %v", err)
@@ -41,14 +42,22 @@ func (s LocalStorageService) PutFile(ctx *gin.Context, name string) (string, err
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
} }
func (s LocalStorageService) PutImg(imageURL string) (string, error) { func (s LocalStorage) PutImg(imageURL string, useProxy bool) (string, error) {
filename := filepath.Base(imageURL) 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) filePath, err := utils.GenUploadPath(s.config.BasePath, filename)
if err != nil { if err != nil {
return "", fmt.Errorf("error with generate image dir: %v", err) return "", fmt.Errorf("error with generate image dir: %v", err)
} }
err = utils.DownloadFile(imageURL, filePath, s.proxyURL) if useProxy {
err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
} else {
err = utils.DownloadFile(imageURL, filePath, "")
}
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)
} }
@@ -56,9 +65,9 @@ func (s LocalStorageService) PutImg(imageURL string) (string, error) {
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
} }
func (s LocalStorageService) Delete(fileURL string) error { func (s LocalStorage) Delete(fileURL string) error {
filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1) filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
return os.Remove(filePath) return os.Remove(filePath)
} }
var _ Uploader = LocalStorageService{} var _ Uploader = LocalStorage{}

View File

@@ -8,35 +8,46 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
) )
type MinioService struct { type MiniOss struct {
config *types.MinioConfig config *types.MiniOssConfig
client *minio.Client client *minio.Client
proxyURL string proxyURL string
} }
func NewMinioService(appConfig *types.AppConfig) (MinioService, error) { func NewMiniOss(appConfig *types.AppConfig) (MiniOss, error) {
config := &appConfig.OSS.Minio config := &appConfig.OSS.Minio
minioClient, err := minio.New(config.Endpoint, &minio.Options{ minioClient, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""), Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
Secure: config.UseSSL, Secure: config.UseSSL,
}) })
if err != nil { if err != nil {
return MinioService{}, err return MiniOss{}, err
} }
return MinioService{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil return MiniOss{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
} }
func (s MinioService) PutImg(imageURL string) (string, error) { func (s MiniOss) PutImg(imageURL string, useProxy bool) (string, error) {
imageData, err := utils.DownloadImage(imageURL, s.proxyURL) var imageData []byte
var err error
if useProxy {
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
} else {
imageData, err = utils.DownloadImage(imageURL, "")
}
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)
} }
fileExt := filepath.Ext(filepath.Base(imageURL)) 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) filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
info, err := s.client.PutObject( info, err := s.client.PutObject(
context.Background(), context.Background(),
@@ -51,7 +62,7 @@ func (s MinioService) PutImg(imageURL string) (string, error) {
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
} }
func (s MinioService) PutFile(ctx *gin.Context, name string) (string, error) { func (s MiniOss) PutFile(ctx *gin.Context, name string) (string, error) {
file, err := ctx.FormFile(name) file, err := ctx.FormFile(name)
if err != nil { if err != nil {
return "", fmt.Errorf("error with get form: %v", err) return "", fmt.Errorf("error with get form: %v", err)
@@ -75,9 +86,9 @@ func (s MinioService) PutFile(ctx *gin.Context, name string) (string, error) {
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
} }
func (s MinioService) Delete(fileURL string) error { func (s MiniOss) Delete(fileURL string) error {
objectName := filepath.Base(fileURL) objectName := filepath.Base(fileURL)
return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{}) return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
} }
var _ Uploader = MinioService{} var _ Uploader = MiniOss{}

View File

@@ -9,20 +9,22 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/qiniu/go-sdk/v7/auth/qbox" "github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage" "github.com/qiniu/go-sdk/v7/storage"
"net/url"
"path/filepath" "path/filepath"
"time" "time"
) )
type QiNiuService struct { type QinNiuOss struct {
config *types.QiNiuConfig config *types.QiNiuOssConfig
token string mac *qbox.Mac
uploader *storage.FormUploader putPolicy storage.PutPolicy
manager *storage.BucketManager uploader *storage.FormUploader
proxyURL string manager *storage.BucketManager
dir string proxyURL string
dir string
} }
func NewQiNiuService(appConfig *types.AppConfig) QiNiuService { func NewQiNiuOss(appConfig *types.AppConfig) QinNiuOss {
config := &appConfig.OSS.QiNiu config := &appConfig.OSS.QiNiu
// build storage uploader // build storage uploader
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone)) zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
@@ -36,17 +38,18 @@ func NewQiNiuService(appConfig *types.AppConfig) QiNiuService {
putPolicy := storage.PutPolicy{ putPolicy := storage.PutPolicy{
Scope: config.Bucket, Scope: config.Bucket,
} }
return QiNiuService{ return QinNiuOss{
config: config, config: config,
token: putPolicy.UploadToken(mac), mac: mac,
uploader: formUploader, putPolicy: putPolicy,
manager: storage.NewBucketManager(mac, &storeConfig), uploader: formUploader,
proxyURL: appConfig.ProxyURL, manager: storage.NewBucketManager(mac, &storeConfig),
dir: "chatgpt-plus", proxyURL: appConfig.ProxyURL,
dir: "chatgpt-plus",
} }
} }
func (s QiNiuService) PutFile(ctx *gin.Context, name string) (string, error) { func (s QinNiuOss) PutFile(ctx *gin.Context, name string) (string, error) {
// 解析表单 // 解析表单
file, err := ctx.FormFile(name) file, err := ctx.FormFile(name)
if err != nil { if err != nil {
@@ -64,7 +67,7 @@ func (s QiNiuService) PutFile(ctx *gin.Context, name string) (string, error) {
// 上传文件 // 上传文件
ret := storage.PutRet{} ret := storage.PutRet{}
extra := storage.PutExtra{} extra := storage.PutExtra{}
err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra) err = s.uploader.Put(ctx, &ret, s.putPolicy.UploadToken(s.mac), key, src, file.Size, &extra)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -72,27 +75,37 @@ func (s QiNiuService) PutFile(ctx *gin.Context, name string) (string, error) {
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
} }
func (s QiNiuService) PutImg(imageURL string) (string, error) { func (s QinNiuOss) PutImg(imageURL string, useProxy bool) (string, error) {
imageData, err := utils.DownloadImage(imageURL, s.proxyURL) var imageData []byte
var err error
if useProxy {
imageData, err = utils.DownloadImage(imageURL, s.proxyURL)
} else {
imageData, err = utils.DownloadImage(imageURL, "")
}
if err != nil { if err != nil {
return "", fmt.Errorf("error with download image: %v", err) return "", fmt.Errorf("error with download image: %v", err)
} }
fileExt := filepath.Ext(filepath.Base(imageURL)) 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) key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
ret := storage.PutRet{} ret := storage.PutRet{}
extra := storage.PutExtra{} extra := storage.PutExtra{}
// 上传文件字节数据 // 上传文件字节数据
err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra) err = s.uploader.Put(context.Background(), &ret, s.putPolicy.UploadToken(s.mac), key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
if err != nil { if err != nil {
return "", err return "", err
} }
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
} }
func (s QiNiuService) Delete(fileURL string) error { func (s QinNiuOss) Delete(fileURL string) error {
objectName := filepath.Base(fileURL) objectName := filepath.Base(fileURL)
key := fmt.Sprintf("%s/%s", s.dir, objectName) key := fmt.Sprintf("%s/%s", s.dir, objectName)
return s.manager.Delete(s.config.Bucket, key) return s.manager.Delete(s.config.Bucket, key)
} }
var _ Uploader = QiNiuService{} var _ Uploader = QinNiuOss{}

View File

@@ -4,6 +4,6 @@ import "github.com/gin-gonic/gin"
type Uploader interface { type Uploader interface {
PutFile(ctx *gin.Context, name string) (string, error) PutFile(ctx *gin.Context, name string) (string, error)
PutImg(imageURL string) (string, error) PutImg(imageURL string, useProxy bool) (string, error)
Delete(fileURL string) error Delete(fileURL string) error
} }

View File

@@ -6,36 +6,46 @@ import (
) )
type UploaderManager struct { type UploaderManager struct {
active string handler Uploader
uploadServices map[string]Uploader
} }
const Local = "LOCAL" const Local = "LOCAL"
const Minio = "MINIO" const Minio = "MINIO"
const QiNiu = "QINIU" const QiNiu = "QINIU"
const AliYun = "ALIYUN"
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) { func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
services := make(map[string]Uploader)
if config.OSS.Minio.AccessKey != "" {
minioService, err := NewMinioService(config)
if err != nil {
return nil, err
}
services[Minio] = minioService
}
if config.OSS.Local.BasePath != "" {
services[Local] = NewLocalStorageService(config)
}
if config.OSS.QiNiu.AccessKey != "" {
services[QiNiu] = NewQiNiuService(config)
}
active := Local active := Local
if config.OSS.Active != "" { if config.OSS.Active != "" {
active = strings.ToUpper(config.OSS.Active) active = strings.ToUpper(config.OSS.Active)
} }
return &UploaderManager{uploadServices: services, active: active}, nil 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) GetActiveService() Uploader { func (m *UploaderManager) GetUploadHandler() Uploader {
return m.uploadServices[m.active] return m.handler
} }

View 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
View 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, &params)
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.Model(&model.User{}).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
View 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
View 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
View 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))
}

View 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
}

View File

@@ -0,0 +1,145 @@
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("ClearOrders", e.ClearOrders)
e.executor.RegTask("ResetVipCalls", e.ResetVipCalls)
return e.executor.Run()
}
// ClearOrders 清理未支付的订单,如果没有抛出异常则表示执行成功
func (e *XXLJobExecutor) ClearOrders(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.Unscoped().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...)
}

View File

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

View File

@@ -1,96 +0,0 @@
package store
import (
"chatplus/store/vo"
"encoding/json"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type LevelDB struct {
driver *leveldb.DB
}
func NewLevelDB() (*LevelDB, error) {
db, err := leveldb.OpenFile("data/leveldb", nil)
if err != nil {
return nil, err
}
return &LevelDB{
driver: db,
}, nil
}
func (db *LevelDB) Put(key string, value interface{}) error {
bytes, err := json.Marshal(value)
if err != nil {
return err
}
return db.driver.Put([]byte(key), bytes, nil)
}
func (db *LevelDB) Get(key string, value interface{}) error {
bytes, err := db.driver.Get([]byte(key), nil)
if err != nil {
return err
}
return json.Unmarshal(bytes, &value)
}
func (db *LevelDB) Search(prefix string) []string {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
for iter.Next() {
items = append(items, string(iter.Value()))
}
return items
}
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *vo.Page {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
res := &vo.Page{Page: page, PageSize: pageSize}
// 计算数据总数和总页数
total := 0
for iter.Next() {
total++
}
res.TotalPage = (total + pageSize - 1) / pageSize
res.Total = int64(total)
// 计算目标页码的起始和结束位置
start := (page - 1) * pageSize
if start > total {
return nil
}
end := start + pageSize
if end > total {
end = total
}
// 跳转到目标页码的起始位置
count := 0
for iter.Next() {
if count >= start {
items = append(items, string(iter.Value()))
}
count++
}
iter.Release()
res.Items = items
return res
}
func (db *LevelDB) Delete(key string) error {
return db.driver.Delete([]byte(key), nil)
}
// Close release resources
func (db *LevelDB) Close() error {
return db.driver.Close()
}

View File

@@ -4,6 +4,7 @@ package model
type ApiKey struct { type ApiKey struct {
BaseModel BaseModel
Platform string Platform string
Type string // 用途 chat => 聊天img => 绘图
Value string // API Key 的值 Value string // API Key 的值
LastUsedAt int64 // 最后使用时间 LastUsedAt int64 // 最后使用时间
} }

View File

@@ -7,4 +7,6 @@ type ChatModel struct {
Value string // API Key 的值 Value string // API Key 的值
SortNum int SortNum int
Enabled bool Enabled bool
Weight int // 对话权重,每次对话扣减多少次对话额度
Open bool // 是否开放模型给所有人使用
} }

View File

@@ -0,0 +1,12 @@
package model
import "time"
type InviteCode struct {
Id uint `gorm:"primarykey;column:id"`
UserId uint
Code string
Hits int // 点击次数
RegNum int // 注册人数
CreatedAt time.Time
}

View File

@@ -0,0 +1,15 @@
package model
import (
"time"
)
type InviteLog struct {
Id uint `gorm:"primarykey;column:id"`
InviterId uint
UserId uint
Username string
InviteCode string
Reward string `gorm:"column:reward_json"` // 邀请奖励
CreatedAt time.Time
}

View File

@@ -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
View 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
}

View 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
View 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"
}

View File

@@ -11,8 +11,11 @@ type User struct {
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
} }

View File

@@ -21,6 +21,9 @@ func NewGormConfig() *gorm.Config {
func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) { func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) {
db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config) db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config)
if err != nil {
return nil, err
}
sqlDB, err := db.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
@@ -29,8 +32,6 @@ func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error)
sqlDB.SetMaxIdleConns(32) sqlDB.SetMaxIdleConns(32)
sqlDB.SetMaxOpenConns(512) sqlDB.SetMaxOpenConns(512)
sqlDB.SetConnMaxLifetime(time.Hour) sqlDB.SetConnMaxLifetime(time.Hour)
if err != nil {
return nil, err
}
return db, nil return db, nil
} }

41
api/store/redis_queue.go Normal file
View 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)
}

View File

@@ -4,6 +4,7 @@ package vo
type ApiKey struct { type ApiKey struct {
BaseVo BaseVo
Platform string `json:"platform"` Platform string `json:"platform"`
Type string `json:"type"`
Value string `json:"value"` // API Key 的值 Value string `json:"value"` // API Key 的值
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间 LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
} }

View File

@@ -6,4 +6,7 @@ type ChatModel struct {
Name string `json:"name"` Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Weight int `json:"weight"`
Open bool `json:"open"`
} }

View File

@@ -0,0 +1,10 @@
package vo
type InviteCode struct {
Id uint `json:"id"`
UserId uint `json:"user_id"`
Code string `json:"code"`
Hits int `json:"hits"`
RegNum int `json:"reg_num"`
CreatedAt int64 `json:"created_at"`
}

View File

@@ -0,0 +1,15 @@
package vo
import (
"chatplus/core/types"
)
type InviteLog struct {
Id uint `json:"id"`
InviterId uint `json:"inviter_id"`
UserId uint `json:"user_id"`
Username string `json:"username"`
InviteCode string `json:"invite_code"`
Reward types.InviteReward `json:"reward"`
CreatedAt int64 `json:"created_at"`
}

17
api/store/vo/mj_job.go Normal file
View 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
View 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
View 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
View 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"`
}

View File

@@ -12,8 +12,11 @@ type User struct {
ImgCalls int `json:"img_calls"` ImgCalls int `json:"img_calls"`
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置 ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
ChatRoles []string `json:"chat_roles"` // 聊天角色集合 ChatRoles []string `json:"chat_roles"` // 聊天角色集合
ChatModels []string `json:"chat_models"` // AI模型集合
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
} }

View File

@@ -1,199 +1,5 @@
package main package main
import (
"bufio"
"chatplus/core"
"chatplus/core/types"
"chatplus/service/oss"
"chatplus/utils"
"context"
"encoding/json"
"fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/pkoukk/tiktoken-go"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
)
func main() { func main() {
imageURL := "https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png"
fmt.Println(filepath.Ext(filepath.Base(imageURL)))
}
// 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 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
}
func minio() {
config := core.NewDefaultConfig()
config.ProxyURL = "http://localhost:7777"
config.OSS.Minio = types.MinioConfig{
Endpoint: "localhost:9010",
AccessKey: "ObWIEyXaQUHOYU26L0oI",
AccessSecret: "AJW3HHhlGrprfPcmiC7jSOSzVCyrlhX4AnOAUzqI",
Bucket: "chatgpt-plus",
UseSSL: false,
Domain: "http://localhost:9010",
}
minioService, err := oss.NewMinioService(config)
if err != nil {
panic(err)
}
url, err := minioService.PutImg("https://cdn.discordapp.com/attachments/1139552247693443184/1141619433752768572/lisamiller4099_A_beautiful_fairy_sister_from_Chinese_mythology__3162726e-5ee4-4f60-932b-6b78b375eaef.png")
if err != nil {
panic(err)
}
fmt.Println(url)
}

View File

@@ -1,13 +1,20 @@
package utils package utils
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"github.com/nfnt/resize"
"github.com/skip2/go-qrcode"
"image"
"image/color"
"image/draw"
"image/jpeg"
"io"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
) )
// CopyObject 拷贝对象 // CopyObject 拷贝对象
@@ -36,7 +43,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
@@ -52,6 +59,8 @@ func CopyObject(src interface{}, dst interface{}) error {
value.Set(reflect.ValueOf("")) value.Set(reflect.ValueOf(""))
} }
} }
} else if field.Type.Kind() != value.Type().Kind() { // 不同类型的字段过滤掉
continue
} else { // 简单数据类型的强制类型转换 } else { // 简单数据类型的强制类型转换
switch value.Kind() { switch value.Kind() {
case reflect.Int: case reflect.Int:
@@ -138,3 +147,59 @@ func IntValue(str string, defaultValue int) int {
} }
return value 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
}

View File

@@ -12,7 +12,7 @@ import (
var logger = logger2.GetLogger() var logger = logger2.GetLogger()
// ReplyChunkMessage 回复客户片段端消息 // ReplyChunkMessage 回复客户片段端消息
func ReplyChunkMessage(client *types.WsClient, message types.WsMessage) { func ReplyChunkMessage(client *types.WsClient, message interface{}) {
msg, err := json.Marshal(message) msg, err := json.Marshal(message)
if err != nil { if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error()) logger.Errorf("Error for decoding json data: %v", err.Error())

View File

@@ -9,7 +9,7 @@ make clean linux
cd ../web cd ../web
npm run build npm run build
cd ../docker cd ../build
# remove docker image if exists # remove docker image if exists
docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version docker rmi -f registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
@@ -23,4 +23,4 @@ docker build --platform linux/amd64 -t registry.cn-shenzhen.aliyuncs.com/geekmas
if [ "$2" = "push" ];then if [ "$2" = "push" ];then
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-api:$version
docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version docker push registry.cn-shenzhen.aliyuncs.com/geekmaster/chatgpt-plus-web:$version
fi fi

View File

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

Some files were not shown because too many files have changed in this diff Show More