Compare commits

...

207 Commits

Author SHA1 Message Date
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
RockYang
f943669e18 use absolute path for all routes 2023-09-07 10:27:18 +08:00
RockYang
3b26735998 feat: add linux package configs for desktop 2023-09-07 09:45:11 +08:00
RockYang
79d25769ee feat: extract css to a single file, add chat_id path variable support 2023-09-07 09:29:16 +08:00
RockYang
1dd6800987 feat: use eletron to build desktop version for chatgpt-plus 2023-09-06 18:19:27 +08:00
RockYang
5e673a9ee0 opt: clear space in the reward transfer number 2023-09-06 17:09:55 +08:00
RockYang
92eb67a2af feat: add QiNiu OSS storage implements 2023-09-06 14:37:13 +08:00
RockYang
b1bed59be2 remove username search for console user list page 2023-09-05 17:52:46 +08:00
RockYang
0ac732a3a3 update sql file, remove truncate table sql 2023-09-05 17:05:33 +08:00
RockYang
bf3f68fa19 fix: 采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug 2023-09-05 16:47:40 +08:00
RockYang
46a551df16 docs: update change log and create a new release version 2023-09-05 14:28:30 +08:00
RockYang
20a12462b1 fix: replace session handler with jwt authorization 2023-09-05 11:47:03 +08:00
RockYang
a49fb1940e refactor: refactor mobile pages for the chat model updating 2023-09-04 18:15:56 +08:00
RockYang
32774d23c7 feat: allow user to set custom api keys for different platforms 2023-09-04 17:34:29 +08:00
RockYang
7ecd7eeba1 refactor: refactor chat model, replace mode value with mode id. refactored system config module, add seperate configration for every chat model 2023-09-04 16:32:20 +08:00
RockYang
0cc9cf8b45 refactor: refactor chat_handler, add support for Azure and ChatGLM 2023-09-04 06:43:15 +08:00
RockYang
d06f94bddd docs: add log dir to docker 2023-08-24 08:55:11 +08:00
RockYang
b5955f08c9 docs: update docker-compose file 2023-08-23 06:56:04 +08:00
RockYang
c120569894 chore: update bin file name 2023-08-22 22:18:07 +08:00
RockYang
aa376f1737 feat: change order by for login log and reward records 2023-08-22 22:04:46 +08:00
RockYang
0f8a0f89e3 opt: optimize element ui dialog styles, make it to the center of screen 2023-08-22 08:57:14 +08:00
RockYang
68dc261b44 opt: upgrade element-plus to 2.3.0, optimize the styles for for element dialog 2023-08-22 07:14:13 +08:00
RockYang
4cf3af0c7b chore: remove debug codes 2023-08-21 10:55:49 +08:00
RockYang
b99b6735d9 doc: update readme docs 2023-08-21 06:57:04 +08:00
RockYang
52189b7880 feat: delete the old avatar image file when update the user profile 2023-08-21 06:27:30 +08:00
RockYang
3dbeb1ccb6 feat: add oss service factory implements, add support for setting custom upload handler, localstorage and minio oss 2023-08-20 22:29:08 +08:00
RockYang
5a0f272fa8 opt: optimize code for remove chat item 2023-08-20 19:06:18 +08:00
RockYang
6561b99f8f feat: add minio service implementation, download midjourney image to local storage 2023-08-20 16:17:42 +08:00
RockYang
329e3eee21 opt:Optimize the algorithm to check whether Midjourney images can continue to variation 2023-08-17 14:20:16 +08:00
RockYang
07049c9afb docs: update demo url 2023-08-17 07:18:44 +08:00
RockYang
36c5dd7eaa chore: show img_calls num for user profile page 2023-08-17 07:09:53 +08:00
RockYang
b84039b506 docs: update docs 2023-08-17 06:55:11 +08:00
RockYang
fab43097dc feat: add authorization for MidJourney function calls 2023-08-16 23:16:44 +08:00
RockYang
c8998ba294 chore: update database sql file 2023-08-15 22:53:32 +08:00
RockYang
40b2466adc opt: unset timeout for websocket connection 2023-08-15 18:29:53 +08:00
RockYang
35fedbe817 feat: midjourney image variation function is ready 2023-08-15 17:31:02 +08:00
RockYang
827acdd3f9 feat: midjourney image upscale function is ready 2023-08-15 15:28:40 +08:00
RockYang
6c76086916 feat: midjourney drawing image function is ready 2023-08-14 17:59:21 +08:00
RockYang
373370fde5 feat: add midjourney message receive handler 2023-08-14 07:09:52 +08:00
RockYang
2165ba3406 feat: add midjouney api implements, optimize function calls 2023-08-11 18:46:56 +08:00
RockYang
b0e02b43fc feat: remove wechat bot, replace with api callback for receive wechat payment transactions 2023-08-10 17:24:23 +08:00
RockYang
2107c13b3d feat: add samples and function introduce in welcome page 2023-08-08 14:19:09 +08:00
RockYang
5f41aecc8d docs: add dashboard image 2023-08-07 20:07:05 +08:00
RockYang
6840a13370 feat: enabled user to set default GPT model for chat 2023-08-04 14:25:54 +08:00
RockYang
8f1e28c0ab feat: chat export function is ready 2023-08-04 12:08:07 +08:00
RockYang
7903eed284 feat: add chat export button 2023-08-03 18:24:30 +08:00
RockYang
0d49ea0d41 feat: add rewards statistic in dashboard page 2023-08-03 10:19:37 +08:00
RockYang
2ee4db5e48 opt: add enabled_msg_service config var to system config database 2023-08-03 10:04:45 +08:00
RockYang
48c4789505 opt: optimize build script, auto push to aliyun docker hub 2023-08-02 18:09:19 +08:00
RockYang
4e65a5b1a1 opt: optimize the layout for regiseter page. add function to disable registration 2023-08-02 17:44:13 +08:00
RockYang
b09d23f97f feat: the dashboard page is ready for admin console 2023-08-02 16:37:47 +08:00
RockYang
3529649ba9 refactor: refactor admin console page layout 2023-08-02 15:00:18 +08:00
RockYang
fdd659f393 fix: fixed bug for chat request's token reach the max token limit for OpenAI 2023-08-01 17:58:03 +08:00
RockYang
9eb8da2789 feat: new function for add user in manger console user list page 2023-08-01 16:02:49 +08:00
RockYang
ffb1ef0470 doc: add video tutorial for deploying system 2023-08-01 10:15:43 +08:00
RockYang
862c6aea43 docs: remove docs for deploy manually 2023-07-31 20:04:22 +08:00
RockYang
54fe4b7588 opt: optimize the build script and docker-compose yaml file 2023-07-31 17:31:02 +08:00
RockYang
c6062ee70e feat: add env var to set log level 2023-07-31 08:34:11 +08:00
RockYang
bed184dc1f feat: update system config cache for AppServer when updating system config in admin console 2023-07-31 08:13:20 +08:00
RockYang
29094ba3b3 add system config cache for AppServer object 2023-07-31 06:56:28 +08:00
RockYang
a18188876c feat: allow user to login with username and mobile no 2023-07-27 15:37:50 +08:00
RockYang
4faee3e48e fix: deauthorize some apis 2023-07-27 10:53:14 +08:00
RockYang
1a6afcd266 fix: fix bug for mobile verify code function not available in register page 2023-07-27 10:29:59 +08:00
RockYang
f567831d92 style: adjust reward image styles 2023-07-26 17:57:19 +08:00
RockYang
cf36ca4285 chore: optimize build script, compress reward img 2023-07-26 17:48:16 +08:00
RockYang
0e4ae01498 feat: add reset password function to user list page 2023-07-26 15:22:11 +08:00
RockYang
7b90f8cb13 docs: add change log file 2023-07-25 17:37:20 +08:00
RockYang
c33215529a docs: 更新文档和数据库文件 2023-07-25 17:25:23 +08:00
RockYang
c5be114db2 feat: 完成人机交互验证 API 接入,增加短信防刷验证 2023-07-25 17:00:24 +08:00
RockYang
cab955c292 feat: 头条,微博热搜等函数 API 实现 2023-07-25 15:02:43 +08:00
RockYang
ca8c8e6490 opt: 取消强制手机号验证,更新配置 2023-07-24 18:18:09 +08:00
RockYang
253951b4b3 feat: 完成众筹后台管理功能 2023-07-24 15:59:29 +08:00
RockYang
4d6444ebf3 chore: add error log for reading chat response buffer 2023-07-24 12:05:38 +08:00
RockYang
94d8d8a9d4 opt: record user_id for reward verify 2023-07-22 08:42:30 +08:00
RockYang
e02badf7bb feat: reward verify is ready 2023-07-21 22:29:14 +08:00
RockYang
dd88622c64 feat: 增加打赏核销功能 2023-07-21 18:26:51 +08:00
RockYang
c4d7126c4d feat: 集成微信收款服务 2023-07-20 17:46:32 +08:00
RockYang
86bc063941 put logs in log file 2023-07-20 16:36:12 +08:00
RockYang
dce85eb519 fix: 修复阿里云短信发送失败的 bug 2023-07-16 16:05:11 +08:00
RockYang
4ab879d697 fix: 用户使用自己绑定的 API 调用,则不计算 calls 和 token 消耗 2023-07-16 11:55:45 +08:00
RockYang
681e52df50 Merge branch 'main' into prod 2023-07-16 09:57:27 +08:00
RockYang
fb554c0315 chore: 更新项目打包脚本 2023-07-16 09:55:00 +08:00
RockYang
accf8eeb77 feat: plugin function is ready 2023-07-15 21:52:30 +08:00
RockYang
3e41edd3b5 feat: 支持上下文深度配置,计算每轮对话消耗的总 token 数量 2023-07-15 18:37:25 +08:00
RockYang
9126cfff20 refactor: 更改 OpenAI 请求 Body 数据结构,兼容函数调用请求 2023-07-15 18:00:40 +08:00
RockYang
9806d5ff4c chore: small fixs 2023-07-14 18:00:01 +08:00
RockYang
d1d13a72e4 feat: 完成每日早报函数开发 2023-07-10 18:59:53 +08:00
RockYang
00c520d066 fix: fixed conflicts 2023-07-10 10:11:17 +08:00
RockYang
797ff66474 docs: update readme file 2023-07-10 09:44:24 +08:00
RockYang
9d51a478b9 refactor: 调整项目目录结构,移除其他语言 API 目录 2023-07-10 09:42:11 +08:00
RockYang
1d4179df75 test: add plugin test code 2023-07-10 07:05:56 +08:00
RockYang
917b6012e8 fixed conflicts 2023-07-06 10:49:38 +08:00
RockYang
da14632794 opt: add tip message when no available key 2023-07-06 10:47:36 +08:00
RockYang
a868a8a8b7 opt: add tip message when no available key 2023-07-06 10:34:01 +08:00
RockYang
5037df744f fix: 取消 ElMessage 的 appendTo 属性,防止被 Dialog 组件覆盖 2023-07-04 17:59:55 +08:00
RockYang
da88a501ad opt: 优化验证码发送逻辑,加入防刷验证 2023-07-04 17:15:02 +08:00
RockYang
b9885e8de4 Merge branch 'main' into feat-sms 2023-07-04 10:55:20 +08:00
RockYang
22efe81080 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-07-04 10:54:35 +08:00
RockYang
2926717aef docs: update README, 增加免责申明 2023-07-04 10:52:52 +08:00
RockYang
a49d54d66c feat: 短信验证码功能已完成,手机端同步实现。 2023-07-03 15:18:15 +08:00
RockYang
ce0267e25b opt: 将短信发送按钮封装成组件 2023-07-03 06:55:15 +08:00
RockYang
9088d22a66 feat: 注册短信验证码验证功能已经开启 2023-07-02 20:51:13 +08:00
RockYang
1ff32d5d0a Merge branch 'main' into prod 2023-07-02 00:04:59 +08:00
RockYang
adf6916598 fix: 修复 nodejs apple M1 跨平台打包,运行报错 exec format error 2023-07-02 00:04:12 +08:00
RockYang
31c14bf748 feat: 增加用户 token 消耗统计功能 2023-07-01 23:29:24 +08:00
RockYang
5395385d1e Merge branch 'main' into prod 2023-07-01 10:24:08 +08:00
RockYang
0035da548b Merge pull request #18 from ly307787186/main
Update Setting.vue
2023-07-01 10:22:01 +08:00
RockYang
9bceaade05 opt: remove chat role info from user login api's response 2023-06-30 18:52:43 +08:00
ly307787186
3194becdad Update Setting.vue
关掉aip-key 必需的验证
2023-06-30 13:20:30 +08:00
RockYang
6174b17c24 fix: fixed bug for Emoji can not insert to MySQL 2023-06-30 09:15:57 +08:00
RockYang
53fa4a20e9 chore: change ubuntu docker image with aliyun 2023-06-29 15:56:11 +08:00
RockYang
43c1de51f5 chore: add test code for fix role icon url of db 2023-06-29 08:58:19 +08:00
RockYang
7eb8c5ec35 fix: 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题 2023-06-28 20:01:44 +08:00
RockYang
296bf63196 fix: 修复 PC 端聊天界面滚动条问题 2023-06-28 18:16:28 +08:00
RockYang
6c65a21692 opt: 优化启动参数接收处理 2023-06-28 05:51:55 +08:00
RockYang
daf83cfc84 opt: 通过环境变量来传参,修正 docker compose 配置参数 2023-06-27 18:29:46 +08:00
RockYang
871f5d39e4 docs: 更新文档,新增移动端预览图 2023-06-27 14:28:00 +08:00
RockYang
3f91f37aff fix: 修复 markdown 换行符不解析的 Bug,修复新发布的模型 token 统计失败错误 2023-06-27 14:18:20 +08:00
RockYang
a08981f876 feat: vue-mobile => 完成用户信息修改功能,前后端都添加文件上传功能。 2023-06-27 12:11:55 +08:00
RockYang
5187a43543 feat: vue-mobile => 完成移动端聊天配置功能 2023-06-26 18:18:45 +08:00
RockYang
6a733de556 feat: vue-mobile => 完成会话聊天页面功能,增加主题切换功能 2023-06-26 16:39:00 +08:00
RockYang
b9e9eae93f feat: vue-mobile => 优化聊天记录拍版样式 2023-06-25 18:21:38 +08:00
RockYang
811f12135a feat: vue-mobile => 完善移动端聊天列表页功能 2023-06-25 17:01:04 +08:00
RockYang
2c172c0851 fixed: go-api => 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug 2023-06-25 11:34:55 +08:00
RockYang
399a16fa28 docs: 增加容器部署文档 2023-06-25 11:06:18 +08:00
RockYang
d971e95900 opt: 优化前端登录判断逻辑 2023-06-25 09:46:23 +08:00
RockYang
0b6940b121 feat: chat list page for mobile is ready 2023-06-25 06:53:22 +08:00
RockYang
ad0f96fcb1 feat: 完成移动端前段框架搭建 2023-06-24 11:45:26 +08:00
RockYang
063b5655f7 style: 调整聊天侧边栏样式 2023-06-23 18:31:50 +08:00
RockYang
d03ed6570b opt: 优化 docker-compse 构建脚本,修复后端路由 Bug 2023-06-23 18:04:16 +08:00
RockYang
1795a891ce chore: 添加 docker 镜像构建脚本 2023-06-23 07:08:16 +08:00
RockYang
05bdd81646 opt: 抽离 session 验证函数,修正前端路由覆盖 bug 2023-06-23 06:31:25 +08:00
RockYang
cba54be913 chore: 使用阿里云镜像仓库 2023-06-22 22:23:48 +08:00
RockYang
da0acfe851 docs: 增加 docker-compose 部署支持 2023-06-22 22:14:18 +08:00
RockYang
133a127d8a docs: 完善新版本文档 2023-06-22 15:47:51 +08:00
RockYang
8da9b52eae refactor: embed xdb file for ip2region 2023-06-22 11:08:44 +08:00
RockYang
2700b63887 style: 调整后台管理框架样式 2023-06-21 18:44:18 +08:00
RockYang
17713d05ec refactor: 更新 iconfont 图标,增加打赏二维码弹窗 2023-06-21 15:34:38 +08:00
RockYang
3674d9da85 refactor: refactor the frame layout of admin module 2023-06-21 14:22:28 +08:00
RockYang
0e6606e469 refactor: user login log list for admin is ready 2023-06-21 06:53:41 +08:00
RockYang
feff1684c4 feat: API Key manage is ready 2023-06-20 18:05:33 +08:00
RockYang
81e08e02ff opt: optimize role sorting 2023-06-20 16:07:26 +08:00
RockYang
f593526bd4 refactor: chat role manage for admin is ready 2023-06-20 11:46:13 +08:00
RockYang
d244ad9983 refactor: user remove is ready for console 2023-06-20 07:08:37 +08:00
RockYang
65a01f4776 refactor: 管理后台用户编辑功能 is ready 2023-06-19 21:53:07 +08:00
RockYang
5f812ae649 refactor: 管理后台用户列表页面重构 2023-06-19 18:23:09 +08:00
RockYang
fda811de97 refactor: 完成管理后台的系统设置页面重构 2023-06-19 15:58:52 +08:00
RockYang
fe7f021ddb feat: admin login page is ready 2023-06-19 11:09:23 +08:00
RockYang
120e54fb29 refactor: refactor controller handler module and admin module 2023-06-19 07:06:59 +08:00
RockYang
cd809d17d3 style: optimize code styles 2023-06-18 15:17:59 +08:00
RockYang
e7fd29b9cb style: optimize styles for change password page 2023-06-17 00:02:49 +08:00
RockYang
c008f33bc3 fix: fixed bu for 'slice bounds out of range' 2023-06-16 23:30:04 +08:00
RockYang
7b5cacd6f1 style: add logo at login and register page 2023-06-16 18:14:43 +08:00
RockYang
d88219c726 chore: tracke the logo file 2023-06-16 17:44:38 +08:00
RockYang
a3ee7ca2d8 feat: import iconfont styles, change password function is ready 2023-06-16 17:28:21 +08:00
RockYang
6f37024e34 fix: add lock map data structure, fixed bug for 'concurrent map writes' 2023-06-16 15:32:11 +08:00
RockYang
3347b4c990 feat: add footbar for login and register page, add send button for chat page 2023-06-16 13:57:05 +08:00
RockYang
6a2122e1ac chore: compress bg images 2023-06-16 11:14:45 +08:00
RockYang
4b024e0ad7 chore: delete bin file 2023-06-16 10:01:34 +08:00
RockYang
b06a065d44 fix: fixed bug for ssl websocket url 2023-06-16 10:00:05 +08:00
RockYang
15b77ad10a chore: update config sample file 2023-06-15 16:03:42 +08:00
RockYang
180bd2a1db opt: add email and mobile validation for register 2023-06-15 15:09:00 +08:00
RockYang
06fb7b41b4 opt: automatic get the host for api and websocket from the 'location' var 2023-06-15 14:39:05 +08:00
RockYang
088fd14c03 opt: update the main chat compnent after updating the user profile 2023-06-15 11:29:16 +08:00
RockYang
1e84332119 refactor: add system config key 'user_init_calls' to init the new register user's api calls 2023-06-15 10:06:21 +08:00
RockYang
01d61ab19b refactor: V3 版本重构已基本完成 2023-06-15 09:41:30 +08:00
RockYang
b4569d7fe2 Merge pull request #6 from EyreFree/master
feat: docker supported
2023-06-05 08:32:34 +08:00
EyreFree
a9ecaed5bd docker 2023-06-03 20:48:32 +08:00
RockYang
654057e7a7 chore: 打招呼的时候输出本项目地址 2023-05-09 16:11:50 +08:00
RockYang
f4e5265aaf Update 1.bug.yml 2023-05-09 15:43:48 +08:00
RockYang
85d85fbe10 Update 1.bug.yml 2023-05-09 15:39:47 +08:00
RockYang
d4bd071487 Update 1.bug.yml 2023-05-09 15:35:07 +08:00
RockYang
3cee291997 Update 1.bug.yml 2023-05-09 15:32:57 +08:00
RockYang
1849d3ddaa chore: 重命名 issue 模版 2023-05-09 15:29:57 +08:00
RockYang
7c6dfa545a chore: 添加 issue 和 PR 模板 2023-05-09 15:26:38 +08:00
RockYang
9a72e51a55 chore: 替换文字描述 用户 => 2023-05-09 14:30:09 +08:00
RockYang
c2d5da27e1 docs: 优化文档 2023-05-06 18:33:27 +08:00
RockYang
176e614457 docs: 优化文档 2023-05-06 18:13:44 +08:00
RockYang
a008b89b42 docs: 优化文档排版 2023-05-06 16:36:04 +08:00
RockYang
fb2108d88b docs: 完善文档 2023-05-06 16:27:44 +08:00
272 changed files with 21066 additions and 7629 deletions

47
.github/ISSUE_TEMPLATE/1.bug.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: Bug 报告 🐛
description: 为 chatgpt-plus 提交错误报告
labels: ['Bug']
body:
- type: checkboxes
attributes:
label: ⚠️ 确认 issue 是否已存在 ⚠️
description: 在提交 Issue 之前,请在 issue 列表搜索一下,确保你不是在提交一个重复的 issue。
options:
- label: 我已经搜索了现有的问题,没有找到跟我问题相关的问题。
required: true
- type: dropdown
attributes:
label: GPT-3 or GPT-4
description: 请选择你使用的 GPT 模型
options:
- GPT-3.5
- GPT-4
validations:
required: true
- type: dropdown
attributes:
label: 操作系统
description: 请选择你使用的操作系统
options:
- Windows
- Linux
- MacOS
validations:
required: true
- type: textarea
attributes:
label: Bug 描述 📝
description: 请简单描述你发现的问题。
- type: textarea
attributes:
label: 重现步骤 🕹
description: |
**⚠️ 无法重现的 issue 将会被关闭**
- type: textarea
attributes:
label: 你的应用配置信息
description: 请提供你的配置文档,**请注意数据脱敏**。
value: |
```toml
把你的配置信息粘贴到这里
```

26
.github/ISSUE_TEMPLATE/2.feature.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: 功能优化 🚀
description: 为 chatgpt-plus 提交优化建议
labels: ['feature']
body:
- type: checkboxes
attributes:
label: ⚠️ 确认 issue 是否已存在 ⚠️
description: >
在提交 Issue 之前,请在 issue 列表搜索一下,确保你不是在提交一个重复的 issue。
options:
- label: 我已经搜索了现有的问题,没有找到相关 issue。
required: true
- type: textarea
attributes:
label: 功能描述 📝
description: |
描述此功能该如何工作。
- type: textarea
attributes:
label: 示例 🌈
description: 是否可以提供类似的样例,比如提供参考项目的链接或者截图。
- type: textarea
attributes:
label: 动机 🔦
description: 为什么要新增或者优化这个功能,缺少这个功能会给你或者其他用户带来什么不便?

24
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,24 @@
<!-- 📢 注意:
在提交的 PR 的时候请确保每个 PR 只包含一个功能修改或者优化,请不要把多个更改组合到一个 PR 提交。保持干净可管理的 git 历史记录至关重要。 为确保我们存储库的质量,我们恳请您在提交 PR 时遵守以下准则:
1. 每个 PR 专注于一个单一的、具体的改进。
2. 不要包括任何不相关或[额外]的修改。
3. 为所做的更改提供清晰的文档和解释。
-->
### Background
<!-- 简要概述此更改背后的基本原理。 包括相关背景、先前的讨论或相关 issue 的链接。 确保变更与项目的总体方向一致。-->
### Changes
<!-- 简要说明你改动的内容 -->
### Test Plan
<!-- 描述您如何测试此功能。 包括重现步骤、相关测试用例和任何其他相关信息-->
### PR 规则验证列表
- [ ] 确保本次 PR 只包含单一的功能修改。
- [ ] 我已经对我的代码更改进行了充分的测试。
- [ ] 我已经考虑了我的更改的潜在风险和缓解措施。
- [ ] 我已经修正了相关文档

13
.gitignore vendored
View File

@@ -1,15 +1,7 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
src/dist
dist-ssr
*.local
# Editor directories and files
@@ -22,8 +14,3 @@ dist-ssr
*.njsproj
*.sln
*.sw?
src/tmp
src/bin
src/data
web/.env.development
config.toml

108
CHANGELOG.md Normal file
View File

@@ -0,0 +1,108 @@
# 更新日志
## v3.1.3
1. 页面重构:重后 Home 页面拆分成聊天MJ绘画SD 绘画,应用广场等多个功能菜单。
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
6. Bug修复修复 JWT token 有效期计算错误的 Bug。
## v3.1.2
1. 功能新增:新增七牛云 OSS 实现目前已支持三种文件上传服务Local, Minio, QiNiu OSS。
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
3. Bug修复自动去除众筹核销时候转账单号中的空格防止复制的时候多复制了空格。
4. 功能优化ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
5. 功能优化:取消导出聊天页面的授权验证
6. 功能优化:所有路由跳转都使用绝对路径
## v3.1.1
紧急修复版本采用弹窗的方式显示验证码解决验证码在低分辨率下被掩盖的Bug
## v3.1.0(大版本更新)
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAIAzure 以及 ChatGLM用户可以在这两个平台的模型中随意切换体验不同的模型聊天。
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
3. 功能重构重构文件夹上传服务支持多种文件上传存储handler目前已经实现本地存储和 minio oss 存储。
4. 功能优化:更新头像自动删除旧的图片资源。
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
6. 功能新增:允许用户配置自己的 OPenAIAzure 以及 ChatGLM API KEY。
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
8. 升级 gin, element-plusredis 组件到最新版本。
9. Bug修复修复若干已知的的 Bug
## v3.0.7
1. 聊天主界面:新增聊天引导页面,介绍产品功能
2. 功能重构拆分项目将函数插件以及微信机器人MidJourney 机器人等功能拆分新项目独立部署。
3. 功能新增:新增 MidJourney AI 绘画支持,当识别到用户的绘画需求时,自动调用 MidJourney 绘画函数进行绘画。
4. 功能新增:支持导出聊天记录为 PDF 文件。
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
6. 功能优化:支持用户设置默认的 GPT 模型
7. Bug修复修复若干已知的的 Bug
## v3.0.6
1. 管理后台:新增用户名和手机号码搜索功能
2. 管理后台:新增重置用户密码功能
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
5. Bug修复修复注册页面验证码不显示 Bug
6. Bug修复优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
7. 功能修正:允许用户使用手机号码登录
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
9. 功能优化:优化打包脚本,减少容器镜像大小
## v3.0.5
重磅功能更新!!! 新增函数插件支持可以轻松地接入你的第三方插件服务ChatGPT 自动帮您调用对应的函数完成任务。
1. 新增函数功能支持,全球早报,今日头条和微博热搜等插件服务,您也可以接入自己的第三方服务。
2. 集成微信机器人模块,可以通过微信个人收款码来完成充值,无需接入微信支付功能也可以完成收款功能。
3. 用户注册添加短信验证码功能,引入交互安全认证服务,有效防刷短信。
4. 支持配置聊天上下文深度,精确统计每轮对话所消耗的总 TOKEN 数量。
5. 修复已知的 Bug。
## v3.0.4
1. 调整项目目录结构,移除其他语言 API 目录
2. 修复 nodejs apple M1 跨平台打包,运行报错 exec format error
3. 增加用户 token 消耗统计功能
## v3.0.3
1. 优化启动参数接收处理,支持环境变量传参
2. 修复 PC 端聊天界面出现滚动条的 Bug
3. 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题
4. 更改 docker 构建镜像的基础镜像,改用作者的阿里云镜像,这样打包更快一些。
## v3.0.2
1. Feat新增移动端的聊天和用户设置功能
2. Fix: 修复 markdown 换行符解析的 Bug
3. Feat: 新增头像上传功能
4. Docs: 增加容器部署支持,支持 docker-compose 一键部署
5. Fix: 增加全局错误处理 handler修复业务处理异常导致服务退出的 Bug
## v3.0.1
1. 紧急修复前端 Home 组件路由被后台管理 Home 组件路由覆盖的 Bug。
2. 增加 docker-compose 部署脚本
## v3.0.0
全新的重构版本!!!
新版的系统前后端都进行大改动的重构,后端还是用的 Gin Web 框架,但是作者整合了 fx 自动注入框架,整个后端应用结构非常简洁,特别适合二次开发。
另外,数据存储用 MySQL 替换了 leveldb, 因为要对 C 端后期会涉及到很多业务数据查询统计leveldb 已经完全不够用了。
前后台技术架构还是基于 `Vue3 + Element-Plus` ,但是页面风格已经全部变了,几乎所有页面样式代码都重写了,希望会你是希望的风格!
此次重构改版主要是为了后面功能的扩展准备了。
新版本已经实现的功能如下:
1. 引入用户体系,新增用户注册和登录功能。
2. 聊天页面改版,实现了跟 ChatGPT 官方版本一致的聊天体验。
3. 创建会话的时候可以选择聊天角色和模型。
4. 新增聊天设置功能,用户可以导入自己的 API KEY
5. 保存聊天记录,支持聊天上下文。
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
7. 引入 ip2region 组件记录用户的登录IP和地址。
8. 支持会话搜索过滤。

375
README.md
View File

@@ -1,68 +1,349 @@
# ChatGPT-Plus
基于 OpenAI API 实现的 ChatGPT Web 应用,一共分为两个版本
**ChatGPT-PLUS** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure, ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。主要有如下特性
* 通用版:交互体验跟 ChatGPT 官方一致,聊天记录保存在客户端(浏览器)
* 角色版:内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求
聊天记录保存在云端(可以配置是否保存聊天记录)
每个版本都有 PC 版和移动版PC 版本的体验要略优于移动版
**本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。如需商用建议联系作者登记,仅做统计使用,优秀项目我们将在项目首页为您展示。**
## 项目介绍
这一套完整的系统,包括两套前端聊天应用和一个后台管理系统。系统有用户鉴权,你可以自己使用,也可以部署直接给 C 端用户提供 ChatGPT 的服务。
项目的技术架构是
> Go + Vue3 + element-plus
后端采用的是 Go 语言开发的 Gin Web 框架。前端用的是 Vue3 + element-plus UI 框架
目前已经实现了以下功能:
1. 通用版的 ChatGPT 聊天界面和功能,聊天记录保存在客户端。
2. 口令机制:输入口令才可以访问,支持设置口令的对话次数,有效期。
3. 角色版的聊天界面和功能,角色设定,预设一些角色,比如程序员,客服,作家,老师,艺术家...
4. 保存聊天记录,支持聊天上下文。
5. OpenAI API 负载均衡,限制每个 API Key 每分钟之内调用次数不超过 15次防止被封。
6. 支持配置多个代理,保证高可用。
7. 实现 markdown 语法解析和代码高亮,支持复制回复内容功能。
8. 后台管理功能,实现系统的动态配置,用户和角色的动态管理。
* 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
* 聊天体验跟 ChatGPT 官方版本完全一致
* 内置了各种预训练好的角色,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
* 支持 MidJourney AI 绘画集成,开箱即用
* 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。(可定制开发其他支付通道支持)
* 集成插件 API 功能,可结合 GPT 开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI 绘画函数插件。
## 功能截图
### 1. 角色版PC端
![ChatGPT Plus](docs/imgs/chat-plus.png)
### PC 端聊天界面
![ChatGPT admin](docs/imgs/gpt.gif)
![ChatGPT Chat Page](/docs/imgs/gpt.gif)
### 2. 角色版移动端
![ChatGPT Mobile](docs/imgs/chat-mobile.png)
### 新版聊天界面
### 3. 通用版
![ChatGPT Free](docs/imgs/chat-free.png)
![ChatGPT new Chat Page](/docs/imgs/chat-new.png)
### 4. 管理后台
![ChatGPT admin](docs/imgs/admin-01.png)
### MidJourney 专业绘画界面(v3.1.3)
![ChatGPT-midjourney](/docs/imgs/mj_image.png)
![ChatGPT admin](docs/imgs/admin-02.png)
### 自动调用函数插件
![ChatGPT admin](docs/imgs/admin-03.png)
![ChatGPT function plugin](/docs/imgs/plugin.png)
### 5. 体验地址
> 体验地址:[https://www.chat-plus.net/chat/#/free](https://www.chat-plus.net/chat/#/free) </br>
> 口令GeekMaster
![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_config.png)
![ChatGPT admin](/docs/imgs/admin_user.png)
### 移动端 Web 页面
![Mobile chat list](/docs/imgs/mobile_chat_list.png)
![Mobile chat session](/docs/imgs/mobile_chat_session.png)
![Mobile chat setting](/docs/imgs/mobile_chat_setting.png)
### 7. 体验地址
> 免费体验地址:[https://ai.r9it.com/chat](https://ai.r9it.com/chat) <br/>
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
## 使用须知
1. 本项目基于 MIT 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
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
* 码云地址https://gitee.com/blackfox/chatgpt-plus
## TODOLIST
* [ ] 让用户配置自己的 API KEY调用自己的 API Key将不记 Token 的使用次数
* [ ] 嵌入 AI 绘画功能,支持根据描述词生成图片
* [ ] 接入自己训练的开源大语言模型
* [ ] 接入 Google 语音 API支持语音聊天
## 本地部署
## 线上发布
* [x] 整合 Midjourney AI 绘画 API
* [x] 开发移动端聊天页面
* [x] 接入微信支付功能
* [x] 支持 ChatGPT 函数功能,通过函数实现插件
* [ ] 支持基于知识库的 AI 问答
* [ ] 开发桌面版应用
* [ ] 开发手机 App 客户端
## 注意事项
## Docker 快速部署
>
鉴于最新不少网友反馈在部署的时候遇到一些问题,大部分问题都是相同的,所以我这边做了一个视频教程 [五分钟部署自己的 ChatGPT 服务](https://www.bilibili.com/video/BV1H14y1B7Qw/)。
> 习惯看视频教程的朋友可以去看视频教程,视频的语速比较慢,建议 2 倍速观看。
V3.0.0 版本以后已经支持使用容器部署了,跳过所有的繁琐的环境准备,一条命令就可以轻松部署上线。
### 1. 导入数据库
首先我们需要创建一个 MySQL 容器,并导入初始数据库。
```shell
cd docker/mysql
# 创建 mysql 容器
docker-compose up -d
# 导入数据库
docker exec -i chatgpt-plus-mysql sh -c 'exec mysql -uroot -p12345678' < ../../database/chatgpt_plus-v3.1.3.sql
```
如果你本地已经安装了 MySQL 服务,那么你只需手动导入数据库即可。
```shell
# 连接数据库
mysql -u username -p password
# 导入数据库
source database/chatgpt_plus.sql
```
### 2. 修改配置文档
修改配置文档 `docker/conf/config.toml` 配置文档,修改代理地址和管理员密码:
```toml
Listen = "0.0.0.0:5678"
ProxyURL = "" # 如 http://127.0.0.1:7777
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
StaticDir = "./static" # 静态资源的目录
StaticUrl = "/static" # 静态资源访问 URL
AesEncryptKey = ""
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
MaxAge = 86400
[Manager]
Username = "admin"
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
[Redis] # redis 配置信息
Host = "localhost"
Port = 6379
Password = ""
DB = 0
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
ApiURL = ""
AppId = ""
Token = ""
[SmsConfig] # 阿里云短信服务配置
AccessKey = ""
AccessSecret = ""
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
ApiURL = "" # 插件扩展 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 = "" # 如 172.22.11.200:9000
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
AccessSecret = ""
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
UseSSL = false
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
[OSS.QiNiu] # 七牛云 OSS 配置
Zone = "z2" # 区域z0华东z1: 华北na0北美as0新加坡
AccessKey = ""
AccessSecret = ""
Bucket = ""
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
```
> 如果要启用微信收款服务和 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 地址
# 静态资源转发
location /static/ {
proxy_pass http://172.22.11.47:5678; # 这里改成后端服务的内网 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` 目录下。
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
如果有兴趣的话,也可以加微信进入微信讨论群(**添加好友时请注明来自Github!!!**)。
![微信名片](docs/imgs/wx.png)
#### 特此声明:不接受在微信或者微信群给开发者提 Bug有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合
### Commit 类型
* feat: 新特性或功能
* fix: 缺陷修复
* docs: 文档更新
* style: 代码风格或者组件样式更新
* refactor: 代码重构,不引入新功能和缺陷修复
* opt: 性能优化
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
## 打赏
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
![微信打赏](docs/imgs/wechat-pay.png)

20
api/.gitignore vendored Normal file
View File

@@ -0,0 +1,20 @@
# Logs
logs
*.log
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
tmp
bin
data
config.toml
static/upload
storage.json

View File

@@ -1,5 +1,5 @@
SHELL=/usr/bin/env bash
NAME := wechatGPT
NAME := chatgpt-plus
all: window linux darwin
@@ -12,7 +12,7 @@ linux:
.PHONY: linux
darwin:
CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o bin/$(NAME)-amd64-darwin main.go
.PHONY: darwin
clean:

5
api/README.md Normal file
View File

@@ -0,0 +1,5 @@
# chatgpt-plus-go
chatgpt-plus 后端 API Go 语言实现。技术选型采用 Gin + Mysql 架构,依赖注入使用的是 fx 框架ORM 采用的是 GORM 框架。

54
api/config.sample.toml Normal file
View File

@@ -0,0 +1,54 @@
Listen = "0.0.0.0:5678"
ProxyURL = "" # 如 http://127.0.0.1:7777
MysqlDns = "root:12345678@tcp(172.22.11.200:3307)/chatgpt_plus?charset=utf8&parseTime=True&loc=Local"
StaticDir = "./static" # 静态资源的目录
StaticUrl = "/static" # 静态资源访问 URL
AesEncryptKey = ""
[Session]
SecretKey = "azyehq3ivunjhbntz78isj00i4hz2mt9xtddysfucxakadq4qbfrt0b7q3lnvg80" # 注意:这个是 JWT Token 授权密钥,生产环境请务必更换
MaxAge = 86400
[Manager]
Username = "admin"
Password = "admin123" # 如果是生产环境的话,这里管理员的密码记得修改
[Redis] # redis 配置信息
Host = "localhost"
Port = 6379
Password = ""
DB = 0
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
ApiURL = ""
AppId = ""
Token = ""
[SmsConfig] # 阿里云短信服务配置
AccessKey = ""
AccessSecret = ""
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
[ExtConfig] # MidJourney和微信机器人服务 API 配置,开通此功能需要配合 chatpgt-plus-exts 项目部署
ApiURL = "" # 插件扩展 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 = "" # 如 172.22.11.200:9000
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
AccessSecret = ""
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
UseSSL = false
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
[OSS.QiNiu] # 七牛云 OSS 配置
Zone = "z2" # 区域z0华东z1: 华北na0北美as0新加坡
AccessKey = ""
AccessSecret = ""
Bucket = ""
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com

209
api/core/app_server.go Normal file
View File

@@ -0,0 +1,209 @@
package core
import (
"chatplus/core/types"
"chatplus/service/function"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
"io"
"net/http"
"runtime/debug"
"strings"
"time"
)
type AppServer struct {
Debug bool
Config *types.AppConfig
Engine *gin.Engine
ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message
ChatConfig *types.ChatConfig // chat config cache
SysConfig *types.SystemConfig // system config cache
// 保存 Websocket 会话 UserId, 每个 UserId 只能连接一次
// 防止第三方直接连接 socket 调用 OpenAI API
ChatSession *types.LMap[string, *types.ChatSession] //map[sessionId]UserId
ChatClients *types.LMap[string, *types.WsClient] // map[sessionId]Websocket 连接集合
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
Functions map[string]function.Function
MjTaskClients *types.LMap[string, *types.WsClient]
}
func NewServer(appConfig *types.AppConfig, functions map[string]function.Function) *AppServer {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
return &AppServer{
Debug: false,
Config: appConfig,
Engine: gin.Default(),
ChatContexts: types.NewLMap[string, []interface{}](),
ChatSession: types.NewLMap[string, *types.ChatSession](),
ChatClients: types.NewLMap[string, *types.WsClient](),
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
MjTaskClients: types.NewLMap[string, *types.WsClient](),
Functions: functions,
}
}
func (s *AppServer) Init(debug bool, client *redis.Client) {
if debug { // 调试模式允许跨域请求 API
s.Debug = debug
logger.Info("Enabled debug mode")
}
s.Engine.Use(corsMiddleware())
s.Engine.Use(authorizeMiddleware(s, client))
s.Engine.Use(errorHandler)
// 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir)
}
func (s *AppServer) Run(db *gorm.DB) error {
// load chat config from database
var chatConfig model.Config
res := db.Where("marker", "chat").First(&chatConfig)
if res.Error != nil {
return res.Error
}
err := utils.JsonDecode(chatConfig.Config, &s.ChatConfig)
if err != nil {
return err
}
// load system configs
var sysConfig model.Config
res = db.Where("marker", "system").First(&sysConfig)
if res.Error != nil {
return res.Error
}
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig)
if err != nil {
return err
}
logger.Infof("http://%s", s.Config.Listen)
return s.Engine.Run(s.Config.Listen)
}
// 全局异常处理
func errorHandler(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
logger.Errorf("Handler Panic: %v", r)
debug.PrintStack()
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: types.ErrorMsg})
c.Abort()
}
}()
//加载完 defer recover继续后续接口调用
c.Next()
}
// 跨域中间件设置
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// 设置允许的请求源
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
//允许跨域设置可以返回其他子段,可以自定义字段
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, Content-Type, Chat-Token, Admin-Authorization")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers")
//设置缓存时间
c.Header("Access-Control-Max-Age", "172800")
//允许客户端传递校验信息比如 cookie (重要)
c.Header("Access-Control-Allow-Credentials", "true")
}
if method == http.MethodOptions {
c.JSON(http.StatusOK, "ok!")
}
defer func() {
if err := recover(); err != nil {
logger.Info("Panic info is: %v", err)
}
}()
c.Next()
}
}
// 用户授权验证
func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/admin/login" ||
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/detail" ||
c.Request.URL.Path == "/api/mj/proxy" ||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
strings.HasPrefix(c.Request.URL.Path, "/static/") ||
c.Request.URL.Path == "/api/admin/config/get" {
c.Next()
return
}
var tokenString string
if strings.Contains(c.Request.URL.Path, "/api/admin/") { // 后台管理 API
tokenString = c.GetHeader(types.AdminAuthHeader)
} else if c.Request.URL.Path == "/api/chat/new" || c.Request.URL.Path == "/api/mj/client" {
tokenString = c.Query("token")
} else {
tokenString = c.GetHeader(types.UserAuthHeader)
}
if tokenString == "" {
resp.ERROR(c, "You should put Authorization in request headers")
c.Abort()
return
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(s.Config.Session.SecretKey), nil
})
if err != nil {
resp.NotAuth(c, fmt.Sprintf("Error with parse auth token: %v", err))
c.Abort()
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
resp.NotAuth(c, "Token is invalid")
c.Abort()
return
}
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
if expr > 0 && int64(expr) < time.Now().Unix() {
resp.NotAuth(c, "Token is expired")
c.Abort()
return
}
key := fmt.Sprintf("users/%v", claims["user_id"])
if _, err := client.Get(context.Background(), key).Result(); err != nil {
resp.NotAuth(c, "Token is not found in redis")
c.Abort()
return
}
c.Set(types.LoginUserID, claims["user_id"])
}
}

71
api/core/config.go Normal file
View File

@@ -0,0 +1,71 @@
package core
import (
"bytes"
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/utils"
"os"
"github.com/BurntSushi/toml"
)
var logger = logger2.GetLogger()
func NewDefaultConfig() *types.AppConfig {
return &types.AppConfig{
Listen: "0.0.0.0:5678",
ProxyURL: "",
Manager: types.Manager{Username: "admin", Password: "admin123"},
StaticDir: "./static",
StaticUrl: "http://localhost/5678/static",
Redis: types.RedisConfig{Host: "localhost", Port: 6379, Password: ""},
AesEncryptKey: utils.RandString(24),
Session: types.Session{
SecretKey: utils.RandString(64),
MaxAge: 86400,
},
ApiConfig: types.ChatPlusApiConfig{},
ExtConfig: types.ChatPlusExtConfig{Token: utils.RandString(32)},
OSS: types.OSSConfig{
Active: "local",
Local: types.LocalStorageConfig{
BaseURL: "http://localhost/5678/static/upload",
BasePath: "./static/upload",
},
},
}
}
func LoadConfig(configFile string) (*types.AppConfig, error) {
var config *types.AppConfig
_, err := os.Stat(configFile)
if err != nil {
logger.Info("creating new config file: ", configFile)
config = NewDefaultConfig()
config.Path = configFile
// save config
err := SaveConfig(config)
if err != nil {
return nil, err
}
return config, nil
}
_, err = toml.DecodeFile(configFile, &config)
if err != nil {
return nil, err
}
return config, err
}
func SaveConfig(config *types.AppConfig) error {
buf := new(bytes.Buffer)
encoder := toml.NewEncoder(buf)
if err := encoder.Encode(&config); err != nil {
return err
}
return os.WriteFile(config.Path, buf.Bytes(), 0644)
}

70
api/core/types/chat.go Normal file
View File

@@ -0,0 +1,70 @@
package types
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
Stream bool `json:"stream"`
Messages []interface{} `json:"messages,omitempty"`
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
Functions []Function `json:"functions,omitempty"`
}
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
}
type ApiResponse struct {
Choices []ChoiceItem `json:"choices"`
}
// ChoiceItem API 响应实体
type ChoiceItem struct {
Delta Delta `json:"delta"`
FinishReason string `json:"finish_reason"`
}
type Delta struct {
Role string `json:"role"`
Name string `json:"name"`
Content interface{} `json:"content"`
FunctionCall FunctionCall `json:"function_call,omitempty"`
}
// ChatSession 聊天会话对象
type ChatSession struct {
SessionId string `json:"session_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
Username string `json:"username"` // 当前登录的 username
UserId uint `json:"user_id"` // 当前登录的 user ID
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
Model ChatModel `json:"model"` // GPT 模型
}
type ChatModel struct {
Id uint `json:"id"`
Platform Platform `json:"platform"`
Value string `json:"value"`
}
type ApiError struct {
Error struct {
Message string
Type string
Param interface{}
Code string
}
}
const PromptMsg = "prompt" // prompt message
const ReplyMsg = "reply" // reply message
const MjMsg = "mj"
var ModelToTokens = map[string]int{
"gpt-3.5-turbo": 4096,
"gpt-3.5-turbo-16k": 16384,
"gpt-4": 8192,
"gpt-4-32k": 32768,
}

View File

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

127
api/core/types/config.go Normal file
View File

@@ -0,0 +1,127 @@
package types
import (
"fmt"
)
type AppConfig struct {
Path string `toml:"-"`
Listen string
Session Session
ProxyURL string
MysqlDns string // mysql 连接地址
Manager Manager // 后台管理员账户信息
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
ApiConfig ChatPlusApiConfig // ChatPlus API authorization configs
AesEncryptKey string
SmsConfig AliYunSmsConfig // AliYun send message service config
ExtConfig ChatPlusExtConfig // ChatPlus extensions callback api config
OSS OSSConfig // OSS config
}
type ChatPlusApiConfig struct {
ApiURL string
AppId string
Token string
}
type ChatPlusExtConfig struct {
ApiURL string
Token string
}
type AliYunSmsConfig struct {
AccessKey string
AccessSecret string
Product string
Domain string
}
type OSSConfig struct {
Active string
Local LocalStorageConfig
Minio MinioConfig
QiNiu QiNiuConfig
}
type MinioConfig struct {
Endpoint string
AccessKey string
AccessSecret string
Bucket string
UseSSL bool
Domain string
}
type QiNiuConfig struct {
Zone string
AccessKey string
AccessSecret string
Bucket string
Domain string
}
type LocalStorageConfig struct {
BasePath string
BaseURL string
}
type RedisConfig struct {
Host string
Port int
Password string
DB int
}
func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
// Manager 管理员
type Manager struct {
Username string `json:"username"`
Password string `json:"password"`
}
// ChatConfig 系统默认的聊天配置
type ChatConfig struct {
OpenAI ModelAPIConfig `json:"open_ai"`
Azure ModelAPIConfig `json:"azure"`
ChatGML ModelAPIConfig `json:"chat_gml"`
EnableContext bool `json:"enable_context"` // 是否开启聊天上下文
EnableHistory bool `json:"enable_history"` // 是否允许保存聊天记录
ContextDeep int `json:"context_deep"` // 上下文深度
}
type Platform string
const OpenAI = Platform("OpenAI")
const Azure = Platform("Azure")
const ChatGLM = Platform("ChatGLM")
// UserChatConfig 用户的聊天配置
type UserChatConfig struct {
ApiKeys map[Platform]string `json:"api_keys"`
}
type ModelAPIConfig struct {
ApiURL string `json:"api_url,omitempty"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens"`
ApiKey string `json:"api_key"`
}
type SystemConfig struct {
Title string `json:"title"`
AdminTitle string `json:"admin_title"`
Models []string `json:"models"`
UserInitCalls int `json:"user_init_calls"` // 新用户注册默认总送多少次调用
InitImgCalls int `json:"init_img_calls"`
VipMonthCalls int `json:"vip_month_calls"` // 会员每个赠送的调用次数
EnabledRegister bool `json:"enabled_register"`
EnabledMsgService bool `json:"enabled_msg_service"`
EnabledDraw bool `json:"enabled_draw"` // 启动 AI 绘画功能
}

View File

@@ -0,0 +1,92 @@
package types
type FunctionCall struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
type Function struct {
Name string `json:"name"`
Description string `json:"description"`
Parameters Parameters `json:"parameters"`
}
type Parameters struct {
Type string `json:"type"`
Required []string `json:"required"`
Properties map[string]Property `json:"properties"`
}
type Property struct {
Type string `json:"type"`
Description string `json:"description"`
}
const (
FuncZaoBao = "zao_bao" // 每日早报
FuncHeadLine = "headline" // 今日头条
FuncWeibo = "weibo_hot" // 微博热搜
FuncMidJourney = "mid_journey" // MJ 绘画
)
var InnerFunctions = []Function{
{
Name: FuncZaoBao,
Description: "每日早报,获取当天全球的热门新闻事件列表",
Parameters: Parameters{
Type: "object",
Properties: map[string]Property{
"text": {
Type: "string",
Description: "",
},
},
Required: []string{},
},
},
{
Name: FuncWeibo,
Description: "新浪微博热搜榜,微博当日热搜榜单",
Parameters: Parameters{
Type: "object",
Properties: map[string]Property{
"text": {
Type: "string",
Description: "",
},
},
Required: []string{},
},
},
{
Name: FuncHeadLine,
Description: "今日头条,给用户推荐当天的头条新闻,周榜热文",
Parameters: Parameters{
Type: "object",
Properties: map[string]Property{
"text": {
Type: "string",
Description: "",
},
},
Required: []string{},
},
},
{
Name: FuncMidJourney,
Description: "AI 绘画工具,使用 MJ MidJourney API 进行 AI 绘画",
Parameters: Parameters{
Type: "object",
Properties: map[string]Property{
"prompt": {
Type: "string",
Description: "提示词,如果该参数中有中文的话,则需要翻译成英文。提示词中的参数作为提示的一部分,不要删除",
},
},
Required: []string{},
},
},
}

View File

@@ -0,0 +1,63 @@
package types
import (
"context"
"sync"
)
type MKey interface {
string | int
}
type MValue interface {
*WsClient | *ChatSession | context.CancelFunc | []interface{}
}
type LMap[K MKey, T MValue] struct {
lock sync.RWMutex
data map[K]T
}
func NewLMap[K MKey, T MValue]() *LMap[K, T] {
return &LMap[K, T]{
lock: sync.RWMutex{},
data: make(map[K]T),
}
}
func (m *LMap[K, T]) Put(key K, value T) {
m.lock.Lock()
defer m.lock.Unlock()
m.data[key] = value
}
func (m *LMap[K, T]) Get(key K) T {
m.lock.RLock()
defer m.lock.RUnlock()
return m.data[key]
}
func (m *LMap[K, T]) Has(key K) bool {
m.lock.RLock()
defer m.lock.RUnlock()
_, ok := m.data[key]
return ok
}
func (m *LMap[K, T]) Delete(key K) {
m.lock.Lock()
defer m.lock.Unlock()
delete(m.data, key)
}
func (m *LMap[K, T]) ToList() []T {
m.lock.Lock()
defer m.lock.Unlock()
var s = make([]T, 0)
for _, v := range m.data {
s = append(s, v)
}
return s
}

14
api/core/types/session.go Normal file
View File

@@ -0,0 +1,14 @@
package types
const LoginUserID = "LOGIN_USER_ID"
const LoginUserCache = "LOGIN_USER_CACHE"
const UserAuthHeader = "Authorization"
const AdminAuthHeader = "Admin-Authorization"
const ChatTokenHeader = "Chat-Token"
// Session configs struct
type Session struct {
SecretKey string
MaxAge int
}

View File

@@ -12,9 +12,8 @@ type BizVo struct {
// WsMessage Websocket message
type WsMessage struct {
Type WsMsgType `json:"type"` // 消息类别start, end
IsHelloMsg bool `json:"is_hello_msg"` // 是否是打招呼的消息
Content string `json:"content"`
Type WsMsgType `json:"type"` // 消息类别start, end, img
Content interface{} `json:"content"`
}
type WsMsgType string
@@ -22,6 +21,7 @@ const (
WsStart = WsMsgType("start")
WsMiddle = WsMsgType("middle")
WsEnd = WsMsgType("end")
WsMjImg = WsMsgType("mj")
)
type BizCode int
@@ -29,12 +29,10 @@ type BizCode int
const (
Success = BizCode(0)
Failed = BizCode(1)
InvalidParams = BizCode(101) // 非法参数
NotAuthorized = BizCode(400) // 未授权
OkMsg = "Success"
ErrorMsg = "系统开小差了"
OkMsg = "Success"
ErrorMsg = "系统开小差了"
InvalidArgs = "非法参数或参数解析失败"
NoData = "No Data"
)
const TokenName = "ChatGPT-TOKEN"
const SessionKey = "WEB_SSH_SESSION"

90
api/go.mod Normal file
View File

@@ -0,0 +1,90 @@
module chatplus
go 1.19
require (
github.com/BurntSushi/toml v1.1.0
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405
github.com/gin-gonic/gin v1.9.1
github.com/go-redis/redis/v8 v8.11.5
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/gorilla/websocket v1.5.0
github.com/imroc/req/v3 v3.37.2
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0
github.com/minio/minio-go/v7 v7.0.62
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
github.com/qiniu/go-sdk/v7 v7.17.1
github.com/syndtr/goleveldb v1.0.0
go.uber.org/zap v1.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.4.7
)
require (
github.com/andybalholm/brotli v1.0.4 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.3 // 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/goccy/go-json v0.10.2 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect
github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/onsi/ginkgo/v2 v2.10.0 // 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/quic-go/qpack v0.4.0 // indirect
github.com/quic-go/qtls-go1-19 v0.3.2 // indirect
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/quic-go/quic-go v0.35.1 // indirect
github.com/refraction-networking/utls v1.3.2 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/dig v1.16.1 // indirect
golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect
golang.org/x/mod v0.11.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.10.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // 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/leodido/go-urn v1.2.4 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/fx v1.19.3
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.12.0
golang.org/x/sys v0.11.0 // indirect
gorm.io/gorm v1.25.1
)

299
api/go.sum Normal file
View File

@@ -0,0 +1,299 @@
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
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/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gaukas/godicttls v0.0.3 h1:YNDIf0d9adcxOijiLrEzpfZGAkNwLRzPaG6OjU7EITk=
github.com/gaukas/godicttls v0.0.3/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
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-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/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.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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
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.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
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/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/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRneVhc=
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
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/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
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/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/HtnEN+ZoUGDT55YgFCymbFJ15kXqs3nv5w=
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
github.com/qiniu/go-sdk/v7 v7.17.1 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
github.com/qiniu/x v1.10.5/go.mod h1:03Ni9tj+N2h2aKnAz+6N0Xfl8FwMEDRC2PAlxekASDs=
github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
github.com/quic-go/qtls-go1-19 v0.3.2 h1:tFxjCFcTQzK+oMxG6Zcvp4Dq8dx4yD3dDiIiyc86Z5U=
github.com/quic-go/qtls-go1-19 v0.3.2/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
github.com/quic-go/qtls-go1-20 v0.2.2 h1:WLOPx6OY/hxtTxKV1Zrq20FtXtDEkeY00CGQm8GEa3E=
github.com/quic-go/qtls-go1-20 v0.2.2/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
github.com/quic-go/quic-go v0.35.1 h1:b0kzj6b/cQAf05cT0CkQubHM31wiA+xH3IBkxP62poo=
github.com/quic-go/quic-go v0.35.1/go.mod h1:+4CVgVppm0FNjpG3UcX8Joi/frKOH7/ciD5yGcwOO1g=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/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/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/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/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8=
go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk=
go.uber.org/fx v1.19.3 h1:YqMRE4+2IepTYCMOvXqQpRa+QAVdiSTnsHU4XNWBceA=
go.uber.org/fx v1.19.3/go.mod h1:w2HrQg26ql9fLK7hlBiZ6JsRUKV+Lj/atT1KCjT8YhM=
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/zap v1.23.0 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
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/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-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/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.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/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/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-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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.4.7 h1:rY46lkCspzGHn7+IYsNpSfEv9tA+SU4SkkB+GFX125Y=
gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -0,0 +1,148 @@
package admin
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
logger2 "chatplus/logger"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"context"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
var logger = logger2.GetLogger()
type ManagerHandler struct {
handler.BaseHandler
db *gorm.DB
redis *redis.Client
}
func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client) *ManagerHandler {
h := ManagerHandler{db: db, redis: client}
h.App = app
return &h
}
// Login 登录
func (h *ManagerHandler) Login(c *gin.Context) {
var data types.Manager
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
manager := h.App.Config.Manager
if data.Username == manager.Username && data.Password == manager.Password {
// 创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": manager.Username,
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
})
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error())
return
}
// 保存到 redis
key := "users/" + manager.Username
if _, err := h.redis.Set(context.Background(), key, tokenString, 0).Result(); err != nil {
resp.ERROR(c, "error with save token: "+err.Error())
return
}
resp.SUCCESS(c, tokenString)
} else {
resp.ERROR(c, "用户名或者密码错误")
}
}
// Logout 注销
func (h *ManagerHandler) Logout(c *gin.Context) {
key := h.GetUserKey(c)
if _, err := h.redis.Del(c, key).Result(); err != nil {
logger.Error("error with delete session: ", err)
} else {
resp.SUCCESS(c)
}
}
// Session 会话检测
func (h *ManagerHandler) Session(c *gin.Context) {
token := c.GetHeader(types.AdminAuthHeader)
if token == "" {
resp.NotAuth(c)
} else {
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

@@ -0,0 +1,92 @@
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 ApiKeyHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
h := ApiKeyHandler{db: db}
h.App = app
return &h
}
func (h *ApiKeyHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Platform string `json:"platform"`
Value string `json:"value"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
apiKey := model.ApiKey{}
if data.Id > 0 {
h.db.Find(&apiKey)
}
apiKey.Platform = data.Platform
apiKey.Value = data.Value
res := h.db.Save(&apiKey)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
var keyVo vo.ApiKey
err := utils.CopyObject(apiKey, &keyVo)
if err != nil {
resp.ERROR(c, "数据拷贝失败!")
return
}
keyVo.Id = apiKey.Id
keyVo.CreatedAt = apiKey.CreatedAt.Unix()
resp.SUCCESS(c, keyVo)
}
func (h *ApiKeyHandler) List(c *gin.Context) {
var items []model.ApiKey
var keys = make([]vo.ApiKey, 0)
res := h.db.Find(&items)
if res.Error == nil {
for _, item := range items {
var key vo.ApiKey
err := utils.CopyObject(item, &key)
if err == nil {
key.Id = item.Id
key.CreatedAt = item.CreatedAt.Unix()
key.UpdatedAt = item.UpdatedAt.Unix()
keys = append(keys, key)
} else {
logger.Error(err)
}
}
}
resp.SUCCESS(c, keys)
}
func (h *ApiKeyHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id > 0 {
res := h.db.Where("id = ?", id).Delete(&model.ApiKey{})
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
}
resp.SUCCESS(c)
}

View File

@@ -0,0 +1,143 @@
package admin
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type ChatModelHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
h := ChatModelHandler{db: db}
h.App = app
return &h
}
func (h *ChatModelHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Platform string `json:"platform"`
CreatedAt int64 `json:"created_at"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
item := model.ChatModel{Platform: data.Platform, Name: data.Name, Value: data.Value, Enabled: data.Enabled}
item.Id = data.Id
if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0)
}
res := h.db.Save(&item)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
var itemVo vo.ChatModel
err := utils.CopyObject(item, &itemVo)
if err != nil {
resp.ERROR(c, "数据拷贝失败!")
return
}
itemVo.Id = item.Id
itemVo.CreatedAt = item.CreatedAt.Unix()
resp.SUCCESS(c, itemVo)
}
// List 模型列表
func (h *ChatModelHandler) List(c *gin.Context) {
session := h.db.Session(&gorm.Session{})
enable := h.GetBool(c, "enable")
if enable {
session = session.Where("enabled", enable)
}
var items []model.ChatModel
var cms = make([]vo.ChatModel, 0)
res := session.Order("sort_num ASC").Find(&items)
if res.Error == nil {
for _, item := range items {
var cm vo.ChatModel
err := utils.CopyObject(item, &cm)
if err == nil {
cm.Id = item.Id
cm.CreatedAt = item.CreatedAt.Unix()
cm.UpdatedAt = item.UpdatedAt.Unix()
cms = append(cms, cm)
} else {
logger.Error(err)
}
}
}
resp.SUCCESS(c, cms)
}
func (h *ChatModelHandler) Enable(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
res := h.db.Model(&model.ChatModel{}).Where("id = ?", data.Id).Update("enabled", data.Enabled)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
resp.SUCCESS(c)
}
func (h *ChatModelHandler) Sort(c *gin.Context) {
var data struct {
Ids []uint `json:"ids"`
Sorts []int `json:"sorts"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
for index, id := range data.Ids {
res := h.db.Model(&model.ChatModel{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
}
resp.SUCCESS(c)
}
func (h *ChatModelHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id > 0 {
res := h.db.Where("id = ?", id).Delete(&model.ChatModel{})
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
}
resp.SUCCESS(c)
}

View File

@@ -0,0 +1,114 @@
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 ChatRoleHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
h := ChatRoleHandler{db: db}
h.App = app
return &h
}
// Save 创建或者更新某个角色
func (h *ChatRoleHandler) Save(c *gin.Context) {
var data vo.ChatRole
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var role model.ChatRole
err := utils.CopyObject(data, &role)
if err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
role.Id = data.Id
if data.CreatedAt > 0 {
role.CreatedAt = time.Unix(data.CreatedAt, 0)
}
res := h.db.Save(&role)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
// 填充 ID 数据
data.Id = role.Id
data.CreatedAt = role.CreatedAt.Unix()
resp.SUCCESS(c, data)
}
func (h *ChatRoleHandler) List(c *gin.Context) {
var items []model.ChatRole
var roles = make([]vo.ChatRole, 0)
res := h.db.Order("sort_num ASC").Find(&items)
if res.Error != nil {
resp.ERROR(c, "No data found")
return
}
for _, v := range items {
var role vo.ChatRole
err := utils.CopyObject(v, &role)
if err == nil {
role.Id = v.Id
role.CreatedAt = v.CreatedAt.Unix()
role.UpdatedAt = v.UpdatedAt.Unix()
roles = append(roles, role)
}
}
resp.SUCCESS(c, roles)
}
// Sort 更新角色排序
func (h *ChatRoleHandler) 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.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index])
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
}
resp.SUCCESS(c)
}
func (h *ChatRoleHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id <= 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
res := h.db.Where("id = ?", id).Delete(&model.ChatRole{})
if res.Error != nil {
resp.ERROR(c, "删除失败!")
return
}
resp.SUCCESS(c)
}

View File

@@ -0,0 +1,89 @@
package admin
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ConfigHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
h := ConfigHandler{db: db}
h.App = app
return &h
}
func (h *ConfigHandler) Update(c *gin.Context) {
var data struct {
Key string `json:"key"`
Config map[string]interface{} `json:"config"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
str := utils.JsonEncode(&data.Config)
config := model.Config{Key: data.Key, Config: str}
res := h.db.FirstOrCreate(&config, model.Config{Key: data.Key})
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
if config.Id > 0 {
config.Config = str
res := h.db.Updates(&config)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
// update config cache for AppServer
var cfg model.Config
h.db.Where("marker", data.Key).First(&cfg)
var err error
if data.Key == "system" {
err = utils.JsonDecode(cfg.Config, &h.App.SysConfig)
} else if data.Key == "chat" {
err = utils.JsonDecode(cfg.Config, &h.App.ChatConfig)
}
if err != nil {
resp.ERROR(c, "Failed to update config cache: "+err.Error())
return
}
logger.Infof("Update AppServer's config successfully: %v", config.Config)
}
resp.SUCCESS(c, config)
}
// Get 获取指定的系统配置
func (h *ConfigHandler) Get(c *gin.Context) {
key := c.Query("key")
var config model.Config
res := h.db.Where("marker", key).First(&config)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
var m map[string]interface{}
err := utils.JsonDecode(config.Config, &m)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, m)
}

View File

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

View File

@@ -0,0 +1,57 @@
package admin
import (
"chatplus/core"
"chatplus/handler"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type RewardHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewRewardHandler(app *core.AppServer, db *gorm.DB) *RewardHandler {
h := RewardHandler{db: db}
h.App = app
return &h
}
func (h *RewardHandler) List(c *gin.Context) {
var items []model.Reward
res := h.db.Order("id DESC").Find(&items)
var rewards = make([]vo.Reward, 0)
if res.Error == nil {
userIds := make([]uint, 0)
for _, v := range items {
userIds = append(userIds, v.UserId)
}
var users []model.User
h.db.Where("id IN ?", userIds).Find(&users)
var userMap = make(map[uint]model.User)
for _, u := range users {
userMap[u.Id] = u
}
for _, v := range items {
var r vo.Reward
err := utils.CopyObject(v, &r)
if err != nil {
continue
}
r.Id = v.Id
r.Username = userMap[v.UserId].Mobile
r.CreatedAt = v.CreatedAt.Unix()
r.UpdatedAt = v.UpdatedAt.Unix()
rewards = append(rewards, r)
}
}
resp.SUCCESS(c, rewards)
}

View File

@@ -0,0 +1,212 @@
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 UserHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewUserHandler(app *core.AppServer, db *gorm.DB) *UserHandler {
h := UserHandler{db: db}
h.App = app
return &h
}
// List 用户列表
func (h *UserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
mobile := h.GetTrim(c, "mobile")
offset := (page - 1) * pageSize
var items []model.User
var users = make([]vo.User, 0)
var total int64
session := h.db.Session(&gorm.Session{})
if mobile != "" {
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
}
session.Model(&model.User{}).Count(&total)
res := session.Offset(offset).Limit(pageSize).Find(&items)
if res.Error == nil {
for _, item := range items {
var user vo.User
err := utils.CopyObject(item, &user)
if err == nil {
user.Id = item.Id
user.CreatedAt = item.CreatedAt.Unix()
user.UpdatedAt = item.UpdatedAt.Unix()
users = append(users, user)
} else {
logger.Error(err)
}
}
}
pageVo := vo.NewPage(total, page, pageSize, users)
resp.SUCCESS(c, pageVo)
}
func (h *UserHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Password string `json:"password"`
Mobile string `json:"mobile"`
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
ChatRoles []string `json:"chat_roles"`
ExpiredTime string `json:"expired_time"`
Status bool `json:"status"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user = model.User{}
var res *gorm.DB
var userVo vo.User
if data.Id > 0 { // 更新
user.Id = data.Id
// 此处需要用 map 更新,用结构体无法更新 0 值
res = h.db.Model(&user).Updates(map[string]interface{}{
"mobile": data.Mobile,
"calls": data.Calls,
"img_calls": data.ImgCalls,
"status": data.Status,
"chat_roles_json": utils.JsonEncode(data.ChatRoles),
"expired_time": utils.Str2stamp(data.ExpiredTime),
})
} else {
salt := utils.RandString(8)
u := model.User{
Mobile: data.Mobile,
Password: utils.GenPassword(data.Password, salt),
Avatar: "/images/avatar/user.png",
Salt: salt,
Status: true,
ChatRoles: utils.JsonEncode(data.ChatRoles),
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
ChatConfig: utils.JsonEncode(types.UserChatConfig{
ApiKeys: map[types.Platform]string{
types.OpenAI: "",
types.Azure: "",
types.ChatGLM: "",
},
}),
Calls: h.App.SysConfig.UserInitCalls,
}
res = h.db.Create(&u)
_ = utils.CopyObject(u, &userVo)
userVo.Id = u.Id
userVo.CreatedAt = u.CreatedAt.Unix()
userVo.UpdatedAt = u.UpdatedAt.Unix()
}
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
}
resp.SUCCESS(c, userVo)
}
// ResetPass 重置密码
func (h *UserHandler) ResetPass(c *gin.Context) {
var data struct {
Id uint
Password string
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.User
res := h.db.First(&user, data.Id)
if res.Error != nil {
resp.ERROR(c, "No user found")
return
}
password := utils.GenPassword(data.Password, user.Salt)
user.Password = password
res = h.db.Updates(&user)
if res.Error != nil {
resp.ERROR(c)
} else {
resp.SUCCESS(c)
}
}
func (h *UserHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id > 0 {
tx := h.db.Begin()
res := h.db.Where("id = ?", id).Delete(&model.User{})
if res.Error != nil {
resp.ERROR(c, "删除失败")
return
}
// 删除聊天记录
res = h.db.Where("user_id = ?", id).Delete(&model.ChatItem{})
if res.Error != nil {
tx.Rollback()
resp.ERROR(c, "删除失败")
return
}
// 删除聊天历史记录
res = h.db.Where("user_id = ?", id).Delete(&model.HistoryMessage{})
if res.Error != nil {
tx.Rollback()
resp.ERROR(c, "删除失败")
return
}
// 删除登录日志
res = h.db.Where("user_id = ?", id).Delete(&model.UserLoginLog{})
if res.Error != nil {
tx.Rollback()
resp.ERROR(c, "删除失败")
return
}
tx.Commit()
}
resp.SUCCESS(c)
}
func (h *UserHandler) LoginLog(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
var total int64
h.db.Model(&model.UserLoginLog{}).Count(&total)
offset := (page - 1) * pageSize
var items []model.UserLoginLog
res := h.db.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
if res.Error != nil {
resp.ERROR(c, "获取数据失败")
return
}
var logs []vo.UserLoginLog
for _, v := range items {
var log vo.UserLoginLog
err := utils.CopyObject(v, &log)
if err == nil {
log.Id = v.Id
log.CreatedAt = v.CreatedAt.Unix()
logs = append(logs, log)
}
}
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
}

View File

@@ -0,0 +1,288 @@
package handler
import (
"bufio"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"context"
"encoding/json"
"fmt"
"gorm.io/gorm"
"io"
"strings"
"time"
"unicode/utf8"
)
// 将消息发送给 Azure API 并获取结果,通过 WebSocket 推送到客户端
func (h *ChatHandler) sendAzureMessage(
chatCtx []interface{},
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
return nil
} else if strings.Contains(err.Error(), "no available key") {
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
return nil
} else {
logger.Error(err)
}
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var functionCall = false
var functionName string
var arguments = make([]string, 0)
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "data:") || len(line) < 30 {
continue
}
var responseBody = types.ApiResponse{}
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
logger.Error(err, line)
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
break
}
fun := responseBody.Choices[0].Delta.FunctionCall
if functionCall && fun.Name == "" {
arguments = append(arguments, fun.Arguments)
continue
}
if !utils.IsEmptyValue(fun) {
functionName = fun.Name
f := h.App.Functions[functionName]
if f != nil {
functionCall = true
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
continue
}
}
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
break
}
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, utils.InterfaceToString(content))
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
})
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
if functionCall { // 调用函数完成任务
var params map[string]interface{}
_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, "![](/images/wx.png)")
} else {
f := h.App.Functions[functionName]
if functionName == types.FuncMidJourney {
params["user_id"] = userVo.Id
params["role_id"] = role.Id
params["chat_id"] = session.ChatId
params["icon"] = "/images/avatar/mid_journey.png"
params["session_id"] = session.SessionId
}
data, err := f.Invoke(params)
if err != nil {
msg := "调用函数出错:" + err.Error()
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: msg,
})
contents = append(contents, msg)
} else {
content := data
if functionName == types.FuncMidJourney {
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
h.App.MjTaskClients.Put(session.SessionId, ws)
// update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: content,
})
contents = append(contents, content)
}
}
}
// 消息发送成功
if len(contents) > 0 {
// 更新用户的对话次数
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext && functionCall == false {
chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx)
}
// 追加聊天记录
if h.App.ChatConfig.EnableHistory {
useContext := true
if functionCall {
useContext = false
}
// for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil {
logger.Error(err)
}
historyUserMsg := model.HistoryMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Tokens: promptToken,
UseContext: useContext,
}
historyUserMsg.CreatedAt = promptCreatedAt
historyUserMsg.UpdatedAt = promptCreatedAt
res := h.db.Save(&historyUserMsg)
if res.Error != nil {
logger.Error("failed to save prompt history message: ", res.Error)
}
// 计算本次对话消耗的总 token 数量
var totalTokens = 0
if functionCall { // prompt + 函数名 + 参数 token
tokens, _ := utils.CalcTokens(functionName, req.Model)
totalTokens += tokens
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
totalTokens += tokens
} else {
totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
}
totalTokens += getTotalTokens(req)
historyReplyMsg := model.HistoryMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.ReplyMsg,
Icon: role.Icon,
Content: message.Content,
Tokens: totalTokens,
UseContext: useContext,
}
historyReplyMsg.CreatedAt = replyCreatedAt
historyReplyMsg.UpdatedAt = replyCreatedAt
res = h.db.Create(&historyReplyMsg)
if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error)
}
// 更新用户信息
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
}
// 保存当前会话
var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil {
chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId
chatItem.RoleId = role.Id
chatItem.ModelId = session.Model.Id
if utf8.RuneCountInString(prompt) > 30 {
chatItem.Title = string([]rune(prompt)[:30]) + "..."
} else {
chatItem.Title = prompt
}
h.db.Create(&chatItem)
}
}
} else {
body, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("error with reading response: %v", err)
}
var res types.ApiError
err = json.Unmarshal(body, &res)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
if strings.Contains(res.Error.Message, "maximum context length") {
logger.Error(res.Error.Message)
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
h.App.ChatContexts.Delete(session.ChatId)
return h.sendMessage(ctx, session, role, prompt, ws)
} else {
utils.ReplyMessage(ws, "请求 Azure API 失败:"+res.Error.Message)
}
}
return nil
}

View File

@@ -0,0 +1,51 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/utils"
"fmt"
"strings"
"github.com/gin-gonic/gin"
)
var logger = logger2.GetLogger()
type BaseHandler struct {
App *core.AppServer
}
func (h *BaseHandler) GetTrim(c *gin.Context, key string) string {
return strings.TrimSpace(c.Query(key))
}
func (h *BaseHandler) PostInt(c *gin.Context, key string, defaultValue int) int {
return utils.IntValue(c.PostForm(key), defaultValue)
}
func (h *BaseHandler) GetInt(c *gin.Context, key string, defaultValue int) int {
return utils.IntValue(c.Query(key), defaultValue)
}
func (h *BaseHandler) GetFloat(c *gin.Context, key string) float64 {
return utils.FloatValue(c.Query(key))
}
func (h *BaseHandler) PostFloat(c *gin.Context, key string) float64 {
return utils.FloatValue(c.PostForm(key))
}
func (h *BaseHandler) GetBool(c *gin.Context, key string) bool {
return utils.BoolValue(c.Query(key))
}
func (h *BaseHandler) PostBool(c *gin.Context, key string) bool {
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)
}

View File

@@ -0,0 +1,47 @@
package handler
import (
"chatplus/core/types"
"chatplus/service"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
)
// 今日头条函数实现
type CaptchaHandler struct {
service *service.CaptchaService
}
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
return &CaptchaHandler{service: s}
}
func (h *CaptchaHandler) Get(c *gin.Context) {
data, err := h.service.Get()
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, data)
}
// Check verify the captcha data
func (h *CaptchaHandler) Check(c *gin.Context) {
var data struct {
Key string `json:"key"`
Dots string `json:"dots"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if h.service.Check(data) {
resp.SUCCESS(c)
} else {
resp.ERROR(c)
}
}

416
api/handler/chat_handler.go Normal file
View File

@@ -0,0 +1,416 @@
package handler
import (
"bytes"
"chatplus/core"
"chatplus/core/types"
"chatplus/store"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"context"
"encoding/json"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
"net/url"
"strings"
"time"
)
const ErrorMsg = "抱歉AI 助手开小差了,请稍后再试。"
type ChatHandler struct {
BaseHandler
db *gorm.DB
leveldb *store.LevelDB
redis *redis.Client
}
func NewChatHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, redis *redis.Client) *ChatHandler {
handler := ChatHandler{db: db, leveldb: levelDB, redis: redis}
handler.App = app
return &handler
}
var chatConfig types.ChatConfig
// ChatHandle 处理聊天 WebSocket 请求
func (h *ChatHandler) ChatHandle(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
return
}
sessionId := c.Query("session_id")
roleId := h.GetInt(c, "role_id", 0)
chatId := c.Query("chat_id")
modelId := h.GetInt(c, "model_id", 0)
client := types.NewWsClient(ws)
// get model info
var chatModel model.ChatModel
res := h.db.First(&chatModel, modelId)
if res.Error != nil || chatModel.Enabled == false {
utils.ReplyMessage(client, "当前AI模型暂未启用连接已关闭")
c.Abort()
return
}
session := h.App.ChatSession.Get(sessionId)
if session == nil {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
logger.Info("用户未登录")
c.Abort()
return
}
session = &types.ChatSession{
SessionId: sessionId,
ClientIP: c.ClientIP(),
Username: user.Mobile,
UserId: user.Id,
}
h.App.ChatSession.Put(sessionId, session)
}
// use old chat data override the chat model and role ID
var chat model.ChatItem
res = h.db.Where("chat_id=?", chatId).First(&chat)
if res.Error == nil {
chatModel.Id = chat.ModelId
roleId = int(chat.RoleId)
}
session.ChatId = chatId
session.Model = types.ChatModel{
Id: chatModel.Id,
Value: chatModel.Value,
Platform: types.Platform(chatModel.Platform)}
logger.Infof("New websocket connected, IP: %s, Username: %s", c.ClientIP(), session.Username)
var chatRole model.ChatRole
res = h.db.First(&chatRole, roleId)
if res.Error != nil || !chatRole.Enable {
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
c.Abort()
return
}
// 初始化聊天配置
var config model.Config
h.db.Where("marker", "chat").First(&config)
err = utils.JsonDecode(config.Config, &chatConfig)
if err != nil {
utils.ReplyMessage(client, "加载系统配置失败,连接已关闭!!!")
c.Abort()
return
}
// 保存会话连接
h.App.ChatClients.Put(sessionId, client)
go func() {
for {
_, msg, err := client.Receive()
if err != nil {
logger.Error(err)
client.Close()
h.App.ChatClients.Delete(sessionId)
h.App.ReqCancelFunc.Delete(sessionId)
return
}
message := string(msg)
logger.Info("Receive a message: ", message)
//utils.ReplyMessage(client, "这是一条测试消息!")
ctx, cancel := context.WithCancel(context.Background())
h.App.ReqCancelFunc.Put(sessionId, cancel)
// 回复消息
err = h.sendMessage(ctx, session, chatRole, message, client)
if err != nil {
logger.Error(err)
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
} else {
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
logger.Info("回答完毕: " + string(message))
}
}
}()
}
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
defer func() {
if r := recover(); r != nil {
logger.Error("Recover message from error: ", r)
}
}()
var user model.User
res := h.db.Model(&model.User{}).First(&user, session.UserId)
if res.Error != nil {
utils.ReplyMessage(ws, "非法用户,请联系管理员!")
return res.Error
}
var userVo vo.User
err := utils.CopyObject(user, &userVo)
userVo.Id = user.Id
if err != nil {
return errors.New("User 对象转换失败," + err.Error())
}
if userVo.Status == false {
utils.ReplyMessage(ws, "您的账号已经被禁用,如果疑问,请联系管理员!")
utils.ReplyMessage(ws, "![](/images/wx.png)")
return nil
}
if userVo.Calls <= 0 && userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
utils.ReplyMessage(ws, "您的对话次数已经用尽请联系管理员或者点击左下角菜单加入众筹获得100次对话")
utils.ReplyMessage(ws, "![](/images/wx.png)")
return nil
}
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
utils.ReplyMessage(ws, "您的账号已经过期,请联系管理员!")
utils.ReplyMessage(ws, "![](/images/wx.png)")
return nil
}
var req = types.ApiRequest{
Model: session.Model.Value,
Stream: true,
}
switch session.Model.Platform {
case types.Azure:
req.Temperature = h.App.ChatConfig.Azure.Temperature
req.MaxTokens = h.App.ChatConfig.Azure.MaxTokens
break
case types.ChatGLM:
req.Temperature = h.App.ChatConfig.ChatGML.Temperature
req.MaxTokens = h.App.ChatConfig.ChatGML.MaxTokens
break
default:
req.Temperature = h.App.ChatConfig.OpenAI.Temperature
req.MaxTokens = h.App.ChatConfig.OpenAI.MaxTokens
var functions = make([]types.Function, 0)
for _, f := range types.InnerFunctions {
if !h.App.SysConfig.EnabledDraw && f.Name == types.FuncMidJourney {
continue
}
functions = append(functions, f)
}
req.Functions = functions
}
// 加载聊天上下文
var chatCtx []interface{}
if h.App.ChatConfig.EnableContext {
if h.App.ChatContexts.Has(session.ChatId) {
chatCtx = h.App.ChatContexts.Get(session.ChatId)
} else {
// calculate the tokens of current request, to prevent to exceeding the max tokens num
tokens := req.MaxTokens
for _, f := range types.InnerFunctions {
tks, _ := utils.CalcTokens(utils.JsonEncode(f), req.Model)
tokens += tks
}
// loading the role context
var messages []types.Message
err := utils.JsonDecode(role.Context, &messages)
if err == nil {
for _, v := range messages {
tks, _ := utils.CalcTokens(v.Content, req.Model)
if tokens+tks >= types.ModelToTokens[req.Model] {
break
}
tokens += tks
chatCtx = append(chatCtx, v)
}
}
// loading recent chat history as chat context
if chatConfig.ContextDeep > 0 {
var historyMessages []model.HistoryMessage
res := h.db.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(chatConfig.ContextDeep).Order("created_at desc").Find(&historyMessages)
if res.Error == nil {
for _, msg := range historyMessages {
if tokens+msg.Tokens >= types.ModelToTokens[session.Model.Value] {
break
}
tokens += msg.Tokens
ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.ReplyMsg {
ms.Role = "assistant"
}
chatCtx = append(chatCtx, ms)
}
}
}
}
logger.Debugf("聊天上下文:%+v", chatCtx)
}
reqMgs := make([]interface{}, 0)
for _, m := range chatCtx {
reqMgs = append(reqMgs, m)
}
req.Messages = append(reqMgs, map[string]interface{}{
"role": "user",
"content": prompt,
})
switch session.Model.Platform {
case types.Azure:
return h.sendAzureMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.OpenAI:
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
case types.ChatGLM:
return h.sendChatGLMMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: fmt.Sprintf("Not supported platform: %s", session.Model.Platform),
})
return nil
}
// Tokens 统计 token 数量
func (h *ChatHandler) Tokens(c *gin.Context) {
var data struct {
Text string `json:"text"`
Model string `json:"model"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
if data.Text == "" {
var item model.HistoryMessage
userId, _ := c.Get(types.LoginUserID)
res := h.db.Where("user_id = ?", userId).Last(&item)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
resp.SUCCESS(c, item.Tokens)
return
}
tokens, err := utils.CalcTokens(data.Text, data.Model)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, tokens)
}
func getTotalTokens(req types.ApiRequest) int {
encode := utils.JsonEncode(req.Messages)
var items []map[string]interface{}
err := utils.JsonDecode(encode, &items)
if err != nil {
return 0
}
tokens := 0
for _, item := range items {
content, ok := item["content"]
if ok && !utils.IsEmptyValue(content) {
t, err := utils.CalcTokens(utils.InterfaceToString(content), req.Model)
if err == nil {
tokens += t
}
}
}
return tokens
}
// StopGenerate 停止生成
func (h *ChatHandler) StopGenerate(c *gin.Context) {
sessionId := c.Query("session_id")
if h.App.ReqCancelFunc.Has(sessionId) {
h.App.ReqCancelFunc.Get(sessionId)()
h.App.ReqCancelFunc.Delete(sessionId)
}
resp.SUCCESS(c, types.OkMsg)
}
// 发送请求到 OpenAI 服务器
// useOwnApiKey: 是否使用了用户自己的 API KEY
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platform types.Platform, apiKey *string) (*http.Response, error) {
var apiURL string
switch platform {
case types.Azure:
md := strings.Replace(req.Model, ".", "", 1)
apiURL = strings.Replace(h.App.ChatConfig.Azure.ApiURL, "{model}", md, 1)
break
case types.ChatGLM:
apiURL = strings.Replace(h.App.ChatConfig.ChatGML.ApiURL, "{model}", req.Model, 1)
req.Prompt = req.Messages
req.Messages = nil
break
default:
apiURL = h.App.ChatConfig.OpenAI.ApiURL
}
// 创建 HttpClient 请求对象
var client *http.Client
requestBody, err := json.Marshal(req)
if err != nil {
return nil, err
}
request, err := http.NewRequest(http.MethodPost, apiURL, bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
request = request.WithContext(ctx)
request.Header.Set("Content-Type", "application/json")
proxyURL := h.App.Config.ProxyURL
if proxyURL != "" && platform == types.OpenAI { // 使用代理
proxy, _ := url.Parse(proxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
} else {
client = http.DefaultClient
}
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)
switch platform {
case types.Azure:
request.Header.Set("api-key", *apiKey)
break
case types.ChatGLM:
token, err := h.getChatGLMToken(*apiKey)
if err != nil {
return nil, err
}
logger.Info(token)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
break
default:
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *apiKey))
}
return client.Do(request)
}

View File

@@ -0,0 +1,103 @@
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

@@ -0,0 +1,105 @@
package handler
import (
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
)
// List 获取会话列表
func (h *ChatHandler) List(c *gin.Context) {
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
resp.ERROR(c, "The parameter 'user_id' is needed.")
return
}
var items = make([]vo.ChatItem, 0)
var chats []model.ChatItem
res := h.db.Where("user_id = ?", userId).Order("id DESC").Find(&chats)
if res.Error == nil {
var roleIds = make([]uint, 0)
for _, chat := range chats {
roleIds = append(roleIds, chat.RoleId)
}
var roles []model.ChatRole
res = h.db.Find(&roles, roleIds)
if res.Error == nil {
roleMap := make(map[uint]model.ChatRole)
for _, role := range roles {
roleMap[role.Id] = role
}
for _, chat := range chats {
var item vo.ChatItem
err := utils.CopyObject(chat, &item)
if err == nil {
item.Id = chat.Id
item.Icon = roleMap[chat.RoleId].Icon
items = append(items, item)
}
}
}
}
resp.SUCCESS(c, items)
}
// Remove 删除会话
func (h *ChatHandler) Remove(c *gin.Context) {
chatId := h.GetTrim(c, "chat_id")
if chatId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
res := h.db.Where("user_id = ? AND chat_id = ?", user.Id, chatId).Delete(&model.ChatItem{})
if res.Error != nil {
resp.ERROR(c, "Failed to update database")
return
}
// 删除当前会话的聊天记录
res = h.db.Where("user_id = ? AND chat_id =?", user.Id, chatId).Delete(&model.ChatItem{})
if res.Error != nil {
resp.ERROR(c, "Failed to remove chat from database.")
return
}
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
// 清空会话上下文
h.App.ChatContexts.Delete(chatId)
resp.SUCCESS(c, types.OkMsg)
}
func (h *ChatHandler) Detail(c *gin.Context) {
chatId := h.GetTrim(c, "chat_id")
if utils.IsEmptyValue(chatId) {
resp.ERROR(c, "Invalid chatId")
return
}
var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", chatId).First(&chatItem)
if res.Error != nil {
resp.ERROR(c, "No chat found")
return
}
var chatItemVo vo.ChatItem
err := utils.CopyObject(chatItem, &chatItemVo)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, chatItemVo)
}

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 ChatModelHandler struct {
BaseHandler
db *gorm.DB
}
func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
h := ChatModelHandler{db: db}
h.App = app
return &h
}
// List 模型列表
func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel
var cms = make([]vo.ChatModel, 0)
res := h.db.Where("enabled = ?", true).Order("sort_num ASC").Find(&items)
if res.Error == nil {
for _, item := range items {
var cm vo.ChatModel
err := utils.CopyObject(item, &cm)
if err == nil {
cm.Id = item.Id
cm.CreatedAt = item.CreatedAt.Unix()
cm.UpdatedAt = item.UpdatedAt.Unix()
cms = append(cms, cm)
} else {
logger.Error(err)
}
}
}
resp.SUCCESS(c, cms)
}

View File

@@ -0,0 +1,59 @@
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 ChatRoleHandler struct {
BaseHandler
db *gorm.DB
}
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
handler := &ChatRoleHandler{db: db}
handler.App = app
return handler
}
// List get user list
func (h *ChatRoleHandler) List(c *gin.Context) {
var roles []model.ChatRole
res := h.db.Where("enable", true).Order("sort_num ASC").Find(&roles)
if res.Error != nil {
resp.ERROR(c, "No roles found,"+res.Error.Error())
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
var roleKeys []string
err = utils.JsonDecode(user.ChatRoles, &roleKeys)
if err != nil {
resp.ERROR(c, "角色解析失败!")
return
}
// 转成 vo
var roleVos = make([]vo.ChatRole, 0)
for _, r := range roles {
if !utils.ContainsStr(roleKeys, r.Key) {
continue
}
var v vo.ChatRole
err := utils.CopyObject(r, &v)
if err == nil {
v.Id = r.Id
roleVos = append(roleVos, v)
}
}
resp.SUCCESS(c, roleVos)
}

View File

@@ -0,0 +1,235 @@
package handler
import (
"bufio"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"context"
"encoding/json"
"fmt"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
"io"
"strings"
"time"
"unicode/utf8"
)
// 将消息发送给 ChatGLM API 并获取结果,通过 WebSocket 推送到客户端
func (h *ChatHandler) sendChatGLMMessage(
chatCtx []interface{},
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
return nil
} else if strings.Contains(err.Error(), "no available key") {
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
return nil
} else {
logger.Error(err)
}
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var event, content string
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
line := scanner.Text()
if len(line) < 5 || strings.HasPrefix(line, "id:") {
continue
}
if strings.HasPrefix(line, "event:") {
event = line[6:]
continue
}
if strings.HasPrefix(line, "data:") {
content = line[5:]
}
switch event {
case "add":
if len(contents) == 0 {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(content),
})
contents = append(contents, content)
case "finish":
break
case "error":
utils.ReplyMessage(ws, fmt.Sprintf("**调用 ChatGLM API 出错:%s**", content))
break
case "interrupted":
utils.ReplyMessage(ws, "**调用 ChatGLM API 出错,当前输出被中断!**")
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
// 消息发送成功
if len(contents) > 0 {
// 更新用户的对话次数
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文
if 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.db.Model(&model.User{}).Where("id = ?", userVo.Id).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
}
// 保存当前会话
var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil {
chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId
chatItem.RoleId = role.Id
chatItem.ModelId = session.Model.Id
if utf8.RuneCountInString(prompt) > 30 {
chatItem.Title = string([]rune(prompt)[:30]) + "..."
} else {
chatItem.Title = prompt
}
h.db.Create(&chatItem)
}
}
} else {
body, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("error with reading response: %v", err)
}
var res struct {
Code int `json:"code"`
Success bool `json:"success"`
Msg string `json:"msg"`
}
err = json.Unmarshal(body, &res)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
if !res.Success {
utils.ReplyMessage(ws, "请求 ChatGLM 失败:"+res.Msg)
}
}
return nil
}
func (h *ChatHandler) getChatGLMToken(apiKey string) (string, error) {
ctx := context.Background()
tokenString, err := h.redis.Get(ctx, apiKey).Result()
if err == nil {
return tokenString, nil
}
expr := time.Hour * 2
key := strings.Split(apiKey, ".")
if len(key) != 2 {
return "", fmt.Errorf("invalid api key: %s", apiKey)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"api_key": key[0],
"timestamp": time.Now().Unix(),
"exp": time.Now().Add(expr).Add(time.Second * 10).Unix(),
})
token.Header["alg"] = "HS256"
token.Header["sign_type"] = "SIGN"
delete(token.Header, "typ")
// Sign and get the complete encoded token as a string using the secret
tokenString, err = token.SignedString([]byte(key[1]))
h.redis.Set(ctx, apiKey, tokenString, expr)
return tokenString, err
}

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

@@ -0,0 +1,560 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/service"
"chatplus/service/oss"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"encoding/base64"
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
"strings"
"sync"
"time"
)
type TaskStatus string
const (
Stopped = TaskStatus("Stopped")
Finished = TaskStatus("Finished")
)
type Image struct {
URL string `json:"url"`
ProxyURL string `json:"proxy_url"`
Filename string `json:"filename"`
Width int `json:"width"`
Height int `json:"height"`
Size int `json:"size"`
Hash string `json:"hash"`
}
type MidJourneyHandler struct {
BaseHandler
redis *redis.Client
db *gorm.DB
mjService *service.MjService
uploaderManager *oss.UploaderManager
lock sync.Mutex
clients *types.LMap[string, *types.WsClient]
}
func NewMidJourneyHandler(
app *core.AppServer,
client *redis.Client,
db *gorm.DB,
manager *oss.UploaderManager,
mjService *service.MjService) *MidJourneyHandler {
h := MidJourneyHandler{
redis: client,
db: db,
uploaderManager: manager,
lock: sync.Mutex{},
mjService: mjService,
clients: types.NewLMap[string, *types.WsClient](),
}
h.App = app
return &h
}
type notifyData 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"`
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *MidJourneyHandler) 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.clients.Delete(sessionId)
h.clients.Put(sessionId, client)
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
}
func (h *MidJourneyHandler) Notify(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != h.App.Config.ExtConfig.Token {
resp.NotAuth(c)
return
}
var data notifyData
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
logger.Debugf("收到 MidJourney 回调请求:%+v", data)
h.lock.Lock()
defer h.lock.Unlock()
err, finished := h.notifyHandler(c, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 解除任务锁定
if finished && (data.Status == Finished || data.Status == Stopped) {
h.redis.Del(c, service.MjRunningJobKey)
}
resp.SUCCESS(c)
}
func (h *MidJourneyHandler) notifyHandler(c *gin.Context, data notifyData) (error, bool) {
taskString, err := h.redis.Get(c, service.MjRunningJobKey).Result()
if err != nil { // 过期任务,丢弃
logger.Warn("任务已过期:", err)
return nil, true
}
var task service.MjTask
err = utils.JsonDecode(taskString, &task)
if err != nil { // 非标准任务,丢弃
logger.Warn("任务解析失败:", err)
return nil, false
}
var job model.MidJourneyJob
res := h.db.Where("message_id = ?", data.MessageId).First(&job)
if res.Error == nil && data.Status == Finished {
logger.Warn("重复消息:", data.MessageId)
return nil, false
}
if task.Src == service.TaskSrcImg { // 绘画任务
var job model.MidJourneyJob
res := h.db.Where("id = ?", task.Id).First(&job)
if res.Error != nil {
logger.Warn("非法任务:", res.Error)
return nil, false
}
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 := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL)
if err != nil {
logger.Error("error with download img: ", err.Error())
return err, false
}
job.ImgURL = imgURL
} else {
// 临时图片直接保存,访问的时候使用代理进行转发
job.ImgURL = data.Image.URL
}
res = h.db.Updates(&job)
if res.Error != nil {
logger.Error("error with update job: ", res.Error)
return res.Error, false
}
var jobVo vo.MidJourneyJob
err := utils.CopyObject(job, &jobVo)
if err == nil {
if data.Progress < 100 {
image, err := utils.DownloadImage(jobVo.ImgURL, h.App.Config.ProxyURL)
if err == nil {
jobVo.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
}
}
// 推送任务到前端
client := h.clients.Get(task.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, jobVo)
}
}
} else if task.Src == service.TaskSrcChat { // 聊天任务
wsClient := h.App.MjTaskClients.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 := h.uploaderManager.GetUploadHandler().PutImg(data.Image.URL)
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 err, false
}
tx := h.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 {
return res.Error, false
}
// 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 {
tx.Rollback()
return res.Error, false
}
tx.Commit()
}
if wsClient == nil { // 客户端断线,则丢弃
logger.Errorf("Client is offline: %+v", data)
return nil, true
}
if data.Status == Finished {
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsMjImg, Content: data})
utils.ReplyChunkMessage(wsClient, types.WsMessage{Type: types.WsEnd})
// 本次绘画完毕,移除客户端
h.App.MjTaskClients.Delete(task.SessionId)
} else {
// 使用代理临时转发图片
if data.Image.URL != "" {
image, err := utils.DownloadImage(data.Image.URL, h.App.Config.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 {
h.db.Model(&model.User{}).Where("id = ?", task.UserId).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}
return nil, true
}
func (h *MidJourneyHandler) 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 *MidJourneyHandler) Image(c *gin.Context) {
var data struct {
SessionId string `json:"session_id"`
Prompt string `json:"prompt"`
Rate string `json:"rate"`
Model string `json:"model"`
Chaos int `json:"chaos"`
Raw bool `json:"raw"`
Seed int64 `json:"seed"`
Stylize int `json:"stylize"`
Img string `json:"img"`
Weight float32 `json:"weight"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if !h.checkLimits(c) {
return
}
var prompt = data.Prompt
if data.Rate != "" && !strings.Contains(prompt, "--ar") {
prompt += " --ar " + data.Rate
}
if data.Seed > 0 && !strings.Contains(prompt, "--seed") {
prompt += fmt.Sprintf(" --seed %d", data.Seed)
}
if data.Stylize > 0 && !strings.Contains(prompt, "--s") && !strings.Contains(prompt, "--stylize") {
prompt += fmt.Sprintf(" --s %d", data.Stylize)
}
if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") {
prompt += fmt.Sprintf(" --c %d", data.Chaos)
}
if data.Img != "" {
prompt = fmt.Sprintf("%s %s", data.Img, prompt)
if data.Weight > 0 {
prompt += fmt.Sprintf(" --iw %f", data.Weight)
}
}
if data.Raw {
prompt += " --style raw"
}
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
prompt += data.Model
}
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
job := model.MidJourneyJob{
Type: service.Image.String(),
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
}
h.mjService.PushTask(service.MjTask{
Id: int(job.Id),
SessionId: data.SessionId,
Src: service.TaskSrcImg,
Type: service.Image,
Prompt: prompt,
UserId: userId,
})
var jobVo vo.MidJourneyJob
err := utils.CopyObject(job, &jobVo)
if err == nil {
// 推送任务到前端
client := h.clients.Get(data.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, jobVo)
}
}
resp.SUCCESS(c)
}
type reqVo struct {
Src string `json:"src"`
Index int32 `json:"index"`
MessageId string `json:"message_id"`
MessageHash string `json:"message_hash"`
SessionId string `json:"session_id"`
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
func (h *MidJourneyHandler) Upscale(c *gin.Context) {
var data reqVo
if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
if !h.checkLimits(c) {
return
}
idValue, _ := c.Get(types.LoginUserID)
jobId := 0
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
src := service.TaskSrc(data.Src)
if src == service.TaskSrcImg {
job := model.MidJourneyJob{
Type: service.Upscale.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.clients.Get(data.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, jobVo)
}
}
}
h.mjService.PushTask(service.MjTask{
Id: jobId,
SessionId: data.SessionId,
Src: src,
Type: service.Upscale,
Prompt: data.Prompt,
UserId: userId,
RoleId: data.RoleId,
Icon: data.Icon,
ChatId: data.ChatId,
Index: data.Index,
MessageId: data.MessageId,
MessageHash: data.MessageHash,
})
wsClient := h.App.ChatClients.Get(data.SessionId)
if wsClient != nil {
content := fmt.Sprintf("**%s** 已推送 upscale 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
utils.ReplyMessage(wsClient, content)
if h.App.MjTaskClients.Get(data.SessionId) == nil {
h.App.MjTaskClients.Put(data.SessionId, wsClient)
}
}
resp.SUCCESS(c)
}
// Variation send variation command to MidJourney Bot
func (h *MidJourneyHandler) Variation(c *gin.Context) {
var data reqVo
if err := c.ShouldBindJSON(&data); err != nil || data.SessionId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
if !h.checkLimits(c) {
return
}
idValue, _ := c.Get(types.LoginUserID)
jobId := 0
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
src := service.TaskSrc(data.Src)
if src == service.TaskSrcImg {
job := model.MidJourneyJob{
Type: service.Variation.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.clients.Get(data.SessionId)
if client != nil {
utils.ReplyChunkMessage(client, jobVo)
}
}
}
h.mjService.PushTask(service.MjTask{
Id: jobId,
SessionId: data.SessionId,
Src: src,
Type: service.Variation,
Prompt: data.Prompt,
UserId: userId,
RoleId: data.RoleId,
Icon: data.Icon,
ChatId: data.ChatId,
Index: data.Index,
MessageId: data.MessageId,
MessageHash: data.MessageHash,
})
// 从聊天窗口发送的请求,记录客户端信息
wsClient := h.App.ChatClients.Get(data.SessionId)
if wsClient != nil {
content := fmt.Sprintf("**%s** 已推送 variation 任务到 MidJourney 机器人,请耐心等待任务执行...", data.Prompt)
utils.ReplyMessage(wsClient, content)
if h.App.MjTaskClients.Get(data.SessionId) == nil {
h.App.MjTaskClients.Put(data.SessionId, wsClient)
}
}
resp.SUCCESS(c)
}
// JobList 获取 MJ 任务列表
func (h *MidJourneyHandler) JobList(c *gin.Context) {
status := h.GetInt(c, "status", 0)
var items []model.MidJourneyJob
var res *gorm.DB
userId, _ := c.Get(types.LoginUserID)
if status == 1 {
res = h.db.Where("user_id = ? AND progress = 100", userId).Order("id DESC").Find(&items)
} else {
res = h.db.Where("user_id = ? AND progress < 100", userId).Order("id ASC").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 && 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,295 @@
package handler
import (
"bufio"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"context"
"encoding/json"
"fmt"
"gorm.io/gorm"
"io"
"strings"
"time"
"unicode/utf8"
)
// 将消息发送给 OpenAI API 并获取结果,通过 WebSocket 推送到客户端
func (h *ChatHandler) sendOpenAiMessage(
chatCtx []interface{},
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
session *types.ChatSession,
role model.ChatRole,
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = userVo.ChatConfig.ApiKeys[session.Model.Platform]
response, err := h.doRequest(ctx, req, session.Model.Platform, &apiKey)
logger.Info("HTTP请求完成耗时", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
return nil
} else if strings.Contains(err.Error(), "no available key") {
utils.ReplyMessage(ws, "抱歉😔😔😔,系统已经没有可用的 API KEY请联系管理员")
return nil
} else {
logger.Error(err)
}
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
return err
} else {
defer response.Body.Close()
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{}
var contents = make([]string, 0)
var functionCall = false
var functionName string
var arguments = make([]string, 0)
scanner := bufio.NewScanner(response.Body)
for scanner.Scan() {
line := scanner.Text()
if !strings.Contains(line, "data:") || len(line) < 30 {
continue
}
var responseBody = types.ApiResponse{}
err = json.Unmarshal([]byte(line[6:]), &responseBody)
if err != nil || len(responseBody.Choices) == 0 { // 数据解析出错
logger.Error(err, line)
utils.ReplyMessage(ws, ErrorMsg)
utils.ReplyMessage(ws, "![](/images/wx.png)")
break
}
fun := responseBody.Choices[0].Delta.FunctionCall
if functionCall && fun.Name == "" {
arguments = append(arguments, fun.Arguments)
continue
}
if !utils.IsEmptyValue(fun) {
functionName = fun.Name
f := h.App.Functions[functionName]
if f != nil {
functionCall = true
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: fmt.Sprintf("正在调用函数 `%s` 作答 ...\n\n", f.Name())})
}
continue
}
if responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
break
}
// 初始化 role
if responseBody.Choices[0].Delta.Role != "" && message.Role == "" {
message.Role = responseBody.Choices[0].Delta.Role
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
continue
} else if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, utils.InterfaceToString(content))
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
})
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
if functionCall { // 调用函数完成任务
var params map[string]interface{}
_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", functionName, params)
// for creating image, check if the user's img_calls > 0
if functionName == types.FuncMidJourney && userVo.ImgCalls <= 0 {
utils.ReplyMessage(ws, "**当前用户剩余绘图次数已用尽,请扫描下面二维码联系管理员!**")
utils.ReplyMessage(ws, "![](/images/wx.png)")
} else {
f := h.App.Functions[functionName]
if functionName == types.FuncMidJourney {
params["user_id"] = userVo.Id
params["role_id"] = role.Id
params["chat_id"] = session.ChatId
params["icon"] = "/images/avatar/mid_journey.png"
params["session_id"] = session.SessionId
}
data, err := f.Invoke(params)
if err != nil {
msg := "调用函数出错:" + err.Error()
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: msg,
})
contents = append(contents, msg)
} else {
content := data
if functionName == types.FuncMidJourney {
content = fmt.Sprintf("绘画提示词:%s 已推送任务到 MidJourney 机器人,请耐心等待任务执行...", data)
h.App.MjTaskClients.Put(session.SessionId, ws)
// update user's img_calls
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("img_calls", gorm.Expr("img_calls - ?", 1))
}
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: content,
})
contents = append(contents, content)
}
}
}
// 消息发送成功
if len(contents) > 0 {
// 更新用户的对话次数
if userVo.ChatConfig.ApiKeys[session.Model.Platform] == "" {
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).UpdateColumn("calls", gorm.Expr("calls - ?", 1))
}
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.ChatConfig.EnableContext && functionCall == false {
chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
h.App.ChatContexts.Put(session.ChatId, chatCtx)
}
// 追加聊天记录
if h.App.ChatConfig.EnableHistory {
useContext := true
if functionCall {
useContext = false
}
// for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil {
logger.Error(err)
}
historyUserMsg := model.HistoryMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: prompt,
Tokens: promptToken,
UseContext: useContext,
}
historyUserMsg.CreatedAt = promptCreatedAt
historyUserMsg.UpdatedAt = promptCreatedAt
res := h.db.Save(&historyUserMsg)
if res.Error != nil {
logger.Error("failed to save prompt history message: ", res.Error)
}
// 计算本次对话消耗的总 token 数量
var totalTokens = 0
if functionCall { // prompt + 函数名 + 参数 token
tokens, _ := utils.CalcTokens(functionName, req.Model)
totalTokens += tokens
tokens, _ = utils.CalcTokens(utils.InterfaceToString(arguments), req.Model)
totalTokens += tokens
} else {
totalTokens, _ = utils.CalcTokens(message.Content, req.Model)
}
totalTokens += getTotalTokens(req)
historyReplyMsg := model.HistoryMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.ReplyMsg,
Icon: role.Icon,
Content: message.Content,
Tokens: totalTokens,
UseContext: useContext,
}
historyReplyMsg.CreatedAt = replyCreatedAt
historyReplyMsg.UpdatedAt = replyCreatedAt
res = h.db.Create(&historyReplyMsg)
if res.Error != nil {
logger.Error("failed to save reply history message: ", res.Error)
}
// 更新用户信息
h.db.Model(&model.User{}).Where("id = ?", userVo.Id).
UpdateColumn("total_tokens", gorm.Expr("total_tokens + ?", totalTokens))
}
// 保存当前会话
var chatItem model.ChatItem
res := h.db.Where("chat_id = ?", session.ChatId).First(&chatItem)
if res.Error != nil {
chatItem.ChatId = session.ChatId
chatItem.UserId = session.UserId
chatItem.RoleId = role.Id
chatItem.ModelId = session.Model.Id
if utf8.RuneCountInString(prompt) > 30 {
chatItem.Title = string([]rune(prompt)[:30]) + "..."
} else {
chatItem.Title = prompt
}
h.db.Create(&chatItem)
}
}
} else {
body, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("error with reading response: %v", err)
}
var res types.ApiError
err = json.Unmarshal(body, &res)
if err != nil {
return fmt.Errorf("error with decode response: %v", err)
}
// OpenAI API 调用异常处理
if strings.Contains(res.Error.Message, "This key is associated with a deactivated account") {
utils.ReplyMessage(ws, "请求 OpenAI API 失败API KEY 所关联的账户被禁用。")
// 移除当前 API key
h.db.Where("value = ?", apiKey).Delete(&model.ApiKey{})
} else if strings.Contains(res.Error.Message, "You exceeded your current quota") {
utils.ReplyMessage(ws, "请求 OpenAI API 失败API KEY 触发并发限制,请稍后再试。")
} else if strings.Contains(res.Error.Message, "This model's maximum context length") {
logger.Error(res.Error.Message)
utils.ReplyMessage(ws, "当前会话上下文长度超出限制,已为您清空会话上下文!")
h.App.ChatContexts.Delete(session.ChatId)
return h.sendMessage(ctx, session, role, prompt, ws)
} else {
utils.ReplyMessage(ws, "请求 OpenAI API 失败:"+res.Error.Message)
}
}
return nil
}

View File

@@ -0,0 +1,121 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"strings"
)
type RewardHandler struct {
BaseHandler
db *gorm.DB
}
func NewRewardHandler(server *core.AppServer, db *gorm.DB) *RewardHandler {
h := RewardHandler{db: db}
h.App = server
return &h
}
func (h *RewardHandler) Notify(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != h.App.Config.ExtConfig.Token {
resp.NotAuth(c)
return
}
var data struct {
TransId string `json:"trans_id"` // 微信转账交易 ID
Amount float64 `json:"amount"` // 微信转账交易金额
Remark string `json:"remark"` // 转账备注
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Amount <= 0 {
resp.ERROR(c, "Amount should not be 0")
return
}
logger.Infof("收到众筹收款信息: %+v", data)
var item model.Reward
res := h.db.Where("tx_id = ?", data.TransId).First(&item)
if res.Error == nil {
resp.ERROR(c, "当前交易 ID 己经存在!")
return
}
res = h.db.Create(&model.Reward{
TxId: data.TransId,
Amount: data.Amount,
Remark: data.Remark,
Status: false,
})
if res.Error != nil {
logger.Errorf("交易保存失败: %v", res.Error)
resp.ERROR(c, "交易保存失败")
return
}
resp.SUCCESS(c)
}
// Verify 打赏码核销
func (h *RewardHandler) Verify(c *gin.Context) {
var data struct {
TxId string `json:"tx_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 移除转账单号中间的空格,防止有人复制的时候多复制了空格
data.TxId = strings.ReplaceAll(data.TxId, " ", "")
var item model.Reward
res := h.db.Where("tx_id = ?", data.TxId).First(&item)
if res.Error != nil {
resp.ERROR(c, "无效的众筹交易流水号!")
return
}
if item.Status {
resp.ERROR(c, "当前众筹交易流水号已经被核销,请不要重复核销!")
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.HACKER(c)
return
}
tx := h.db.Begin()
calls := (item.Amount + 0.1) * 10
res = h.db.Model(&user).UpdateColumn("calls", gorm.Expr("calls + ?", calls))
if res.Error != nil {
resp.ERROR(c, "更新数据库失败!")
return
}
// 更新核销状态
item.Status = true
item.UserId = user.Id
res = h.db.Updates(&item)
if res.Error != nil {
tx.Rollback()
resp.ERROR(c, "更新数据库失败!")
return
}
tx.Commit()
resp.SUCCESS(c)
}

View File

@@ -0,0 +1,70 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/service"
"chatplus/store"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
)
const CodeStorePrefix = "/verify/codes/"
type SmsHandler struct {
BaseHandler
leveldb *store.LevelDB
sms *service.AliYunSmsService
captcha *service.CaptchaService
}
func NewSmsHandler(app *core.AppServer, db *store.LevelDB, sms *service.AliYunSmsService, captcha *service.CaptchaService) *SmsHandler {
handler := &SmsHandler{leveldb: db, sms: sms, captcha: captcha}
handler.App = app
return handler
}
// SendCode 发送验证码短信
func (h *SmsHandler) SendCode(c *gin.Context) {
var data struct {
Mobile string `json:"mobile"`
Key string `json:"key"`
Dots string `json:"dots"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if !h.captcha.Check(data) {
resp.ERROR(c, "验证码错误,请先完人机验证")
return
}
code := utils.RandomNumber(6)
err := h.sms.SendVerifyCode(data.Mobile, code)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 存储验证码,等待后面注册验证
err = h.leveldb.Put(CodeStorePrefix+data.Mobile, code)
if err != nil {
resp.ERROR(c, "验证码保存失败")
return
}
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

@@ -0,0 +1,31 @@
package handler
import (
"chatplus/core"
"chatplus/service/oss"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type UploadHandler struct {
BaseHandler
db *gorm.DB
uploaderManager *oss.UploaderManager
}
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
handler := &UploadHandler{db: db, uploaderManager: manager}
handler.App = app
return handler
}
func (h *UploadHandler) Upload(c *gin.Context) {
fileURL, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, fileURL)
}

351
api/handler/user_handler.go Normal file
View File

@@ -0,0 +1,351 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"gorm.io/gorm"
)
type UserHandler struct {
BaseHandler
db *gorm.DB
searcher *xdb.Searcher
leveldb *store.LevelDB
redis *redis.Client
}
func NewUserHandler(
app *core.AppServer,
db *gorm.DB,
searcher *xdb.Searcher,
levelDB *store.LevelDB,
client *redis.Client) *UserHandler {
handler := &UserHandler{db: db, searcher: searcher, leveldb: levelDB, redis: client}
handler.App = app
return handler
}
// Register user register
func (h *UserHandler) Register(c *gin.Context) {
// parameters process
var data struct {
Mobile string `json:"mobile"`
Password string `json:"password"`
Code int `json:"code"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
data.Password = strings.TrimSpace(data.Password)
if len(data.Mobile) < 10 {
resp.ERROR(c, "请输入合法的手机号")
return
}
if len(data.Password) < 8 {
resp.ERROR(c, "密码长度不能少于8个字符")
return
}
// 检查验证码
key := CodeStorePrefix + data.Mobile
if h.App.SysConfig.EnabledMsgService {
var code int
err := h.leveldb.Get(key, &code)
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
}
// check if the username is exists
var item model.User
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
if res.RowsAffected > 0 {
resp.ERROR(c, "该手机号码已经被注册,请更换其他手机号")
return
}
// 默认订阅所有角色
var chatRoles []model.ChatRole
h.db.Find(&chatRoles)
var roleKeys = make([]string, 0)
for _, r := range chatRoles {
roleKeys = append(roleKeys, r.Key)
}
salt := utils.RandString(8)
user := model.User{
Password: utils.GenPassword(data.Password, salt),
Avatar: "/images/avatar/user.png",
Salt: salt,
Status: true,
Mobile: data.Mobile,
ChatRoles: utils.JsonEncode(roleKeys),
ChatConfig: utils.JsonEncode(types.UserChatConfig{
ApiKeys: map[types.Platform]string{
types.OpenAI: "",
types.Azure: "",
types.ChatGLM: "",
},
}),
Calls: h.App.SysConfig.UserInitCalls,
ImgCalls: h.App.SysConfig.InitImgCalls,
}
res = h.db.Create(&user)
if res.Error != nil {
resp.ERROR(c, "保存数据失败")
logger.Error(res.Error)
return
}
if h.App.SysConfig.EnabledMsgService {
_ = h.leveldb.Delete(key) // 注册成功,删除短信验证码
}
resp.SUCCESS(c, user)
}
// Login 用户登录
func (h *UserHandler) Login(c *gin.Context) {
var data struct {
Mobile string `json:"username"`
Password string `json:"password"`
}
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
}
password := utils.GenPassword(data.Password, user.Salt)
if password != user.Password {
resp.ERROR(c, "用户名或密码错误")
return
}
if user.Status == false {
resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
return
}
// 更新最后登录时间和IP
user.LastLoginIp = c.ClientIP()
user.LastLoginAt = time.Now().Unix()
h.db.Model(&user).Updates(user)
h.db.Create(&model.UserLoginLog{
UserId: user.Id,
Username: user.Mobile,
LoginIp: c.ClientIP(),
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
})
// 创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id,
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
})
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error())
return
}
// 保存到 redis
key := fmt.Sprintf("users/%d", user.Id)
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil {
resp.ERROR(c, "error with save token: "+err.Error())
return
}
resp.SUCCESS(c, tokenString)
}
// Logout 注 销
func (h *UserHandler) Logout(c *gin.Context) {
sessionId := c.GetHeader(types.ChatTokenHeader)
key := h.GetUserKey(c)
if _, err := h.redis.Del(c, key).Result(); err != nil {
logger.Error("error with delete session: ", err)
}
// 删除 websocket 会话列表
h.App.ChatSession.Delete(sessionId)
// 关闭 socket 连接
client := h.App.ChatClients.Get(sessionId)
if client != nil {
client.Close()
}
resp.SUCCESS(c)
}
// Session 获取/验证会话
func (h *UserHandler) Session(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db)
if err == nil {
var userVo vo.User
err := utils.CopyObject(user, &userVo)
if err != nil {
resp.ERROR(c)
}
userVo.Id = user.Id
resp.SUCCESS(c, userVo)
} else {
resp.NotAuth(c)
}
}
type userProfile struct {
Id uint `json:"id"`
Mobile string `json:"mobile"`
Avatar string `json:"avatar"`
ChatConfig types.UserChatConfig `json:"chat_config"`
Calls int `json:"calls"`
ImgCalls int `json:"img_calls"`
TotalTokens int64 `json:"total_tokens"`
}
func (h *UserHandler) Profile(c *gin.Context) {
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
h.db.First(&user, user.Id)
var profile userProfile
err = utils.CopyObject(user, &profile)
if err != nil {
logger.Error("对象拷贝失败:", err.Error())
resp.ERROR(c, "获取用户信息失败")
return
}
profile.Id = user.Id
resp.SUCCESS(c, profile)
}
func (h *UserHandler) ProfileUpdate(c *gin.Context) {
var data userProfile
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
h.db.First(&user, user.Id)
user.Avatar = data.Avatar
user.ChatConfig = utils.JsonEncode(data.ChatConfig)
res := h.db.Updates(&user)
if res.Error != nil {
resp.ERROR(c, "更新用户信息失败")
return
}
resp.SUCCESS(c)
}
// Password 更新密码
func (h *UserHandler) Password(c *gin.Context) {
var data struct {
OldPass string `json:"old_pass"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if len(data.Password) < 8 {
resp.ERROR(c, "密码长度不能少于8个字符")
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
password := utils.GenPassword(data.OldPass, user.Salt)
logger.Info(user.Salt, ",", user.Password, ",", password, ",", data.OldPass)
if password != user.Password {
resp.ERROR(c, "原密码错误")
return
}
newPass := utils.GenPassword(data.Password, user.Salt)
res := h.db.Model(&user).UpdateColumn("password", newPass)
if res.Error != nil {
logger.Error("更新数据库失败: ", res.Error)
resp.ERROR(c, "更新数据库失败")
return
}
resp.SUCCESS(c)
}
// BindMobile 绑定手机号
func (h *UserHandler) BindMobile(c *gin.Context) {
var data struct {
Mobile string `json:"mobile"`
Code int `json:"code"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 检查手机号是否被其他账号绑定
var item model.User
res := h.db.Where("mobile = ?", data.Mobile).First(&item)
if res.Error == nil {
resp.ERROR(c, "该手机号已经被其他账号绑定")
return
}
// 检查验证码
key := CodeStorePrefix + data.Mobile
var code int
err := h.leveldb.Get(key, &code)
if err != nil || code != data.Code {
resp.ERROR(c, "短信验证码错误")
return
}
user, err := utils.GetLoginUser(c, h.db)
if err != nil {
resp.NotAuth(c)
return
}
res = h.db.Model(&user).UpdateColumn("mobile", data.Mobile)
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
}
_ = h.leveldb.Delete(key) // 删除短信验证码
resp.SUCCESS(c)
}

74
api/logger/logger.go Normal file
View File

@@ -0,0 +1,74 @@
package logger
import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
)
var logger *zap.Logger
var sugarLogger *zap.SugaredLogger
func GetLogger() *zap.SugaredLogger {
if sugarLogger != nil {
return sugarLogger
}
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
encoder := getEncoder()
writerSyncer := getLogWriter()
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
consoleOutput := zapcore.Lock(os.Stdout)
consoleCore := zapcore.NewCore(
encoder,
consoleOutput,
logLevel,
)
core := zapcore.NewTee(fileCore, consoleCore)
logger = zap.New(core, zap.AddCaller())
sugarLogger = logger.Sugar()
return sugarLogger
}
// core 三个参数之 编码
func getEncoder() zapcore.Encoder {
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.SecondsDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
EncodeLevel: zapcore.CapitalLevelEncoder,
}
return zapcore.NewConsoleEncoder(encoderConfig)
}
func getLogWriter() zapcore.WriteSyncer {
lumberJackLogger := &lumberjack.Logger{
Filename: "logs/app.log",
MaxSize: 10,
MaxBackups: 5,
MaxAge: 30,
Compress: false,
}
return zapcore.AddSync(lumberJackLogger)
}
func getLogLevel(level string) zapcore.Level {
switch strings.ToUpper(level) {
case "DEBUG":
return zapcore.DebugLevel
case "WARN":
return zapcore.WarnLevel
case "ERROR":
return zapcore.ErrorLevel
default:
return zapcore.InfoLevel
}
}

294
api/main.go Normal file
View File

@@ -0,0 +1,294 @@
package main
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/handler"
"chatplus/handler/admin"
logger2 "chatplus/logger"
"chatplus/service"
"chatplus/service/function"
"chatplus/service/oss"
"chatplus/store"
"context"
"embed"
"github.com/go-redis/redis/v8"
"io"
"log"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
"go.uber.org/fx"
"gorm.io/gorm"
)
var logger = logger2.GetLogger()
//go:embed res/ip2region.xdb
var xdbFS embed.FS
// AppLifecycle 应用程序生命周期
type AppLifecycle struct {
}
// OnStart 应用程序启动时执行
func (l *AppLifecycle) OnStart(context.Context) error {
log.Println("AppLifecycle OnStart")
return nil
}
// OnStop 应用程序停止时执行
func (l *AppLifecycle) OnStop(context.Context) error {
log.Println("AppLifecycle OnStop")
return nil
}
func main() {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
configFile = "config.toml"
}
var debug bool
debugEnv := os.Getenv("DEBUG")
if debugEnv == "" {
debug = true
} else {
debug, _ = strconv.ParseBool(os.Getenv("DEBUG"))
}
logger.Info("Loading config file: ", configFile)
defer func() {
if err := recover(); err != nil {
logger.Error("Panic Error:", err)
}
}()
app := fx.New(
// 初始化配置应用配置
fx.Provide(func() *types.AppConfig {
config, err := core.LoadConfig(configFile)
if err != nil {
log.Fatal(err)
}
config.Path = configFile
if debug {
_ = core.SaveConfig(config)
}
return config
}),
// 创建应用服务
fx.Provide(core.NewServer),
// 初始化
fx.Invoke(func(s *core.AppServer, client *redis.Client) {
s.Init(debug, client)
}),
// 初始化数据库
fx.Provide(store.NewGormConfig),
fx.Provide(store.NewMysql),
fx.Provide(store.NewLevelDB),
fx.Provide(store.NewRedisClient),
// 创建 Ip2Region 查询对象
fx.Provide(func() (*xdb.Searcher, error) {
file, err := xdbFS.Open("res/ip2region.xdb")
if err != nil {
return nil, err
}
cBuff, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return xdb.NewWithBuffer(cBuff)
}),
// 创建函数
fx.Provide(function.NewFunctions),
// 创建控制器
fx.Provide(handler.NewChatRoleHandler),
fx.Provide(handler.NewUserHandler),
fx.Provide(handler.NewChatHandler),
fx.Provide(handler.NewUploadHandler),
fx.Provide(handler.NewSmsHandler),
fx.Provide(handler.NewRewardHandler),
fx.Provide(handler.NewCaptchaHandler),
fx.Provide(handler.NewMidJourneyHandler),
fx.Provide(handler.NewChatModelHandler),
fx.Provide(admin.NewConfigHandler),
fx.Provide(admin.NewAdminHandler),
fx.Provide(admin.NewApiKeyHandler),
fx.Provide(admin.NewUserHandler),
fx.Provide(admin.NewChatRoleHandler),
fx.Provide(admin.NewRewardHandler),
fx.Provide(admin.NewDashboardHandler),
fx.Provide(admin.NewChatModelHandler),
// 创建服务
fx.Provide(service.NewAliYunSmsService),
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
return service.NewCaptchaService(config.ApiConfig)
}),
fx.Provide(oss.NewUploaderManager),
fx.Provide(service.NewMjService),
fx.Invoke(func(mjService *service.MjService) {
go func() {
mjService.Run()
}()
}),
// 注册路由
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
group := s.Engine.Group("/api/role/")
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
group := s.Engine.Group("/api/user/")
group.POST("register", h.Register)
group.POST("login", h.Login)
group.GET("logout", h.Logout)
group.GET("session", h.Session)
group.GET("profile", h.Profile)
group.POST("profile/update", h.ProfileUpdate)
group.POST("password", h.Password)
group.POST("bind/mobile", h.BindMobile)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/")
group.Any("new", h.ChatHandle)
group.GET("list", h.List)
group.GET("detail", h.Detail)
group.POST("update", h.Update)
group.GET("remove", h.Remove)
group.GET("history", h.History)
group.GET("clear", h.Clear)
group.POST("tokens", h.Tokens)
group.GET("stop", h.StopGenerate)
}),
fx.Invoke(func(s *core.AppServer, h *handler.UploadHandler) {
s.Engine.POST("/api/upload", h.Upload)
}),
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
group := s.Engine.Group("/api/sms/")
group.GET("status", h.Status)
group.POST("code", h.SendCode)
}),
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
group := s.Engine.Group("/api/captcha/")
group.GET("get", h.Get)
group.POST("check", h.Check)
}),
fx.Invoke(func(s *core.AppServer, h *handler.RewardHandler) {
group := s.Engine.Group("/api/reward/")
group.POST("notify", h.Notify)
group.POST("verify", h.Verify)
}),
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
group := s.Engine.Group("/api/mj/")
group.POST("notify", h.Notify)
group.POST("image", h.Image)
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 *admin.ConfigHandler) {
group := s.Engine.Group("/api/admin/config/")
group.POST("update", h.Update)
group.GET("get", h.Get)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
group := s.Engine.Group("/api/admin/")
group.POST("login", h.Login)
group.GET("logout", h.Logout)
group.GET("session", h.Session)
group.GET("migrate", h.Migrate)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
group := s.Engine.Group("/api/admin/apikey/")
group.POST("save", h.Save)
group.GET("list", h.List)
group.GET("remove", h.Remove)
}),
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
group := s.Engine.Group("/api/admin/user/")
group.GET("list", h.List)
group.POST("save", h.Save)
group.GET("remove", h.Remove)
group.GET("loginLog", h.LoginLog)
group.POST("resetPass", h.ResetPass)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ChatRoleHandler) {
group := s.Engine.Group("/api/admin/role/")
group.GET("list", h.List)
group.POST("save", h.Save)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
}),
fx.Invoke(func(s *core.AppServer, h *admin.RewardHandler) {
group := s.Engine.Group("/api/admin/reward/")
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/")
group.GET("stats", h.Stats)
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
group := s.Engine.Group("/api/model/")
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
group := s.Engine.Group("/api/admin/model/")
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
err := s.Run(db)
if err != nil {
log.Fatal(err)
}
}),
// 注册生命周期回调函数
fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
lifecycle.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return lc.OnStart(ctx)
},
OnStop: func(ctx context.Context) error {
return lc.OnStop(ctx)
},
})
}),
)
// 启动应用程序
go func() {
if err := app.Start(context.Background()); err != nil {
log.Fatal(err)
}
}()
// 监听退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// 关闭应用程序
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.Stop(ctx); err != nil {
log.Fatal(err)
}
}

BIN
api/res/ip2region.xdb Normal file

Binary file not shown.

View File

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

View File

@@ -0,0 +1,62 @@
package service
import (
"chatplus/core/types"
"errors"
"fmt"
"github.com/imroc/req/v3"
"time"
)
type CaptchaService struct {
config types.ChatPlusApiConfig
client *req.Client
}
func NewCaptchaService(config types.ChatPlusApiConfig) *CaptchaService {
return &CaptchaService{
config: config,
client: req.C().SetTimeout(10 * time.Second),
}
}
func (s *CaptchaService) Get() (interface{}, error) {
if s.config.Token == "" {
return nil, errors.New("无效的 API Token")
}
url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("AppId", s.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return nil, fmt.Errorf("请求 API 失败:%v", err)
}
if res.Code != types.Success {
return nil, fmt.Errorf("请求 API 失败:%s", res.Message)
}
return res.Data, nil
}
func (s *CaptchaService) Check(data interface{}) bool {
url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("AppId", s.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetBodyJsonMarshal(data).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
return false
}
if res.Code != types.Success {
return false
}
return true
}

View File

@@ -0,0 +1,41 @@
package function
import (
"chatplus/service"
"chatplus/utils"
)
// AI 绘画函数
type FuncMidJourney struct {
name string
service *service.MjService
}
func NewMidJourneyFunc(mjService *service.MjService) FuncMidJourney {
return FuncMidJourney{
name: "MidJourney AI 绘画",
service: mjService}
}
func (f FuncMidJourney) Invoke(params map[string]interface{}) (string, error) {
logger.Infof("MJ 绘画参数:%+v", params)
prompt := utils.InterfaceToString(params["prompt"])
f.service.PushTask(service.MjTask{
SessionId: utils.InterfaceToString(params["session_id"]),
Src: service.TaskSrcChat,
Type: service.Image,
Prompt: prompt,
UserId: utils.IntValue(utils.InterfaceToString(params["user_id"]), 0),
RoleId: utils.IntValue(utils.InterfaceToString(params["role_id"]), 0),
Icon: utils.InterfaceToString(params["icon"]),
ChatId: utils.InterfaceToString(params["chat_id"]),
})
return prompt, nil
}
func (f FuncMidJourney) Name() string {
return f.name
}
var _ Function = &FuncMidJourney{}

View File

@@ -0,0 +1,39 @@
package function
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/service"
)
type Function interface {
Invoke(map[string]interface{}) (string, error)
Name() string
}
var logger = logger2.GetLogger()
type resVo struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
Data struct {
Title string `json:"title"`
UpdatedAt string `json:"updated_at"`
Items []dataItem `json:"items"`
} `json:"data"`
}
type dataItem struct {
Title string `json:"title"`
Url string `json:"url"`
Remark string `json:"remark"`
}
func NewFunctions(config *types.AppConfig, mjService *service.MjService) map[string]Function {
return map[string]Function{
types.FuncZaoBao: NewZaoBao(config.ApiConfig),
types.FuncWeibo: NewWeiboHot(config.ApiConfig),
types.FuncHeadLine: NewHeadLines(config.ApiConfig),
types.FuncMidJourney: NewMidJourneyFunc(mjService),
}
}

View File

@@ -0,0 +1,58 @@
package function
import (
"chatplus/core/types"
"errors"
"fmt"
"github.com/imroc/req/v3"
"strings"
"time"
)
// 今日头条函数实现
type FuncHeadlines struct {
name string
config types.ChatPlusApiConfig
client *req.Client
}
func NewHeadLines(config types.ChatPlusApiConfig) FuncHeadlines {
return FuncHeadlines{
name: "今日头条",
config: config,
client: req.C().SetTimeout(10 * time.Second)}
}
func (f FuncHeadlines) Invoke(map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
url := fmt.Sprintf("%s/api/headline/fetch", f.config.ApiURL)
var res resVo
r, err := f.client.R().
SetHeader("AppId", f.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("%v%v", err, r.Err)
}
if res.Code != types.Success {
return "", errors.New(res.Message)
}
builder := make([]string, 0)
builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
for i, v := range res.Data.Items {
builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [%s]", i+1, v.Title, v.Url, v.Remark))
}
return strings.Join(builder, "\n\n"), nil
}
func (f FuncHeadlines) Name() string {
return f.name
}
var _ Function = &FuncHeadlines{}

View File

@@ -0,0 +1,58 @@
package function
import (
"chatplus/core/types"
"errors"
"fmt"
"github.com/imroc/req/v3"
"strings"
"time"
)
// 微博热搜函数实现
type FuncWeiboHot struct {
name string
config types.ChatPlusApiConfig
client *req.Client
}
func NewWeiboHot(config types.ChatPlusApiConfig) FuncWeiboHot {
return FuncWeiboHot{
name: "微博热搜",
config: config,
client: req.C().SetTimeout(10 * time.Second)}
}
func (f FuncWeiboHot) Invoke(map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
url := fmt.Sprintf("%s/api/weibo/fetch", f.config.ApiURL)
var res resVo
r, err := f.client.R().
SetHeader("AppId", f.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("%v%v", err, r.Err)
}
if res.Code != types.Success {
return "", errors.New(res.Message)
}
builder := make([]string, 0)
builder = append(builder, fmt.Sprintf("**%s**,最新更新:%s", res.Data.Title, res.Data.UpdatedAt))
for i, v := range res.Data.Items {
builder = append(builder, fmt.Sprintf("%d、 [%s](%s) [热度:%s]", i+1, v.Title, v.Url, v.Remark))
}
return strings.Join(builder, "\n\n"), nil
}
func (f FuncWeiboHot) Name() string {
return f.name
}
var _ Function = &FuncWeiboHot{}

View File

@@ -0,0 +1,59 @@
package function
import (
"chatplus/core/types"
"errors"
"fmt"
"github.com/imroc/req/v3"
"strings"
"time"
)
// 每日早报函数实现
type FuncZaoBao struct {
name string
config types.ChatPlusApiConfig
client *req.Client
}
func NewZaoBao(config types.ChatPlusApiConfig) FuncZaoBao {
return FuncZaoBao{
name: "每日早报",
config: config,
client: req.C().SetTimeout(10 * time.Second)}
}
func (f FuncZaoBao) Invoke(map[string]interface{}) (string, error) {
if f.config.Token == "" {
return "", errors.New("无效的 API Token")
}
url := fmt.Sprintf("%s/api/zaobao/fetch", f.config.ApiURL)
var res resVo
r, err := f.client.R().
SetHeader("AppId", f.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", f.config.Token)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return "", fmt.Errorf("%v%v", err, r.Err)
}
if res.Code != types.Success {
return "", errors.New(res.Message)
}
builder := make([]string, 0)
builder = append(builder, fmt.Sprintf("**%s 早报:**", res.Data.UpdatedAt))
for _, v := range res.Data.Items {
builder = append(builder, v.Title)
}
builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
return strings.Join(builder, "\n\n"), nil
}
func (f FuncZaoBao) Name() string {
return f.name
}
var _ Function = &FuncZaoBao{}

203
api/service/mj_service.go Normal file
View File

@@ -0,0 +1,203 @@
package service
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"chatplus/store"
"chatplus/store/model"
"chatplus/utils"
"context"
"errors"
"fmt"
"github.com/go-redis/redis/v8"
"github.com/imroc/req/v3"
"gorm.io/gorm"
"time"
)
var logger = logger2.GetLogger()
// MJ 绘画服务
const MjRunningJobKey = "MidJourney_Running_Job"
type TaskType string
func (t TaskType) String() string {
return string(t)
}
const (
Image = TaskType("image")
Upscale = TaskType("upscale")
Variation = TaskType("variation")
)
type TaskSrc string
const (
TaskSrcChat = TaskSrc("chat")
TaskSrcImg = TaskSrc("img")
)
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 int32 `json:"index,omitempty"`
MessageId string `json:"message_id,omitempty"`
MessageHash string `json:"message_hash,omitempty"`
RetryCount int `json:"retry_count"`
}
type MjService struct {
config types.ChatPlusExtConfig
client *req.Client
taskQueue *store.RedisQueue
redis *redis.Client
db *gorm.DB
}
func NewMjService(appConfig *types.AppConfig, client *redis.Client, db *gorm.DB) *MjService {
return &MjService{
config: appConfig.ExtConfig,
redis: client,
db: db,
taskQueue: store.NewRedisQueue("midjourney_task_queue", client),
client: req.C().SetTimeout(30 * time.Second)}
}
func (s *MjService) Run() {
logger.Info("Starting MidJourney job consumer.")
ctx := context.Background()
for {
_, err := s.redis.Get(ctx, MjRunningJobKey).Result()
if err == nil {
time.Sleep(time.Second * 3)
continue
}
var task 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 Image:
err = s.image(task.Prompt)
break
case Upscale:
err = s.upscale(MjUpscaleReq{
Index: task.Index,
MessageId: task.MessageId,
MessageHash: task.MessageHash,
})
break
case Variation:
err = s.variation(MjVariationReq{
Index: task.Index,
MessageId: task.MessageId,
MessageHash: task.MessageHash,
})
}
if err != nil {
logger.Error("绘画任务执行失败:", err)
if task.RetryCount > 5 {
// 取消并删除任务
s.db.Where("id = ?", task.Id).Delete(&model.MidJourneyJob{})
continue
}
task.RetryCount += 1
s.taskQueue.RPush(task)
// TODO: 执行失败通知聊天客户端
time.Sleep(time.Second * 3)
continue
}
// 锁定任务执行通道直到任务超时5分钟
s.redis.Set(ctx, MjRunningJobKey, utils.JsonEncode(task), time.Minute*5)
}
}
func (s *MjService) PushTask(task MjTask) {
logger.Infof("add a new MidJourney Task: %+v", task)
s.taskQueue.RPush(task)
}
func (s *MjService) image(prompt string) error {
logger.Infof("MJ 绘画参数:%+v", prompt)
body := map[string]string{"prompt": prompt}
url := fmt.Sprintf("%s/api/mj/image", s.config.ApiURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("Authorization", s.config.Token).
SetHeader("Content-Type", "application/json").
SetBody(body).
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 MjUpscaleReq struct {
Index int32 `json:"index"`
MessageId string `json:"message_id"`
MessageHash string `json:"message_hash"`
}
func (s *MjService) upscale(upReq MjUpscaleReq) error {
url := fmt.Sprintf("%s/api/mj/upscale", s.config.ApiURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("Authorization", s.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 (s *MjService) variation(upReq MjVariationReq) error {
url := fmt.Sprintf("%s/api/mj/variation", s.config.ApiURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("Authorization", s.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
}

View File

@@ -0,0 +1,64 @@
package oss
import (
"chatplus/core/types"
"chatplus/utils"
"fmt"
"github.com/gin-gonic/gin"
"os"
"path/filepath"
"strings"
)
type LocalStorageService struct {
config *types.LocalStorageConfig
proxyURL string
}
func NewLocalStorageService(config *types.AppConfig) LocalStorageService {
return LocalStorageService{
config: &config.OSS.Local,
proxyURL: config.ProxyURL,
}
}
func (s LocalStorageService) PutFile(ctx *gin.Context, name string) (string, error) {
file, err := ctx.FormFile(name)
if err != nil {
return "", fmt.Errorf("error with get form: %v", err)
}
filePath, err := utils.GenUploadPath(s.config.BasePath, file.Filename)
if err != nil {
return "", fmt.Errorf("error with generate filename: %s", err.Error())
}
// 将文件保存到指定路径
err = ctx.SaveUploadedFile(file, filePath)
if err != nil {
return "", fmt.Errorf("error with save upload file: %s", err.Error())
}
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
}
func (s LocalStorageService) PutImg(imageURL string) (string, error) {
filename := filepath.Base(imageURL)
filePath, err := utils.GenUploadPath(s.config.BasePath, filename)
if err != nil {
return "", fmt.Errorf("error with generate image dir: %v", err)
}
err = utils.DownloadFile(imageURL, filePath, s.proxyURL)
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
return utils.GenUploadUrl(s.config.BasePath, s.config.BaseURL, filePath), nil
}
func (s LocalStorageService) Delete(fileURL string) error {
filePath := strings.Replace(fileURL, s.config.BaseURL, s.config.BasePath, 1)
return os.Remove(filePath)
}
var _ Uploader = LocalStorageService{}

View File

@@ -0,0 +1,83 @@
package oss
import (
"chatplus/core/types"
"chatplus/utils"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"path/filepath"
"strings"
"time"
)
type MinioService struct {
config *types.MinioConfig
client *minio.Client
proxyURL string
}
func NewMinioService(appConfig *types.AppConfig) (MinioService, error) {
config := &appConfig.OSS.Minio
minioClient, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.AccessKey, config.AccessSecret, ""),
Secure: config.UseSSL,
})
if err != nil {
return MinioService{}, err
}
return MinioService{config: config, client: minioClient, proxyURL: appConfig.ProxyURL}, nil
}
func (s MinioService) PutImg(imageURL string) (string, error) {
imageData, err := utils.DownloadImage(imageURL, s.proxyURL)
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
fileExt := filepath.Ext(filepath.Base(imageURL))
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
info, err := s.client.PutObject(
context.Background(),
s.config.Bucket,
filename,
strings.NewReader(string(imageData)),
int64(len(imageData)),
minio.PutObjectOptions{ContentType: "image/png"})
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
}
func (s MinioService) PutFile(ctx *gin.Context, name string) (string, error) {
file, err := ctx.FormFile(name)
if err != nil {
return "", fmt.Errorf("error with get form: %v", err)
}
// Open the uploaded file
fileReader, err := file.Open()
if err != nil {
return "", fmt.Errorf("error opening file: %v", err)
}
defer fileReader.Close()
fileExt := filepath.Ext(file.Filename)
filename := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
info, err := s.client.PutObject(ctx, s.config.Bucket, filename, fileReader, file.Size, minio.PutObjectOptions{
ContentType: file.Header.Get("Content-Type"),
})
if err != nil {
return "", fmt.Errorf("error uploading to MinIO: %v", err)
}
return fmt.Sprintf("%s/%s/%s", s.config.Domain, s.config.Bucket, info.Key), nil
}
func (s MinioService) Delete(fileURL string) error {
objectName := filepath.Base(fileURL)
return s.client.RemoveObject(context.Background(), s.config.Bucket, objectName, minio.RemoveObjectOptions{})
}
var _ Uploader = MinioService{}

View File

@@ -0,0 +1,98 @@
package oss
import (
"bytes"
"chatplus/core/types"
"chatplus/utils"
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/qiniu/go-sdk/v7/auth/qbox"
"github.com/qiniu/go-sdk/v7/storage"
"path/filepath"
"time"
)
type QiNiuService struct {
config *types.QiNiuConfig
token string
uploader *storage.FormUploader
manager *storage.BucketManager
proxyURL string
dir string
}
func NewQiNiuService(appConfig *types.AppConfig) QiNiuService {
config := &appConfig.OSS.QiNiu
// build storage uploader
zone, ok := storage.GetRegionByID(storage.RegionID(config.Zone))
if !ok {
zone = storage.ZoneHuanan
}
storeConfig := storage.Config{Zone: &zone}
formUploader := storage.NewFormUploader(&storeConfig)
// generate token
mac := qbox.NewMac(config.AccessKey, config.AccessSecret)
putPolicy := storage.PutPolicy{
Scope: config.Bucket,
}
return QiNiuService{
config: config,
token: putPolicy.UploadToken(mac),
uploader: formUploader,
manager: storage.NewBucketManager(mac, &storeConfig),
proxyURL: appConfig.ProxyURL,
dir: "chatgpt-plus",
}
}
func (s QiNiuService) PutFile(ctx *gin.Context, name string) (string, error) {
// 解析表单
file, err := ctx.FormFile(name)
if err != nil {
return "", err
}
// 打开上传文件
src, err := file.Open()
if err != nil {
return "", err
}
defer src.Close()
fileExt := filepath.Ext(file.Filename)
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
// 上传文件
ret := storage.PutRet{}
extra := storage.PutExtra{}
err = s.uploader.Put(ctx, &ret, s.token, key, src, file.Size, &extra)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
}
func (s QiNiuService) PutImg(imageURL string) (string, error) {
imageData, err := utils.DownloadImage(imageURL, s.proxyURL)
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
fileExt := filepath.Ext(filepath.Base(imageURL))
key := fmt.Sprintf("%s/%d%s", s.dir, time.Now().UnixMicro(), fileExt)
ret := storage.PutRet{}
extra := storage.PutExtra{}
// 上传文件字节数据
err = s.uploader.Put(context.Background(), &ret, s.token, key, bytes.NewReader(imageData), int64(len(imageData)), &extra)
if err != nil {
return "", err
}
return fmt.Sprintf("%s/%s", s.config.Domain, ret.Key), nil
}
func (s QiNiuService) Delete(fileURL string) error {
objectName := filepath.Base(fileURL)
key := fmt.Sprintf("%s/%s", s.dir, objectName)
return s.manager.Delete(s.config.Bucket, key)
}
var _ Uploader = QiNiuService{}

View File

@@ -0,0 +1,9 @@
package oss
import "github.com/gin-gonic/gin"
type Uploader interface {
PutFile(ctx *gin.Context, name string) (string, error)
PutImg(imageURL string) (string, error)
Delete(fileURL string) error
}

View File

@@ -0,0 +1,42 @@
package oss
import (
"chatplus/core/types"
"strings"
)
type UploaderManager struct {
handler Uploader
}
const Local = "LOCAL"
const Minio = "MINIO"
const QiNiu = "QINIU"
func NewUploaderManager(config *types.AppConfig) (*UploaderManager, error) {
active := Local
if config.OSS.Active != "" {
active = strings.ToUpper(config.OSS.Active)
}
var handler Uploader
switch active {
case Local:
handler = NewLocalStorageService(config)
break
case Minio:
service, err := NewMinioService(config)
if err != nil {
return nil, err
}
handler = service
break
case QiNiu:
handler = NewQiNiuService(config)
}
return &UploaderManager{handler: handler}, nil
}
func (m *UploaderManager) GetUploadHandler() Uploader {
return m.handler
}

View File

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

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

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

View File

@@ -1,8 +1,8 @@
package utils
package store
import (
"chatplus/store/vo"
"encoding/json"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
@@ -11,8 +11,8 @@ type LevelDB struct {
driver *leveldb.DB
}
func NewLevelDB(path string) (*LevelDB, error) {
db, err := leveldb.OpenFile(path, nil)
func NewLevelDB() (*LevelDB, error) {
db, err := leveldb.OpenFile("data/leveldb", nil)
if err != nil {
return nil, err
}
@@ -29,13 +29,13 @@ func (db *LevelDB) Put(key string, value interface{}) error {
return db.driver.Put([]byte(key), bytes, nil)
}
func (db *LevelDB) Get(key string) ([]byte, error) {
func (db *LevelDB) Get(key string, value interface{}) error {
bytes, err := db.driver.Get([]byte(key), nil)
if err != nil {
return nil, err
return err
}
return bytes, nil
return json.Unmarshal(bytes, &value)
}
func (db *LevelDB) Search(prefix string) []string {
@@ -49,27 +49,19 @@ func (db *LevelDB) Search(prefix string) []string {
return items
}
type PageVo struct {
Items []string
Page int
PageSize int
Total int
TotalPage int
}
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *PageVo {
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 := &PageVo{Page: page, PageSize: pageSize}
res := &vo.Page{Page: page, PageSize: pageSize}
// 计算数据总数和总页数
total := 0
for iter.Next() {
total++
}
res.TotalPage = (total + pageSize - 1) / pageSize
res.Total = total
res.Total = int64(total)
// 计算目标页码的起始和结束位置
start := (page - 1) * pageSize

View File

@@ -0,0 +1,9 @@
package model
// ApiKey OpenAI API 模型
type ApiKey struct {
BaseModel
Platform string
Value string // API Key 的值
LastUsedAt int64 // 最后使用时间
}

9
api/store/model/base.go Normal file
View File

@@ -0,0 +1,9 @@
package model
import "time"
type BaseModel struct {
Id uint `gorm:"primarykey;column:id"`
CreatedAt time.Time
UpdatedAt time.Time
}

View File

@@ -0,0 +1,20 @@
package model
import "gorm.io/gorm"
type HistoryMessage struct {
BaseModel
ChatId string // 会话 ID
UserId uint // 用户 ID
RoleId uint // 角色 ID
Type string
Icon string
Tokens int
Content string
UseContext bool // 是否可以作为聊天上下文
DeletedAt gorm.DeletedAt
}
func (HistoryMessage) TableName() string {
return "chatgpt_chat_history"
}

View File

@@ -0,0 +1,13 @@
package model
import "gorm.io/gorm"
type ChatItem struct {
BaseModel
ChatId string `gorm:"column:chat_id;unique"` // 会话 ID
UserId uint // 用户 ID
RoleId uint // 角色 ID
ModelId uint // 会话模型
Title string // 会话标题
DeletedAt gorm.DeletedAt
}

View File

@@ -0,0 +1,10 @@
package model
type ChatModel struct {
BaseModel
Platform string
Name string
Value string // API Key 的值
SortNum int
Enabled bool
}

View File

@@ -0,0 +1,12 @@
package model
type ChatRole struct {
BaseModel
Key string `gorm:"column:marker;unique"` // 角色唯一标识
Name string // 角色名称
Context string `gorm:"column:context_json"` // 角色语料信息 json
HelloMsg string // 打招呼的消息
Icon string // 角色聊天图标
Enable bool // 是否启用被启用
SortNum int //排序数字
}

View File

@@ -0,0 +1,7 @@
package model
type Config struct {
Id uint `gorm:"primarykey;column:id"`
Key string `gorm:"column:marker;unique"`
Config string `gorm:"column:config_json"`
}

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

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

12
api/store/model/reward.go Normal file
View File

@@ -0,0 +1,12 @@
package model
// 用户打赏
type Reward struct {
BaseModel
UserId uint // 用户 ID
TxId string // 交易ID
Amount float64 // 打赏金额
Remark string // 打赏备注
Status bool // 核销状态
}

18
api/store/model/user.go Normal file
View File

@@ -0,0 +1,18 @@
package model
type User struct {
BaseModel
Mobile string
Password string
Avatar string
Salt string // 密码盐
TotalTokens int64 // 总消耗 tokens
Calls int // 剩余对话次数
ImgCalls int // 剩余绘图次数
ChatConfig string `gorm:"column:chat_config_json"` // 聊天配置 json
ChatRoles string `gorm:"column:chat_roles_json"` // 聊天角色
ExpiredTime int64 // 账户到期时间
Status bool `gorm:"default:true"` // 当前状态
LastLoginAt int64 // 最后登录时间
LastLoginIp string // 最后登录 IP
}

View File

@@ -0,0 +1,9 @@
package model
type UserLoginLog struct {
BaseModel
UserId uint
Username string
LoginIp string
LoginAddress string
}

36
api/store/mysql.go Normal file
View File

@@ -0,0 +1,36 @@
package store
import (
"chatplus/core/types"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"time"
)
func NewGormConfig() *gorm.Config {
return &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
NamingStrategy: schema.NamingStrategy{
TablePrefix: "chatgpt_", // 设置表前缀
SingularTable: false, // 使用单数表名形式
},
}
}
func NewMysql(config *gorm.Config, appConfig *types.AppConfig) (*gorm.DB, error) {
db, err := gorm.Open(mysql.Open(appConfig.MysqlDns), config)
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(32)
sqlDB.SetMaxOpenConns(512)
sqlDB.SetConnMaxLifetime(time.Hour)
if err != nil {
return nil, err
}
return db, nil
}

20
api/store/redis.go Normal file
View File

@@ -0,0 +1,20 @@
package store
import (
"chatplus/core/types"
"context"
"github.com/go-redis/redis/v8"
)
func NewRedisClient(config *types.AppConfig) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: config.Redis.Url(),
Password: config.Redis.Password,
DB: config.Redis.DB,
})
_, err := client.Ping(context.Background()).Result()
if err != nil {
return nil, err
}
return client, nil
}

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

9
api/store/vo/api_key.go Normal file
View File

@@ -0,0 +1,9 @@
package vo
// ApiKey OpenAI API 模型
type ApiKey struct {
BaseVo
Platform string `json:"platform"`
Value string `json:"value"` // API Key 的值
LastUsedAt int64 `json:"last_used_at"` // 最后使用时间
}

7
api/store/vo/base.go Normal file
View File

@@ -0,0 +1,7 @@
package vo
type BaseVo struct {
Id uint `json:"id"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
}

View File

@@ -0,0 +1,13 @@
package vo
type HistoryMessage struct {
BaseVo
ChatId string `json:"chat_id"`
UserId uint `json:"user_id"`
RoleId uint `json:"role_id"`
Type string `json:"type"`
Icon string `json:"icon"`
Tokens int `json:"tokens"`
Content string `json:"content"`
UseContext bool `json:"use_context"`
}

11
api/store/vo/chat_item.go Normal file
View File

@@ -0,0 +1,11 @@
package vo
type ChatItem struct {
BaseVo
UserId uint `json:"user_id"`
Icon string `json:"icon"`
RoleId uint `json:"role_id"`
ChatId string `json:"chat_id"`
ModelId uint `json:"model_id"`
Title string `json:"title"`
}

View File

@@ -0,0 +1,9 @@
package vo
type ChatModel struct {
BaseVo
Platform string `json:"platform"`
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
}

14
api/store/vo/chat_role.go Normal file
View File

@@ -0,0 +1,14 @@
package vo
import "chatplus/core/types"
type ChatRole struct {
BaseVo
Key string `json:"key"` // 角色唯一标识
Name string `json:"name"` // 角色名称
Context []types.Message `json:"context"` // 角色语料信息
HelloMsg string `json:"hello_msg"` // 打招呼的消息
Icon string `json:"icon"` // 角色聊天图标
Enable bool `json:"enable"` // 是否启用被启用
SortNum int `json:"sort"` // 排序
}

10
api/store/vo/config.go Normal file
View File

@@ -0,0 +1,10 @@
package vo
import "chatplus/core/types"
type Config struct {
Id uint `json:"id"`
Key string `json:"key"`
ChatConfig types.ChatConfig `json:"chat_config"`
SystemConfig types.SystemConfig `json:"system_config"`
}

16
api/store/vo/mj_job.go Normal file
View File

@@ -0,0 +1,16 @@
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"`
}

22
api/store/vo/page.go Normal file
View File

@@ -0,0 +1,22 @@
package vo
import "math"
type Page struct {
Items interface{} `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
TotalPage int `json:"total_page"`
}
func NewPage(total int64, page int, pageSize int, items interface{}) Page {
totalPage := math.Ceil(float64(total) / float64(pageSize))
return Page{
Items: items,
Page: page,
PageSize: pageSize,
Total: total,
TotalPage: int(totalPage),
}
}

11
api/store/vo/reward.go Normal file
View File

@@ -0,0 +1,11 @@
package vo
type Reward struct {
BaseVo
UserId uint `json:"user_id"` // 用户 ID
Username string `json:"username"`
TxId string `json:"tx_id"` // 交易ID
Amount float64 `json:"amount"` // 打赏金额
Remark string `json:"remark"` // 打赏备注
Status bool `json:"status"` // 核销状态
}

19
api/store/vo/user.go Normal file
View File

@@ -0,0 +1,19 @@
package vo
import "chatplus/core/types"
type User struct {
BaseVo
Mobile string `json:"mobile"`
Avatar string `json:"avatar"`
Salt string `json:"salt"` // 密码盐
TotalTokens int64 `json:"total_tokens"` // 总消耗tokens
Calls int `json:"calls"` // 剩余对话次数
ImgCalls int `json:"img_calls"`
ChatConfig types.UserChatConfig `json:"chat_config"` // 聊天配置
ChatRoles []string `json:"chat_roles"` // 聊天角色集合
ExpiredTime int64 `json:"expired_time"` // 账户到期时间
Status bool `json:"status"` // 当前状态
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
}

View File

@@ -0,0 +1,9 @@
package vo
type UserLoginLog struct {
BaseVo
UserId uint `json:"user_id"`
Username string `json:"username"`
LoginIp string `json:"login_ip"`
LoginAddress string `json:"login_address"`
}

13
api/test/test.go Normal file
View File

@@ -0,0 +1,13 @@
package main
import (
"fmt"
"net/url"
)
func main() {
parse, _ := url.Parse("http://localhost:5678/static")
imgURLPrefix := fmt.Sprintf("%s://%s", parse.Scheme, parse.Host)
fmt.Println(imgURLPrefix)
}

140
api/utils/common.go Normal file
View File

@@ -0,0 +1,140 @@
package utils
import (
"encoding/json"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
)
// CopyObject 拷贝对象
func CopyObject(src interface{}, dst interface{}) error {
srcType := reflect.TypeOf(src)
srcValue := reflect.ValueOf(src)
dstValue := reflect.ValueOf(dst).Elem()
reflect.TypeOf(dst)
for i := 0; i < srcType.NumField(); i++ {
field := srcType.Field(i)
value := dstValue.FieldByName(field.Name)
if !value.IsValid() {
continue
}
// 数据类型相同,直接赋值
v := srcValue.FieldByName(field.Name)
if value.Type() == field.Type {
value.Set(v)
} else {
// src data type is stringdst data type is slice, map, struct
// use json decode the data
if field.Type.Kind() == reflect.String && (value.Type().Kind() == reflect.Struct ||
value.Type().Kind() == reflect.Map ||
value.Type().Kind() == reflect.Slice) {
pType := reflect.New(value.Type())
v2 := pType.Interface()
err := json.Unmarshal([]byte(v.String()), &v2)
if err == nil {
value.Set(reflect.ValueOf(v2).Elem())
}
// map, struct, slice to string
} else if (field.Type.Kind() == reflect.Struct ||
field.Type.Kind() == reflect.Map ||
field.Type.Kind() == reflect.Slice) && value.Type().Kind() == reflect.String {
ba, err := json.Marshal(v.Interface())
if err == nil {
val := string(ba)
if strings.Contains(val, "{") {
value.Set(reflect.ValueOf(string(ba)))
} else {
value.Set(reflect.ValueOf(""))
}
}
} else { // 简单数据类型的强制类型转换
switch value.Kind() {
case reflect.Int:
case reflect.Int8:
case reflect.Int16:
case reflect.Int32:
case reflect.Int64:
value.SetInt(v.Int())
break
case reflect.Float32:
case reflect.Float64:
value.SetFloat(v.Float())
break
case reflect.Bool:
value.SetBool(v.Bool())
break
}
}
}
}
return nil
}
func Ip2Region(searcher *xdb.Searcher, ip string) string {
str, err := searcher.SearchByStr(ip)
if err != nil {
return ""
}
arr := strings.Split(str, "|")
if len(arr) < 3 {
return arr[0]
}
return fmt.Sprintf("%s-%s-%s", arr[0], arr[2], arr[3])
}
func IsEmptyValue(obj interface{}) bool {
if obj == nil {
return true
}
v := reflect.ValueOf(obj)
switch v.Kind() {
case reflect.Ptr, reflect.Interface:
return v.IsNil()
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Complex64, reflect.Complex128:
return v.Complex() == 0
default:
return reflect.DeepEqual(obj, reflect.Zero(reflect.TypeOf(obj)).Interface())
}
}
func BoolValue(str string) bool {
value, err := strconv.ParseBool(str)
if err != nil {
return false
}
return value
}
func FloatValue(str string) float64 {
value, err := strconv.ParseFloat(str, 64)
if err != nil {
return 0
}
return value
}
func IntValue(str string, defaultValue int) int {
value, err := strconv.Atoi(str)
if err != nil {
return defaultValue
}
return value
}

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

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

63
api/utils/net.go Normal file
View File

@@ -0,0 +1,63 @@
package utils
import (
"chatplus/core/types"
logger2 "chatplus/logger"
"encoding/json"
"io"
"net/http"
"net/url"
)
var logger = logger2.GetLogger()
// ReplyChunkMessage 回复客户片段端消息
func ReplyChunkMessage(client *types.WsClient, message interface{}) {
msg, err := json.Marshal(message)
if err != nil {
logger.Errorf("Error for decoding json data: %v", err.Error())
return
}
err = client.Send(msg)
if err != nil {
logger.Errorf("Error for reply message: %v", err.Error())
}
}
// ReplyMessage 回复客户端一条完整的消息
func ReplyMessage(ws *types.WsClient, message interface{}) {
ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: message})
ReplyChunkMessage(ws, types.WsMessage{Type: types.WsEnd})
}
func DownloadImage(imageURL string, proxy string) ([]byte, error) {
var client *http.Client
if proxy == "" {
client = http.DefaultClient
} else {
proxyURL, _ := url.Parse(proxy)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
}
}
req, err := http.NewRequest("GET", imageURL, nil)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
imageBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return imageBytes, nil
}

20
api/utils/openai.go Normal file
View File

@@ -0,0 +1,20 @@
package utils
import (
"fmt"
"github.com/pkoukk/tiktoken-go"
)
func CalcTokens(text string, model string) (int, error) {
encoding, ok := tiktoken.MODEL_TO_ENCODING[model]
if !ok {
encoding = "cl100k_base"
}
tke, err := tiktoken.GetEncoding(encoding)
if err != nil {
return 0, fmt.Errorf("getEncoding: %v", err)
}
token := tke.Encode(text, nil, nil)
return len(token), nil
}

View File

@@ -0,0 +1,36 @@
package resp
import (
"chatplus/core/types"
"github.com/gin-gonic/gin"
"net/http"
)
func SUCCESS(c *gin.Context, values ...interface{}) {
if values != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success, Data: values[0]})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Success})
}
}
func ERROR(c *gin.Context, messages ...string) {
if messages != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: messages[0]})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed})
}
}
func HACKER(c *gin.Context) {
c.JSON(http.StatusOK, types.BizVo{Code: types.Failed, Message: "Hacker attempt!!!"})
}
func NotAuth(c *gin.Context, messages ...string) {
if messages != nil {
c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: messages[0]})
} else {
c.JSON(http.StatusOK, types.BizVo{Code: types.NotAuthorized, Message: "Not Authorized"})
}
}

View File

@@ -1,9 +1,12 @@
package utils
import (
"encoding/json"
"fmt"
"math/rand"
"strconv"
"time"
"golang.org/x/crypto/sha3"
)
// RandString generate rand string with specified length
@@ -18,12 +21,20 @@ func RandString(length int) string {
return string(result)
}
func Long2IP(ipInt int64) string {
b0 := strconv.FormatInt((ipInt>>24)&0xff, 10)
b1 := strconv.FormatInt((ipInt>>16)&0xff, 10)
b2 := strconv.FormatInt((ipInt>>8)&0xff, 10)
b3 := strconv.FormatInt(ipInt&0xff, 10)
return b0 + "." + b1 + "." + b2 + "." + b3
func RandomNumber(bit int) int {
min := intPow(10, bit-1)
max := intPow(10, bit) - 1
rand.Seed(time.Now().UnixNano())
return rand.Intn(max-min+1) + min
}
func intPow(x, y int) int {
result := 1
for i := 0; i < y; i++ {
result *= x
}
return result
}
func ContainsStr(slice []string, item string) bool {
@@ -45,6 +56,10 @@ func Stamp2str(timestamp int64) string {
// Str2stamp 字符串转时间戳
func Str2stamp(str string) int64 {
if len(str) == 0 {
return 0
}
layout := "2006-01-02 15:04:05"
t, err := time.Parse(layout, str)
if err != nil {
@@ -52,3 +67,28 @@ func Str2stamp(str string) int64 {
}
return t.Unix()
}
func GenPassword(pass string, salt string) string {
data := []byte(pass + salt)
hash := sha3.Sum256(data)
return fmt.Sprintf("%x", hash)
}
func JsonEncode(value interface{}) string {
bytes, err := json.Marshal(value)
if err != nil {
return ""
}
return string(bytes)
}
func JsonDecode(src string, dest interface{}) error {
return json.Unmarshal([]byte(src), dest)
}
func InterfaceToString(value interface{}) string {
if str, ok := value.(string); ok {
return str
}
return JsonEncode(value)
}

68
api/utils/upload.go Normal file
View File

@@ -0,0 +1,68 @@
package utils
import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
)
// GenUploadPath 生成上传文件路径
func GenUploadPath(basePath, filename string) (string, error) {
now := time.Now()
dir := fmt.Sprintf("%s/%d/%d", basePath, now.Year(), now.Month())
_, err := os.Stat(dir)
if err != nil {
err = os.MkdirAll(dir, 0755)
if err != nil {
return "", fmt.Errorf("error with create upload dir%v", err)
}
}
fileExt := filepath.Ext(filename)
return fmt.Sprintf("%s/%d%s", dir, now.UnixMicro(), fileExt), nil
}
// GenUploadUrl 生成上传文件 URL
func GenUploadUrl(basePath, baseUrl string, filePath string) string {
return strings.Replace(filePath, basePath, baseUrl, 1)
}
func DownloadFile(fileURL string, filepath string, proxy string) error {
var client *http.Client
if proxy == "" {
client = http.DefaultClient
} else {
proxyURL, _ := url.Parse(proxy)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxyURL),
},
}
}
req, err := http.NewRequest("GET", fileURL, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
file, err := os.Create(filepath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
return nil
}

30
api/utils/user.go Normal file
View File

@@ -0,0 +1,30 @@
package utils
import (
"chatplus/core/types"
"chatplus/store/model"
"errors"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func GetLoginUser(c *gin.Context, db *gorm.DB) (model.User, error) {
value, exists := c.Get(types.LoginUserCache)
if exists {
return value.(model.User), nil
}
userId, ok := c.Get(types.LoginUserID)
if !ok {
return model.User{}, errors.New("user not login")
}
var user model.User
res := db.First(&user, userId)
// 更新缓存
if res.Error == nil {
c.Set(types.LoginUserCache, user)
}
return user, res.Error
}

View File

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

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