Compare commits

...

1419 Commits

Author SHA1 Message Date
RockYang b18b8ccb02 release: v4.2.8
整合开源版 v4.2.8 功能:Sora2 视频、路由重构、手机站开关、DALL-E 参考图,以及启动时自动同步数据表字段等修复与优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 11:12:37 +08:00
RockYang f8a01cb9a2 feat: 启动时自动同步 model 与数据表字段
用 AutoMigrate 补齐缺列,并删除 model 中已不存在的多余列(保护 id/时间戳),修复 ChatModel.desc 等字段缺失导致的 1054 错误。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 10:37:38 +08:00
RockYang 64832a2a04 update readme 2026-05-24 10:14:28 +08:00
RockYang b1dd781a3d update readme 2026-05-24 10:12:51 +08:00
RockYang 71dc082e40 Merge tag 'v4.2.7' 2026-05-09 20:56:07 +08:00
RockYang 10836bbb4a merge v4.2.6
整合 v4.2.6 的后端中间件与服务层重构、前端样式体系迁移和管理端/移动端功能更新,统一清理历史冲突并完成版本升级。

Made-with: Cursor
2026-04-08 15:08:34 +08:00
RockYang d8237ae301 merge v4.2.5 2026-04-05 21:35:30 +08:00
RockYang 3974f1be87 merge v4.2.4 2026-02-14 17:20:53 +08:00
RockYang 126c9921a8 merge v4.2.3 2026-01-29 20:11:50 +08:00
RockYang c84754fc0c merge v4.2.2 2025-12-02 11:18:30 +08:00
RockYang 172807d326 add EasyDevelop deploy docs 2025-12-02 11:15:54 +08:00
RockYang 72608af77e merge v4.2.1 2025-11-26 20:09:48 +08:00
RockYang 7879459c3f update docker images version to docker-compose.yaml 2025-11-11 11:07:22 +08:00
RockYang ad482dad7f merge v4.2.0 2025-11-11 10:17:03 +08:00
RockYang 58e99976b3 更新数据库文件 2025-10-27 16:25:58 +08:00
RockYang 567803ebeb 添加备案信息配置项,给登录页面 Logo 增加圆角 2025-10-27 16:25:58 +08:00
RockYang 8e3d67cc7f 优化编译指令,减少程序体积 2025-10-27 16:25:58 +08:00
RockYang b463608ab7 支持微模型绑定 Dalle 绘图的 API KEY 2025-10-27 16:25:58 +08:00
RockYang eefd36562d 管理后台用户算力日志页面增加过滤查询功能 2025-10-27 16:25:58 +08:00
RockYang ebf4497b34 允许配置登录注册页面的自定义 Logo 2025-10-27 16:25:58 +08:00
RockYang 194b0343bf 修复微信登录功能 2025-10-27 16:25:58 +08:00
RockYang adb1dfbded 首页增加码云连接地址 2025-10-27 16:25:58 +08:00
RockYang 9e689f6022 支持在 Chat 页面显示,隐藏对话列表 2025-10-27 16:25:55 +08:00
RockYang 499f5c915f merge and resolve conflicts 2025-10-27 16:21:46 +08:00
RockYang a9f11d79a9 optimize login page styles 2025-10-27 16:19:34 +08:00
RockYang db5e9ea2d0 更改文档地址 2025-10-27 16:19:34 +08:00
GeekMaster 64f8693871 Merge branch 'main' of gitee.com:blackfox/geekai-plus 2025-09-17 16:31:41 +08:00
GeekMaster 4270847b5d optimize regenerate for mobile chat page 2025-09-17 16:18:16 +08:00
GeekMaster 3eb0177188 add prompt edit function 2025-09-17 16:04:04 +08:00
GeekMaster 48203e0d31 optimize jimeng power config 2025-09-16 20:35:53 +08:00
GeekMaster 6e6a496f1b remove reset password page 2025-09-16 19:31:06 +08:00
GeekMaster ddfa588163 update version 2025-09-16 19:25:46 +08:00
GeekMaster 809d8d71bd Jimeng AI 4.0 for mobile is ready 2025-09-16 19:13:41 +08:00
GeekMaster 49254b2a32 optimize ParamBuilderMobile components 2025-09-15 22:18:46 +08:00
GeekMaster 5979451ea6 ParamBuilder component support hidden input 2025-09-15 21:05:34 +08:00
GeekMaster 46c3d65bea update change log file 2025-09-15 20:36:34 +08:00
GeekMaster c4b44d84e3 Jimeng VirtualHuman and actionTransfer is ready 2025-09-15 20:29:46 +08:00
RockYang 822d1831cd rm unuse certs 2025-09-13 10:48:00 +08:00
RockYang 0d28402aa3 fixed bug for sub user power to negtive nuber 2025-09-13 10:35:55 +08:00
RockYang 359999c045 check user power for create ai3d job 2025-09-13 10:29:40 +08:00
GeekMaster 2c6eee7fc1 acommpelish jimeng AI refactor for PC 2025-09-12 18:58:52 +08:00
GeekMaster c5badb3e13 add user lock for chat api, Prevent insufficient deduction of user power caused by submitting multiple requests at one time 2025-09-12 15:05:14 +08:00
GeekMaster 65fb58585c refactor parameters for JimengCreate page 2025-09-11 15:48:07 +08:00
RockYang 02aedd9edc Merge branch 'bug-fix' 2025-09-10 19:19:21 +08:00
GeekMaster 896b5de0a4 param builder component is ready 2025-09-10 17:04:37 +08:00
GeekMaster 1ca58606da optimize jimeng AI task params build 2025-09-09 18:05:51 +08:00
GeekMaster 3d8c37cb38 allow user to set default index page 2025-09-09 11:11:34 +08:00
GeekMaster c8eec6c97e Merge branch 'bug-fix' 2025-09-09 10:40:47 +08:00
RockYang 330ac397d4 fixed bug for change admin user password 2025-09-08 22:10:29 +08:00
RockYang 8aa75a49d2 fixed bug for can not change admin user password 2025-09-08 18:59:31 +08:00
RockYang 5d1cbb9050 Merge branch 'bug-fix' 2025-09-08 16:39:02 +08:00
RockYang 8cf40ec6fd update database sql file 2025-09-08 16:38:42 +08:00
RockYang 37b757d910 Merge branch 'bug-fix' 2025-09-08 15:19:01 +08:00
RockYang ef6b642011 Dalle supported image to image 2025-09-08 10:57:11 +08:00
RockYang 8b2cf90aab allow user to upload attachment for mobile chat page 2025-09-08 08:43:32 +08:00
RockYang 18961d3c26 acomplish re-generating for mobile chat page 2025-09-07 18:10:45 +08:00
RockYang 976b71e592 merge Login page Register page 2025-09-07 16:36:56 +08:00
RockYang da63446e64 Merge branch 'bug-fix' 2025-09-07 15:19:02 +08:00
RockYang 2522be1738 fixed FootBar error 2025-09-06 21:24:24 +08:00
RockYang 8f37ab859b Merge branch 'bug-fix' 2025-09-06 13:12:31 +08:00
RockYang 1cb3c42bb9 optimize code for function call handler, fixed bug for dalle plugin 2025-09-06 13:12:08 +08:00
RockYang a60ffca135 remove AI3D module files 2025-09-06 12:38:49 +08:00
RockYang 1ff0636745 allow the el-dialog body show scrollbar 2025-09-06 12:19:59 +08:00
GeekMaster 26f3db09a7 增加订单超时系统配置,WebFooter 增加公安备案号 2025-09-05 10:42:02 +08:00
GeekMaster 52d297624d AI3D 功能完成 2025-09-04 18:36:49 +08:00
GeekMaster 53866d1461 AI3D 页面支持 dark 模式 2025-09-03 18:10:29 +08:00
GeekMaster 54c8856adf AI3D 页面功能完成 2025-09-03 16:00:28 +08:00
RockYang ead30c8779 调整 3D 生成 PC 页面 2025-09-03 07:28:16 +08:00
GeekMaster f8e4d2880f 3D生成服务已经完成 2025-09-02 18:55:45 +08:00
GeekMaster 85b4cc0a3c 增加3D生成功能需求文档 2025-09-01 19:39:19 +08:00
GeekMaster 3271b924fa 文本审核记录功能完成 2025-09-01 17:41:45 +08:00
GeekMaster 2e2b4ee370 修复重新生成的 Bug 2025-09-01 14:28:20 +08:00
GeekMaster 14524f0559 修复重新生成的 Bug 2025-09-01 10:58:15 +08:00
GeekMaster 957954f5ee 数据表重命名 2025-09-01 09:26:12 +08:00
RockYang 3531c7f356 重命名数据表 2025-09-01 07:35:37 +08:00
GeekMaster 9a4239290b 完成文本审查服务开发 2025-08-31 18:21:09 +08:00
GeekMaster 0585edd895 迁移 License,privacy, 用户协议数据 2025-08-31 11:07:42 +08:00
GeekMaster 52313fc7f6 修正部分API授权 2025-08-31 10:52:25 +08:00
GeekMaster 9254b8fafe 微信登录验证完成 2025-08-30 20:30:52 +08:00
GeekMaster c83c88ef27 移动端支付已完成 2025-08-30 18:05:38 +08:00
RockYang 3c065b99fb 支付模块重构完成 2025-08-30 16:27:39 +08:00
RockYang 3a6f8ccc16 验证码配置重构完成 2025-08-29 17:45:31 +08:00
GeekMaster 539e91c12e 插件配置已完成 2025-08-29 14:49:51 +08:00
RockYang 696ef20a80 存储配置功能完成 2025-08-29 09:08:17 +08:00
GeekMaster ebaaefaf7a 算力配置功能已完成 2025-08-28 14:59:42 +08:00
RockYang 77a8969f73 算力配置完成 2025-08-28 11:07:19 +08:00
GeekMaster b3b1981264 更新配置代码 2025-08-26 15:33:32 +08:00
GeekMaster b6d81890cf 控制器中间件授权改造完成 2025-08-26 14:22:14 +08:00
RockYang 728de61bd6 采用中间件授权的方式 2025-08-26 10:45:57 +08:00
RockYang fb76e24c51 micro fix 2025-08-26 00:00:56 +08:00
GeekMaster d9a177864d 支持本地 geekai service api 2025-08-25 17:38:49 +08:00
GeekMaster 536b4b8056 支付,OSS 服务重构完成 2025-08-24 19:32:45 +08:00
RockYang 7fb0aad3c7 重构系统配置 2025-08-24 12:45:51 +08:00
GeekMaster b05334bb77 调整支付配置页面UI 2025-08-21 16:28:30 +08:00
GeekMaster b155597e28 完成配置分组 2025-08-20 16:16:20 +08:00
RockYang 0956bef9db 系统配置重构,支持后台管理页面配置 2025-08-20 10:47:17 +08:00
GeekMaster 6242f648f1 登录注册页面支持 dark 主题模式 2025-08-15 18:51:33 +08:00
GeekMaster c0832a24b9 登录注册页面支持 dark 主题模式 2025-08-15 18:51:12 +08:00
GeekMaster c9a7449d2d 退出登录之后不是刷新页面,而是直接跳转到登录页面 2025-08-14 16:59:16 +08:00
RockYang f82270d097 给所有的 AI 任务提交按钮添加loading状态 2025-08-12 09:17:01 +08:00
RockYang 5a4778074e 登录的时候提示正在登录中,并锁定按钮 2025-08-12 07:56:30 +08:00
RockYang 9070531531 修复聊天页面报错重连的 bug 2025-08-11 21:32:21 +08:00
RockYang ad52613738 修复聊天页面报错重连的 bug 2025-08-11 20:29:19 +08:00
RockYang e7db720986 增加批量删除模型功能 2025-08-11 13:53:03 +08:00
RockYang c651103eff 移动端 VideoCreate 页面支持 dark 主题 2025-08-11 13:34:56 +08:00
RockYang 80ee41aee7 JiMeng, Suno 支持 dark 主题 2025-08-11 11:43:24 +08:00
RockYang 000969d902 优化生成按钮 UI 2025-08-09 23:49:29 +08:00
RockYang 9a94d98725 手机端即梦页面功能完成 2025-08-09 23:25:47 +08:00
RockYang c0a89d6f32 优化手机端 profile 页面 2025-08-09 09:49:41 +08:00
GeekMaster 604ce985bd 调整即梦AI移动端功能 2025-08-08 18:01:42 +08:00
GeekMaster 8c03ecad2b 调整即梦AI移动端参数 2025-08-08 16:30:45 +08:00
GeekMaster af31f91973 调整即梦AI移动端参数 2025-08-08 13:29:36 +08:00
GeekMaster 0ce5df6811 手机端视频生成页面调整完成 2025-08-08 12:47:12 +08:00
RockYang 4e237c9560 优化移动端即梦页面 2025-08-07 22:27:09 +08:00
RockYang e456210944 视频生成移动端页面重构完成 2025-08-07 20:04:20 +08:00
GeekMaster cb7235bb83 Suno页面重构完成 2025-08-07 16:26:12 +08:00
RockYang eab8265b9c Suno PC 端页面重构完成 2025-08-07 14:00:09 +08:00
GeekMaster 5f24df6cee 重构 suno 页面 2025-08-07 11:57:32 +08:00
RockYang e8e3783af0 视频创作和即梦 AI 手机页面 2025-08-07 08:02:20 +08:00
RockYang ec00f156f0 完成移动端Suno页面功能 2025-08-06 17:58:39 +08:00
RockYang bb6e90d50a 完成移动端邀请页面功能 2025-08-06 09:57:14 +08:00
GeekMaster 8d2519d5a1 整合登录注册页面 2025-08-04 17:39:23 +08:00
GeekMaster 6c35c69ed7 移动端首页整合完毕 2025-08-04 15:04:06 +08:00
GeekMaster e994060e93 调整移动端页面UI布局 2025-08-04 12:08:42 +08:00
GeekMaster f7cf992598 移动端重构第一版 2025-08-02 11:17:18 +08:00
GeekMaster 92915f7678 stylus 语法换成 saas 语法全部完成 2025-08-02 10:24:10 +08:00
GeekMaster 54f8494b5c stylus 语法换成 saas 语法 2025-08-01 17:32:06 +08:00
GeekMaster 068b5ddeef Dashboard page is ready 2025-08-01 10:45:49 +08:00
RockYang 8168377e47 优化 dashboard 页面 2025-08-01 07:22:59 +08:00
GeekMaster 4d048dbfa7 增加 dashboard 页面 2025-07-31 16:59:59 +08:00
GeekMaster 0ee230cf41 重构路由注册功能 2025-07-31 12:05:31 +08:00
GeekMaster 9fba68fb14 即梦绘画添加图片特效预览 2025-07-29 18:34:29 +08:00
GeekMaster ff96fada02 1. 首页应用图标同时支持 iconfont 和自定义上传图标
2. 所有模型外键字段都改成int(11) 而不是 bigint
2025-07-29 11:35:11 +08:00
GeekMaster c19b7db8c0 优化支付宝和微信支付服务参数:
1. 删减支付宝参数,只需要传入应用私钥和支付宝公钥。
2. 所有的秘钥直接写入配置文档传入,而不是传入秘钥文件路径,简化参数配置
2025-07-25 19:15:36 +08:00
GeekMaster 6cfc7175e8 重构:优化认证逻辑、界面一致性和错误处理
- 移除聊天和音乐接口的冗余认证绕过配置
  - 为聊天列表查询添加调试日志记录
  - 在即梦和视频模块集成登录弹窗提升用户体验
  - 修复模型属性映射问题(category->tag, description->desc)
  - 移除即梦生成按钮的禁用状态限制
  - 简化即梦设置管理页面布局,去除标签页结构
  - 清理控制台日志输出并改进错误处理一致性
2025-07-25 15:31:58 +08:00
GeekMaster a3f6a641aa 文生视频和图生视频功能完成 2025-07-23 19:11:30 +08:00
GeekMaster 54fe49de5d 合并服务代码 2025-07-22 16:46:58 +08:00
GeekMaster 454dfc1aa7 即梦 AI 管理后台功能完成 2025-07-22 15:12:49 +08:00
GeekMaster 3156701d4e 即梦AI绘图功能前端页面完成 2025-07-21 20:05:20 +08:00
RockYang 41eb0e634a 完成即梦配置功能页面 2025-07-21 07:08:06 +08:00
GeekMaster 73d003d6c3 即梦视频后台管理页面初始化 2025-07-18 21:19:59 +08:00
GeekMaster 5e4ba6d971 整理即梦生视频的API 代码 2025-07-18 20:38:39 +08:00
GeekMaster 76d32c78d8 增加即梦AI功能页面 2025-07-18 18:04:32 +08:00
GeekMaster 66776556d8 调整 mj 绘图页面操作按钮样式 2025-07-17 18:45:18 +08:00
GeekMaster 149f598f6d 视频页面整合完成 2025-07-17 18:05:19 +08:00
GeekMaster 1d6f0ab714 解决聊天页面异常退出重连的bug 2025-07-17 11:47:17 +08:00
GeekMaster 545f257476 代码显示高亮加上展开和收起按钮 2025-06-15 15:38:58 +08:00
GeekMaster 6cb1f16f56 update menu add label 2025-06-14 20:44:45 +08:00
GeekMaser 31fbf7e48a Merge pull request #247 from zayn-code/themechange
主题切换按钮:月亮图标重复了
2025-06-03 09:20:57 +08:00
GeekMaser e2e24078c5 Merge pull request #248 from zayn-code/chatlist
移动端会话列表:切换tab的时候有时会出现列表一直在加载中
2025-06-03 09:20:19 +08:00
zhangyao 39ef92e1ce 移动端会话列表:切换tab的时候有时会出现列表一直在加载中 2025-05-30 15:55:18 +08:00
zhangyao f63ba187dd 移动端会话列表:切换tab的时候有时会出现列表一直在加载中 2025-05-30 15:48:50 +08:00
zhangyao 23d1ac1021 主题切换按钮:月亮图标重复了 2025-05-30 15:46:33 +08:00
GeekMaster 598f6c48fb fixed hilinejs plugin 2025-05-29 11:39:55 +08:00
RockYang 97e489901a 优化管理后台对话显示样式 2025-05-28 22:55:53 +08:00
RockYang eea57790de 优化重新生成的逻辑,删除重新生成之后的聊天记录 2025-05-28 21:32:15 +08:00
RockYang 81b32523ed 重写重新生成的逻辑 2025-05-28 20:57:27 +08:00
GeekMaster 6b6fe1bebd 如果管理后台没有启用会员充值菜单,移动端也不显示充值套餐功能 2025-05-27 16:49:31 +08:00
GeekMaster a7063bf30a 调整任务列表样式 2025-05-27 16:04:30 +08:00
GeekMaster 32fc4d86a2 SSE 消息重构已完成 2025-05-27 15:48:07 +08:00
RockYang e685876cc0 完善 SSE 功能 2025-05-27 08:16:02 +08:00
GeekMaster 41e4b1c7ac SSE 替换 websocket 2025-05-26 18:26:36 +08:00
GeekMaster 76a3ada85f acomplish replacing Vue-cli with Vite 2025-05-26 15:56:18 +08:00
GeekMaster b1ddcef593 替换 vite 架构 2025-05-26 14:14:29 +08:00
google-labs-jules[bot] 94a5187e75 feat: Migrate frontend build tool from Vue CLI to Vite
This commit includes the necessary changes to migrate the web application's frontend build system from vue-cli to Vite.

Key changes include:
- Installed Vite and @vitejs/plugin-vue.
- Removed vue-cli dependencies.
- Created vite.config.js with configurations for aliases, proxy, and dev server, based on the previous vue.config.js.
- Moved public/index.html to web/index.html and updated it for Vite.
- Updated package.json scripts to use Vite commands.

Note: Due to persistent Node.js environment issues, I could not test the application after these changes. Manual testing and potential debugging will be required to ensure everything works as expected.
2025-05-25 09:32:50 +00:00
GeekMaster 521ca77541 update change log file 2025-05-25 15:30:04 +08:00
GeekMaster 5dd3c1835a 修复聊天应用绑定模型后,还是会轮询 API KEY,而不会使用模型绑定的 API KEY的bug 2025-05-25 14:24:29 +08:00
GeekMaser f08329eef4 Merge pull request #241 from coderwei99/chore/migrate-to-pnpm
Chore/migrate to pnpm
2025-05-18 12:24:19 +08:00
coderwei e993a582cd chore: 修正环境变量配置文件 2025-05-16 14:42:58 +08:00
coderwei 1524e189dc chore: migrate from npm to pnpm 2025-05-16 14:32:55 +08:00
清柯 9fb01ee3ee 模型名长度修改 2025-05-16 01:11:42 +08:00
RockYang 9d72edc048 dev 环境下自动使用 API、代理 2025-05-13 20:58:26 +08:00
RockYang a9505ff72d 修复首页‘注销失败’ bug 2025-05-11 08:31:55 +08:00
GeekMaster 628ae15fd7 聊天记录模型名称改成 255 2025-05-08 15:32:29 +08:00
GeekMaster 1ce71374ac 更新版本号v4.2.3 2025-05-08 13:38:50 +08:00
GeekMaster 7c81d946a7 首页增加退出登录按钮 2025-05-08 09:46:35 +08:00
GeekMaster cdaeb2a404 Merge branch 'dev-4.2.4' of gitee.com:blackfox/geekai-plus into dev-4.2.4 2025-05-08 09:37:41 +08:00
GeekMaster ca2de54438 删除无用字段 2025-05-08 09:37:11 +08:00
清柯 303e9ed052 更新更新日志 2025-05-08 09:29:50 +08:00
GeekMaster 347b640614 修复模型添加报数据库字段不存在的 bug 2025-05-08 09:29:31 +08:00
清柯 19099aed6f 修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文 2025-05-08 09:28:53 +08:00
清柯 615515094b 更新更新日志 2025-05-08 03:20:51 +08:00
清柯 c4fe6c825e 用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。 2025-05-08 03:19:51 +08:00
GeekMaster 26c18fcd5a 支持 suno 4.5 音乐模型 2025-05-07 09:42:53 +08:00
GeekMaster a914994483 fixed conflicts 2025-05-07 08:23:23 +08:00
清柯 9edd6621b1 提交更新日志 2025-05-07 04:19:26 +08:00
清柯 10e3e61b2c 1.管理后台支持设置默认昵称。2.Suno音乐创作支持4.5版本 2025-05-07 04:17:18 +08:00
GeekMaster 643cf6085a 增加一键安装部署脚本 2025-05-06 20:12:10 +08:00
GeekMaster fa74ae18ee 设置 gorm 日志等级 2025-05-06 18:54:18 +08:00
GeekMaster dffdbf697b 修复用户表数据迁移bug 2025-05-06 18:51:55 +08:00
GeekMaster 5e59b3a708 修正模型外键的数据类型为 uint 2025-05-05 17:27:55 +08:00
GeekMaster 7bc55f3ed1 修复模型绑定 API KEY 失败的 bug 2025-05-05 17:19:38 +08:00
GeekMaster 73f5a44e0a AI对话页面增加显示AI思考中 2025-05-05 16:38:50 +08:00
GeekMaster 2c6abbe7e4 功能优化:在聊天页面增加对话列表展开和隐藏功能 2025-05-05 14:57:18 +08:00
GeekMaster 1f0cf11636 移除首页文字动画效果 2025-05-05 14:21:50 +08:00
GeekMaster c44f5d40fe Merge branch 'dev-4.2.3' 2025-04-30 19:30:03 +08:00
清柯 314d81303b 更新日志 2025-04-30 10:28:05 +08:00
清柯 b9859e5591 Merge branch 'dev-4.2.3' of gitee.com:blackfox/geekai-plus into dev-4.2.3 2025-04-30 10:24:07 +08:00
清柯 a3d65ba939 MJ绘画U1-V1,拼写错误修改,MJ画图的分辨率有自定义需求,例如21:9优化, 最新版本:plus
MJ画图页面
已画出的图,点复制指令,无效,,修改
2025-04-30 10:23:42 +08:00
RockYang 8a2d2f66b5 增加自动迁移数据表代码 2025-04-29 22:55:30 +08:00
清柯 d1c9fd6eba 增加v4.2.3更新日志 2025-04-29 18:35:45 +08:00
GeekMaster d629d842be 选中模型的时候自动隐藏 pop 组件 2025-04-29 18:27:28 +08:00
GeekMaster f752ec5b06 取消打印激活日志 2025-04-25 17:57:08 +08:00
清柯 c7b09f29ca 修改模型选择功能,提升用户体验 2025-04-25 03:07:51 +08:00
清柯 51c270fb29 修改模型选择功能,提升用户体验 2025-04-25 03:06:22 +08:00
GeekMaster b97d4b7895 修复DALLE function call bug 2025-04-22 15:47:21 +08:00
清柯 0627109b2b 优化思维导图下载图片的清晰度以及解决拖动、缩放操作后会下载图片不全的问题 2025-04-22 03:36:26 +08:00
GeekMaster c2d4530395 add map data dir to docker 2025-04-18 11:06:40 +08:00
GeekMaster c3be47d4ce update gitignore 2025-04-17 13:55:31 +08:00
GeekMaster a1acca6f7a 更新数据库 SQL 2025-04-17 11:37:51 +08:00
GeekMaster 2b5165324c update docker image version 2025-04-17 10:22:41 +08:00
GeekMaster 8f67826072 update docker image version 2025-04-17 10:08:38 +08:00
GeekMaster ac9a31f049 merge code for v4.1.8 2025-04-17 10:07:04 +08:00
GeekMaster ccfc9f17e9 修复 MJ 任务没有自动刷新的 Bug 2025-04-16 18:12:55 +08:00
GeekMaster 4641482865 调整移动端App列表样式 2025-04-11 17:10:21 +08:00
GeekMaster 79522d9ab5 修复管理后台对话列表样式 2025-04-11 15:05:21 +08:00
GeekMaster e0b4e8970a 增加生成一键登录链接功能 2025-04-11 14:36:27 +08:00
RockYang 4d93e901e0 生成思维导图时候自动缓存上一次的结果 2025-04-10 21:34:56 +08:00
GeekMaster bcc72a3091 修复绘图任务失败后,完成列表不更新的bug 2025-04-10 18:21:06 +08:00
RockYang 1c1ddf76fb 添加爬虫搜索 2025-04-08 15:34:17 +08:00
RockYang ed2cdbbc31 更新 README 2025-04-07 17:52:06 +08:00
RockYang 04caf92702 修复Dall图片下载不更新的bug 2025-04-03 18:36:38 +08:00
RockYang c797b35f5a 完成新瀑布流组件整合 2025-04-03 16:48:56 +08:00
RockYang 0746cd49f4 更改工作流组件 2025-04-03 09:41:06 +08:00
RockYang a3a2500498 remove debug cod 2025-04-01 18:34:06 +08:00
RockYang ff69cb231a 增加语音合成功能 2025-04-01 17:03:51 +08:00
RockYang afb9193985 语音播报 2025-03-31 18:12:12 +08:00
RockYang 14fa4fdaa0 修复上传本地文件,在对话框输入会两次显示的 bug 2025-03-31 09:57:05 +08:00
RockYang 2a71d5d557 支持 gpt-4o 生图功能 2025-03-29 13:38:00 +08:00
RockYang cd31333d0c 支持 DeepSeek 原生推理模型 2025-03-27 09:42:29 +08:00
RockYang f080425ee6 启动时自动调用 installation 统计接口 2025-03-25 18:32:44 +08:00
RockYang 8bd2feaf7e Merge branch 'main' of gitee.com:blackfox/geekai 2025-03-25 10:48:46 +08:00
RockYang 4eccfe6d2c remove sensitive message for openai API KEY 2025-03-25 10:48:18 +08:00
RockYang 5ba2e000a1 update database file 2025-03-24 20:29:03 +08:00
RockYang fe9adadb3a update database file 2025-03-24 20:29:03 +08:00
RockYang 805c2a045e merge release v4.1.7 2025-03-24 17:13:16 +08:00
RockYang fd7b196134 merge release v4.1.7 2025-03-24 17:13:16 +08:00
RockYang 1b1c327c35 Merge tag 'v4.1.7' of gitee.com:blackfox/geekai-plus 2025-03-24 11:27:11 +08:00
RockYang a228f603d2 Merge tag 'v4.1.7' of gitee.com:blackfox/geekai-plus 2025-03-24 11:27:11 +08:00
RockYang 96dd0ddb99 check geekai api appId and token when enable verification function 2025-03-11 14:21:20 +08:00
RockYang a4ee5cdeff fixed bug for Keling video downloading, update database SQL 2025-03-10 17:02:14 +08:00
RockYang d0025032b0 优化 prompt 文件列表显示 2025-03-07 18:08:21 +08:00
RockYang 7d0a05ee11 update v4.1.5 sql file 2025-03-06 14:03:44 +08:00
RockYang 5f0a62b63e update v4.1.5 sql file 2025-03-06 14:03:44 +08:00
RockYang 825a1b1027 文档对话不显示文档列表的 bug 2025-03-06 14:00:13 +08:00
RockYang 38dde1a373 文档对话不显示文档列表的 bug 2025-03-06 14:00:13 +08:00
RockYang 950e7d1b00 Merge branch 'dev' 2025-03-05 18:42:44 +08:00
RockYang 89ded74345 Merge branch 'dev' 2025-03-05 18:42:44 +08:00
RockYang d8f9f48278 merge v4.1.6 2025-03-05 18:42:30 +08:00
RockYang 818bb38617 merge v4.1.6 2025-03-05 18:42:30 +08:00
RockYang 43f00b1481 clear setInterval() when the component is unMounted 2025-03-05 14:36:41 +08:00
RockYang f580f671a3 the ai video generating for KeLing is ready 2025-03-05 14:19:20 +08:00
lqins b1fb16995a keling 2025-03-05 00:30:18 +08:00
lqins 47907b9f0c keling 优化 2025-03-05 00:14:40 +08:00
RockYang ba55fca7cc 调整好可灵 API 2025-03-04 21:57:24 +08:00
RockYang f687a10416 调整好可灵 API 2025-03-04 21:52:08 +08:00
RockYang 393bfa137e fixed keling model selection 2025-03-04 21:21:28 +08:00
lqins 4dcb0d850c Merge branch 'v4.2.1' of https://gitee.com/blackfox/geekai-plus into v4.2.1 2025-03-04 19:45:50 +08:00
lqins 88fa374104 keling样式 2025-03-04 19:43:57 +08:00
lqins e1b1c195f6 keling 头像和轮询 2025-03-04 19:41:01 +08:00
RockYang 1352369af0 修复 suno 生成歌曲没有歌词的 bug 2025-03-04 19:00:56 +08:00
RockYang ded041da0f optimize the prompt for generate suno lyrics 2025-03-04 18:50:59 +08:00
RockYang 7b9a7475a9 优化可灵视频算力配置 2025-03-04 16:59:20 +08:00
RockYang 3958e99e4d use http pull message to page notify 2025-03-04 06:54:30 +08:00
RockYang 0ef51714c9 将 license 存储到数据库 2025-03-04 05:55:09 +08:00
RockYang 668ff70bc1 Merge branch 'front-1.0' 2025-03-04 05:23:01 +08:00
RockYang ed063a1d9d 重构异步任务更新方式,使用 Http 替代 websocket 2025-03-03 19:00:10 +08:00
lqins 88eaddbd1d 瀑布流样式更改 2025-03-03 17:58:45 +08:00
RockYang 8369e18bf0 Merge branch 'front-1.0' into v4.2.1 2025-03-03 11:21:53 +08:00
RockYang 79b9476d3d update sql 2025-03-03 11:21:27 +08:00
RockYang 41bfa3a974 Merge branch 'front-1.0' into v4.2.1 2025-03-02 21:52:08 +08:00
RockYang 6b0d4e81bf 将 license 存储到数据库 2025-03-02 21:49:34 +08:00
lqins 41e66d85d5 keling 样式和瀑布流 2025-03-02 20:59:42 +08:00
RockYang f98dcee7d4 优化 AI 绘图提示词模板,给文件上传增加锁定遮罩层 2025-02-28 07:50:10 +08:00
RockYang 04b364c1cd update version 2025-02-27 19:29:55 +08:00
RockYang 6c84d2557c 增加可灵视频算力配置 2025-02-26 18:48:33 +08:00
RockYang 8a4596b36a 更新绘图和视频生成提示词字段长度限制,优化图生图逻辑,统一转成base64 的格式发送到远程 API 2025-02-26 16:06:19 +08:00
RockYang 6e2deeed87 Merge branch 'main' of gitee.com:blackfox/geekai-plus 2025-02-26 14:10:39 +08:00
RockYang bf6834da4e Merge branch 'v4.2.1' 2025-02-26 08:24:00 +08:00
lqins 68dcd054f9 Merge branch 'v4.2.1' into front-1.0 2025-02-25 14:52:56 +08:00
GeekMaser bdd76addf3 Merge pull request #238 from gotoworld/main
v4.1.5中,存在Merge冲突脚本 #237
2025-02-25 09:27:15 +08:00
GeekMaser 0202d75ff4 Merge pull request #238 from gotoworld/main
v4.1.5中,存在Merge冲突脚本 #237
2025-02-25 09:27:15 +08:00
RockYang a77bebbc29 update iconfonts 2025-02-25 06:39:07 +08:00
Welcome Aboard 2936f21f12 Merge pull request #1 from gotoworld/gotoworld-patch-1
Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql
2025-02-24 19:59:00 +08:00
Welcome Aboard a0831ec6cd Merge pull request #1 from gotoworld/gotoworld-patch-1
Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql
2025-02-24 19:59:00 +08:00
Welcome Aboard 705ca4d20a Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql 2025-02-24 19:57:40 +08:00
Welcome Aboard 2926aab7a4 Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql 2025-02-24 19:57:40 +08:00
RockYang 36beb74de8 Merge branch 'main' of gitee.com:blackfox/geekai-plus 2025-02-24 15:19:52 +08:00
RockYang dd1f98db1e Merge branch 'v4.2.1' 2025-02-24 15:19:31 +08:00
RockYang b7f41c524a update build script 2025-02-23 09:10:20 +08:00
RockYang a3f0576535 限制绘画提示词长度,修复移动端角色和模型绑定失败问题 2025-02-23 06:56:38 +08:00
mario-b 5c8a237e27 fix: 去除渠道 2025-02-22 23:38:07 +08:00
lqins 447adf45eb style:left box padding add ten px 2025-02-22 22:53:37 +08:00
lqins ca77288a69 style:比例样式 2025-02-22 22:48:37 +08:00
lqins 63be3f5f56 style:menu tab 2025-02-22 22:44:28 +08:00
lqins cad1ce6943 changed keling in the dark theme text color 2025-02-22 22:40:14 +08:00
lqins 54b5a78c0e textarea change 2025-02-22 22:30:26 +08:00
lqins 98d4d58393 keling style changed 2025-02-22 22:25:32 +08:00
mario 887fdb6679 feat: 增加 可灵功能 2025-02-21 15:44:19 +08:00
RockYang 63fd125439 fixed bug for redis pool connection timeout 2025-02-21 15:19:58 +08:00
RockYang c39dd913fd Merge branch 'front-1.0' 2025-02-20 11:52:05 +08:00
RockYang b40f7ed5f3 update change log 2025-02-20 11:33:56 +08:00
RockYang 183829a08b Merge branch 'main' into v4.2.1 2025-02-20 11:22:52 +08:00
RockYang 03d33c784c 修复公式解析的 Bug 2025-02-20 11:17:12 +08:00
mario eec10fdfbc feat: 增加 可灵功能 2025-02-17 08:44:34 +08:00
lqins 0a3c74cd6f 代码优化 2025-02-14 18:56:49 +08:00
lqins 5768c7959e 解决部分报错 2025-02-14 16:56:36 +08:00
mario d124eddd9d feat: 增加 可灵功能 2025-02-14 15:03:29 +08:00
RockYang 6a066f1b8e 默认允许跨域请求API 2025-02-11 14:34:31 +08:00
RockYang 775fb1f853 默认允许跨域请求API 2025-02-11 14:34:31 +08:00
RockYang b7d137247a 优化 docker 容器依赖关系 2025-02-11 10:10:43 +08:00
RockYang 22278c40bf 优化 docker 容器依赖关系 2025-02-11 10:10:43 +08:00
RockYang 4373642ebd 优化打包脚本,新增 arm64 架构打包脚本 2025-02-11 10:01:05 +08:00
RockYang e29b32bd2d 优化打包脚本,新增 arm64 架构打包脚本 2025-02-11 10:01:05 +08:00
RockYang 5bf90920f5 resolve conflicts 2025-02-11 09:53:41 +08:00
RockYang fa6bb5e46a resolve conflicts 2025-02-11 09:53:41 +08:00
RockYang edcbb3e226 merge v4.1.5 2025-02-11 09:45:26 +08:00
RockYang cebf8497a4 merge v4.1.5 2025-02-11 09:45:26 +08:00
RockYang dd675c9a9b 增加容器相互之间的依赖关系 2025-02-10 14:48:38 +08:00
RockYang f975f9b0b8 update database sql file 2025-02-10 11:08:38 +08:00
RockYang fbefe5b308 默认允许 API 跨域访问 2025-02-08 11:07:31 +08:00
RockYang 312abbc273 兼容 O3 模型 2025-02-07 21:55:08 +08:00
RockYang 8ced447a14 增加签到功能 2025-02-07 18:02:11 +08:00
RockYang 5213bdf08b fixed bug for calculate chat message tokens 2025-01-19 13:08:38 +08:00
RockYang cded4bdcc7 fixed bug for calculate chat message tokens 2025-01-19 13:08:38 +08:00
RockYang cf817fd8ea merge code for v4.1.4 2025-01-18 23:20:41 +08:00
RockYang 596b700d63 merge code for v4.1.4 2025-01-18 23:20:41 +08:00
RockYang f8e32148c8 adjust console banner print styles 2025-01-17 17:58:45 +08:00
RockYang 2c899f6057 在浏览器控制台输出 Banner 2025-01-16 11:19:40 +08:00
RockYang be799000ee update arm64 archtecture dockerfile 2025-01-15 15:47:03 +08:00
RockYang 22cb2270af 增加 arm64 架构打包 dockerfile 2025-01-15 12:02:05 +08:00
RockYang 4e440b7910 修复主题切换组件Bug,优化前端公告 markdown 样式 2025-01-13 12:03:24 +08:00
RockYang 4e6f14cb9e 更新数据库文件 2025-01-10 17:56:00 +08:00
RockYang 8dc03a7509 添加备案信息配置项,给登录页面 Logo 增加圆角 2025-01-10 17:33:25 +08:00
RockYang 57b1b44645 优化编译指令,减少程序体积 2025-01-08 16:53:09 +08:00
RockYang aa17a33093 支持微模型绑定 Dalle 绘图的 API KEY 2025-01-08 10:50:37 +08:00
RockYang 80e27c40e9 管理后台用户算力日志页面增加过滤查询功能 2025-01-08 10:19:35 +08:00
RockYang 8250e876a5 允许配置登录注册页面的自定义 Logo 2025-01-07 15:13:34 +08:00
RockYang 9f98491368 修复微信登录功能 2025-01-07 14:44:47 +08:00
RockYang a2481ff1cf update readme 2025-01-07 11:58:16 +08:00
RockYang 59dfa95bf8 update readme 2025-01-07 11:58:16 +08:00
RockYang fe160f978b 首页增加码云连接地址 2025-01-06 19:35:45 +08:00
RockYang 7da5b7163c 支持在 Chat 页面显示,隐藏对话列表 2025-01-06 19:17:18 +08:00
RockYang cffc722622 O1 模型支持流式输出 2025-01-06 11:56:29 +08:00
RockYang a7baf1dc9e optimize login page styles 2024-12-31 08:37:21 +08:00
RockYang 488169683f micro fixs, update database SQL file 2024-12-27 17:02:27 +08:00
RockYang 2ba3c52e6e 移动端菜单按需加载,后台可以配置是否显示 2024-12-26 18:59:58 +08:00
RockYang 18179613fc 更新移动端 Dalle 绘图页面,支持模型选择 2024-12-26 18:50:45 +08:00
RockYang 8af0fec8ec 增加移动端登录页面 2024-12-26 16:52:20 +08:00
RockYang acee2d9d81 add 'type' field for ChatModel, support Chat and Image model 2024-12-25 18:57:18 +08:00
RockYang cbf06eea24 fine-tune page styles, use iframe to load external page in navigation bar 2024-12-25 11:10:23 +08:00
RockYang 989b4a64d6 set white background color for close icon 2024-12-24 18:13:59 +08:00
RockYang b01b10014a fine-tune the new UI theme 2024-12-24 17:43:40 +08:00
RockYang e857f98e5c fine-tune the new UI theme, merge the code and fixed conflicts 2024-12-24 16:13:36 +08:00
RockYang 274cff71b1 引入tailwind css,调整样式 2024-12-24 11:07:04 +08:00
lqins 06573c5d12 邀请列表修改 2024-12-23 18:28:11 +08:00
lqins 937e5befa2 修改部分细节 2024-12-23 10:49:28 +08:00
lqins fb403bde8b 颜色修改 2024-12-22 13:55:01 +08:00
lqins ba174ef3ee 发送style 2024-12-22 13:00:19 +08:00
lqins b7b702862f add hover color 2024-12-22 12:52:02 +08:00
lqins 6df2b5735b 暂无数据 2024-12-22 12:43:40 +08:00
lqins 130e151a06 Merge branch 'front-1.0' of https://gitee.com/blackfox/geekai-plus into front-1.0 2024-12-22 12:41:35 +08:00
lqins ab903e3cc1 细节处修改 2024-12-22 12:37:32 +08:00
RockYang f6b5a94b29 修复登录失效的 Bug 2024-12-21 21:44:52 +08:00
RockYang 237387b2ab 支持按次收费的 OpenAI 实时语音通话功能 2024-12-20 18:21:54 +08:00
lqins 0c1f650e9c style:样式切换 2024-12-19 17:09:47 +08:00
lqins 357c77ef30 style:样式切换 2024-12-19 16:57:57 +08:00
RockYang dc7c049a7b fixed bug for luma api response data parse error 2024-12-16 16:44:24 +08:00
RockYang 188fb23f08 update database name 2024-12-16 11:26:54 +08:00
RockYang 5347a12035 update database file 2024-12-16 11:11:42 +08:00
RockYang 691d453f41 merge config.toml file 2024-12-16 10:09:23 +08:00
RockYang 5b7f2603ae merge v4.1.3 2024-12-16 10:07:52 +08:00
lqins 710b008453 feat:chat style 2024-12-11 09:36:12 +08:00
RockYang 6e7aecc568 update version 2024-11-27 16:51:14 +08:00
RockYang 3fd7f810f0 Merge branch 'dev' 2024-11-27 15:04:26 +08:00
RockYang e04a48623d merge v4.1.2 2024-11-27 15:00:02 +08:00
RockYang b68f7e3fd1 update database file 2024-11-27 14:34:39 +08:00
RockYang d30d5585c6 redeem export function is ready 2024-11-27 11:52:18 +08:00
RockYang 2e1bad387c redeem export function is ready 2024-11-27 11:52:18 +08:00
lqins 1b7c7a0dc1 delect clearable 2024-11-23 15:51:44 +08:00
lqins 207f2b5ac4 change color 2024-11-23 15:47:57 +08:00
lqins d13fa1392f feat:about account 2024-11-23 15:40:05 +08:00
lqins 9bf886fe98 go 2024-11-20 18:54:50 +08:00
lqins aeef77ac24 test 2024-11-20 00:18:14 +08:00
廖庆斯 9a97a1ee72 feat: change theme and index style 2024-11-20 00:09:25 +08:00
RockYang 5c18a50330 update docker-compose.yaml 2024-11-14 16:37:43 +08:00
RockYang d78d3ffe02 update database 2024-11-14 16:01:27 +08:00
RockYang 44240f65d5 merge v4.1.1 and fixed conflicts 2024-11-13 18:40:04 +08:00
RockYang 6aaf607ed7 fixed bug for chat context not work for chating with image 2024-11-12 18:23:27 +08:00
RockYang 97e81a7dcc fixed bug for chat context not work for chating with image 2024-11-12 18:23:27 +08:00
RockYang cff0397735 meta prompt function is ready 2024-11-12 17:13:38 +08:00
RockYang 9e8f1ed6bf meta prompt function is ready 2024-11-12 17:13:38 +08:00
RockYang 2aa0b51c09 auto restore user's power for failure tasks 2024-11-11 18:12:35 +08:00
RockYang 4dbfdab50d auto restore user's power for failure tasks 2024-11-11 18:12:35 +08:00
RockYang ce8a2d0222 save task origin info for AI generating jobs 2024-11-11 17:22:08 +08:00
RockYang c39814ce2b save task origin info for AI generating jobs 2024-11-11 17:22:08 +08:00
RockYang 135755d21d enable to set the translate model 2024-11-08 18:06:39 +08:00
RockYang 95a071014c enable to set the translate model 2024-11-08 18:06:39 +08:00
RockYang 5be4e83876 fixed bug for audio and video downloading 2024-11-05 11:38:32 +08:00
RockYang 6e03f4b363 fixed bug for audio and video downloading 2024-11-05 11:38:32 +08:00
RockYang bb8644dea0 remove sensitive words 2024-11-04 10:05:57 +08:00
RockYang cbc9eb3a59 merge database files 2024-10-30 18:12:50 +08:00
RockYang ff9142ddd4 micro fix 2024-10-30 18:11:36 +08:00
RockYang 0593359ef8 micro fix 2024-10-30 18:11:36 +08:00
RockYang 35f469fb82 update the default api url to https://api.geekai.pro 2024-10-29 14:09:55 +08:00
RockYang 705ad58a8f update database sql file 2024-10-29 14:07:49 +08:00
RockYang 2081d3ce29 add databse sql file 2024-10-23 20:01:17 +08:00
RockYang 41d9c097e8 update database sql file 2024-10-23 18:16:36 +08:00
RockYang c96f86fbbf 优化实时语音对话组件,处理异常 2024-10-23 18:04:09 +08:00
RockYang 1fe1e40a43 优化实时语音对话组件,处理异常 2024-10-23 18:04:09 +08:00
RockYang 4b3b64e9e2 modify text link color for register page 2024-10-21 18:26:19 +08:00
RockYang ad6e2dd370 modify text link color for register page 2024-10-21 18:26:19 +08:00
RockYang 2f2e146951 Merge branch 'main' of gitee.com:blackfox/geekai-plus 2024-10-21 18:21:21 +08:00
RockYang bb63f23414 Merge branch 'main' of gitee.com:blackfox/geekai-plus 2024-10-21 18:21:21 +08:00
RockYang a09c529414 更换登录页面背景图片 2024-10-21 18:21:04 +08:00
RockYang 43f6bf74f2 更换登录页面背景图片 2024-10-21 18:21:04 +08:00
RockYang a8d1d58e95 给 realtime 语音对话增加音效 2024-10-18 06:26:05 +08:00
RockYang 662d7b099e 给 realtime 语音对话增加音效 2024-10-18 06:26:05 +08:00
RockYang 6695be815e 优化充值产品定价逻辑,确保手机端和PC端显示的价格一致 2024-10-17 18:15:25 +08:00
RockYang d5eeeea764 优化充值产品定价逻辑,确保手机端和PC端显示的价格一致 2024-10-17 18:15:25 +08:00
RockYang fbd3478772 the relay server for openai websocket is ready 2024-10-17 16:46:41 +08:00
RockYang 43c507c597 the relay server for openai websocket is ready 2024-10-17 16:46:41 +08:00
RockYang 2102e1afbb add websocket relayer for openai realtime api 2024-10-16 18:16:09 +08:00
RockYang e356771049 add websocket relayer for openai realtime api 2024-10-16 18:16:09 +08:00
RockYang 155c56f502 integrated openai realtime console 2024-10-15 19:25:18 +08:00
RockYang 48139290ed integrated openai realtime console 2024-10-15 19:25:18 +08:00
RockYang 37a9b0e485 add PCM16 audio stream to wave is reday 2024-10-14 18:39:50 +08:00
RockYang bd852c82b7 add PCM16 audio stream to wave is reday 2024-10-14 18:39:50 +08:00
RockYang 52a3f13f1e add voice chat test case 2024-10-12 19:07:29 +08:00
RockYang 13564993d7 add voice chat test case 2024-10-12 19:07:29 +08:00
RockYang a678a11c33 suno and luma task management funtion in admin console is ready 2024-10-10 17:07:40 +08:00
RockYang bfc1e1bc2c suno and luma task management funtion in admin console is ready 2024-10-10 17:07:40 +08:00
RockYang d34b785238 image task list page for admin console is ready 2024-10-09 18:17:44 +08:00
RockYang ba20717a09 image task list page for admin console is ready 2024-10-09 18:17:44 +08:00
RockYang 52e40daf23 fixed bug in FileSelect component for deleting files 2024-10-08 18:00:46 +08:00
RockYang 1086e1d631 fixed bug in FileSelect component for deleting files 2024-10-08 18:00:46 +08:00
RockYang 3094b9c8fd merge v4.1.0 and fixed conflicts 2024-10-08 17:54:08 +08:00
RockYang 0f0a4c0a7e merge v4.1.0 and fixed conflicts 2024-10-08 17:51:14 +08:00
RockYang 430a7b2297 fixed bug for websocket message handler rebind 2024-10-08 16:41:19 +08:00
RockYang c374126f69 fixed bug for websocket message handler rebind 2024-10-08 16:41:19 +08:00
RockYang c91a38a882 fixed webscoket event re-bind bug 2024-10-05 21:18:59 +08:00
RockYang 8498cd71dc fixed webscoket event re-bind bug 2024-10-05 21:18:59 +08:00
RockYang 6e02bee4b7 fixed alipay mobile payment 2024-10-05 11:45:44 +08:00
RockYang bf30517393 fixed alipay mobile payment 2024-10-05 11:45:44 +08:00
RockYang b62218110e fixed alipay mobile payment 2024-10-05 10:23:00 +08:00
RockYang 495f86e7fc fixed alipay mobile payment 2024-10-05 10:23:00 +08:00
RockYang e2960b2607 auto jump to mobile page when use mobile device access the page 2024-10-04 11:25:01 +08:00
RockYang 471017657f auto jump to mobile page when use mobile device access the page 2024-10-04 11:25:01 +08:00
RockYang 88e7c39066 fixed bug for: websocket is not auto connected when user not login 2024-10-02 07:26:34 +08:00
RockYang 4861dd75be fixed bug for: websocket is not auto connected when user not login 2024-10-02 07:26:34 +08:00
RockYang 2a6dd636fa update database 2024-09-30 17:12:23 +08:00
RockYang 342ceea371 update database 2024-09-30 17:12:23 +08:00
RockYang 6bf38f78d5 add message handler ONLY when websocket connect successfully 2024-09-30 16:33:26 +08:00
RockYang 880c34dfee add message handler ONLY when websocket connect successfully 2024-09-30 16:33:26 +08:00
RockYang 5a04a935be support wechat and alipay payment for mobile page 2024-09-30 16:20:40 +08:00
RockYang a1e487100d support wechat and alipay payment for mobile page 2024-09-30 16:20:40 +08:00
RockYang 8923e938d2 optimize the vue component communication, replace event listening with share data 2024-09-30 14:20:59 +08:00
RockYang 77948b1e16 optimize the vue component communication, replace event listening with share data 2024-09-30 14:20:59 +08:00
RockYang 1a1734abf0 websocket api refactor is ready 2024-09-29 19:28:47 +08:00
RockYang e28a12a1ee websocket api refactor is ready 2024-09-29 19:28:47 +08:00
RockYang 8093a3eeb2 mj websocket refactor is ready 2024-09-29 07:51:08 +08:00
RockYang 00a8bc6784 mj websocket refactor is ready 2024-09-29 07:51:08 +08:00
RockYang 9edb3d0a82 sd websocket refactor is finished 2024-09-27 18:28:54 +08:00
RockYang 8fffa60569 sd websocket refactor is finished 2024-09-27 18:28:54 +08:00
RockYang d95fab11be refactor websocket message protocol, keep the only connection for all clients 2024-09-27 17:50:54 +08:00
RockYang 2debe7e927 refactor websocket message protocol, keep the only connection for all clients 2024-09-27 17:50:54 +08:00
RockYang 6ef09c8ad5 add ws handler 2024-09-25 18:43:12 +08:00
RockYang 478bc32ddd add ws handler 2024-09-25 18:43:12 +08:00
RockYang 283a023a06 update database sql file for v4.1.4 2024-09-23 15:54:22 +08:00
RockYang dfd2be1265 update database sql file for v4.1.4 2024-09-23 15:54:22 +08:00
RockYang d315edef5f logout the user when it has been disabled 2024-09-20 16:49:03 +08:00
RockYang 33ff2e29e5 logout the user when it has been disabled 2024-09-20 16:49:03 +08:00
RockYang 5fa17b300e add release v4.1.4 2024-09-20 15:50:04 +08:00
RockYang 4df1c7a136 add release v4.1.4 2024-09-20 15:50:04 +08:00
RockYang 32919de7a7 add email white list check in register handler 2024-09-20 14:10:40 +08:00
RockYang c522248d39 add email white list check in register handler 2024-09-20 14:10:40 +08:00
RockYang 7d126aab41 add email white list 2024-09-20 10:40:37 +08:00
RockYang 246d96ee63 add email white list 2024-09-20 10:40:37 +08:00
RockYang 16ac57ced3 payment for mobile page is ready 2024-09-19 19:03:03 +08:00
RockYang 909ae4aa00 payment for mobile page is ready 2024-09-19 19:03:03 +08:00
RockYang 4976b967e7 wechat payment for mobile page is ready 2024-09-19 17:59:27 +08:00
RockYang 26e3ababcf wechat payment for mobile page is ready 2024-09-19 17:59:27 +08:00
RockYang e874178782 wechat payment for pc is ready 2024-09-19 14:42:25 +08:00
RockYang 4d9f89f630 wechat payment for pc is ready 2024-09-19 14:42:25 +08:00
RockYang 8cb66ad01b fixed bug geek-plus#6, register page first tab not auto active 2024-09-19 09:03:14 +08:00
RockYang b989199edb fixed bug geek-plus#6, register page first tab not auto active 2024-09-19 09:03:14 +08:00
RockYang f887a39912 geek payment notify api is ready 2024-09-18 22:24:05 +08:00
RockYang 0d81fe6c8e geek payment notify api is ready 2024-09-18 22:24:05 +08:00
RockYang 2beffd3dd3 urgent bug fix: remove suno and luma task will recharge user power 2024-09-18 20:33:29 +08:00
RockYang be45f41e34 urgent bug fix: remove suno and luma task will recharge user power 2024-09-18 20:33:29 +08:00
RockYang d8cb92d8d4 Geek Pay notify is ready 2024-09-18 18:07:49 +08:00
RockYang e9ac58b1ef Geek Pay notify is ready 2024-09-18 18:07:49 +08:00
RockYang 158db83965 add geek payment 2024-09-18 07:03:46 +08:00
RockYang 59d9ae96ac add geek payment 2024-09-18 07:03:46 +08:00
RockYang 603bfa7def recommend user to use Google Chrome 2024-09-15 10:25:50 +08:00
RockYang 3cff6f7189 recommend user to use Google Chrome 2024-09-15 10:25:50 +08:00
RockYang 829fb879a6 merge app type function branch 2024-09-14 18:17:55 +08:00
RockYang 14aee28289 merge app type function branch 2024-09-14 18:17:55 +08:00
RockYang 0385e60ce1 refactor AI chat message struct, allow users to set whether the AI responds in stream, compatible with the GPT-o1 model 2024-09-14 17:06:13 +08:00
RockYang 8c1b4d4516 refactor AI chat message struct, allow users to set whether the AI responds in stream, compatible with the GPT-o1 model 2024-09-14 17:06:13 +08:00
胡双明 aaea23f785 feat: 应用分类功能 2024-09-14 11:05:49 +08:00
胡双明 068df8fa15 feat: 应用分类功能 2024-09-14 11:05:49 +08:00
RockYang 131efd6ba5 refactor chat message body struct 2024-09-14 07:11:45 +08:00
RockYang e371310d02 refactor chat message body struct 2024-09-14 07:11:45 +08:00
RockYang 866564370d return at least one chat role for getUserRoles API 2024-09-14 05:55:56 +08:00
RockYang 96b8121210 return at least one chat role for getUserRoles API 2024-09-14 05:55:56 +08:00
RockYang dcdc0d8918 add tid field for chat app role 2024-09-13 18:32:13 +08:00
RockYang 1960a85ead add tid field for chat app role 2024-09-13 18:32:13 +08:00
RockYang 6c7fa17e50 fixed bug, filelist page support pagination, do not load captcha component for user login first time 2024-09-13 17:03:05 +08:00
RockYang 69cb479e01 fixed bug, filelist page support pagination, do not load captcha component for user login first time 2024-09-13 17:03:05 +08:00
胡双明 38a0d00142 Merge branch 'main' into husm_2024-09-02 2024-09-13 10:25:15 +08:00
胡双明 db90131bd6 Merge branch 'main' into husm_2024-09-02 2024-09-13 10:25:15 +08:00
RockYang 5c77e67b0f fixed bug for reset password 2024-09-12 17:25:19 +08:00
RockYang c18d272413 fixed bug for reset password 2024-09-12 17:25:19 +08:00
RockYang 961cee5e41 fixed bug for register page code verification 2024-09-12 15:42:09 +08:00
RockYang cd0952e170 fixed bug for register page code verification 2024-09-12 15:42:09 +08:00
胡双明 c9cc93be8c Merge branch 'main' into husm_2024-09-02 2024-09-11 09:57:01 +08:00
胡双明 e6a6d0485d Merge branch 'main' into husm_2024-09-02 2024-09-11 09:57:01 +08:00
RockYang 49f2e1a71e optimize download function for suno 2024-09-11 08:39:28 +08:00
RockYang a642ae0332 optimize download function for suno 2024-09-11 08:39:28 +08:00
胡双明 97eff6085a feat: 210 AI对话页面文件列表增加分页功能 2024-09-10 18:14:34 +08:00
胡双明 67f508c7da feat: 210 AI对话页面文件列表增加分页功能 2024-09-10 18:14:34 +08:00
RockYang 8b2e2d61af file list api support pagination 2024-09-10 15:24:36 +08:00
RockYang 2225f09797 file list api support pagination 2024-09-10 15:24:36 +08:00
胡双明 c096efb416 Merge branch 'main' into husm_2024-09-02 2024-09-10 14:29:08 +08:00
胡双明 fe7fa46a0c Merge branch 'main' into husm_2024-09-02 2024-09-10 14:29:08 +08:00
RockYang 6d8901fa21 update v4.1.3 database sql file 2024-09-10 11:15:26 +08:00
RockYang 7750cb0021 update v4.1.3 database sql file 2024-09-10 11:11:17 +08:00
RockYang 327d68a347 support multiple delete users, update database sql file 2024-09-10 10:56:04 +08:00
RockYang 7c272b0efd update config file 2024-09-09 18:58:03 +08:00
RockYang 5eb4ed157e merge luma page code for v4.1.3 2024-09-09 18:07:10 +08:00
RockYang d4e00b960c Merge remote-tracking branch 'inet/husm_2024-09-02' into dev-4.1.3 2024-09-09 10:54:43 +08:00
RockYang af0eff88b8 export the newest database sql file 2024-09-05 15:29:00 +08:00
RockYang c755befa28 优化聊天页面代码,刷新页面之后自动加载当前对话的 role_id 和 model_id 2024-09-05 14:50:37 +08:00
胡双明 3f6890da27 feat: 生成视频页面2 2024-09-05 11:56:02 +08:00
RockYang 08319391ea merged v4.0.9 and fixed conflicts 2024-09-05 11:02:32 +08:00
RockYang ae7f1c03e2 add auto execute task to downloading video files 2024-09-05 10:54:58 +08:00
胡双明 151017ed47 feat: 生成视频页面 2024-09-05 08:50:49 +08:00
RockYang 941d15b635 remove unused css file 2024-09-04 22:42:56 +08:00
RockYang d8058400dd adjust chat page styles 2024-09-04 18:07:39 +08:00
RockYang 5a1ed953de user can select function tools by themself 2024-09-04 14:53:21 +08:00
RockYang 58d127b602 add page and total field for pagination vo 2024-09-03 18:32:15 +08:00
RockYang 13bf73e6cf add sync lock for sub or add user's power 2024-09-03 12:09:36 +08:00
RockYang aa46fdb113 luma create and list api is ready 2024-09-02 18:08:50 +08:00
胡双明 4a09da3a25 feat(Luma): 加上传前后帧调换功能 2024-09-02 17:33:03 +08:00
RockYang d70fcbb9f3 optimize ChatPlus page, fixed bug for websocket reconnection 2024-09-02 16:35:15 +08:00
RockYang a16ef6476d add luma api service 2024-08-30 18:12:14 +08:00
RockYang dae75343d0 update change log file 2024-08-30 16:47:52 +08:00
RockYang d7564127d2 suno add new function for merging full songs and upload custom music 2024-08-30 16:46:48 +08:00
RockYang c6bf54fd9d add logo for favirate icon 2024-08-29 13:36:35 +08:00
RockYang 8d47072e1c luma page, upload image and remove image function is ready 2024-08-26 17:59:05 +08:00
RockYang d601226187 add download for video 2024-08-26 07:24:04 +08:00
RockYang 4801832d9d luma page video list component is ready 2024-08-23 18:25:58 +08:00
RockYang 6b0f42d0b8 luma page is ready 2024-08-20 14:31:40 +08:00
RockYang 08379c21ac refactor reset password functions 2024-08-19 12:05:00 +08:00
RockYang 2499bd9ad3 fixed styles 2024-08-19 06:42:53 +08:00
RockYang ad6da0d320 add bind mobile, bind email, bind wechat function is ready 2024-08-14 15:56:50 +08:00
RockYang a2583c0591 add mobile and email filed for user 2024-08-13 18:40:50 +08:00
RockYang 0ea4a29152 add verification code for login and register page 2024-08-13 14:55:47 +08:00
RockYang 7fe50788a5 add Captcha components 2024-08-13 10:01:07 +08:00
RockYang c4a68076d6 fixed conflicts 2024-08-13 06:48:22 +08:00
RockYang 9e365895af add wechat login for login dialog 2024-08-12 18:00:34 +08:00
RockYang 9ece25e17d add drag icon for dragable rows 2024-08-12 14:00:50 +08:00
RockYang 2140eff51b release v4.1.2 2024-08-09 18:50:02 +08:00
RockYang dfba3c30a5 redeem code function is ready 2024-08-09 18:19:51 +08:00
RockYang 4b717109d2 add redeem code function 2024-08-08 18:28:50 +08:00
RockYang e54e908fbc add cache for getting user info and system configs 2024-08-08 14:37:33 +08:00
RockYang 7a11a9ef15 add clear unpaid order functions 2024-08-07 18:00:28 +08:00
RockYang c9d0700fd9 refactor stable diffusion service, use api key instead of configs 2024-08-07 17:30:59 +08:00
RockYang f9b809801d refactor midjourney service, use api key in database 2024-08-06 18:30:57 +08:00
RockYang cc551ba266 show sql error message 2024-08-05 16:14:44 +08:00
RockYang 23787ff462 fixed build script 2024-08-01 18:45:53 +08:00
RockYang 9f603c5e77 merge v4.0.8 2024-08-01 18:09:00 +08:00
RockYang 3d720bdd81 update change log 2024-08-01 08:54:04 +08:00
RockYang a8eccbe43b update readme 2024-08-01 08:52:46 +08:00
RockYang 2ac77ac39f restore use power when removed not finish jobs 2024-07-31 16:08:46 +08:00
RockYang 4eaa518cf3 remove platform field for api key and chat model 2024-07-30 17:24:21 +08:00
RockYang 5622f94fc8 update datebasesl 2024-07-30 14:55:49 +08:00
RockYang 95457b7dcd add back-to-top component for all list page 2024-07-29 11:00:53 +08:00
RockYang 6a9de72c78 remove chat debug log 2024-07-28 18:55:17 +08:00
RockYang 088a614160 add function to generate lyrics 2024-07-28 10:04:53 +08:00
RockYang 013ee98f53 song detail page is ready 2024-07-26 19:12:44 +08:00
RockYang dc46ed0e05 adjust chat records layout styles 2024-07-25 11:01:27 +08:00
RockYang 2a0c657ca3 add put url file for oss interface 2024-07-23 18:36:26 +08:00
RockYang a0aee80c63 remove other platform supports, ONLY use chatGPT API 2024-07-22 18:36:58 +08:00
RockYang 4910be3403 optimize foot copyright snaps 2024-07-22 17:54:09 +08:00
RockYang 1541d74c84 add close button for music player 2024-07-22 07:12:21 +08:00
RockYang 34c9151dc1 enable use random pure color background for index page 2024-07-19 18:43:01 +08:00
RockYang 6c69770ed6 the music player is ready 2024-07-18 18:34:11 +08:00
RockYang 5b7c38c67f add suno page 2024-07-17 18:58:09 +08:00
RockYang b7bec8ecb7 support upload file from clipboard 2024-07-17 10:23:02 +08:00
RockYang 82fe9c3596 allow user to use chat role directly, no need to add to workspace 2024-07-16 18:28:08 +08:00
RockYang afd84516b0 update docs url 2024-07-16 18:15:34 +08:00
RockYang 024f00b73c show error message for Midjourney task list page 2024-07-16 17:16:58 +08:00
RockYang 24a21bf2ee fixed bug for function call for openai 2024-07-16 06:25:40 +08:00
RockYang f22c6bf658 优化版权显示逻辑,允许激活用户更改自定义版权 2024-07-15 18:44:14 +08:00
RockYang 0df700ec18 update docker-compose file 2024-07-12 18:13:43 +08:00
RockYang 46141f87b8 update database file, add tika host config 2024-07-12 18:10:32 +08:00
RockYang eecce10018 tidy apis 2024-07-12 14:39:14 +08:00
RockYang c8506e3d3b update geekai image version 2024-07-05 11:09:13 +08:00
RockYang bddd611cc1 wechat login is ready 2024-07-04 15:34:32 +08:00
RockYang b399fc557a feat: support wechat login function 2024-07-02 18:27:06 +08:00
RockYang b20ea734fd fixed conflicts 2024-06-30 06:09:12 +08:00
RockYang 7ab0acd6a4 optimize ngin configuration for chat-plus.conf 2024-06-30 05:44:39 +08:00
RockYang 1d60ccc516 docs: update change log file 2024-06-28 16:21:43 +08:00
RockYang 2a8d2213b1 feat: optimize chat page data list style, support list style and chat style 2024-06-28 15:53:49 +08:00
RockYang a27ce36a32 feat: chat with file function is ready 2024-06-27 18:01:49 +08:00
RockYang 3fdcc895ed enable set custom index background image 2024-06-27 10:49:31 +08:00
RockYang 88e510d07a add test code for reading pdf files 2024-06-26 18:50:48 +08:00
RockYang 2526feb0d9 feat: new UI for chat file manager is ready 2024-06-25 18:59:27 +08:00
RockYang 4171b2bdfb fixed bug: mobile chat list could not update chat title 2024-06-25 09:53:08 +08:00
RockYang 0611f8de6c update version 2024-06-23 17:54:42 +08:00
RockYang df9e495dc5 add wechat payment configs sample 2024-06-22 16:04:04 +08:00
RockYang 8a48476105 update docker-compose file 2024-06-22 12:37:41 +08:00
RockYang 9d4a1705a5 add database files 2024-06-22 12:17:35 +08:00
RockYang 871e5733c7 finish mobile wechat payment 2024-06-22 12:10:43 +08:00
RockYang e8a663e3c7 update docker image mirror url 2024-06-19 08:35:05 +08:00
RockYang 422d439627 Merge branch 'main' of gitee.com:blackfox/geekai 2024-06-19 08:22:00 +08:00
RockYang edeff3c169 update docker image version to 4.0.6 2024-06-15 15:35:32 +08:00
RockYang 13076421a1 update docker image version to 4.0.6 2024-06-15 15:33:38 +08:00
RockYang 3943e7f05f update change log files 2024-06-14 18:23:54 +08:00
GeekMaster cd0d9dad98 Merge branch 'main' of https://github.com/yangjian102621/geekai into main 2024-06-12 15:44:26 +08:00
RockYang 857da34b9c wechat payment is ready for PC 2024-06-12 14:20:37 +08:00
RockYang ccad7e7bb5 change payment component, upgrade golang to 1.22.4 2024-06-11 11:48:41 +08:00
RockYang b8de15d66e fix bug: free model not record the chat history 2024-06-06 15:01:32 +08:00
RockYang c02661ea29 fixe page styles 2024-06-05 18:08:23 +08:00
RockYang 3c70c8ae59 fixed bug markmap generation 2024-06-04 16:21:08 +08:00
RockYang b985b62068 Merge pull request #208 from mari1995/main
图片墙,选择框单选问题
2024-06-04 08:33:51 +08:00
RockYang 297b760293 dalle3 and gptt-4o api compatible with azure 2024-06-03 18:34:37 +08:00
RockYang 088abfe7ab merge v4.0.6 2024-06-03 14:22:08 +08:00
RockYang 17340ae0ac fixed bug for function call error None is not of type 'array' 2024-05-30 09:59:44 +08:00
RockYang fe687af8ca fixed bug for mobile chat share 2024-05-30 08:37:14 +08:00
RockYang 7e26029268 add v4.0.8 database sql file 2024-05-29 17:41:37 +08:00
RockYang 03f7f2a53e feat: add dalle3 page for h5 2024-05-29 17:25:01 +08:00
RockYang 296260bf6a feat: add system config for enable rand background image for index page 2024-05-29 16:24:56 +08:00
RockYang dd07acd21a feat: add system config for enable rand background image for index page 2024-05-29 16:23:42 +08:00
RockYang 89f6402fbf fix markdown formula parse plugin 2024-05-29 13:49:45 +08:00
SSMario 853059c814 Update Image.vue 2024-05-27 21:23:11 +08:00
SSMario 5a0876f8dc Update Image.vue 2024-05-27 21:00:59 +08:00
RockYang 2e32238652 micro fixs 2024-05-27 17:39:17 +08:00
SSMario ad1a99aa44 Update ImagesWall.vue 2024-05-27 17:04:15 +08:00
RockYang 24e4be019a fixed bug for dalle prompt translate 2024-05-27 11:42:14 +08:00
RockYang 13c917ad7e put model and app selector on the top of chat page 2024-05-24 12:33:22 +08:00
RockYang 0a8cf6870f fixed bug for payment api authorization 2024-05-24 11:31:38 +08:00
RockYang 061291cebb chore: use config value for order pay timeout 2024-05-22 18:15:06 +08:00
RockYang b8e0d7760b feat: add sign check for PC QR code payment 2024-05-22 17:47:53 +08:00
RockYang 962de0183c extract code for saving chat history 2024-05-22 15:32:44 +08:00
RockYang 627396dbf7 check if the api url in whitelist for mj plus client 2024-05-22 11:47:04 +08:00
RockYang 6c300bb018 fixed bug for mobile chat page change chat model not work 2024-05-21 17:54:03 +08:00
RockYang 9273df4af2 auto resize the input element rows, when use inputed more than one line 2024-05-21 17:36:47 +08:00
RockYang 669d784f54 remove license code 2024-05-21 16:20:29 +08:00
RockYang 5253d657b6 add logs for updating database failed 2024-05-21 11:55:38 +08:00
RockYang c2548c5007 merge conflicts for v4.0.5 2024-05-21 11:30:40 +08:00
RockYang bdec823148 update docker image name 2024-05-21 11:21:27 +08:00
RockYang a6de958daa update docker image url 2024-05-21 11:03:11 +08:00
RockYang 50c25d4574 rename project name to geekai 2024-05-20 15:14:02 +08:00
RockYang 68100f7f24 rename project name to geekai 2024-05-20 15:11:14 +08:00
RockYang 38777ea285 update database file 2024-05-19 19:37:16 +08:00
RockYang 00c8f08179 remove code for set left component fixed height 2024-05-18 08:07:09 +08:00
RockYang 45d6579fb6 refactor login dialog for front page 2024-05-18 00:27:32 +08:00
RockYang dad9254128 finished refactor chat page UI 2024-05-17 19:25:38 +08:00
RockYang 9934143f00 fixed bugs for send message captcha component 2024-05-17 08:41:09 +08:00
RockYang f617bde81b fixed bugs for send message captcha component 2024-05-16 22:33:00 +08:00
RockYang b2771b7b3f feat: add top navbar for front page 2024-05-16 20:10:00 +08:00
RockYang 44b3237dd6 feat: support add external link menu 2024-05-16 10:53:00 +08:00
RockYang b987a2d365 handler chat error in the chat entry func 2024-05-15 15:30:34 +08:00
RockYang 1cf299f090 add charge link for insufficient of power 2024-05-15 07:10:31 +08:00
RockYang c87d78c666 fix bug for white-list api key check 2024-05-14 22:30:42 +08:00
RockYang 79c8d90049 update readme 2024-05-14 18:23:12 +08:00
RockYang 1afc3e5e1c update change log 2024-05-14 18:20:52 +08:00
RockYang 87e8bbfa41 update version 2024-05-14 18:03:58 +08:00
RockYang 3e3d3e162e refactor: use waterflow component in mj, sd and dall image drawing page 2024-05-13 19:04:00 +08:00
RockYang cc80b87fee fixed sd page waterfall component 2024-05-11 18:27:35 +08:00
RockYang b614ecccc2 enable to update AI Drawing configuarations in admin console page 2024-05-11 17:27:14 +08:00
RockYang 8251c6589b fix: markmap do not cost power in front page 2024-05-11 07:08:14 +08:00
RockYang 4edf2ea02a optimize chat handler error handle code 2024-05-10 18:26:19 +08:00
RockYang 12dd4659cd fix bug: remove chat role failed 2024-05-10 17:38:55 +08:00
RockYang 5d4f40c004 fixed bug for dalle3 task not decrease power 2024-05-10 11:18:37 +08:00
RockYang 7441a3a717 fixed bug for dalle3 task not decrease power 2024-05-10 11:17:52 +08:00
RockYang 3eef9a836b add toolbar for markmap component 2024-05-10 06:38:34 +08:00
RockYang fffd7e375f fixed bug for markmap 2024-05-09 21:55:40 +08:00
RockYang d87f09c957 use proxy for downloading discord images 2024-05-09 18:48:53 +08:00
RockYang b29884f8df add stable diffusion default negtive prompt system config 2024-05-07 16:49:54 +08:00
RockYang c0ee2c2d8e remove license code 2024-05-07 16:41:35 +08:00
RockYang be1580e949 upgrade to v4.0.4 2024-05-07 16:32:05 +08:00
RockYang 6424eb871c add stable diffusion default negtive prompt system config 2024-05-07 16:21:31 +08:00
RockYang 8a8c43c7a5 handle the exception for web front page 2024-05-06 17:39:58 +08:00
RockYang 9a503ddc72 fixed conflicts 2024-05-06 14:44:09 +08:00
RockYang b130466c8f chore: change module name to geekai, add copyright in source code 2024-05-06 14:41:27 +08:00
RockYang 3ab5459778 Update README.md 2024-05-06 10:45:50 +08:00
RockYang 4eb1bf2de2 update LICENSE.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-05-05 03:05:23 +00:00
RockYang 6c47551b03 fix bug for waterflow component 2024-05-05 10:52:29 +08:00
RockYang b4ba70c0f3 update changelog 2024-05-04 21:32:43 +08:00
RockYang c2bf5e845a fix bug for license synchronize 2024-05-04 21:30:29 +08:00
RockYang 8900e72e45 opt: optimize mobile images page styles 2024-05-03 08:14:33 +08:00
RockYang 0cb192135d fix: fix bug for dalle power refund 2024-05-01 08:48:32 +08:00
RockYang c1ca687630 chore: update docker image name 2024-05-01 07:59:20 +08:00
RockYang 29cc24508b opt: styles and view micro optimization 2024-05-01 07:40:56 +08:00
RockYang 3db55cbd48 opt: close unused websocket connections 2024-04-30 22:54:39 +08:00
RockYang c0a7f41747 chore: replace docker image url with AliYun 2024-04-30 19:08:33 +08:00
RockYang 1ba08bbfa6 feat: change theme for mobile site is ready 2024-04-30 18:57:15 +08:00
RockYang 51a8f42d89 feat: mobile page refactor is finished 2024-04-29 19:22:00 +08:00
RockYang 3b081ff0f4 opt: add chat config for mobile chat session 2024-04-29 09:39:23 +08:00
RockYang 7f31a301e3 feat: add index page for mobile 2024-04-28 19:09:26 +08:00
RockYang 039d949523 theme change is ready 2024-04-27 13:13:28 +08:00
RockYang 5b16dc6ee7 feat: admin console dark theme 2024-04-26 18:10:17 +08:00
RockYang 34648c704f opt: optimize index and login page UI 2024-04-26 16:07:02 +08:00
RockYang 1966e69460 synchronize license every 10 secs 2024-04-26 14:35:01 +08:00
RockYang 74b1b07b31 change index page background 2024-04-25 18:54:33 +08:00
RockYang 118c0e1ff1 update readme 2024-04-25 06:27:30 +08:00
RockYang 81d075c028 remove unused files 2024-04-24 21:22:56 +08:00
RockYang 514dd6c76a show license info in admin active page, optimize markdown generate prompt 2024-04-24 19:00:28 +08:00
RockYang ddc323a8c3 output the error to chat page directly, replace the common error message 'AI开小差' 2024-04-24 10:10:03 +08:00
RockYang 2933c057a2 fixed bug for markmap 2024-04-23 20:47:06 +08:00
RockYang 20ed6d1d3d release v4.0.4 2024-04-23 18:47:23 +08:00
RockYang 90f5275201 optimize styles and release v4.0.4 2024-04-23 18:46:32 +08:00
RockYang fe64b203da feat: hide more navigator items 2024-04-22 16:27:53 +08:00
RockYang 9eb1353455 feat: support markmap svg export and download as png image 2024-04-22 14:20:51 +08:00
RockYang 8a1c55c731 allow users to select a chatApp to chat in chat app list page 2024-04-22 11:18:55 +08:00
RockYang 9059bebd07 optimize index page UI 2024-04-22 10:43:33 +08:00
RockYang d18f79fe74 optimize code for remove timeout and failed image drawing job 2024-04-21 21:44:28 +08:00
RockYang 3a8f0be28a image wall page add dalle 2024-04-21 20:42:42 +08:00
RockYang 60cf380f96 dalle image page is ready 2024-04-21 20:23:47 +08:00
RockYang ab8240613e DO NOT refresh finished jobs when job is running 2024-04-20 21:30:55 +08:00
RockYang d6e9dc6839 fixed markdown generating styles 2024-04-19 18:22:45 +08:00
RockYang 6891bdde53 support send email use TLS 2024-04-19 12:04:59 +08:00
RockYang ecadfeab19 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2024-04-19 10:56:05 +08:00
RockYang a38d547200 fixed bug for QWen response blank quotes 2024-04-19 10:55:29 +08:00
RockYang a15f431a7f fixed bug for QWen response blank quotes 2024-04-19 10:30:02 +08:00
RockYang 3bb5b66e8b fixed bug for chat handler doRequest method 2024-04-16 23:49:56 +08:00
RockYang d9ff3242c6 compatible freeGPT35 API 2024-04-15 21:03:19 +08:00
RockYang cd562ab8ed fixed bug for websocket close tip message 2024-04-15 18:15:15 +08:00
RockYang 3a8a69ac2e feat: markmap function is ready 2024-04-15 17:23:59 +08:00
RockYang 0d5b4936bd fixed upscale and variation action url 2024-04-15 15:14:49 +08:00
RockYang b5f6eaf159 opt: close the old connection for mj and sd clients 2024-04-15 09:34:20 +08:00
RockYang 25c80921e0 update version 2024-04-15 09:05:54 +08:00
RockYang 5c51ed48c3 release v4.0.3 2024-04-15 08:26:07 +08:00
RockYang 22febfc42a markmap enable to select ai model 2024-04-15 06:16:53 +08:00
RockYang de482dd9f4 fixed chat page styles 2024-04-12 21:57:41 +08:00
RockYang 74af67a41f feat: markmap page view is ready 2024-04-12 18:49:24 +08:00
RockYang 170b41441b feat: allow chat model bind a fixed api key 2024-04-12 17:09:22 +08:00
RockYang 4991e50e16 chore: update build.sh 2024-04-11 21:26:33 +08:00
RockYang 1f236ef929 feat: use custom mode for mj upscale and variation operarions 2024-04-11 17:32:34 +08:00
RockYang 7f86c5984d feat: add index page 2024-04-10 18:23:55 +08:00
RockYang 6fcee49b19 feat: support for freeGPT35 API 2024-04-10 14:49:07 +08:00
RockYang ac527851a0 feat: support gpt-4-turbo-2014-04-09 vision function 2024-04-10 11:47:10 +08:00
RockYang 64bbeaedcb fixed bug for gpt-4-turbo-2024-0409 model function calls 2024-04-10 10:23:45 +08:00
RockYang 3e984a1bcb fix bug: remove timeout task ONLY for unfinished(progress < 100) 2024-04-10 06:25:54 +08:00
RockYang 79165214d7 Merge branch 'main' of gitee.com:blackfox/chatgpt-plus 2024-04-07 18:33:55 +08:00
RockYang 3f7855dd6e Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2024-04-07 18:32:20 +08:00
RockYang f0610281e5 update database files 2024-04-07 18:26:45 +08:00
RockYang e87e7e2164 set the enable status for adding new api key with default value true 2024-04-07 10:17:10 +08:00
RockYang 1a3ad390fd update change log 2024-04-07 08:40:43 +08:00
RockYang c383367e9f Merge branch 'dev' 2024-04-07 08:03:14 +08:00
RockYang 7a9f45c96b optimize ngin configuration for chat-plus.conf 2024-04-07 08:02:47 +08:00
RockYang 7277cb289f fix 404 error with remove api keys 2024-04-07 08:01:25 +08:00
RockYang 86b2d5eff2 fix bug for remove api 404 errorc 2024-04-06 20:36:52 +08:00
RockYang 7873847616 feat: show bind model on chat role list page 2024-04-05 21:21:28 +08:00
RockYang 5e46f149d9 feat: support uploading role icon 2024-04-05 17:41:23 +08:00
RockYang 574fc52332 feat: allow bind a chat model for chat role 2024-04-05 12:51:18 +08:00
RockYang 8c924ca98f update change log 2024-04-05 06:58:13 +08:00
RockYang a9adc0c1a2 feat: stable diffusion image drawing on mobile is ready 2024-04-03 18:13:48 +08:00
RockYang ed286c8894 feat: midjourney role and style consistency is ready 2024-04-02 19:01:28 +08:00
RockYang a55da15202 update README.md.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-04-02 10:00:45 +00:00
RockYang 9728118f94 Update README.md 2024-04-02 17:59:53 +08:00
RockYang 9cc2ea412b feat: image preview for stable-diffusion task is ready 2024-04-02 17:24:38 +08:00
RockYang 3f1ad4b7dc feat: support midjourney --cref and --sref for role consistency 2024-04-02 14:59:53 +08:00
RockYang 56c225bf20 feat: update menu icons, add version in site titles 2024-04-01 18:20:00 +08:00
RockYang 8b68bfb28c show power for chat and imaging page 2024-03-31 20:49:12 +08:00
RockYang a5299b52d4 opt: change the relative path with absolute path for midjourney image uploading 2024-03-31 17:45:22 +08:00
RockYang 9e9bc52737 Merge branch 'main' into dev 2024-03-30 11:57:31 +08:00
RockYang 76106e4504 remove dead code 2024-03-30 11:57:23 +08:00
RockYang 4e23005b5b fixed conflicts 2024-03-29 18:10:59 +08:00
RockYang f27e2de220 update readme file 2024-03-29 18:06:55 +08:00
RockYang eefcabcff3 update databases 2024-03-29 17:43:38 +08:00
RockYang 63e4669e3f feat: support custom menu 2024-03-29 15:41:58 +08:00
RockYang 7d5428f775 add tip for mj image buttons 2024-03-28 21:41:49 +08:00
RockYang ab3c4562fa fix: fix overflow hidden for admin page 2024-03-28 18:51:09 +08:00
RockYang 13f89bf335 fix: fix overflow hidden for mobile page 2024-03-28 18:13:33 +08:00
RockYang e6a18445c3 fix: use slide captcha for iphone 2024-03-28 15:00:53 +08:00
RockYang c8ab209426 feat: add mj_action_power system config item 2024-03-28 09:53:41 +08:00
RockYang 360fea4085 feat: change midjourney origin implements, replace midjourney bot with midjourney-proxy 2024-03-27 18:57:15 +08:00
RockYang 9794d67eaa feat: auto translate and rewrite prompt for midjourney and stable-diffusion 2024-03-27 13:45:52 +08:00
RockYang b60a639312 feat: stable-diffusion refactored, replace websocket api with sdapi 2024-03-26 18:23:08 +08:00
RockYang 870706c4ff fix: can not change user's power in admin console 2024-03-25 11:40:03 +08:00
RockYang 491471fa10 fix: fix bug for update user's power in admin page did not work 2024-03-23 16:21:37 +08:00
RockYang 6dbf61d4e4 No need to login with Stable-Diffusion page and Invite page 2024-03-23 15:45:37 +08:00
RockYang 81545d192b fix: fix code highlight error when add formule detecting 2024-03-22 19:09:04 +08:00
RockYang 37cdc26160 chore: correct prompt messages 2024-03-22 18:27:57 +08:00
RockYang fb6a35ebe4 feat: add chart for admin dashbord 2024-03-22 16:57:30 +08:00
RockYang f7a565bb80 feat: integrate xxl-job-admin to implements automatic task scheduling 2024-03-22 13:47:16 +08:00
RockYang bf5e72b7e0 feat: save prompt in power log for dalle-3 2024-03-21 15:55:39 +08:00
RockYang ec6186596d feat: add manager list page in console page 2024-03-21 15:24:28 +08:00
RockYang dece19cec6 feat: add powerlog page for admin console 2024-03-21 13:46:39 +08:00
RockYang 6aa1d27711 opt: optimize the formula show styles 2024-03-21 11:04:12 +08:00
RockYang 264e77f383 always parse authorization token for all request 2024-03-20 21:11:52 +08:00
RockYang 0cbc284e42 fixed conflicts 2024-03-20 20:40:22 +08:00
RockYang 9e42a334fa update README.md.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-03-20 20:39:44 +08:00
RockYang 49438d2ee1 feat: Async loading midjourney job for mobile MidJourney page 2024-03-20 18:39:14 +08:00
RockYang 735aa24020 feat: h5 payment for payjs is ready 2024-03-20 17:46:39 +08:00
RockYang 14f63a203d feat: payment for mobile pages is ready 2024-03-20 16:14:02 +08:00
RockYang e11c0a3633 feat: the power log page is ready 2024-03-20 14:14:30 +08:00
RockYang ca83f139f7 feat: no need login refactor with member and chatApps page 2024-03-19 18:59:02 +08:00
RockYang 58e44b7d12 feat: load preview page do not require user to login 2024-03-19 18:25:01 +08:00
RockYang 8f47474edd feat: optimize login dialog 2024-03-19 10:47:13 +08:00
RockYang c5200aada8 feat: refactoring adjustments for reward page is ready 2024-03-18 18:28:34 +08:00
RockYang 4e39b93673 feat: refactoring adjustments for member pages 2024-03-18 16:59:07 +08:00
RockYang f215643f2e feat: The 'chat_models' field of user table, holds the model IDS in place of the model values 2024-03-18 15:37:46 +08:00
RockYang aa72be6ff3 remove new-ui files 2024-03-18 12:01:34 +08:00
RockYang b567e56b60 chore: adjust page styles 2024-03-18 06:46:08 +08:00
RockYang 17431c1707 重构主体工作完成 2024-03-15 18:35:10 +08:00
RockYang 7c9ebef5e9 restore new ui files 2024-03-15 11:13:02 +08:00
廖彦棋 72b0e11543 fix(ui): 交互修复调整 2024-03-15 11:07:41 +08:00
RockYang 9e2af9dbca feat: refactor user list page for new UI 2024-03-15 09:29:19 +08:00
廖彦棋 b7cc987132 fix(ui): 用户管理有效期传参调整,权限标识补充 2024-03-15 09:25:06 +08:00
廖彦棋 8476c73b0c Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 17:49:18 +08:00
廖彦棋 e76b49afd2 refactor(ui): 无权限页面调整 2024-03-14 17:49:15 +08:00
吴汉强 c712b95e0d feat(ui): 后台首页去掉权限判断 2024-03-14 17:24:40 +08:00
廖彦棋 1ee1a8d6d9 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 17:19:41 +08:00
廖彦棋 f01c764589 feat(ui): 无权限判断 2024-03-14 17:19:39 +08:00
吴汉强 6df66d150b Merge remote-tracking branch 'origin/ui' into ui 2024-03-14 17:12:53 +08:00
吴汉强 6132f94eff feat(ui): 新增 sql 2024-03-14 17:12:48 +08:00
廖彦棋 ac5e67ea73 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 17:06:15 +08:00
吴汉强 143d2b44d0 feat(ui): 403,没权限 2024-03-14 16:41:38 +08:00
吴汉强 ef130fe377 feat(ui): 网站配置需授权,去掉 2024-03-14 16:28:49 +08:00
廖彦棋 d7d1e2100c fix(ui): 调整 2024-03-14 16:14:49 +08:00
吴汉强 af5afd7700 feat(ui): 后端加权限验证 2024-03-14 15:39:12 +08:00
廖彦棋 a64df5cf0c feat(ui): 角色管理 2024-03-14 15:25:17 +08:00
廖彦棋 3866319373 feat(ui): 新增系统分类菜单 2024-03-14 15:17:53 +08:00
廖彦棋 ec5ad809f2 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 10:56:56 +08:00
廖彦棋 268acb174d fix(ui): 修复 2024-03-14 10:56:54 +08:00
吴汉强 3733877be1 Merge remote-tracking branch 'origin/ui' into ui 2024-03-14 10:28:39 +08:00
吴汉强 6d5579a8d6 feat(ui): 登录接口返回权限 2024-03-14 10:28:32 +08:00
廖彦棋 306cd2f945 feat(ui): 管理后台新增权限及部分组合式函数优化 2024-03-14 10:27:09 +08:00
廖彦棋 5d4dd1e66f Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-13 17:30:26 +08:00
廖彦棋 5f371bdc4a feat(ui): web移动端初始化 2024-03-13 17:30:24 +08:00
吴汉强 aa58ba392e Merge remote-tracking branch 'origin/ui' into ui 2024-03-13 17:24:38 +08:00
吴汉强 0485610818 feat(ui): 增加角色管理,管理员方法新增角色关联 2024-03-13 17:24:30 +08:00
chenzifan 7cb8becc09 Merge remote-tracking branch 'origin/ui' into ui 2024-03-13 14:40:45 +08:00
chenzifan 8fa535a01b refactor: remove api change to post request 2024-03-13 14:40:38 +08:00
吴汉强 5f202e03a2 Merge remote-tracking branch 'origin/ui' into ui 2024-03-13 11:41:07 +08:00
吴汉强 ef2dfe330b feat(ui): 增加系统权限管理 2024-03-13 11:41:01 +08:00
廖彦棋 fcd86fbebd fix(ui): ui调整 2024-03-13 09:54:20 +08:00
RockYang 960d294aa2 docs: update sql file 2024-03-13 08:49:40 +08:00
RockYang 27f8e63f3d Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-13 08:48:10 +08:00
chenzifan e8d7ad58be fix: 删除系统管理员失效的问题 2024-03-13 08:47:17 +08:00
chenzifan f4d537e0f5 Merge remote-tracking branch 'origin/ui' into ui
# Conflicts:
#	api/handler/admin/admin_user_handler.go
2024-03-13 08:46:16 +08:00
chenzifan d6c93dd537 fix: 删除系统管理员失效的问题 2024-03-13 08:45:09 +08:00
廖彦棋 aa5fff47de Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-13 08:44:50 +08:00
廖彦棋 c920d8423d feat(ui): 调整 2024-03-13 08:44:48 +08:00
chenzf@pvc123.com b8fcfe2e7c feat: 超级管理员不支持修改和删除 2024-03-12 21:16:05 +08:00
RockYang 072c96e1ec Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-12 18:07:24 +08:00
RockYang 38c8ece642 fix conflicts 2024-03-12 18:07:19 +08:00
chenzifan 9bf1576fb4 feat: 增加系统管理员 2024-03-12 18:06:49 +08:00
RockYang d648e177c4 fix conflicts 2024-03-12 18:03:24 +08:00
RockYang e8f62af7f6 refactor: use power replace calls for front pages 2024-03-12 17:47:06 +08:00
RockYang e746aafa2f refactor: 重构项目,为所有的 AI 工具都引入算力,采用算力统一结算各个工具的调用次数和权限 2024-03-12 15:40:44 +08:00
廖彦棋 71f7a1b166 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-12 08:37:29 +08:00
廖彦棋 678f570a90 refactor(ui): 调整 2024-03-12 08:37:27 +08:00
huangqj 4bc1f195db fix(ui):环境变量 2024-03-11 16:10:49 +08:00
廖彦棋 b2ff49ee94 feat(ui): 新增系统管理员 2024-03-11 15:59:15 +08:00
廖彦棋 d9558f13ad fix(ui): 删除冗余 2024-03-11 14:23:11 +08:00
廖彦棋 8dc8f2567e fix(ui): 删除冗余 2024-03-11 14:22:39 +08:00
RockYang 316636f83c feat: replace Tools param with Function param for OpenAI chat API 2024-03-11 14:09:19 +08:00
廖彦棋 e3c2fbf82a Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-11 13:52:24 +08:00
廖彦棋 1b6175e598 fix(ui): type 2024-03-11 13:52:22 +08:00
chenzifan 26dc479596 feat: 优化后台UI 2024-03-11 13:51:26 +08:00
chenzifan dca25dbb78 Merge remote-tracking branch 'origin/ui' into ui 2024-03-11 13:46:46 +08:00
廖彦棋 c55275872e refactor(ui): 调整优化 2024-03-11 13:46:08 +08:00
廖彦棋 d4d1fb26f6 feat(ui): 细节优化 2024-03-11 12:02:20 +08:00
廖彦棋 a48cb7bc34 feat(ui): 上传功能补充 2024-03-11 11:41:50 +08:00
廖彦棋 8af6e9290f feat(ui): 登录新增验证码及记住密码功能 2024-03-11 10:49:13 +08:00
廖彦棋 14b277d813 feat(ul): 顶部信息 2024-03-11 09:00:00 +08:00
huangqj 20915038a3 feat(ui):看板图表 样式调整 2024-03-11 08:35:21 +08:00
chenzifan 15723457cb Merge remote-tracking branch 'origin/ui' into ui 2024-03-11 08:01:54 +08:00
RockYang ec79fc933c opt: remove global keyup event bind 2024-03-10 09:45:59 +08:00
RockYang 3ed5a420a7 docs: update config.toml 2024-03-10 09:45:59 +08:00
RockYang 711e701933 docs: add database sql file for v3.2.7 2024-03-10 09:45:59 +08:00
RockYang e6bbd83c6b feat: allow to view chat message in manager console 2024-03-10 09:45:59 +08:00
RockYang b99b310306 feat: download image which ai generated in dialog and replace the image url 2024-03-10 09:45:59 +08:00
RockYang e3c46016e0 feat: image-wall page for mobile is ready 2024-03-10 09:45:59 +08:00
RockYang eb739fcccb feat: allow user config third-party platform openai and mj api key 2024-03-10 09:45:59 +08:00
RockYang 9a94505f43 feat: added delete file function 2024-03-10 09:45:59 +08:00
RockYang 7c5c3d8a3c fix: verifycation component touch event coordinates misplace in iphone browser 2024-03-10 09:45:59 +08:00
RockYang 7027f9a55b fix: fix bug for regenerate button did not work 2024-03-10 09:45:59 +08:00
RockYang cf3d8af260 fix: Upscale and Variation task overrite each other 2024-03-10 09:45:59 +08:00
RockYang 4ea7352674 feat: midjourney mobile page all function is ready 2024-03-10 09:45:59 +08:00
RockYang 6e6885849b feat: mobile mj list page is ready 2024-03-10 09:45:59 +08:00
RockYang 827f2b9739 feat: add mj image list component for mobile page. fixed bug for html tag escape 2024-03-10 09:45:59 +08:00
RockYang 54d70623ab feat: add functions mj page for mobile 2024-03-10 09:45:59 +08:00
RockYang ed819e8c79 opt: add default extension for mj image 2024-03-10 09:45:59 +08:00
RockYang 94680c4411 feat: mj for mobile page payout is ready 2024-03-10 09:45:59 +08:00
RockYang 8bcbb68438 add docs and github link 2024-03-10 09:45:59 +08:00
RockYang b9f31d42f4 opt: enable use cdn url for mj-plus 2024-03-10 09:45:59 +08:00
RockYang dc2b768c6b feat: LaTeX parse is ready 2024-03-10 09:45:59 +08:00
RockYang 60df732db7 feat: add model field for chat_item and and chat_history data table 2024-03-10 09:45:59 +08:00
RockYang 6d85d128cc feat: add err_msg field for mj and sd jobs 2024-03-10 09:45:59 +08:00
RockYang 96785ca037 feat: blend and swap face function for midjourney-plus is ready 2024-03-10 09:45:59 +08:00
RockYang 7cfc747653 feat: add blend and swapface task implements for midjourney 2024-03-10 09:45:59 +08:00
RockYang 05f911628a opt: refactor chat session page for mobile device 2024-03-10 09:45:59 +08:00
RockYang 18b718a328 opt: optimize chat list page for mobile 2024-03-10 09:45:59 +08:00
RockYang 6d9b9bd3f7 feat: use vant replace element-plus as mobile UI framework 2024-03-10 09:45:59 +08:00
RockYang 04e990aeb3 feat: add websocket heartbeat message for mj page 2024-03-10 09:45:59 +08:00
廖彦棋 cbbcd4c571 fix(ui): 细节调整 2024-03-08 17:46:48 +08:00
huangqj 5ad9adeb4a feat(ui):细节调整 2024-03-08 10:24:38 +08:00
廖彦棋 13c9594be0 feat(ui): prettier 2024-03-08 09:59:40 +08:00
廖彦棋 e4f5f78bfa refactor(ui): 登录页重构 2024-03-08 09:45:09 +08:00
廖彦棋 bfa0208692 refactor(ui): 优化 2024-03-08 09:12:39 +08:00
huangqj 64994cdeda feat(ui):路由 2024-03-08 08:35:41 +08:00
廖彦棋 9796a841d9 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-07 18:03:08 +08:00
廖彦棋 404b493d23 feat(ui): 过期跳转登录 2024-03-07 18:03:06 +08:00
huangqj c04f9eb993 feat(ui):apiKey 语言模型 角色管理 产品 2024-03-07 17:58:25 +08:00
廖彦棋 825da42208 feat(ui): 新增系统设置 2024-03-07 17:24:50 +08:00
廖彦棋 721a8f1ecb feat(ui): 对话管理 2024-03-07 15:32:32 +08:00
huangqj 586a174d6c feat(ui):用户 2024-03-07 15:05:01 +08:00
huangqj bec6966542 feat(ui):simpleTable 2024-03-07 14:59:45 +08:00
huangqj b1f4e1d4d8 feat(ui):searchtable 2024-03-07 14:59:16 +08:00
huangqj d7d64e8259 feat(ui):用户 2024-03-07 14:03:55 +08:00
廖彦棋 08b498fccd feat(ui): 函数管理 2024-03-07 11:40:57 +08:00
廖彦棋 ecc5a52232 feat(ui): 新增弹窗及时间格式化 2024-03-07 09:23:45 +08:00
chenzifan 7327b90255 Merge branch 'main' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-07 08:37:54 +08:00
chenzifan 6ce7a9c16f feat: 增加系统用户管理 2024-03-07 08:37:48 +08:00
廖彦棋 eb2658a91c fix(ui): ts类型 2024-03-06 18:20:07 +08:00
RockYang 9f59a18756 feat: update changelogs 2024-03-06 17:58:17 +08:00
廖彦棋 ef06f0da98 feat(ui): 新增登录 2024-03-06 17:54:38 +08:00
RockYang 4dec0e2c69 feat: Mj and sd jobs data loading in pages 2024-03-06 17:31:54 +08:00
RockYang 5f36f1af11 add prompt translating function for mobile midjourney page 2024-03-06 16:22:03 +08:00
huangqj 5a1a596098 feat(ui):simpleTable 2024-03-06 15:33:37 +08:00
廖彦棋 7a25c6e062 feat(ui): 新增请求方法及表格 2024-03-06 13:55:38 +08:00
廖彦棋 a73ed71b9d feat(ui): 管理后台基础配置 2024-03-06 10:23:55 +08:00
廖彦棋 2abbb95ea2 chore(ui): 目录结构调整 2024-03-06 09:32:47 +08:00
廖彦棋 1f80ba9463 feat(ui): 初始化 2024-03-06 09:27:11 +08:00
chenzifan 4327fdac12 refactor: 初始化UI重构 2024-03-06 08:57:46 +08:00
RockYang a6f05a5874 Merge branch 'dev' of gitee.com:blackfox/chatgpt-plus-pro into dev 2024-03-04 08:34:12 +08:00
RockYang 3fb74a1598 feat: add removing order button in admin order list page 2024-03-03 19:27:22 +08:00
RockYang 2ac44cdeb6 fix: fix major bugs for unauthorized access to data 2024-03-03 10:40:32 +08:00
RockYang 855a953010 Update README.md 2024-03-01 23:08:05 +08:00
RockYang 3264fda06e fix: fixed bug image preview im mobile chat session page 2024-02-29 15:41:45 +08:00
RockYang 2c7d472069 feat: add draw same image for midjourney page 2024-02-29 11:44:09 +08:00
RockYang 8182e6797f opt: add logs for mj-plus api error 2024-02-28 15:50:42 +08:00
RockYang 99cb48ce88 opt: replace proxy url for discord image url 2024-02-27 17:45:57 +08:00
RockYang e391bfca80 feat: add change password in with mobile page 2024-02-27 15:36:20 +08:00
RockYang 0181ad3715 feat: add image preview for mobile chat page 2024-02-26 18:11:37 +08:00
RockYang b95dff0751 feat: replace http polling with webscoket notify in sd image page 2024-02-26 15:45:54 +08:00
RockYang 668d4c9c64 chore: replace 'token' with power 2024-02-23 18:11:57 +08:00
RockYang 66c0d1b2f7 docs: update mj-plus api domain 2024-02-23 15:41:02 +08:00
RockYang bef0996375 opt: remove global keyup event bind 2024-02-23 11:43:27 +08:00
RockYang 391a1e4cf0 docs: update config.toml 2024-02-22 17:53:39 +08:00
RockYang 38facf517a docs: add database sql file for v3.2.7 2024-02-22 17:28:22 +08:00
RockYang 5c9b1e8764 feat: allow to view chat message in manager console 2024-02-22 17:16:44 +08:00
RockYang dbcf14b566 feat: download image which ai generated in dialog and replace the image url 2024-02-20 18:38:03 +08:00
RockYang cc59f0c761 feat: image-wall page for mobile is ready 2024-02-20 17:38:18 +08:00
RockYang f263fc067a feat: allow user config third-party platform openai and mj api key 2024-02-20 11:23:55 +08:00
RockYang 6635b88baa feat: added delete file function 2024-02-19 16:43:03 +08:00
RockYang 87ea2a611f fix: verifycation component touch event coordinates misplace in iphone browser 2024-02-19 14:04:50 +08:00
RockYang ea8a2135cd fix: fix bug for regenerate button did not work 2024-02-19 11:22:42 +08:00
RockYang 76d86395ef fix: Upscale and Variation task overrite each other 2024-02-16 18:08:29 +08:00
RockYang e268870636 feat: midjourney mobile page all function is ready 2024-02-16 15:55:04 +08:00
RockYang 49e9f41ef2 feat: mobile mj list page is ready 2024-02-15 18:11:22 +08:00
RockYang 68cda968a1 feat: add mj image list component for mobile page. fixed bug for html tag escape 2024-02-15 11:39:04 +08:00
RockYang f6826fcefc feat: add functions mj page for mobile 2024-01-31 07:24:35 +08:00
RockYang 56a91db7e7 opt: add default extension for mj image 2024-01-30 21:46:17 +08:00
RockYang 8f4d94f046 feat: mj for mobile page payout is ready 2024-01-30 18:34:01 +08:00
RockYang 9446bf5f98 add docs and github link 2024-01-30 16:18:27 +08:00
RockYang 23e353b18b opt: enable use cdn url for mj-plus 2024-01-28 21:56:25 +08:00
RockYang 46daaa4ba1 feat: LaTeX parse is ready 2024-01-26 18:04:53 +08:00
RockYang f66d59531d feat: add model field for chat_item and and chat_history data table 2024-01-26 16:54:00 +08:00
RockYang b76cd1d5ae feat: add err_msg field for mj and sd jobs 2024-01-26 14:50:36 +08:00
RockYang 55ba0d08a5 feat: blend and swap face function for midjourney-plus is ready 2024-01-26 11:57:08 +08:00
RockYang cc78780a8e feat: add blend and swapface task implements for midjourney 2024-01-25 18:50:24 +08:00
RockYang 4a00809061 opt: refactor chat session page for mobile device 2024-01-25 14:07:10 +08:00
RockYang cd760bbd84 opt: optimize chat list page for mobile 2024-01-24 18:23:24 +08:00
RockYang 84bdc6be7f feat: use vant replace element-plus as mobile UI framework 2024-01-24 17:34:30 +08:00
RockYang 8dd6bf8933 feat: add websocket heartbeat message for mj page 2024-01-24 09:33:04 +08:00
RockYang 08f170d217 add v3.2.6 database sql file 2024-01-23 18:00:49 +08:00
RockYang 20da6a1c20 doc: update config sample file 2024-01-23 17:56:22 +08:00
RockYang 8586931630 fix: auto fill apiURL when platform changed for ApiKey add page 2024-01-23 17:30:54 +08:00
RockYang 667fc79a6f feat: merge sms branch,add DuanXinBao sms service implemetation 2024-01-23 16:16:47 +08:00
RockYang ac7430dded opt: add heartbeat message for websocket connects 2024-01-22 18:42:51 +08:00
whale_fall 9c73e16560 feat: 添加支持多个短信服务商支持 添加短信宝服务商支持,同时添加配置示例 2024-01-22 16:38:44 +08:00
RockYang c605e222a7 fix: fix bug for wechat transfer message parse failed 2024-01-22 16:10:08 +08:00
RockYang 578f26a738 feat: HuPiPay order check function is ready 2024-01-22 15:17:26 +08:00
RockYang 24218e7570 opt: verify the order in notify callback 2024-01-22 13:58:25 +08:00
RockYang 16f946550e chore: print error detail when call http api failed with mj 2024-01-21 22:30:24 +08:00
RockYang 9960a31485 opt: add image upload support for md-editor-3 2024-01-19 18:43:13 +08:00
RockYang 437632f859 fixed conflicts 2024-01-19 18:21:49 +08:00
RockYang 98f0f442eb feat: system notice function is ready 2024-01-19 18:19:51 +08:00
RockYang 3b999dc5e2 feat: system notice function is ready 2024-01-19 18:18:10 +08:00
RockYang 20943ad3d3 fix: fixed bug for img_call increased when upscale task run failed 2024-01-19 17:10:52 +08:00
RockYang 3b1544b5e4 feat: add system config item for wechat qrcode 2024-01-19 16:58:13 +08:00
RockYang b317d597ac chore: optimize variable name 2024-01-19 11:26:22 +08:00
RockYang 3bb19811e7 Merge branch 'main' into dev 2024-01-19 10:09:18 +08:00
RockYang 065a01de48 Merge branch 'main' of gitee.com:blackfox/chatgpt-plus 2024-01-19 10:09:01 +08:00
RockYang 09fb2f6557 !4 添加支持阿里旗下的大模型 通义千问对话
Merge pull request !4 from 鲸落/qwen
2024-01-19 02:06:17 +00:00
whale_fall 9ca97bf4bc feat: 添加支持阿里的通义千问对话 2024-01-19 09:52:16 +08:00
RockYang 0c59503edb feat: add image publish function, ONLY published image show in image wall page 2024-01-19 06:52:23 +08:00
RockYang 6fa08861f8 update change log 2024-01-18 18:11:25 +08:00
RockYang 72b95151a1 feat: add system config disable user registeration 2024-01-18 17:24:02 +08:00
RockYang 378e6ec9af opt: compatible wechat old message format for parsing wechat transfer message 2024-01-18 16:58:20 +08:00
RockYang 27b68c1174 opt: optimize order query alg, reduce polling times 2024-01-18 09:39:36 +08:00
RockYang 1ae8eabe99 update config 2024-01-16 15:24:06 +08:00
RockYang 72726c1cf9 docs: update change log 2024-01-16 15:05:54 +08:00
RockYang 6203d9a150 docs: update config files 2024-01-16 14:38:18 +08:00
RockYang 861f11af66 opt: optimize markdown image parser, identify image and blockquote tags 2024-01-16 10:13:00 +08:00
RockYang 39fdfae541 feat: attachments manage function is ready 2024-01-15 18:48:01 +08:00
RockYang 9f57fb1421 feat: gpt-4-gizmo-g-* model is supported 2024-01-15 15:03:05 +08:00
RockYang 059f57db2d feat: gpt-4-all model is ready 2024-01-15 14:07:24 +08:00
RockYang 13c8be8d06 opt: optimize vip recharge logic 2024-01-15 11:01:57 +08:00
RockYang e0dbfb0488 Merge branch 'dev' 2024-01-15 10:29:01 +08:00
RockYang 722ed128a1 opt: optimize vip recharge logic 2024-01-15 10:28:46 +08:00
RockYang cc628821e6 feat: add asynchronously pull midjourney task progress in case the synchronization callback is fails 2024-01-12 18:24:28 +08:00
RockYang 857af61af1 feat: midjourney plus service is ready 2024-01-11 18:16:48 +08:00
RockYang f6fc484cb3 feat: update video tutorial 2024-01-10 08:50:13 +08:00
RockYang ba07946f1e feat: update video tutorial 2024-01-10 08:48:05 +08:00
RockYang 600f668c63 remove api key for hupipay 2024-01-09 17:16:27 +08:00
RockYang 8a8990b12b fix: fixed bug for gorm insert record failed and Error is not nil 2024-01-08 18:10:32 +08:00
RockYang fe85c1bbb9 feat: change mobile field to username 2024-01-08 17:34:09 +08:00
RockYang a89ff72308 add changelog 2024-01-08 12:01:58 +08:00
RockYang a99e58184b chore: do not close pop window when click model 2024-01-08 11:01:19 +08:00
RockYang d981bc8fd0 fix: function call 兼容中转 API 2024-01-07 22:32:59 +08:00
RockYang ff22480d89 opt: add support to disable code verify 2024-01-07 17:31:26 +08:00
RockYang 8ec85e2829 feat: payjs payment channel is ready 2024-01-07 14:36:02 +08:00
RockYang 706aacd8e6 fix: add user failed in admin user list page 2024-01-07 10:49:36 +08:00
RockYang 803bc2d9fc release v3.2.4 2024-01-06 21:09:19 +08:00
RockYang 7cc67cf8f0 chore: remove useless system config items 2024-01-06 17:38:55 +08:00
RockYang 9909c3a0ed chore: error recover is enable ONLY in debug mode 2024-01-06 17:16:02 +08:00
RockYang 60a3751839 feat: payjs service is ready 2024-01-06 15:53:30 +08:00
RockYang ce87e0c40b chore: rename bind username api 2024-01-05 18:21:47 +08:00
RockYang 71cd548c00 feat: email registration function is ready 2024-01-05 18:17:11 +08:00
RockYang 1ddd05302b feat: update api key last_use_time after dalle3 call 2024-01-04 18:15:00 +08:00
RockYang ea3301c75c feat: support dall-e3 api mirrors, add name field for ApiKey 2024-01-04 16:29:57 +08:00
RockYang 4b1c4f7ccc feat: refactor LLM api request code, get API URL from ApiKey object 2024-01-04 14:51:33 +08:00
RockYang 21f2622a4b feat: api key manage page funciton is ready 2024-01-04 10:48:04 +08:00
RockYang 79f6a43019 fix: fixed bug for concurrency risk for getting token for chat histroy with issue #92 2024-01-04 09:03:19 +08:00
RockYang 3d75093b2c fix: add unique key for MidJourney task_id 2024-01-03 18:06:10 +08:00
RockYang 023dc89c3e feat: show notice in chat page 2024-01-03 15:19:24 +08:00
RockYang b5641be30d feat: fixed bug for wechat bot to parse transactions. enable user to exchange reward with img_calls 2024-01-03 11:15:54 +08:00
RockYang ff4515bbf1 fix: fixed chat export page styles 2024-01-02 11:32:36 +08:00
RockYang 5dc0fe05af fix: fixed for img_call repeated reductions 2024-01-01 18:54:48 +08:00
RockYang 25bba912f6 feat: add switch for enable|disable chat role 2023-12-29 17:51:56 +08:00
RockYang cb6e323596 feat: add nickname field for user 2023-12-29 17:39:37 +08:00
RockYang cf4b04e047 feat: add authorization for local function call 2023-12-29 17:21:29 +08:00
RockYang 243b5be31c update changelog 2023-12-29 11:53:37 +08:00
RockYang c3d753cf38 feat: add router for function manager 2023-12-29 11:22:26 +08:00
RockYang 70c46d098b fix: restore user img_calls quota when image task run failed 2023-12-29 10:41:29 +08:00
RockYang 816e4a096e merge pull request #72 2023-12-29 10:09:37 +08:00
RockYang 1d1e819817 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-12-29 09:39:52 +08:00
RockYang 342daf1be8 Merge pull request #72 from Unclesimonlau/main
重新设计了移动端web页面,新增了移动端CSS,增加移动端SD绘图页面
2023-12-29 09:39:33 +08:00
RockYang f6bb9b582a merge pull request #71 2023-12-29 09:31:25 +08:00
RockYang aad62bc976 Merge pull request #71 from JingHong0202/main
fix: Azure Api request failure after changing the API-version parameter
2023-12-29 09:27:23 +08:00
RockYang 38a49a7f37 chore: remove dead code 2023-12-29 09:02:55 +08:00
RockYang f603bf6be7 feat: function manager refactor is ready 2023-12-28 18:14:38 +08:00
JingHong 6cc05e9e27 Merge branch 'yangjian102621:main' into main 2023-12-27 23:33:46 +08:00
UncleSimonlau c00e67542f Merge branch 'main' of https://github.com/Unclesimonlau/chatgpt-plus 2023-12-26 14:19:25 +08:00
RockYang 22660362a3 fix: fixed bug #70, XunFei 1.5 url version map error 2023-12-26 14:19:00 +08:00
RockYang b1ab9975b7 fix: fixed bug #70, XunFei 1.5 url version map error 2023-12-25 08:54:17 +08:00
RockYang 7ca89d8b54 feat: function CRUD operation is ready 2023-12-24 22:12:12 +08:00
jinghong0202 998b07ab8c fix: Azure Api 更换api-version参数后请求失败的问题 2023-12-24 08:36:34 +01:00
RockYang 4936896ff7 feat: function add for admin page is ready 2023-12-23 22:30:27 +08:00
RockYang 18b7484c5b feat: support CDN reverse proxy for MidJourney and OpenAI API 2023-12-22 17:25:31 +08:00
RockYang 6754c8e85e feat: add function list page in admin console 2023-12-21 18:06:09 +08:00
RockYang 57243ff010 opt: optimize image compress alg, add cache control for image 2023-12-21 15:00:46 +08:00
RockYang 7c4dfe96ee feat: add funcitons manger page 2023-12-21 08:58:24 +08:00
RockYang bf19120c27 feat: auto translate image creating prompt 2023-12-19 18:54:19 +08:00
RockYang 3d37087916 fix: fixed bug for HuPiPay qrcode generation. set field 'openid' of result struct to Any data type 2023-12-19 11:31:57 +08:00
RockYang 6949f1328c chore: update copyright information 2023-12-18 18:19:41 +08:00
RockYang b63090c20f feat: add remove action to remove task and images for MJ and SD task list page 2023-12-18 17:44:52 +08:00
RockYang b15ebda948 opt: merge RAG branch 2023-12-18 16:41:40 +08:00
RockYang 252eef2e5e opt: make sure the Upscale and Variation task is assign to the same mj service with Image task 2023-12-18 16:34:33 +08:00
RockYang 3bea8f9706 fix: fixed bug for mj service pool config pointer 2023-12-15 22:52:57 +08:00
RockYang dfe808bee7 feat: add img_calls field for recharge products 2023-12-15 16:56:56 +08:00
RockYang 58f3dd5336 chore: update default config.toml 2023-12-15 11:23:13 +08:00
RockYang b00f16648f opt: limit the image display size in reply component 2023-12-15 10:48:13 +08:00
RockYang fc66c31e89 docs: update changelog 2023-12-15 09:04:02 +08:00
RockYang a26c849532 docs: add sql file 2023-12-14 17:05:51 +08:00
RockYang d2991e60b6 refactor: refactor stable diffusion service, add service pool support 2023-12-14 16:48:54 +08:00
RockYang 10ba1430f9 chore: add sub dir support for OSS 2023-12-13 17:02:49 +08:00
RockYang 6d71f24f75 refactor: add midjourney pool implementation, add translate prompt for mj drawing 2023-12-13 16:38:27 +08:00
RockYang 8f4d20e411 fix: fixed bug for aliyun OSS img url 2023-12-13 09:49:55 +08:00
RockYang cf758d773e refactor mj service, add mj service pool support 2023-12-12 18:33:24 +08:00
RockYang c012f0c4c5 opt: remove default value for stable-diffusion page 2023-12-12 09:59:20 +08:00
RockYang 9e85900057 docs: update readme file 2023-12-12 09:52:28 +08:00
RockYang 68e3b787f9 docs: update build config.toml 2023-12-12 07:25:36 +08:00
RockYang 6706704ad6 docs: update changelog file 2023-12-11 17:17:17 +08:00
RockYang 1067fa015e add translate api for midjourney 2023-12-11 17:01:02 +08:00
RockYang 411335ebcb feat: add prompt translate handler 2023-12-11 06:56:00 +08:00
RockYang f19c15491e feat: add system config item for dall e3 generate image num 2023-12-10 17:13:25 +08:00
RockYang 0da81a4d10 chore: change default params for stable diffusion 2023-12-10 14:45:22 +08:00
RockYang 049839a5c7 feat: add HuPiPay payment support 2023-12-08 19:43:13 +08:00
RockYang a901927844 feat: add image generation API URL in chat configurations 2023-12-07 16:31:32 +08:00
RockYang 07ab0bc5a1 docs: add arm64 build script 2023-12-07 15:44:20 +08:00
RockYang a6025e6fab fix: fixed bug for prompt code format, prevent xss attacks 2023-12-07 14:02:13 +08:00
RockYang e841a61bf0 opt: save chat ID when the chat websocket disconnect 2023-12-07 11:07:08 +08:00
RockYang 2f1c2110c6 feat: adjust task list component styles 2023-12-06 19:05:51 +08:00
RockYang 8e573eea4c Merge branch 'pr_3' into dev 2023-12-06 18:54:45 +08:00
RockYang 149765764e feat: refactor midjourney image creating page 2023-12-06 18:54:30 +08:00
RockYang 45d6444c7e docs: update readme file 2023-12-06 14:44:06 +08:00
liyuwanglan aa65f49190 修改 2023-12-05 11:08:03 +08:00
RockYang 1bda41ff67 docs: update comments 2023-11-30 17:35:56 +08:00
RockYang 5a799139cd fix: fixed bug for upload image failed 2023-11-29 17:46:46 +08:00
RockYang eb36d0742a opt: create new chat session when change role or model, fix bug for mobile no validate 2023-11-29 17:36:27 +08:00
RockYang d8a9123852 docs: update change logs 2023-11-28 15:33:30 +08:00
RockYang 19031c2197 docs: 添加一键部署脚本 2023-11-28 15:25:51 +08:00
RockYang 7a2ffdf39c opt: 缩略图生成算法 2023-11-28 14:50:19 +08:00
RockYang ce8fa79206 feat: 为大图片生成缩略图,加快前端图片加载速度 2023-11-28 12:04:02 +08:00
RockYang b05c63549d fix: fix bug for oss image domain 2023-11-28 07:27:18 +08:00
RockYang 9f98660423 docs: update build script 2023-11-27 18:24:52 +08:00
RockYang 7b0fa862d2 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-27 12:03:23 +08:00
RockYang bfe2d4d573 feat: merge mysql and redis docker service to docker-compose.yaml file 2023-11-27 10:56:18 +08:00
RockYang 26950c5673 Merge pull request #55 from openjst/main
修改code显示颜色样式
2023-11-27 10:27:13 +08:00
RockYang 86788362a5 docs: make a full docker-compose.yaml 2023-11-27 07:21:37 +08:00
openjst deedc5fb2e 修改code显示颜色样式 2023-11-26 23:26:08 +08:00
RockYang f4fbe67db9 feat: implements image function replace Mj with DALL-E-3 2023-11-26 20:37:48 +08:00
RockYang 599ce0eade feat: add type field for api key 2023-11-24 18:05:59 +08:00
RockYang 11f3ab8dc7 feat: add support for registing use force use invite code 2023-11-24 12:02:28 +08:00
RockYang 75b4a6dd46 opt: optimize image preview for MidJourney image list page, only preview current image not for all images 2023-11-23 17:55:12 +08:00
RockYang 86bbea337d opt: 增加中间件自动对HTTP请求的参数去掉首尾空格 2023-11-23 17:50:55 +08:00
RockYang e63a30064b opt: optimize styles for invitation page 2023-11-23 17:40:15 +08:00
RockYang 222b1ddbd9 feat: add invitation and promotion functions 2023-11-23 16:30:15 +08:00
RockYang a88dd88a07 docs: add database sql file for v3.1.9 2023-11-23 09:58:01 +08:00
RockYang a47c01fd41 docs: update change log 2023-11-23 09:46:49 +08:00
RockYang 35e385f7a7 docs: update change log and readme file 2023-11-23 09:14:42 +08:00
RockYang 191e3b7d2c feat: 支持讯飞大模型 v3.0 2023-11-23 07:11:13 +08:00
RockYang 13fbbc190a feat: reset password function is ready 2023-11-22 18:00:45 +08:00
RockYang 61c6a6e7f3 feat: add copy code btn in chat page, fixed bug for code wrap in model of ChatGLM and XunFei 2023-11-22 17:00:43 +08:00
RockYang 911f218f0b feat: add feekback orcode in chat page 2023-11-22 12:05:58 +08:00
RockYang af41350eb9 fix: fixed bug for enable user vip in admin console not work 2023-11-22 11:30:58 +08:00
RockYang 8f63e348b3 option: replace leveldb with redis in storing message code 2023-11-22 10:57:24 +08:00
RockYang 8901cf8e81 Merge pull request #52 from KunMingStar/fix_migrate
fix: remove dead code
2023-11-22 08:23:01 +08:00
tongkunming e11736e1d2 fix: remove dead code 2023-11-21 22:18:53 +08:00
RockYang c062fba69c chore: remove dead code 2023-11-21 19:04:53 +08:00
RockYang 4628095fe7 feat: add wechat id card for closing register 2023-11-20 16:38:04 +08:00
RockYang 787ed9bc0f docs: update docs installation docs url 2023-11-17 18:42:13 +08:00
RockYang daaff8974e docs: update readme and config sample files 2023-11-16 17:20:38 +08:00
RockYang 4fcbd511c0 fix: fix bug with missing chat context 2023-11-15 18:20:34 +08:00
RockYang 64d6bff6d9 fix: unbind event for login page when the component is unmount 2023-11-14 18:19:40 +08:00
RockYang 998cbace92 feat: add link on logo 2023-11-14 13:50:44 +08:00
RockYang 2204038951 fix: fix member page flex styles 2023-11-14 11:21:16 +08:00
RockYang 998f67ac5d fix: deducating the user's img call quota after stable diffusion callback 2023-11-14 08:59:39 +08:00
RockYang be981a2c9b feat: 更改 MJ 和 SD 菜单图标 2023-11-13 11:27:42 +08:00
RockYang da3cf0d4a3 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-13 10:11:45 +08:00
RockYang 7124b9a36c feat: 更改 MJ 和 SD 菜单图标 2023-11-13 10:11:21 +08:00
RockYang 712a6b6c39 chore: change xxl-job name 2023-11-12 15:39:12 +08:00
RockYang ed0a1c57d6 opt: 优化 ItemList 组件样式,调整支付页面布局 2023-11-11 22:11:04 +08:00
RockYang 7858228505 docs: update change log file 2023-11-11 12:58:57 +08:00
RockYang 2525a22d78 fix: fix bug for issue 49, stable diffusion service not decrease img_calls 2023-11-11 11:00:26 +08:00
RockYang 7b4ef8fc31 opt: 优化 ItemList 组件样式 2023-11-11 10:53:33 +08:00
RockYang ce14cd02e4 feat: add order list compponent 2023-11-10 18:06:32 +08:00
RockYang 05f501af52 Merge branch 'alipay' 2023-11-10 16:51:00 +08:00
RockYang e94fa4844f docs: export database, update readme doc, remove useless configs for alipay 2023-11-10 16:49:07 +08:00
RockYang e0965aae5e feat: add switch to disable xxl-job service, update readme 2023-11-10 15:22:35 +08:00
RockYang 414c1de963 feat: 增加订单倒计时组件,自动清理过期未支付订单 2023-11-10 14:39:27 +08:00
RockYang b589102be8 feat: add system configration switch option for order pay service, support sandbox env for alipay 2023-11-09 18:28:56 +08:00
RockYang df367e0d47 feat: check if the user's chat quota is gt than current chat model required before starting a conversation 2023-11-09 16:56:44 +08:00
RockYang 680219ebcb Merge pull request #47 from KunMingStar/fix/sendMessage
fix: 添加messages为空校验
2023-11-09 08:21:24 +08:00
RockYang ef87487f60 fix: fix bug for token expired with QiNiu oss upload file 2023-11-08 21:14:09 +08:00
RockYang 8bafe72434 feat: adjust layout for user profile page 2023-11-08 18:33:26 +08:00
RockYang 76dcc69e44 feat: order payment function is ready 2023-11-08 17:48:07 +08:00
tongkunming 682ef22194 fix: 添加messages为空校验 2023-11-08 16:51:36 +08:00
RockYang ee8dd41605 feat: update stable diffusion api version 2023-11-08 14:54:10 +08:00
RockYang d0b8d666e4 feat: finish payment page layout 2023-11-07 18:10:28 +08:00
RockYang 4a81826d19 feat: adjust table styles for markdown 2023-11-07 12:02:16 +08:00
RockYang 848a10c3a8 feat: integrated Alipay payment module 2023-11-06 17:55:46 +08:00
RockYang a4baa29678 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-06 15:04:11 +08:00
RockYang a9b5280691 opt:update mj api version, wrapper chat api error message as a constant 2023-11-06 15:03:56 +08:00
RockYang 264706210b chore: remove invalid filepath for windows 2023-11-02 19:16:02 +08:00
RockYang 7677ae254f feat: different AI model consuming different amounts of use_calls 2023-10-26 14:38:06 +08:00
RockYang 7ccb4c5f06 docs: add database full backup 2023-10-26 14:00:06 +08:00
RockYang 01205af018 feat: 支持文心4.0模型,不同的用户可以订阅不同的AI模型 2023-10-26 13:41:49 +08:00
RockYang 5b5150e6d4 feat: add system configration item to close rewarding function 2023-10-19 11:19:29 +08:00
RockYang 9d8e3f5049 docs: add alipay reward qrcode 2023-10-17 17:58:03 +08:00
RockYang 99ede83bdc chore: set app list page as the index page 2023-10-16 14:20:44 +08:00
RockYang 61488e840d update changelog file 2023-10-16 13:40:44 +08:00
RockYang a80d01209c opt: adjust styles for ItemList component, cut string for chat role's hello message 2023-10-16 10:46:10 +08:00
RockYang d2a8d655c8 feat: finish adding chat role to user function 2023-10-16 06:56:42 +08:00
RockYang 80e2b34abc opt: adjust ItemList component styles 2023-10-15 15:47:42 +08:00
RockYang fb3d43d2d5 opt: optimize imgae loading for image wall page, loading thumb image replace source image 2023-10-13 23:07:41 +08:00
RockYang ebdec4fa44 feat: create chat app list page, build the layout of page 2023-10-13 18:05:40 +08:00
RockYang ead8dbbaa5 feat: auto login when register successfully 2023-10-13 16:27:40 +08:00
RockYang 95615a5501 docs: update images for readme 2023-10-13 15:55:30 +08:00
RockYang 75487b1f58 Merge branch 'main' into image-wall 2023-10-13 15:39:34 +08:00
RockYang c7daaa00cd feat: image wall stable diffusion image list component is ready 2023-10-13 15:16:40 +08:00
RockYang 606f24e981 feat: optimize the midjourney image list styles 2023-10-13 11:14:39 +08:00
RockYang c83d9287a7 docs: update readme, database files 2023-10-13 09:59:05 +08:00
RockYang fa8092b2cd docs: update readme file 2023-10-13 06:49:21 +08:00
RockYang b2b07a2be6 docs: update change log 2023-10-13 06:39:44 +08:00
RockYang a21628e350 style: fix style for update chat title input element 2023-10-13 06:29:25 +08:00
RockYang 9fcd686fda feat: image wall page is ready 2023-10-12 18:09:50 +08:00
RockYang 1759fd4cf9 opt: close websocket connection when finish a chat call for XunFei API 2023-10-12 10:02:12 +08:00
RockYang 12e7837602 feat: XunFei ai mode api implements is ready 2023-10-11 18:17:03 +08:00
RockYang ff349f2edf feat: adjust package struct, put chat code the seperate 'chatimpl' package, fix bug: baidu api chat context number must be even number 2023-10-11 15:46:40 +08:00
RockYang 088d8ca207 feat: add system configration for enable/disable funciton in chat session 2023-10-11 14:35:47 +08:00
RockYang c37902d3a4 finish baidu ai model api implementation 2023-10-11 14:21:16 +08:00
RockYang da38684cdd add baidu ai model api configrations 2023-10-10 18:19:56 +08:00
RockYang cb2cb72333 docs: update readme 2023-10-09 12:13:21 +08:00
RockYang d53c8f9830 docs: add 3.1.4 release version change logs 2023-10-09 12:05:51 +08:00
RockYang c8136d4231 doc: update database sql file 2023-10-08 20:44:55 +08:00
RockYang e4b8380530 docs: udpate readme and config.toml template 2023-10-08 18:09:46 +08:00
RockYang fbd599194c feat: add system config item for reward image, add app config item to use custom text2img param json file 2023-10-08 17:48:50 +08:00
RockYang 0270ec26fb feat: add configuration handler for AliYun SMS signature and template ID 2023-10-08 12:01:09 +08:00
RockYang 79e342be57 optimze the style for stable diffusion image dialog 2023-10-08 11:39:55 +08:00
RockYang c89f23b018 feat: stable diffusion page function is ready 2023-10-06 22:25:37 +08:00
RockYang f971ec5d34 fix: fixed bug for generating the upload file path 2023-10-05 18:09:42 +08:00
RockYang c5776ce41f feat: stable diffusion page is ready 2023-09-28 18:09:45 +08:00
RockYang 75c5ebbffa feat: migrate the chatgpt-plus-ext project code to this project 2023-09-27 18:14:07 +08:00
RockYang d51a724ade feat: add implements for stable diffusion service 2023-09-26 18:16:51 +08:00
RockYang c1143d7a6d fix: fixed bug for register error with parse args 2023-09-26 09:30:18 +08:00
RockYang f3697431a4 fix: fixed bug for add user in admin console page that calls and img_calls parameter not work 2023-09-22 18:15:32 +08:00
RockYang e3d6e5f420 feat: the upload handler for AliYun OSS is ready 2023-09-22 09:56:45 +08:00
RockYang af6f7a7146 docs: adjust ui styles for mj image page 2023-09-20 14:33:15 +08:00
RockYang c18d95e89a docs: update docker-compose.yaml, change image version to v3.1.3 2023-09-20 11:42:23 +08:00
RockYang fa42d03a65 some page ui optimization, add release v3.1.3 2023-09-20 11:39:11 +08:00
RockYang 22f7de2c09 fix socket connect for mj task notify 2023-09-20 06:59:30 +08:00
RockYang b4b9df81cb opt: add sessionId for mj task 2023-09-19 18:15:08 +08:00
RockYang 2a71c2b0e7 feat: mj advance drawing page function is ready, use better task scheduling argorithm 2023-09-17 18:03:45 +08:00
RockYang c37b3d3df5 feat: add task type params for add new mj task 2023-09-16 14:01:53 +08:00
RockYang ae135b89d6 feat: adjust UI for task list page 2023-09-15 18:16:59 +08:00
RockYang 4112426848 feat: midjourney page task and image list component is ready 2023-09-15 17:40:39 +08:00
RockYang 638b399b31 feat: midjourney page parameter ui is ready 2023-09-14 18:28:24 +08:00
RockYang 8a5713ef7c feat: finish mj model layout 2023-09-14 06:55:03 +08:00
RockYang 382d7bfaa1 feat: add ImageMj page view 2023-09-13 18:04:31 +08:00
RockYang 3daf2f7738 feat: optimize mj notidy api, use job queue to send ai drawing request 2023-09-13 15:50:00 +08:00
RockYang 944d35971d feat: chat chrawing function is refactored 2023-09-13 06:57:25 +08:00
RockYang 9ce8af0b82 feat: refactor MidJourney service for conpatible drawing in chat and draw in app 2023-09-12 18:01:24 +08:00
RockYang d825b9ec26 fix: fixed bug for jwt token expire caculation 2023-09-12 10:49:55 +08:00
RockYang 9afe4aea4b feat: new WebUI for the main page, add MJ and SD drawing function pages 2023-09-11 16:22:11 +08:00
RockYang 3cc8c3284a 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 e2c18c4e1e chore: update gpt avatar 2023-09-10 12:02:37 +08:00
RockYang 81fe768f6a opt: refactor the web page's router and layout 2023-09-08 22:14:58 +08:00
RockYang 8923779ab0 opt: return error when download mj image failed 2023-09-08 21:02:21 +08:00
RockYang 583218a045 opt: add lock for mj task callback 2023-09-08 18:12:18 +08:00
RockYang 153b2bfa53 opt: add lock for mj task callback 2023-09-08 17:23:32 +08:00
RockYang 0a9b325360 opt: Only create actived upload service 2023-09-08 06:46:24 +08:00
RockYang af9e6f2c46 update desktop package deps 2023-09-07 18:05:13 +08:00
RockYang 0e31726fd3 fix: remove username and nickname field for updating user info 2023-09-07 15:41:01 +08:00
RockYang 590d9c2e61 Update README.md 2023-09-07 15:23:38 +08:00
RockYang c12fe9f25e adjust home page styles 2023-09-07 15:12:43 +08:00
RockYang 9bd83f3acf use absolute path for all routes 2023-09-07 10:27:18 +08:00
RockYang aada15fc41 feat: add linux package configs for desktop 2023-09-07 09:45:11 +08:00
RockYang 6a2d383be6 feat: extract css to a single file, add chat_id path variable support 2023-09-07 09:29:16 +08:00
RockYang 545c8fa482 feat: use eletron to build desktop version for chatgpt-plus 2023-09-06 18:19:27 +08:00
RockYang e3d18643b8 opt: clear space in the reward transfer number 2023-09-06 17:09:55 +08:00
RockYang 2f318bbe9f feat: add QiNiu OSS storage implements 2023-09-06 14:37:13 +08:00
RockYang fac7eb61b3 remove username search for console user list page 2023-09-05 17:52:46 +08:00
RockYang 4450d03a54 update sql file, remove truncate table sql 2023-09-05 17:05:33 +08:00
RockYang e4e7868b30 fix: 采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug 2023-09-05 16:47:40 +08:00
RockYang 3d7fa680cb docs: update change log and create a new release version 2023-09-05 14:28:30 +08:00
RockYang c38035e25e fix: replace session handler with jwt authorization 2023-09-05 11:47:03 +08:00
RockYang 8d4fdaf902 refactor: refactor mobile pages for the chat model updating 2023-09-04 18:15:56 +08:00
RockYang 2820adad53 feat: allow user to set custom api keys for different platforms 2023-09-04 17:34:29 +08:00
RockYang f7a427d2c0 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 59ed8c9660 refactor: refactor chat_handler, add support for Azure and ChatGLM 2023-09-04 06:43:15 +08:00
RockYang 71562ab0e5 docs: add log dir to docker 2023-08-24 08:55:11 +08:00
RockYang 278ddf037f docs: update docker-compose file 2023-08-23 06:56:04 +08:00
RockYang ac3151af92 chore: update bin file name 2023-08-22 22:18:07 +08:00
RockYang 1189ad7862 feat: change order by for login log and reward records 2023-08-22 22:04:46 +08:00
RockYang 88c13e7b9a opt: optimize element ui dialog styles, make it to the center of screen 2023-08-22 08:57:14 +08:00
RockYang 596baac62e opt: upgrade element-plus to 2.3.0, optimize the styles for for element dialog 2023-08-22 07:14:13 +08:00
RockYang 5a8503ec14 chore: remove debug codes 2023-08-21 10:55:49 +08:00
RockYang e44f95724d doc: update readme docs 2023-08-21 06:57:04 +08:00
RockYang dec0d0dea7 feat: delete the old avatar image file when update the user profile 2023-08-21 06:27:30 +08:00
RockYang 46f96e94ec 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 89b30bcf58 opt: optimize code for remove chat item 2023-08-20 19:06:18 +08:00
RockYang 473d8f5f85 feat: add minio service implementation, download midjourney image to local storage 2023-08-20 16:17:42 +08:00
RockYang 08d8d65599 opt:Optimize the algorithm to check whether Midjourney images can continue to variation 2023-08-17 14:20:16 +08:00
RockYang 2964c1d82e docs: update demo url 2023-08-17 07:18:44 +08:00
RockYang 75ab8552d9 chore: show img_calls num for user profile page 2023-08-17 07:09:53 +08:00
RockYang b78d5b3d03 docs: update docs 2023-08-17 06:55:11 +08:00
RockYang 064081c771 feat: add authorization for MidJourney function calls 2023-08-16 23:16:44 +08:00
RockYang 3525d35d15 chore: update database sql file 2023-08-15 22:53:32 +08:00
RockYang 9d9ee7c585 opt: unset timeout for websocket connection 2023-08-15 18:29:53 +08:00
RockYang e65c32c4c7 feat: midjourney image variation function is ready 2023-08-15 17:31:02 +08:00
RockYang 113769791f feat: midjourney image upscale function is ready 2023-08-15 15:28:40 +08:00
RockYang 5d2a1d21d5 feat: midjourney drawing image function is ready 2023-08-14 17:59:21 +08:00
RockYang 6e40f92aaf feat: add midjourney message receive handler 2023-08-14 07:09:52 +08:00
RockYang 302cb8a5be feat: add midjouney api implements, optimize function calls 2023-08-11 18:46:56 +08:00
RockYang 0b27890484 feat: remove wechat bot, replace with api callback for receive wechat payment transactions 2023-08-10 17:24:23 +08:00
RockYang 5db247e632 feat: add samples and function introduce in welcome page 2023-08-08 14:19:09 +08:00
RockYang c1155a4338 docs: add dashboard image 2023-08-07 20:07:05 +08:00
RockYang da806b9492 feat: enabled user to set default GPT model for chat 2023-08-04 14:25:54 +08:00
RockYang 3d6d46b30d feat: chat export function is ready 2023-08-04 12:08:07 +08:00
RockYang 57c69738ba feat: add chat export button 2023-08-03 18:24:30 +08:00
RockYang 0c8157dbc0 feat: add rewards statistic in dashboard page 2023-08-03 10:19:37 +08:00
RockYang d7b278f2f7 opt: add enabled_msg_service config var to system config database 2023-08-03 10:04:45 +08:00
RockYang 18d74f1057 opt: optimize build script, auto push to aliyun docker hub 2023-08-02 18:09:19 +08:00
RockYang e04856f794 opt: optimize the layout for regiseter page. add function to disable registration 2023-08-02 17:44:13 +08:00
RockYang d7bbfb0fc3 feat: the dashboard page is ready for admin console 2023-08-02 16:37:47 +08:00
RockYang a0857817de refactor: refactor admin console page layout 2023-08-02 15:00:18 +08:00
RockYang 8d39234fd0 fix: fixed bug for chat request's token reach the max token limit for OpenAI 2023-08-01 17:58:03 +08:00
RockYang 0fce1c0fd8 feat: new function for add user in manger console user list page 2023-08-01 16:02:49 +08:00
RockYang c23c285f82 doc: add video tutorial for deploying system 2023-08-01 10:15:43 +08:00
RockYang 81b560d791 docs: remove docs for deploy manually 2023-07-31 20:04:22 +08:00
RockYang 55459a4f42 opt: optimize the build script and docker-compose yaml file 2023-07-31 17:31:02 +08:00
RockYang 9eafd3e6ca feat: add env var to set log level 2023-07-31 08:34:11 +08:00
RockYang e7ac26ff5a feat: update system config cache for AppServer when updating system config in admin console 2023-07-31 08:13:20 +08:00
RockYang fad78641d6 add system config cache for AppServer object 2023-07-31 06:56:28 +08:00
RockYang a32cb0142b feat: allow user to login with username and mobile no 2023-07-27 15:37:50 +08:00
RockYang 4a3c133152 fix: deauthorize some apis 2023-07-27 10:53:14 +08:00
RockYang ae79c34508 fix: fix bug for mobile verify code function not available in register page 2023-07-27 10:29:59 +08:00
RockYang 7eb69c1ad7 style: adjust reward image styles 2023-07-26 17:57:19 +08:00
RockYang 8aaff0af89 chore: optimize build script, compress reward img 2023-07-26 17:48:16 +08:00
RockYang 4b77280c65 feat: add reset password function to user list page 2023-07-26 15:22:11 +08:00
RockYang a0df84e4f2 docs: add change log file 2023-07-25 17:37:20 +08:00
RockYang 8876e6a098 docs: 更新文档和数据库文件 2023-07-25 17:25:23 +08:00
RockYang 9bee2a90f9 feat: 完成人机交互验证 API 接入,增加短信防刷验证 2023-07-25 17:00:24 +08:00
RockYang cd6a811bbd feat: 头条,微博热搜等函数 API 实现 2023-07-25 15:02:43 +08:00
RockYang 84f9a83f55 opt: 取消强制手机号验证,更新配置 2023-07-24 18:18:09 +08:00
RockYang 2efc669ab2 feat: 完成众筹后台管理功能 2023-07-24 15:59:29 +08:00
RockYang 5444ed77ad chore: add error log for reading chat response buffer 2023-07-24 12:05:38 +08:00
RockYang dd71fe80a5 opt: record user_id for reward verify 2023-07-22 08:42:30 +08:00
RockYang b73092eb64 feat: reward verify is ready 2023-07-21 22:29:14 +08:00
RockYang aa28f87b0c feat: 增加打赏核销功能 2023-07-21 18:26:51 +08:00
RockYang 1e1bcd4a30 feat: 集成微信收款服务 2023-07-20 17:46:32 +08:00
RockYang d24b3c46bf put logs in log file 2023-07-20 16:36:12 +08:00
RockYang 37222f07d9 fix: 修复阿里云短信发送失败的 bug 2023-07-16 16:05:11 +08:00
RockYang 49646a79e5 fix: 用户使用自己绑定的 API 调用,则不计算 calls 和 token 消耗 2023-07-16 11:55:45 +08:00
RockYang 9940e1210e Merge branch 'main' into prod 2023-07-16 09:57:27 +08:00
RockYang 10892812c3 chore: 更新项目打包脚本 2023-07-16 09:55:00 +08:00
RockYang d8ff5987dd feat: plugin function is ready 2023-07-15 21:52:30 +08:00
RockYang d014d418e9 feat: 支持上下文深度配置,计算每轮对话消耗的总 token 数量 2023-07-15 18:37:25 +08:00
RockYang cc1b56501d refactor: 更改 OpenAI 请求 Body 数据结构,兼容函数调用请求 2023-07-15 18:00:40 +08:00
RockYang a5ad9648bf chore: small fixs 2023-07-14 18:00:01 +08:00
RockYang 3630099234 feat: 完成每日早报函数开发 2023-07-10 18:59:53 +08:00
RockYang 941a24c75b fix: fixed conflicts 2023-07-10 10:11:17 +08:00
RockYang 50bdc01484 docs: update readme file 2023-07-10 09:44:24 +08:00
RockYang d9e340ddc4 refactor: 调整项目目录结构,移除其他语言 API 目录 2023-07-10 09:42:11 +08:00
RockYang beee1e91d6 test: add plugin test code 2023-07-10 07:05:56 +08:00
RockYang 35935d2bac fixed conflicts 2023-07-06 10:49:38 +08:00
RockYang 5d0b0ad33e opt: add tip message when no available key 2023-07-06 10:47:36 +08:00
RockYang d1a6ac531f opt: add tip message when no available key 2023-07-06 10:34:01 +08:00
RockYang ae255a3bd9 fix: 取消 ElMessage 的 appendTo 属性,防止被 Dialog 组件覆盖 2023-07-04 17:59:55 +08:00
RockYang fa733afa51 opt: 优化验证码发送逻辑,加入防刷验证 2023-07-04 17:15:02 +08:00
RockYang b60c723457 Merge branch 'main' into feat-sms 2023-07-04 10:55:20 +08:00
RockYang 2bb7ff3c13 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-07-04 10:54:35 +08:00
RockYang 68b4cf00e2 docs: update README, 增加免责申明 2023-07-04 10:52:52 +08:00
RockYang 440169ff60 feat: 短信验证码功能已完成,手机端同步实现。 2023-07-03 15:18:15 +08:00
RockYang 1fc361242e opt: 将短信发送按钮封装成组件 2023-07-03 06:55:15 +08:00
RockYang 72cc6f3d75 feat: 注册短信验证码验证功能已经开启 2023-07-02 20:51:13 +08:00
RockYang a2b1924e00 Merge branch 'main' into prod 2023-07-02 00:04:59 +08:00
RockYang 7e926167f0 fix: 修复 nodejs apple M1 跨平台打包,运行报错 exec format error 2023-07-02 00:04:12 +08:00
RockYang 32484bd32e feat: 增加用户 token 消耗统计功能 2023-07-01 23:29:24 +08:00
RockYang 04ade1008b Merge branch 'main' into prod 2023-07-01 10:24:08 +08:00
RockYang 4603de0e70 Merge pull request #18 from ly307787186/main
Update Setting.vue
2023-07-01 10:22:01 +08:00
RockYang 12c1ff65e9 opt: remove chat role info from user login api's response 2023-06-30 18:52:43 +08:00
ly307787186 7b8cbf7f86 Update Setting.vue
关掉aip-key 必需的验证
2023-06-30 13:20:30 +08:00
RockYang c198e83f70 fix: fixed bug for Emoji can not insert to MySQL 2023-06-30 09:15:57 +08:00
RockYang e60863a290 chore: change ubuntu docker image with aliyun 2023-06-29 15:56:11 +08:00
RockYang 946563d3b2 chore: add test code for fix role icon url of db 2023-06-29 08:58:19 +08:00
RockYang 3f2ef1d54e fix: 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题 2023-06-28 20:01:44 +08:00
RockYang 0d2b60b905 fix: 修复 PC 端聊天界面滚动条问题 2023-06-28 18:16:28 +08:00
RockYang 161b11e428 opt: 优化启动参数接收处理 2023-06-28 05:51:55 +08:00
RockYang f7748d51df opt: 通过环境变量来传参,修正 docker compose 配置参数 2023-06-27 18:29:46 +08:00
RockYang 5d13b4b705 docs: 更新文档,新增移动端预览图 2023-06-27 14:28:00 +08:00
RockYang 92e52a2284 fix: 修复 markdown 换行符不解析的 Bug,修复新发布的模型 token 统计失败错误 2023-06-27 14:18:20 +08:00
RockYang 3efd5fb77a feat: vue-mobile => 完成用户信息修改功能,前后端都添加文件上传功能。 2023-06-27 12:11:55 +08:00
RockYang c3d62bb8d8 feat: vue-mobile => 完成移动端聊天配置功能 2023-06-26 18:18:45 +08:00
RockYang c099e843d5 feat: vue-mobile => 完成会话聊天页面功能,增加主题切换功能 2023-06-26 16:39:00 +08:00
RockYang 5f3a5871f1 feat: vue-mobile => 优化聊天记录拍版样式 2023-06-25 18:21:38 +08:00
RockYang f01fdd0070 feat: vue-mobile => 完善移动端聊天列表页功能 2023-06-25 17:01:04 +08:00
RockYang d20cc367b8 fixed: go-api => 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug 2023-06-25 11:34:55 +08:00
RockYang eadb9a733f docs: 增加容器部署文档 2023-06-25 11:06:18 +08:00
RockYang 32ac454b5b opt: 优化前端登录判断逻辑 2023-06-25 09:46:23 +08:00
RockYang 2cadd6af44 feat: chat list page for mobile is ready 2023-06-25 06:53:22 +08:00
RockYang 3c8b5cb313 feat: 完成移动端前段框架搭建 2023-06-24 11:45:26 +08:00
RockYang fcf2387794 style: 调整聊天侧边栏样式 2023-06-23 18:31:50 +08:00
RockYang f9783b4806 opt: 优化 docker-compse 构建脚本,修复后端路由 Bug 2023-06-23 18:04:16 +08:00
RockYang cbdc532f83 chore: 添加 docker 镜像构建脚本 2023-06-23 07:08:16 +08:00
RockYang 124554bae5 opt: 抽离 session 验证函数,修正前端路由覆盖 bug 2023-06-23 06:31:25 +08:00
RockYang 2725536d7d chore: 使用阿里云镜像仓库 2023-06-22 22:23:48 +08:00
RockYang beeef7a4f7 docs: 增加 docker-compose 部署支持 2023-06-22 22:14:18 +08:00
RockYang 7f2ebb6aeb docs: 完善新版本文档 2023-06-22 15:47:51 +08:00
RockYang c8e30383ba refactor: embed xdb file for ip2region 2023-06-22 11:08:44 +08:00
RockYang a7d5a6ccb9 style: 调整后台管理框架样式 2023-06-21 18:44:18 +08:00
RockYang be3380aaf3 refactor: 更新 iconfont 图标,增加打赏二维码弹窗 2023-06-21 15:34:38 +08:00
RockYang 40a4ab5410 refactor: refactor the frame layout of admin module 2023-06-21 14:22:28 +08:00
RockYang 54a2181960 refactor: user login log list for admin is ready 2023-06-21 06:53:41 +08:00
RockYang e490e15bc4 feat: API Key manage is ready 2023-06-20 18:05:33 +08:00
RockYang 469844d97b opt: optimize role sorting 2023-06-20 16:07:26 +08:00
RockYang 892ea29ba8 refactor: chat role manage for admin is ready 2023-06-20 11:46:13 +08:00
RockYang acde1c6742 refactor: user remove is ready for console 2023-06-20 07:08:37 +08:00
RockYang 07da11d852 refactor: 管理后台用户编辑功能 is ready 2023-06-19 21:53:07 +08:00
RockYang 502b8c2270 refactor: 管理后台用户列表页面重构 2023-06-19 18:23:09 +08:00
RockYang af3f7ac810 refactor: 完成管理后台的系统设置页面重构 2023-06-19 15:58:52 +08:00
RockYang 935c6caf96 feat: admin login page is ready 2023-06-19 11:09:23 +08:00
RockYang 90bce1d437 refactor: refactor controller handler module and admin module 2023-06-19 07:06:59 +08:00
RockYang 831dd3e2e0 style: optimize code styles 2023-06-18 15:17:59 +08:00
RockYang b8e208eb32 style: optimize styles for change password page 2023-06-17 00:02:49 +08:00
RockYang 542c908e30 fix: fixed bu for 'slice bounds out of range' 2023-06-16 23:30:04 +08:00
RockYang 08529242bf style: add logo at login and register page 2023-06-16 18:14:43 +08:00
RockYang 1ae9449d92 chore: tracke the logo file 2023-06-16 17:44:38 +08:00
RockYang 7fd0b1fa08 feat: import iconfont styles, change password function is ready 2023-06-16 17:28:21 +08:00
RockYang 111572e3f2 fix: add lock map data structure, fixed bug for 'concurrent map writes' 2023-06-16 15:32:11 +08:00
RockYang c9875d24b4 feat: add footbar for login and register page, add send button for chat page 2023-06-16 13:57:05 +08:00
RockYang 94a75603c3 chore: compress bg images 2023-06-16 11:14:45 +08:00
RockYang d55925bccd chore: delete bin file 2023-06-16 10:01:34 +08:00
RockYang 0b5adcbfab fix: fixed bug for ssl websocket url 2023-06-16 10:00:05 +08:00
RockYang 8d84562f32 chore: update config sample file 2023-06-15 16:03:42 +08:00
RockYang 136d159833 opt: add email and mobile validation for register 2023-06-15 15:09:00 +08:00
RockYang e3026062d8 opt: automatic get the host for api and websocket from the 'location' var 2023-06-15 14:39:05 +08:00
RockYang 8a69c69b7f opt: update the main chat compnent after updating the user profile 2023-06-15 11:29:16 +08:00
RockYang 197714a57a 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 567cdc6f8d refactor: V3 版本重构已基本完成 2023-06-15 09:41:30 +08:00
RockYang be6e8c7713 Merge pull request #6 from EyreFree/master
feat: docker supported
2023-06-05 08:32:34 +08:00
EyreFree f667b35403 docker 2023-06-03 20:48:32 +08:00
RockYang 942e482ca4 chore: 打招呼的时候输出本项目地址 2023-05-09 16:11:50 +08:00
RockYang 10b381965e Update 1.bug.yml 2023-05-09 15:43:48 +08:00
RockYang 79c3a99a38 Update 1.bug.yml 2023-05-09 15:39:47 +08:00
RockYang 463d3c8e97 Update 1.bug.yml 2023-05-09 15:35:07 +08:00
RockYang da007729c6 Update 1.bug.yml 2023-05-09 15:32:57 +08:00
RockYang b7b42b5fd4 chore: 重命名 issue 模版 2023-05-09 15:29:57 +08:00
RockYang 735f9bd053 chore: 添加 issue 和 PR 模板 2023-05-09 15:26:38 +08:00
RockYang 6096b65374 chore: 替换文字描述 用户 => 2023-05-09 14:30:09 +08:00
RockYang 727d342eb3 docs: 优化文档 2023-05-06 18:33:27 +08:00
RockYang 58815ba8bc docs: 优化文档 2023-05-06 18:13:44 +08:00
RockYang 49d0502775 docs: 优化文档排版 2023-05-06 16:36:04 +08:00
RockYang 0b6c6e6d2c docs: 完善文档 2023-05-06 16:27:44 +08:00
RockYang 45ede6047a opt: 优化配置文档加载 2023-05-06 13:58:56 +08:00
RockYang 8f469d4ebb Merge branch 'master' of gitee.com:blackfox/wechat-gpt 2023-05-06 12:07:17 +08:00
RockYang f3264d056d add LICENSE.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2023-05-06 04:03:33 +00:00
636 changed files with 77271 additions and 60992 deletions
+37
View File
@@ -0,0 +1,37 @@
---
name: frontend-developer
description: Use this agent when you need assistance with frontend development tasks including Vue.js components, UI implementation, styling, responsive design, state management, or frontend architecture decisions. Examples: <example>Context: User is working on a Vue.js component and needs help with implementing a responsive layout. user: 'I need to create a mobile-friendly chat interface component' assistant: 'I'll use the frontend-developer agent to help design and implement this responsive chat component' <commentary>Since this involves frontend development work with Vue.js and responsive design, use the frontend-developer agent.</commentary></example> <example>Context: User encounters styling issues with Element Plus components. user: 'The Element Plus dialog is not displaying correctly on mobile devices' assistant: 'Let me use the frontend-developer agent to troubleshoot this mobile styling issue' <commentary>This is a frontend styling problem that requires expertise in Element Plus and responsive design.</commentary></example>
color: purple
---
You are a Senior Frontend Development Engineer with deep expertise in modern web development technologies, particularly Vue.js 3, Element Plus, Vant, and responsive design patterns. You specialize in creating high-quality, maintainable frontend applications with excellent user experience.
Your core responsibilities include:
- Developing Vue.js 3 components using Composition API and best practices
- Implementing responsive designs that work seamlessly across desktop and mobile devices
- Working with Element Plus for desktop UI and Vant for mobile components
- Managing application state using Pinia store patterns
- Styling with Stylus preprocessor and Tailwind CSS utilities
- Optimizing build processes with Vite and ensuring proper code organization
- Implementing theme switching (dark/light mode) and accessibility features
- Follow decoupled development, with HTML, CSS, and JS codes placed in separate files for easier maintenance
When working on frontend tasks, you will:
1. Analyze requirements and suggest the most appropriate Vue.js patterns and component structures
2. Ensure responsive design principles are followed, considering both desktop and mobile viewports
3. Choose appropriate UI components from Element Plus (desktop) or Vant (mobile) libraries
4. Write clean, maintainable code following Vue.js 3 Composition API best practices
5. Consider performance implications and suggest optimizations when relevant
6. Ensure proper state management using Pinia when component state needs to be shared
7. Follow the project's established patterns for routing, API integration, and component organization
8. Provide specific code examples and explain the reasoning behind architectural decisions
You have deep knowledge of:
- Vue.js 3 ecosystem (Vue Router, Pinia, Composition API)
- Modern CSS techniques and preprocessors (Stylus, Tailwind)
- Component library integration (Element Plus, Vant)
- Build tools and development workflow (Vite, npm scripts)
- Cross-browser compatibility and mobile-first design principles
- Performance optimization and code splitting strategies
Always consider the user experience, code maintainability, and alignment with modern frontend development standards. When suggesting solutions, provide clear explanations and consider both immediate needs and long-term scalability.
+6
View File
@@ -0,0 +1,6 @@
重构当前页面代码
1. 把当前页面 JS 代码全部抽离,然后是采用 Pinia 重构
2. 把当前页面 CSS 代码全部抽离,如果是 stylus 语法代码,则需要改成 SCSS 语法代码
3. 尽量做到代码的复用性,不要重复造轮子
4. 移动端的 css 和 js 分别放到对应的 mobile 目录下,不要覆盖 PC 端的代码
+5
View File
@@ -0,0 +1,5 @@
web/src/assets/fonts filter=lfs diff=lfs merge=lfs -text
web/src/assets/iconfont filter=lfs diff=lfs merge=lfs -text
web/src/assets/img filter=lfs diff=lfs merge=lfs -text
web/public filter=lfs diff=lfs merge=lfs -text
api/res filter=lfs diff=lfs merge=lfs -text
+1 -1
View File
@@ -1,5 +1,5 @@
name: Bug 报告 🐛
description: geekai 提交错误报告
description: chatgpt-plus 提交错误报告
labels: ['Bug']
body:
- type: checkboxes
+1 -1
View File
@@ -1,5 +1,5 @@
name: 功能优化 🚀
description: geekai 提交优化建议
description: chatgpt-plus 提交优化建议
labels: ['feature']
body:
- type: checkboxes
+1
View File
@@ -14,3 +14,4 @@ logs
*.njsproj
*.sln
*.sw?
miniprogram
+410 -237
View File
@@ -1,313 +1,486 @@
# 更新日志
## v4.2.8
- Bug 修复:修复管理后台邮件配置报“参数错误”问题
- 功能优化:手机端也不显示后端禁用的菜单功能页面
- 功能优化:**聊天页面支持 Sora2 文生视频/图生视频功能** 🔥🔥🔥
- 功能优化:**重构路由代码,优化路由加载逻辑,支持关闭手机站功能** 🔥🔥🔥
- Bug 修复:修复用户算力扣减日志记录逻辑错误问题
- 功能优化:优化 404 页面样式,兼容 PC 端和移动端
- Bug 修复:ipv6 地址登录报错:Data too long for column 'last_login_ip'
- Bug 修复:移动端登录页面输入密码的时候会覆盖在确认按钮上层
- 功能优化:**移动端的 DALL-E 绘图页面支持上传参考图** 🔥🔥🔥
- 功能优化:优化 WebFooter 组件,如果配置了备案号,则显示备案号,否则不显示
- 功能优化:启动时自动同步 model 与数据表字段,缺列自动新建、多余列自动删除
## v4.2.7
- Bug 修复:修复超级管理员无法修改密码的 Bug
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥
- 功能新增:新增 AI 对话编辑功能,并优化了重新生成逻辑
## v4.2.6
- 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉
- 功能优化:重构 API 授权代码,采用中间件鉴权方式,实现更加精准的 API 鉴权 🎉🎉🎉
- 功能优化:优化 PC 端的 Suno 音乐,视频生成,以及即梦 AI 页面 UI
- 功能优化:重构登录和注册页面,兼容移动端和 PC 端,并且所有的登录组件共用了同一套组件代码,大大降低维护成本 🎉🎉🎉
- 功能优化:管理后台增加模型批量删除功能
- 功能优化:优化 Table 组件 UI,并支持 dark 主题
- 功能优化:移动端对话页面支持上传文件和图片
- 功能新增:新增微信扫码登录支持
- 功能新增:新增安全监控,内容审核功能,支持敏感内容过滤拦截
- 功能新增:DALL-E 绘图支持参 Google Banana 图片编辑功能
## v4.2.5
- 功能优化:在代码右下角增加复制代码功能按钮,增加收起和展开代码功能
- Bug 修复:修复 Shift + Enter 不换行的 Bug
- Bug 修复:修复管理后台菜单添加页面的文本错误
- Bug 修复:解决聊天页面异常退出不断重连的 bug
- 功能优化:把 Luma 和可灵视频生成页面整合成一个视频创作中心页面,统一管理视频任务
- 功能新增:增加即梦 AI 专题页面,支持即梦官方原生 API 的图片和视频生成 🎉🎉🎉
## v4.2.4
- 功能优化:更改前端构建技术选型,使用 Vite 构建,提升构建速度和兼容性
- 功能优化:使用 SSE 发送消息,替换原来的 Websocket 消息方案
- 功能新增:管理后台支持设置默认昵称
- 功能优化:支持 Suno v4.5 模型支持
- 功能新增:用户注册和用户登录增加用户协议和隐私政策功能,需要用户同意协议才可注册和登录。
- 功能优化:修改重新回答功能,撤回千面的问答内容为可编辑内容,撤回的内容不会增加额外的上下文
- 功能优化:优化聊天记录的存储结构,增加模型名称字段,支持存储更长的模型名称
- Bug 修复:聊天应用绑定模型后无效,还是会轮询 API KEY,导致一会成功,一会请求失败。
- 功能优化:如果管理后台没有启用会员充值菜单,移动端也不显示充值套餐功能
## v4.2.3
- 功能优化:增加模型分组与模型描述,采用卡片展示模式改进模型选择功能体验
- 功能优化:化思维导图下载图片的清晰度以及解决拖动、缩放操作后下载图片内容不全问题
- Bug 修复:修复 MJ 画图页面已画出的图,点复制指令无效问题
- 功能优化:MJ 画图的分辨率支持自定义,优先使用 prompt 中--ar 参数
- Bug 修复:修复 MJ 绘画 U1-V1,拼写错误
- 功能优化:支持自动迁移数据表结构,无需在手动执行 SQL 了
- 功能优化:移除首页的文字动画效果
- 功能优化:在聊天页面增加对话列表展开和隐藏功能
- 功能优化:聊天页面增加 AI 思考中动画效果
## v4.2.2
- 功能优化:开启图形验证码功能的时候现检查是否配置了 API 服务,防止开启之后没法登录的 Bug。
- 功能优化:支持原生的 DeepSeek 推理模型 API,聊天 API KEY 支持设置完整的 API 路径,比如 https://api.geekai.pro/v1/chat/completions
- 功能优化:支持 GPT-4o 图片编辑功能。
- 功能新增:对话页面支持 AI 输出语音播报(TTS)。
- 功能优化:替换瀑布流组件,优化用户体验。
- 功能优化:生成思维导图时候自动缓存上一次的结果。
- 功能优化:优化 MJ 绘图页面,增加 MJ-V7 模型支持。
- 功能优化:后台管理增加生成一键登录链接地址功能
## v4.2.1
- 功能新增:新增支持可灵生成视频,支持文生视频,图生生视频。
- Bug 修复:修复手机端登录页面 Logo 无法修改的问题。
- 功能新增:重构所有异步任务(绘图,音乐,视频)更新方式,使用 http pull 来替代 websocket。
- 功能优化:优化 Luma 图生视频功能,支持本地上传图片和远程图片。
- Bug 修复:修复移动端聊天页面新建对话时候角色没有更模型绑定的 Bug。
- 功能优化:优化聊天页面代码块样式,优化公式的解析。
- 功能优化:在绘图,视频相关 API 增加提示词长度的检查,防止提示词超出导致写入数据库失败。
- Bug 修复:优化 Redis 连接池配置,增加连接池超时时间,单核服务器报错 `redis: connection pool timeout`
- 功能优化:优化邮件验证码发送逻辑,更新邮件发送成功提示。
## v4.2.0
- 功能优化:优化聊天页面 Notice 组件样式,采用 Vuepress 文档样式
- Bug 修复:修复主题切换的组件显示异常问题
- 功能优化:支持 DeepSeek-R1 推理模型,优化推理样式输出
- 功能优化:优化 Suno 歌曲播放按钮样式,居中显示
- 功能优化:后台管理新增模型的时候,可以绑定所有的 API KEY,而不只是能绑定 Chat 类型的 API KEY
- 功能新增:新增每日签到功能,每日签到可以获得算力奖励
- 功能优化:兼容 OpenAI o3 系列模型
- 功能优化:API 默认开启允许跨域调用
- 功能优化:优化 docker-compose.yaml 配置,增加各容器依赖关系
## v4.1.9
- 功能优化:优化系统配置,移除已废弃的配置项
- 功能优化:GPT-O1 模型支持流式输出
- 功能优化:优化代码引用快样式,支持主题切换
- 功能优化:登录,注册页面允许替换用户自己的 Logo 和 Title
- Bug 修复:修复 OpenAI 实时语音通话没有检测用户算力不足的 Bug
- 功能新增:管理后台增加算力日志查询功能,支持按用户,按模型,按日期,按类型查询算力日志
- 功能优化:支持为模型绑定 Dalle 和 chat 类型的 API KEY
- 功能新增:支持管理后台设置 ICP 备案号
## v4.1.8
- 功能优化:**UI 全新改版,支持主题切换**。 :rocket: :rocket: :rocket:
- 功能新增:Gitee AI API 接口接入,目前支持 Gitee 的 SD 绘图接口,支持 Gitee 的 AI 对话接口。:rocket: :rocket: :rocket:
- Bug 修复:修复音 Luma API 更新导致任务响应解析失败的错误
- 功能优化:支持 Suno v4.0 模型支持
- Bug 修复:修复 Suno 已完成任务删除失败的 错误
- 功能新增:支持 OpenAI 实时语音通话功能,目前已经支持按次收费,支持管理员设置每次实时语音通话的算力消耗
- 功能新增:生成提示词需要消耗算力,支持管理员设置每次生成提示词的算力消耗,防止被白嫖
- 功能新增:DALL-E-3 绘图支持 Flux 绘图模型,支持在管理后添加 Flux,SD 等绘图模型
- 功能优化:Markdown 支持解析 emoji 表情
- 功能优化:当管理后台禁用了某个绘图菜单的时候,移动端绘图菜单也会同步禁用(不显示该功能)
## v4.1.7
- Bug 修复:手机邮箱相关的注册问题 [#IB0HS5](https://gitee.com/blackfox/geekai/issues/IB0HS5)
- Bug 修复:音乐视频无法下载,思维导图下载后看不清文字[#IB0N2E](https://gitee.com/blackfox/geekai/issues/IB0N2E)
- 功能优化:保存所有 AIGC 任务的原始信息,程序启动之后自动将未执行的任务加入到 redis 队列
- 功能优化:失败的任务自动退回算力,而不需要在删除的时候再退回
- 功能新增:支持设置一个专门的模型来翻译提示词,提供 Mate 提示词生成功能
- Bug 修复:修复图片对话的时候,上下文不起作用的 Bug
- 功能新增:管理后台新增批量导出兑换码功能
## v4.1.6
- 功能新增:**支持 OpenAI 实时语音对话功能** :rocket: :rocket: :rocket:, Beta 版,目前没有做算力计费控制,目前只有 VIP 用户可以使用。
- 功能优化:优化 MysQL 容器配置文档,解决 MysQL 容器资源占用过高问题
- 功能新增:管理后台增加 AI 绘图任务管理,可在管理后台浏览和删除用户的绘图任务
- 功能新增:管理后台增加 Suno 和 Luma 任务管理功能
- Bug 修复:修复管理后台删除兑换码报 404 错误
- 功能优化:优化充值产品定价逻辑,可以设置原价和优惠价,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**,**升级当前版本之后请务必要到管理后台去重新设置一下产品价格,以免造成损失!!!**。
## v4.1.5
- 功能优化:重构 websocket 组件,减少 websocket 连接数,全站共享一个 websocket 连接
- Bug 修复:兼容手机端原生微信支付和支付宝支付渠道
- Bug 修复:修复删除绘图任务时候因为字段长度过短导致 SQL 执行失败问题
- 功能优化:优化 Vue 组件通信代码,使用共享数据来替换之前的事件订阅模式,效率更高一些
- 功能优化:优化思维导图生成功果页面,优化用户体验
## v4.1.4
- 功能优化:用户文件列表组件增加分页功能支持
- Bug 修复:修复用户注册失败 Bug,注册操作只弹出一次行为验证码
- 功能优化:首次登录不需要验证码,直接登录,登录失败之后才弹出验证码
- 功能新增:给 AI 应用(角色)增加分类,前端支持分类筛选
- 功能优化:允许用户在聊天页面设置是否使用流式输出或者一次性输出,兼容 GPT-O1 模型。
- 功能优化:移除 PayJS 支付渠道支持,PayJs 已经关闭注册服务,请使用其他支付方式。
- 功能新增:新增 GeeK 易支付支付渠道,支持支付宝,微信支付,QQ 钱包,京东支付,抖音支付,Paypal 支付等支付方式
- Bug 修复:修复注册页面 tab 组件没有自动选中问题 [#6](https://github.com/yangjian102621/geekai-plus/issues/6)
- 功能优化:Luma 生成视频任务增加自动翻译功能
- Bug 修复:Suno 和 Luma 任务没有判断用户算力
- 功能新增:邮箱注册增加邮箱后缀白名单,防止使用某些垃圾邮箱注册薅羊毛
- 功能优化:清空未支付订单时,只清空超过 15 分钟未支付的订单
## v4.1.3
* 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信
* 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码
* 功能优化:管理后台给可以拖动排序的组件添加拖动图标
* 功能优化:Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作
* Bug修复:手机端角色和模型选择不生效
* Bug修复:用户登录过期之后聊天页面出现大量报错,需要刷新页面才能正常
* 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验
* 功能优化:给算力增减服务全部加上数据库事务和同步锁
* 功能优化:支持用户在前端对话界面选择插件
* 功能新增:支持 Luma 文生视频功能
- 功能优化:重构用户登录模块,给所有的登录组件增加行为验证码功能,支持用户绑定手机,邮箱和微信
- 功能优化:重构找回密码模块,支持通过手机或者邮箱找回密码
- 功能优化:管理后台给可以拖动排序的组件添加拖动图标
- 功能优化:Suno 支持合成完整歌曲,和上传自己的音乐作品进行二次创作
- Bug 修复:手机端角色和模型选择不生效
- Bug 修复:用户登录过期之后聊天页面出现大量报错,需要刷新页面才能正常
- 功能优化:优化聊天页面 Websocket 断线重连代码,提高用户体验
- 功能优化:给算力增减服务全部加上数据库事务和同步锁
- 功能优化:支持用户在前端对话界面选择插件
- 功能新增:支持 Luma 文生视频功能
## v4.1.2
* Bug修复:修复思维导图页面获取模型失败的问题
* 功能优化:优化MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力
* Bug修复:修复后台拖动排序组件 Bug
* 功能优化:更新数据库失败时候显示具体的的报错信息
* Bug修复:修复管理后台对话详情页内容显示异常问题
* 功能优化:管理后台新增清空所有未支付订单的功能
* 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求
* 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力
- Bug 修复:修复思维导图页面获取模型失败的问题
- 功能优化:优化 MJ,SD,DALL-E 任务列表页面,显示失败任务的错误信息,删除失败任务可以恢复扣减算力
- Bug 修复:修复后台拖动排序组件 Bug
- 功能优化:更新数据库失败时候显示具体的的报错信息
- Bug 修复:修复管理后台对话详情页内容显示异常问题
- 功能优化:管理后台新增清空所有未支付订单的功能
- 功能优化:给会话信息和系统配置数据加上缓存功能,减少 http 请求
- 功能新增:移除微信机器人收款功能,增加卡密功能,支持用户使用卡密兑换算力
## v4.1.1
* Bug修复:修复 GPT 模型 function call 调用后没有输出的问题
* 功能新增:允许获取 License 授权用户可以自定义版权信息
* 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
* 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
* 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
* 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
* 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
* 功能新增:允许在管理后台设置首页显示的导航菜单
* Bug修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
* 功能新增:增加 Suno 文生歌曲功能
* 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
* 功能优化:在所有列表页面增加返回顶部按钮
- Bug 修复:修复 GPT 模型 function call 调用后没有输出的问题
- 功能新增:允许获取 License 授权用户可以自定义版权信息
- 功能新增:聊天对话框支持粘贴剪切板内容来上传截图和文件
- 功能优化:增加 session 和系统配置缓存,确保每个页面只进行一次 session 和 get system config 请求
- 功能优化:在应用列表页面,无需先添加模型到用户工作区,可以直接使用
- 功能新增:MJ 绘图失败的任务不会自动删除,而是会在列表页显示失败详细错误信息
- 功能新增:允许在设置首页纯色背景,背景图片,随机背景图片三种背景模式
- 功能新增:允许在管理后台设置首页显示的导航菜单
- Bug 修复:修复注册页面先显示关闭注册组件,然后再显示注册组件
- 功能新增:增加 Suno 文生歌曲功能
- 功能优化:移除多平台模型支持,统一使用 one-api 接口形式,其他平台的模型需要通过 one-api 接口添加
- 功能优化:在所有列表页面增加返回顶部按钮
## v4.1.0
* bug修复:修复移动端修改聊天标题不生效的问题
* Bug修复:修复用户注册不显示用户名的问题
* Bug修复:修复管理后台拖动排序不生效的问题
* 功能优化:允许用户设置自定义首页背景图片
* 功能新增:**支持AI解读 PDF, Word, Excel等文件**
* 功能优化:优化聊天界面的用户上传文件的列表样式
* 功能优化:优化聊天页面对话样式,支持列表样式和对话样式切换
* 功能新增:支持微信扫码登录,未注册用户微信扫码后会自动注册并登录。移动使用微信浏览器打开可以实现无感登录。
- bug 修复:修复移动端修改聊天标题不生效的问题
- Bug 修复:修复用户注册不显示用户名的问题
- Bug 修复:修复管理后台拖动排序不生效的问题
- 功能优化:允许用户设置自定义首页背景图片
- 功能新增:**支持 AI 解读 PDF, Word, Excel 等文件**
- 功能优化:优化聊天界面的用户上传文件的列表样式
- 功能优化:优化聊天页面对话样式,支持列表样式和对话样式切换
- 功能新增:支持微信扫码登录,未注册用户微信扫码后会自动注册并登录。移动使用微信浏览器打开可以实现无感登录。
## v4.0.9
* 环境升级:升级 Golang 到 go1.22.4
* 功能增加:接入微信商户号支付渠道
* Bug修复:修复前端页面菜单把页面撑开,底部留白问题
* 功能优化:聊天页面自动根据内容调整输入框的高度
* Bug修复:修复Dalle绘图失败退回算力的问题
* 功能优化:邀请码注册时被邀请人也可以获得赠送的算力
* 功能优化:允许设置邮件验证码的抬头
* Bug修复:修复免费模型不会记录聊天记录的bug
* Bug修复:修复聊天输入公式显示异常的Bug
- 环境升级:升级 Golang 到 go1.22.4
- 功能增加:接入微信商户号支付渠道
- Bug 修复:修复前端页面菜单把页面撑开,底部留白问题
- 功能优化:聊天页面自动根据内容调整输入框的高度
- Bug 修复:修复 Dalle 绘图失败退回算力的问题
- 功能优化:邀请码注册时被邀请人也可以获得赠送的算力
- 功能优化:允许设置邮件验证码的抬头
- Bug 修复:修复免费模型不会记录聊天记录的 bug
- Bug 修复:修复聊天输入公式显示异常的 Bug
## v4.0.8
* 功能优化:升级 mathjax 公式解析插件,修复公式因为图片访问限制而无法显示的问题
* 功能优化:当数据库更新失败的时候记录错误日志
* 功能优化:聊天输入框会随着输入内容的增多自动调整高度
* Bug修复:修复移动端聊天页面模型切换不生效的Bug
* 功能优化:给PC端扫码支付增加签名验证和有效期验证
* Bug修复:修复支付码生成API权限控制的问题
* Bug修复:模型算力设置为0时,不扣减用户算力,并且不记录算力消费日志
* 功能优化:新增随机背景配置项,可以在后台设置,首页使用 Bing 壁纸作为背景图片
* 功能新增:H5端支持 Dalle 绘图
- 功能优化:升级 mathjax 公式解析插件,修复公式因为图片访问限制而无法显示的问题
- 功能优化:当数据库更新失败的时候记录错误日志
- 功能优化:聊天输入框会随着输入内容的增多自动调整高度
- Bug 修复:修复移动端聊天页面模型切换不生效的 Bug
- 功能优化:给 PC 端扫码支付增加签名验证和有效期验证
- Bug 修复:修复支付码生成 API 权限控制的问题
- Bug 修复:模型算力设置为 0 时,不扣减用户算力,并且不记录算力消费日志
- 功能优化:新增随机背景配置项,可以在后台设置,首页使用 Bing 壁纸作为背景图片
- 功能新增:H5 端支持 Dalle 绘图
## v4.0.7
* 功能优化:升级quic-go,支持 Go1.21
* 功能优化:添加导航菜单的时候支持框入外部链接,并支持上传自定义菜单图片
* Bug修复:修复弹窗等于图形验证码一直验证失败的问题
* 功能重构:重构前端 UI 页面,增加顶部导航
* 功能优化:优化 Vue 非父子组件之间的通信方式
* 功能优化:优化 ItemList 组件,自动根据页面宽度计算 cols 数量
- 功能优化:添加导航菜单的时候支持框入外部链接,并支持上传自定义菜单图片
- Bug 修复:修复弹窗等于图形验证码一直验证失败的问题
- 功能重构:重构前端 UI 页面,增加顶部导航
- 功能优化:优化 Vue 非父子组件之间的通信方式
- 功能优化:优化 ItemList 组件,自动根据页面宽度计算 cols 数量
## v4.0.6
* Bug修复:修复PC端画廊页面的瀑布流组件样式错乱问题
* 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位
* Bug修复:修复思维导图不扣费的Bug
* Bug修复:修复管理后台角色删除失败的Bug
* Bug修复:兼容最新版秋叶SD懒人包的 SD API,新增 scheduler 参数
* 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY
* Bug修复:修复注册用户提示注册人数达到上限的 Bug
* 功能优化:将MJ,SD,Dall绘画页面的任务列表全改成瀑布流组件
- Bug 修复:修复 PC 端画廊页面的瀑布流组件样式错乱问题
- 功能新增:给思维导图增加 ToolBar,实现思维导图的放大缩小和定位
- Bug 修复:修复思维导图不扣费的 Bug
- Bug 修复:修复管理后台角色删除失败的 Bug
- Bug 修复:兼容最新版秋叶 SD 懒人包的 SD API,新增 scheduler 参数
- 功能优化:支持在管理后台配置 AI 绘图相关配置,包括 SD, MJ-PLUS, MJ-PROXY
- Bug 修复:修复注册用户提示注册人数达到上限的 Bug
- 功能优化:将 MJ,SD,Dall 绘画页面的任务列表全改成瀑布流组件
## v4.0.5
* 功能优化:已授权系统在后台显示授权信息
* 功能优化:使用思维链提示词生成思维导图,确保生成的思维导图不会出现格式错误
* 功能优化:优化首页登录注册页面的 UI
* BUG修复:修复License验证的逻辑漏洞
* Bug修复:后台添加用户的时候密码规则限制跟前台注册保持一致
* 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题
* 功能新增:移动端新增 DALL-E 绘画功能
* 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题
* 功能新增:移动支持免登录预览功能
* Bug修复:解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题
* Bug修复:修复部分中转 API 模型会出现第一输出的字符被淹没的Bug
- 功能优化:已授权系统在后台显示授权信息
- 功能优化:使用思维链提示词生成思维导图,确保生成的思维导图不会出现格式错误
- 功能优化:优化首页登录注册页面的 UI
- BUG 修复:修复 License 验证的逻辑漏洞
- Bug 修复:后台添加用户的时候密码规则限制跟前台注册保持一致
- 功能新增:管理后台支持切换主题,支持 light 和 dark 两种主题
- 功能新增:移动端新增 DALL-E 绘画功能
- 功能新增:新增移动端首页功能,移动端支持 light 和 dark 两种主题
- 功能新增:移动支持免登录预览功能
- Bug 修复:解决在同一个浏览器开启多个对话时候对话内容会相互乱串的问题
- Bug 修复:修复部分中转 API 模型会出现第一输出的字符被淹没的 Bug
## v4.0.4
* Bug修复:修复统一千问第二句不回复的问题
* 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度
* 功能新增:Dalle AI 绘画功能实现
* Bug修复:修复思维导图格式乱码问题
* 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题
* 功能新增:支持从应用列表直接和某个应用对话
* 功能优化:优化算力日志的页面和首页的UI
* 功能新增:支持思维导图导出 PNG 图片下载
- Bug 修复:修复统一千问第二句不回复的问题
- 功能优化:MJ 和 SD 任务正在执行时不更新已完成任务列表,加快页面渲染速度
- 功能新增:Dalle AI 绘画功能实现
- Bug 修复:修复思维导图格式乱码问题
- 功能优化:支持使用 TLS 邮件协议,解决国内服务器无法使用 25 号端口发送邮件的问题
- 功能新增:支持从应用列表直接和某个应用对话
- 功能优化:优化算力日志的页面和首页的 UI
- 功能新增:支持思维导图导出 PNG 图片下载
## v4.0.3
* 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型
* Bug修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug
* Bug修复:修复MidJourney在任务超时后出现后面的任务覆盖前面任务的问题
* 功能新增:支持上传图片和视觉模型
* 功能优化:优化聊天页面的复制代码按钮样式乱码
* 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图
* 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用GPT3.5或者 GPT4
* 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。
* 功能新增:支持管理后台 Logo 修改
- 功能新增:允许为角色应用绑定模型,如指定某个角色只能使用某个模型
- Bug 修复:兼容 gpt-4-turbo-2024-04-09 模型的函数调用 Bug
- Bug 修复:修复 MidJourney 在任务超时后出现后面的任务覆盖前面任务的问题
- 功能新增:支持上传图片和视觉模型
- 功能优化:优化聊天页面的复制代码按钮样式乱码
- 功能新增:增加思维导图功能,支持选择不同的对话模型来生成思维导图
- 功能新增:支持为角色绑定对话模型,比如绑定某个角色只能用 GPT3.5 或者 GPT4
- 功能新增:支持为模型绑定 API KEY,比如为 GPT3.5 模型绑定免费的 API KEY 给用户免费使用来引流不至于消耗你的收费 KEY。
- 功能新增:支持管理后台 Logo 修改
## 4.0.2
* 功能新增:支持前端菜单可以配置
* 功能优化:在登录和注册界面标题显示软件版本号
* 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数
* 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题
* Bug修复:解决因为图片上传使用相对路径而导致融图失败的问题。
* 功能新增:手机端支持 Stable-Diffusion 绘画
* 功能新增:管理后台登录页面增加行为验证码,防止爆破
- 功能新增:支持前端菜单可以配置
- 功能优化:在登录和注册界面标题显示软件版本号
- 功能优化:MJ 绘画支持 --sref 和 --cref 图片一致性参数
- 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题
- Bug 修复:解决因为图片上传使用相对路径而导致融图失败的问题。
- 功能新增:手机端支持 Stable-Diffusion 绘画
- 功能新增:管理后台登录页面增加行为验证码,防止爆破
## v4.0.1
* 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion
- 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion
发行版,稳定性更强一些
* 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容
- 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容
MJ-Plus 中转
* 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力
* Bug修复:修复 iphone 手机无法通过图形验证码的Bug,使用滑动验证码替换
* Bug修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的Bug
- 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力
- Bug 修复:修复 iphone 手机无法通过图形验证码的 Bug,使用滑动验证码替换
- Bug 修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的 Bug
## v4.0.0
非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI对话,MJ绘画,SD绘画,DALL绘画)全部使用算力来兑换。
只要你的算力值余额不为0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗1个单位算力,一次 GPT4 对话消耗10个算力。一次 MJ
对话消耗15个算力...
非兼容版本,重大重构,引入算力概念,将系统中所有的能力(AI 对话,MJ 绘画,SD 绘画,DALL 绘画)全部使用算力来兑换。
只要你的算力值余额不为 0,你就可以进行任何操作。比如一次 GPT3.5 对话消耗 1 个单位算力,一次 GPT4 对话消耗 10 个算力。一次 MJ
对话消耗 15 个算力...
* 功能重构:重构整体系统,全部采用算力来进行结算
* 功能优化:SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
* 功能优化:移动端聊天页面图片支持预览和放大功能
* 功能优化:MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题
* 功能优化:**PC端不登录也可以预览功能,只有在发起操作的时候才需要登录**
* 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能
* 功能新增:支持H5支付
* 功能优化:支持数学公式的识别和美化输出
* 功能新增:新增算力消费日志功能
* 功能优化:整合 XXL-JOB 实现订单清理,每日算力派发,VIP 算力重置等任务
* 功能新增:管理后台新增7日内新增用户和新增订单统计
- 功能重构:重构整体系统,全部采用算力来进行结算
- 功能优化:SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
- 功能优化:移动端聊天页面图片支持预览和放大功能
- 功能优化:MJ 和 SD 页面数据分页加载,解决一次性加载太多数据导致页面卡顿的问题
- 功能优化:**PC 端不登录也可以预览功能,只有在发起操作的时候才需要登录**
- 功能优化:控制台订单管理页面显示未支付订单,并提供订单删除功能
- 功能新增:支持 H5 支付
- 功能优化:支持数学公式的识别和美化输出
- 功能新增:新增算力消费日志功能
- 功能优化:整合 XXL-JOB 实现订单清理,每日算力派发,VIP 算力重置等任务
- 功能新增:管理后台新增 7 日内新增用户和新增订单统计
## v3.2.7
* 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能
* 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能
* Bug修复:修复 issue [
- 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能
- 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能
- Bug 修复:修复 issue [
管理界面操作用户存在的两个问题](https://github.com/yangjian102621/chatgpt-plus/issues/117#issuecomment-1909201532)
* 功能优化:在对话和聊天记录表中新增冗余字段 model,存储对话模型
* Bug修复:IPhone 手机验证码触摸事件坐标错位 [issue 144](https://github.com/yangjian102621/chatgpt-plus/issues/144)
* Bug修复:重新生成按钮功能失效问题
* Bug修复:对话输入HTML标签不显示的问题
* 功能优化:gpt-4-all/gpts/midjourney-plus 支持第三方平台的 API KEY
* 功能新增:新增删除文件功能
* Bug修复:解决 MJ-Plus discord 图片下载失败问题,使用第三方平台中转地址下载
* 功能新增:后台管理新怎对话查看和检索功能
- 功能优化:在对话和聊天记录表中新增冗余字段 model,存储对话模型
- Bug 修复:IPhone 手机验证码触摸事件坐标错位 [issue 144](https://github.com/yangjian102621/chatgpt-plus/issues/144)
- Bug 修复:重新生成按钮功能失效问题
- Bug 修复:对话输入 HTML 标签不显示的问题
- 功能优化:gpt-4-all/gpts/midjourney-plus 支持第三方平台的 API KEY
- 功能新增:新增删除文件功能
- Bug 修复:解决 MJ-Plus discord 图片下载失败问题,使用第三方平台中转地址下载
- 功能新增:后台管理新怎对话查看和检索功能
## v3.2.6
* 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
* 功能优化:兼用旧版本微信收款消息解析
* 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
* 功能新增:新增图片发布功能,画廊只显示用户已发布的图片
* 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码
* 功能新增:新增网站公告,可以在管理后台自定义配置
* 功能新增:新增阿里通义千问大模型支持
* Bug修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug
* 功能优化:新增虎皮椒和PayJS订单状态校验功能,增加安全性
* Bug修复:修复微信转账交易 ID 提取失败 Bug
* 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
* 功能新增:新增短信宝短信平台发送平台集成
- 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
- 功能优化:兼用旧版本微信收款消息解析
- 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
- 功能新增:新增图片发布功能,画廊只显示用户已发布的图片
- 功能新增:后台新增配置微信客服二维码,可以上传自己的微信客服二维码
- 功能新增:新增网站公告,可以在管理后台自定义配置
- 功能新增:新增阿里通义千问大模型支持
- Bug 修复:修复 MJ 放大任务失败时候 img_call 会增加的 Bug
- 功能优化:新增虎皮椒和 PayJS 订单状态校验功能,增加安全性
- Bug 修复:修复微信转账交易 ID 提取失败 Bug
- 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
- 功能新增:新增短信宝短信平台发送平台集成
## v3.2.5
* 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
* 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅
- 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
- 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅
Plus 账号了!!!
* 功能优化:增强 markdown 图片和引用块解析。
* 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
* 功能优化:function call 兼用中转 API。
* Bug修复:修复部分已知的 Bug。
- 功能优化:增强 markdown 图片和引用块解析。
- 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
- 功能优化:function call 兼用中转 API。
- Bug 修复:修复部分已知的 Bug。
## v3.2.4.1
* 功能新增:新增 PayJs 支付通道
* Bug修复:紧急修复后台添加用户失败问题
* Bug修复:紧急修复使用中转 API-KEY 无法绘图的问题
* Bug修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖
- 功能新增:新增 PayJs 支付通道
- Bug 修复:紧急修复后台添加用户失败问题
- Bug 修复:紧急修复使用中转 API-KEY 无法绘图的问题
- Bug 修复:允许用户关闭手机和邮箱注册通道,移除验证码依赖
## v3.2.4
* 功能新增:重磅更新,支持邮箱注册
* 功能优化:优化函数调用授权
* 功能优化:给用户表新增 nickname 字段
* 功能优化:管理后台给聊天角色增加启用/禁用开关
* Bug修复:SD绘画出现重复扣减绘图次数
* 功能优化:优化聊天对话导出样式,适应移动端
* 功能新增:众筹核销可以选择兑换对话还是绘图的额度
* Bug修复:修复[从历史记录获取reply有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92)
* Bug修复:修复 MidJourney 绘图任务调度Bug,为 task_id 建议唯一索引
* 功能重构:重构了 API KEY模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。
- 功能新增:重磅更新,支持邮箱注册
- 功能优化:优化函数调用授权
- 功能优化:给用户表新增 nickname 字段
- 功能优化:管理后台给聊天角色增加启用/禁用开关
- Bug 修复:SD 绘画出现重复扣减绘图次数
- 功能优化:优化聊天对话导出样式,适应移动端
- 功能新增:众筹核销可以选择兑换对话还是绘图的额度
- Bug 修复:修复[从历史记录获取 reply 有并发风险 #92](https://github.com/yangjian102621/chatgpt-plus/issues/92)
- Bug 修复:修复 MidJourney 绘图任务调度 Bug,为 task_id 建议唯一索引
- 功能重构:重构了 API KEY 模块,支持为每个 API KEY 都设置不同的 API 地址,并可以单独开启是否使用代理。
## v3.2.3
* 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现
* 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数
* Bug修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73)
* Bug修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道
* 功能新增:新增删除绘图任何和图片功能
* Bug修复:修复虎皮椒支付二维码重复扫码时报错问题
* 功能优化:自动将 AI 绘画中的中文提示词翻译成英文
* 功能优化:优化AI绘画的大图压缩算法,新增图片缓存
* 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率
* Bug修复:修复[Azure Api 更换api-version参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71)
* Bug修复:修复科大讯飞 V1.5 API 请求失败的问题
* Bug修复:绘图失败后,自动恢复用户的剩余绘图次数
* 功能新增:为移动端新增 SD 绘图功能,分享功能
- 功能重构:重构函数工具模块,设计成可以后台动态管理函数。支持添加自定义函数实现
- 功能新增:为充值产品数据表添加 img_calls 字段,支持充值绘图次数
- Bug 修复:修复 [MJ 机器人空指针异常的 Bug](https://github.com/yangjian102621/chatgpt-plus/issues/73)
- Bug 修复:确保相同 Prompt 的绘图任务的 Upscale 和 Variation 任务调度给相同的频道
- 功能新增:新增删除绘图任何和图片功能
- Bug 修复:修复虎皮椒支付二维码重复扫码时报错问题
- 功能优化:自动将 AI 绘画中的中文提示词翻译成英文
- 功能优化:优化 AI 绘画的大图压缩算法,新增图片缓存
- 功能优化:支持为 MJ 绘图 API 增加反代功能,提高图片的加载速度,大大降低绘图任务的失败率
- Bug 修复:修复[Azure Api 更换 api-version 参数后请求失败的问题](https://github.com/yangjian102621/chatgpt-plus/pull/71)
- Bug 修复:修复科大讯飞 V1.5 API 请求失败的问题
- Bug 修复:绘图失败后,自动恢复用户的剩余绘图次数
- 功能新增:为移动端新增 SD 绘图功能,分享功能
## v3.2.2
* 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务
* 功能新增:AI绘画页面增加翻译和重写提示词功能
* 功能优化:OSS上传组件支持在 Bucket 下设置二级目录
* Bug修复:修复阿里云 OSS 访问路径错误
* 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket
- 功能重构:重构 MidJourney 和 Stable-Diffusion 绘图模块,支持使用多组配置创建池子提供绘画服务
- 功能新增:AI 绘画页面增加翻译和重写提示词功能
- 功能优化:OSS 上传组件支持在 Bucket 下设置二级目录
- Bug 修复:修复阿里云 OSS 访问路径错误
- 功能优化:在 AI 绘图页面使用 HTTP 轮询替换 Websocket
## v3.2.1
* 功能优化:切换角色和模型的时候自动创建新的对话
* Bug修复:修复文件上传失败No such file bug
* 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
* Bug修复:[PC端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
* 功能新增:增加 arm64 架构打包脚本
* 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
* 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
- 功能优化:切换角色和模型的时候自动创建新的对话
- Bug 修复:修复文件上传失败 No such file bug
- 功能新增:MidJourney 绘画页面新增提示词翻译功能,新增多个绘画参数
- Bug 修复:[PC 端对话在刷新后异常](https://github.com/yangjian102621/chatgpt-plus/issues/59)
- 功能新增:增加 arm64 架构打包脚本
- 功能新增:支持 dall-e3 绘图的 API 地址自定义配置
- 功能新增:新增虎皮椒支付功能接入,支持微信和支付宝通道
## v3.2.0
* 功能新增:新增邀请注册功能
* 功能优化:增加中间件自动对HTTP请求的参数去掉首尾空格
* 功能优化:增加中间件自动为大图片生成缩略图
* 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
* 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
* Bug修复:修复阿里云 OSS 域名设置不起做用的bug
* Bug修复:修复MidJourney绘图失败后重复添加到队列的问题
- 功能新增:新增邀请注册功能
- 功能优化:增加中间件自动对 HTTP 请求的参数去掉首尾空格
- 功能优化:增加中间件自动为大图片生成缩略图
- 功能优化:MidJourney 页面图片加载优化,实现图片预览懒加载
- 功能新增:新增 DALL-E-3 绘画支持,并作为对话页面默认绘画插件
- Bug 修复:修复阿里云 OSS 域名设置不起做用的 bug
- Bug 修复:修复 MidJourney 绘图失败后重复添加到队列的问题
## v3.1.9
* 功能新增:增加讯飞星火大模型 v3.0 支持
* 功能新增:新增找回密码功能
* 功能新增:支持 Markdown 代码复制功能
* Bug修复: xxl-job 任务调度失败的 Bug
* 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
* Bug修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
* 功能优化:优化会员充值页面 ItemList 组件
* 功能优化:给首页 Logo 增加链接
* Bug修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
* Bug修复:聊天上下文失效问题
* 功能优化:关闭注册时显示联系管理员二维码
* 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
* Bug修复:后台启用用户 VIP 不生效问题
* 功能优化:充值支付页面的支付说明文字可以后台配置
* Bug修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
- 功能新增:增加讯飞星火大模型 v3.0 支持
- 功能新增:新增找回密码功能
- 功能新增:支持 Markdown 代码复制功能
- Bug 修复: xxl-job 任务调度失败的 Bug
- 功能优化:优化前端页面菜单图标,使用自定义图标替换 icon-font
- Bug 修复:Stable-Diffusion 绘画成功之后没有扣减用户画图次数
- 功能优化:优化会员充值页面 ItemList 组件
- 功能优化:给首页 Logo 增加链接
- Bug 修复:[新建会话时,提示"请输入合法的手机号" ](https://github.com/yangjian102621/chatgpt-plus/issues/51)
- Bug 修复:聊天上下文失效问题
- 功能优化:关闭注册时显示联系管理员二维码
- 功能优化:移除 leveldb 依赖,使用 redis 替换相应的功能
- Bug 修复:后台启用用户 VIP 不生效问题
- 功能优化:充值支付页面的支付说明文字可以后台配置
- Bug 修复:ChatGLM,百度文心,科大讯飞模型输出代码不换行问题
## v3.1.8
1. 功能新增:新增会员套餐充值,点卡充值,订单系统,集成支付宝支付通道
2. Bug修复:修复 MidJourney API 参数版本更新导致调用失败问题
3. Bug修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
4. Bug修复:修复七牛云上传报错 expired token
5. Bug修复:修复高权重模型导致的对话次数为负数的漏洞
2. Bug 修复:修复 MidJourney API 参数版本更新导致调用失败问题
3. Bug 修复:修复 Stable Diffusion 调用后没有更新绘图调用次数问题
4. Bug 修复:修复七牛云上传报错 expired token
5. Bug 修复:修复高权重模型导致的对话次数为负数的漏洞
6. 功能优化:将聊天报错信息定义为统一常量,方便修改
7. 功能优化:优化 markdown 表格显示样式,覆写 Element-Plus 表格样式
8. 功能优化:增加倒数计时组件,定期自动清理未支付的订单
## v3.1.7
1. 功能新增:支持文心4.0 AI 模型
1. 功能新增:支持文心 4.0 AI 模型
2. 功能新增:可以在管理后台为用户绑定指定的 AI 模型,如只给某个用户使用 GPT-4 模型
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如GPT4GPT3.510倍
3. 功能新增:模型新增权重字段,不同的模型每次调用耗费的点数可以设置不同,比如 GPT4GPT3.510
4. 功能新增:新增系统配置关闭 AI 模型的函数功能
5. 功能优化:优化 MidJourney 专业绘画页面图片预览样式
## v3.1.6
1. 功能新增:新增AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
1. 功能新增:新增 AI 绘画照片墙功能页面,供用户查看所有的 AI 绘画作品
2. 功能新增:新增 AI 角色应用功能页面,用户可以添加自己感兴趣的应用
3. 功能优化:优化瀑布流组件的页面布局
4. 功能优化:新注册用户成功之后自动登录
@@ -319,55 +492,55 @@
2. 功能新增:新增科大讯飞星火大模型 API 接入支持
3. 功能重构:将 chat_handler 的所有功能实现放入单独的包中
4. 功能新增:新增系统配置 `enabled_function` 用于启用和关闭函数功能
5. Bug修复:修复管理后台更新 API Key 失败的 Bug
6. Bug修复:修复新建的对话无法更新对话标题的 Bug
5. Bug 修复:修复管理后台更新 API Key 失败的 Bug
6. Bug 修复:修复新建的对话无法更新对话标题的 Bug
7. 功能优化:其他一些小的体验优化工作
## v3.1.4
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio和阿里云 OSS 四种存储介质。
1. 功能新增:新增阿里云 OSS 图片上传实现,目前已支持本地存储,七牛云,Minio 和阿里云 OSS 四种存储介质。
2. 功能新增:**增加 Stable Diffusion 绘画功能页面**。
3. 功能重构:将 [chatgpt-plus-exts](https://github.com/yangjian102621/chatgpt-plus-exts) 合并到本项目,部署更加简单,无需部署两个项目了。
4. Bug修复:修复[用户注册报错BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
5. Bug修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
4. Bug 修复:修复[用户注册报错 BUG #37](https://github.com/yangjian102621/chatgpt-plus/issues/37)。
5. Bug 修复:修复 MidJourney API 接口升级导致图片文保存失败的 Bug。
6. 功能优化:增加阿里云短信服务配置项 `Sign``CodeTempId` 用来配置自己的短信签名和短信验证码模版 ID。
7. 功能优化:添加系统配置用来设置自定义的众筹微信收款二维码。
8. 功能优化:优化绘画页面的弹窗样式和页面布局。
## v3.1.3
1. 页面重构:重后 Home 页面,拆分成聊天,MJ绘画,SD 绘画,应用广场等多个功能菜单。
1. 页面重构:重后 Home 页面,拆分成聊天,MJ 绘画,SD 绘画,应用广场等多个功能菜单。
2. 功能新增:新增 MidJourney 专业绘画页面,开放更高级的 MJ 绘画姿势。
3. 功能优化:采用队列的方式控制绘画任务并发,简化任务回调通知逻辑,给任务回调加锁。
4. 功能优化:精简用户表字段,删除用户名和昵称,只保留手机号。
5. 功能优化:优化文件上传服务工厂实现,只创建激活的 Uploader 服务,节省资源。
6. Bug修复:修复 JWT token 有效期计算错误的 Bug。
6. Bug 修复:修复 JWT token 有效期计算错误的 Bug。
## v3.1.2
1. 功能新增:新增七牛云 OSS 实现,目前已支持三种文件上传服务:Local, Minio, QiNiu OSS。
2. 功能新增:新增桌面版,使用 electron 套壳网页版。
3. Bug修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
3. Bug 修复:自动去除众筹核销时候转账单号中的空格,防止复制的时候多复制了空格。
4. 功能优化:ChatPlus.vue 页面支持通过 chat_id path variable 来定位到指定的聊天。
5. 功能优化:取消导出聊天页面的授权验证
6. 功能优化:所有路由跳转都使用绝对路径
## v3.1.1
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug
紧急修复版本,采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的 Bug
## v3.1.0(大版本更新)
1. 功能重构:将聊天模型独立拆分,以便支持多平台模型,目前已经内置支持 OPenAI,Azure 以及
ChatGLM,用户可以在这两个平台的模型中随意切换,体验不同的模型聊天。
2. 功能重构:重写系统 API 授权机制,使用 JWT 替换传统的 session 会话授权,使得 API 授权变得更加灵活。
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储handler,目前已经实现本地存储和 minio oss 存储。
3. 功能重构:重构文件夹上传服务,支持多种文件上传存储 handler,目前已经实现本地存储和 minio oss 存储。
4. 功能优化:更新头像自动删除旧的图片资源。
5. 功能优化:将应用日志在终端输出的同时存盘,方便 docker 部署查看日志。
6. 功能新增:允许用户配置自己的 OPenAIAzure 以及 ChatGLM API KEY。
7. 功能优化:优化移动版的行为验证码样式,修复低分辨率显示器验证码被遮挡的 Bug
8. 升级 gin, element-plusredis 组件到最新版本。
9. Bug修复:修复若干已知的的 Bug
9. Bug 修复:修复若干已知的的 Bug
## v3.0.7
@@ -377,7 +550,7 @@
4. 功能新增:支持导出聊天记录为 PDF 文件。
5. 功能优化:在后台 dashboard 页面新增统计今日众筹收入。
6. 功能优化:支持用户设置默认的 GPT 模型
7. Bug修复:修复若干已知的的 Bug
7. Bug 修复:修复若干已知的的 Bug
## v3.0.6
@@ -385,8 +558,8 @@
2. 管理后台:新增重置用户密码功能
3. 管理后台:支持关闭注册功能,新增添加用户功能,适用于内部使用场景
4. 管理后台:新增仪表盘页面,统计当天的新增用户,新增会话数据,以及 Token 消耗
5. Bug修复:修复注册页面验证码不显示 Bug
6. Bug修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
5. Bug 修复:修复注册页面验证码不显示 Bug
6. Bug 修复:优化上下文 Token 计算算法,修复聊天上下文超出限制时循环发送消息的 Bug
7. 功能修正:允许用户使用手机号码登录
8. 功能优化:更新系统配置后同步更新服务端内存变量数据
9. 功能优化:优化打包脚本,减少容器镜像大小
@@ -444,5 +617,5 @@
4. 新增聊天设置功能,用户可以导入自己的 API KEY
5. 保存聊天记录,支持聊天上下文。
6. 重构后台管理模块,更友好,扩展性更好的后台管理系统。
7. 引入 ip2region 组件,记录用户的登录IP和地址。
8. 支持会话搜索过滤。
7. 引入 ip2region 组件,记录用户的登录 IP 和地址。
8. 支持会话搜索过滤。
+66
View File
@@ -0,0 +1,66 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Build Commands
### Go Backend (api/)
- **Development**: `cd api && go run main.go` (uses config.toml)
- **Build**: `cd api && make` (builds both amd64 and arm64 binaries)
- **Individual builds**: `make amd64` or `make arm64`
- **Clean**: `make clean`
- **Config**: Copy `config.sample.toml` to `config.toml` and configure
### Web Frontend (web/)
- **Development**: `cd web && npm run dev` (runs on Vite dev server with --host)
- **Build**: `cd web && npm run build`
- **Lint**: `cd web && npm run lint` (ESLint with auto-fix)
### Testing
- Backend tests: `cd api/test && bash run_crawler_test.sh`
- No specific frontend test configuration found
## Project Architecture
### Backend (Go)
- **Framework**: Gin web framework with dependency injection via uber-go/fx
- **Database**: GORM with MySQL, Redis for caching, LevelDB for local storage
- **Authentication**: JWT tokens with Redis session storage
- **Middleware**: CORS, authorization, parameter handling, static resource serving
- **Structure**:
- `handler/`: HTTP request handlers (REST API endpoints)
- `service/`: Business logic services (AI integrations, payments, etc.)
- `store/`: Database models and data access layer
- `core/`: Application server and middleware configuration
- `utils/`: Utility functions and helpers
### Frontend (Vue.js)
- **Framework**: Vue 3 with Composition API
- **UI Components**: Element Plus + Vant (mobile components)
- **State Management**: Pinia
- **Routing**: Vue Router with nested routes
- **Build Tool**: Vite
- **CSS**: Stylus preprocessor with Tailwind CSS utilities
- **Features**: Responsive design (desktop/mobile views), theme switching (dark/light)
### Key Features
- **AI Chat**: Multiple chat models and conversation management
- **Image Generation**: MidJourney, Stable Diffusion, DALL-E integration
- **Audio/Video**: Suno music creation, Luma/KeLing video generation
- **User Management**: Authentication, payments, power logs, invitations
- **Admin Panel**: Comprehensive management interface
### Database Models
Key entities: User, ChatItem, ChatMessage, ChatRole, ChatModel, Order, Product, AdminUser, and various job types for AI services.
### API Structure
- User APIs: `/api/user/*` (auth, profile, settings)
- Chat APIs: `/api/chat/*` (conversations, messages)
- AI Service APIs: `/api/mj/*`, `/api/sd/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*`
- Admin APIs: `/api/admin/*` (management functions)
### Configuration
- Backend: TOML configuration file (`config.toml`)
- Database: MySQL with automatic migrations
- Services: Redis, various AI API integrations
- File Storage: Local, Aliyun OSS, MinIO, Qiniu options
+67 -82
View File
@@ -1,92 +1,77 @@
# GeekAI
> 根据[《生成式人工智能服务管理暂行办法》](https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。
# 🚀 GeekAI-PLUS:一站式 AI 创意生产力平台
**GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Azure,
ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI绘画功能。
**重新定义 AI 创作体验,让每个人都能成为内容创作大师**
主要特性:
基于 GeekAI 项目开发的高级版,增加了很多高级功能,比如思维导图,Dalle 绘画等。**高级版源码不会一次性开放,只提供镜像给大家免费使用**,源码会逐步逐步按照版同步迁移到[社区版(GeekAI](https://github.com/yangjian102621/geekai)。所以如果大家想要二次开发,请移步去社区版。
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。
- 基于 Websocket 实现,完美的打字机体验。
- 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
- 支持 OPenAI,Azure,文心一言,讯飞星火,清华 ChatGLM等多个大语言模型。
- 支持 Suno 文生音乐
- 支持 MidJourney / Stable Diffusion AI 绘画集成,文生图,图生图,换脸,融图。开箱即用。
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
## ✨ 核心特色
### 🚀 更多功能请查看 [GeekAI-PLUS](https://github.com/yangjian102621/geekai-plus)
### 🎨 **全能 AI 创作矩阵**
- [x] 更友好的 UI 界面
- [x] 支持 Dall-E 文生图功能
- [x] 支持文生思维导图
- [x] 支持为模型绑定指定的 API KEY,支持为角色绑定指定的模型等功能
- [x] 支持网站 Logo 版权等信息的修改
- **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话
- **图像生成**:整合 MidJourney、DALL-E、Stable Diffusion 三大主流 AI 绘画引擎
- **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐
- **视频制作**Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限
- **思维导图**:AI 辅助思维整理,复杂想法可视化呈现
### 🏗️ **企业级技术架构**
- **高性能后端**Go + Gin + MySQL + Redis,支持高并发访问
- **现代化前端**Vue3 + Element Plus + Vant,桌面移动双端适配
- **智能缓存**:多层缓存策略,响应速度提升 80%
- **弹性部署**:Docker 容器化部署,一键启动,轻松扩展
- **私有化部署**:支持私有化部署,私有化部署不支持升级,需要手动升级
- **文档支持**:丰富且详细的部署和 API 开发文档支持,二次开发轻松上手
### 💼 **商业化就绪**
- **完整用户系统**:注册登录、权限管理、积分充值
- **灵活计费模式**:支持按次付费、包月订阅等多种商业模式
- **数据统计分析**:用户行为、消费记录、系统性能全方位监控
- **管理后台**:功能完备的管理员界面,运营数据一目了然
### 🎯 **用户体验优势**
- **响应式设计**:完美适配桌面、平板、手机等全终端设备
- **暗黑模式**:支持明暗主题切换,护眼舒适
- **实时交互**:WebSocket 实时通信,创作过程流畅无卡顿
- **文件管理**:支持多种云存储,作品安全可靠
## 🎪 **应用场景**
- **内容创作者**:博客写作、社交媒体素材、短视频制作
- **企业营销**:品牌宣传材料、产品介绍、创意广告
- **教育培训**:课件制作、知识图谱、互动内容
- **个人娱乐**:AI 聊天、创意绘画、音乐创作
## 🔥 **为什么选择 GeekAI-PLUS**
1. **技术领先**:集成当前最先进的 AI 技术,始终保持创新前沿
2. **开箱即用**:完整的商业化解决方案,无需从零开发
3. **高度定制**:模块化架构设计,支持个性化功能扩展
4. **稳定可靠**:经过大量用户验证,性能稳定,安全可信
5. **持续更新**:紧跟 AI 技术发展,功能持续迭代升级
## 演示站点
[Geek-AI 创作系统](https://www.geekai.me)
## 文档地址
[Geek-AI 文档](https://www.geekai.me/docs/)
## 部署
1. 安装 docker 和 docker-compose 程序,这个自行解决。
2. 直接在项目根目录运行启动命令:
```shell
docker-compose up -d
```
## 功能截图
请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。
### 体验地址
---
> 免费体验地址:[https://chat.geekai.me](https://chat.geekai.me) <br/>
> **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!**
## 快速部署
请参考文档 [**GeekAI 快速部署**](https://docs.geekai.me/install/)。
## 使用须知
1. 本项目基于 Apache2.0 协议,免费开放全部源代码,可以作为个人学习使用或者商用。
2. 如需商用必须保留版权信息,请自觉遵守。确保合法合规使用,在运营过程中产生的一切任何后果自负,与作者无关。
## 项目地址
* Github 地址:https://github.com/yangjian102621/geekai
* 码云地址:https://gitee.com/blackfox/geekai
## 客户端下载
目前已经支持 Win/Linux/Mac/Android 客户端,下载地址为:https://github.com/yangjian102621/geekai/releases/tag/v3.1.2
## TODOLIST
* [ ] 支持基于知识库的 AI 问答
* [ ] 文生视频,文生歌曲功能
* [ ] 微信支付功能
## 项目文档
最新的部署视频教程:[https://www.bilibili.com/video/BV1Cc411t7CX/](https://www.bilibili.com/video/BV1Cc411t7CX/)
详细的部署和开发文档请参考 [**GeekAI 文档**](https://docs.geekai.me)。
加微信进入微信讨论群可获取 **一键部署脚本(添加好友时请注明来自Github!!!)。**
![微信名片](docs/imgs/wx.png)
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
### Commit 类型
* feat: 新特性或功能
* fix: 缺陷修复
* docs: 文档更新
* style: 代码风格或者组件样式更新
* refactor: 代码重构,不引入新功能和缺陷修复
* opt: 性能优化
* chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
## 打赏
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
![打赏](docs/imgs/donate.png)
![Star History Chart](https://api.star-history.com/svg?repos=yangjian102621/geekai&type=Date)
_让 AI 成为你最强大的创作伙伴,开启无限创意可能!_
+2
View File
@@ -16,6 +16,8 @@ tmp
bin
data
config.toml
config.toml.*.bak
static/upload
static/audio
storage.json
res/certs/wechat/apiclient_key.pem
+2 -2
View File
@@ -3,11 +3,11 @@ NAME := geekai
all: amd64 arm64
amd64:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/$(NAME)-linux main.go
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o bin/$(NAME)-linux main.go
.PHONY: amd64
arm64:
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -o bin/$(NAME)-linux main.go
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build -ldflags "-s -w" -o bin/$(NAME)-linux main.go
.PHONY: arm64
clean:
+18 -22
View File
@@ -1,6 +1,6 @@
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=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
MysqlDns = "root:12345678@tcp(localhost:3306)/geekai?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=True&loc=Local"
StaticDir = "./static" # 静态资源的目录
StaticUrl = "/static" # 静态资源访问 URL
TikaHost = "http://tika:9998"
@@ -71,6 +71,15 @@ TikaHost = "http://tika:9998"
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
RegistryKey = "chatgpt-plus" # 任务注册 key
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
UseTls = false
Host = "smtp.163.com"
Port = 25
AppName = "极客学长"
From = "test@163.com" # 发件邮箱人地址
Password = "" #邮箱 stmp 服务授权码
# 支付宝商户支付
[AlipayConfig]
Enabled = false # 启用支付宝支付通道
SandBox = false # 是否启用沙盒模式
@@ -80,31 +89,13 @@ TikaHost = "http://tika:9998"
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
NotifyURL = "https://ai.r9it.com/api/payment/alipay/notify" # 支付异步回调地址
# 虎皮椒支付
[HuPiPayConfig]
Enabled = false
Name = "wechat"
AppId = ""
AppSecret = ""
ApiURL = "https://api.xunhupay.com"
NotifyURL = "https://ai.r9it.com/api/payment/hupipay/notify"
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
UseTls = false
Host = "smtp.163.com"
Port = 25
AppName = "极客学长"
From = "test@163.com" # 发件邮箱人地址
Password = "" #邮箱 stmp 服务授权码
[JPayConfig] # PayJs 支付配置
Enabled = false
Name = "wechat" # 请不要改动
AppId = "" # 商户 ID
PrivateKey = "" # 秘钥
ApiURL = "https://payjs.cn"
NotifyURL = "https://ai.r9it.com/api/payment/payjs/notify" # 异步回调地址,域名改成你自己的
# 微信商户支付
[WechatPayConfig]
@@ -114,6 +105,11 @@ TikaHost = "http://tika:9998"
SerialNo = "" # API 证书序列号
PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内
ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的
NotifyURL = "https://ai.r9it.com/api/payment/wechat/notify" # 支付成功异步回调地址,域名改成自己的
ReturnURL = "" # 支付成功同步回调地址
# 易支付
[GeekPayConfig]
Enabled = true
AppId = "" # 商户ID
PrivateKey = "" # 商户私钥
ApiURL = "https://pay.geekai.cn"
Methods = ["alipay", "wxpay", "qqpay", "jdpay", "douyin", "paypal"] # 支持的支付方式
+38 -316
View File
@@ -8,55 +8,44 @@ package core
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bytes"
"context"
"fmt"
"geekai/core/middleware"
"geekai/core/types"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"github.com/nfnt/resize"
"golang.org/x/image/webp"
"gorm.io/gorm"
"image"
"image/jpeg"
"io"
"net/http"
"os"
"runtime/debug"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/imroc/req/v3"
"github.com/shirou/gopsutil/host"
"gorm.io/gorm"
)
type AppServer struct {
Debug bool
Config *types.AppConfig
Engine *gin.Engine
SysConfig *types.SystemConfig // system config cache
Redis *redis.Client
}
func NewServer(appConfig *types.AppConfig) *AppServer {
func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types.SystemConfig) *AppServer {
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard
return &AppServer{
Debug: false,
Config: appConfig,
Engine: gin.Default(),
Config: appConfig,
Redis: redis,
Engine: gin.Default(),
SysConfig: sysConfig,
}
}
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(staticResourceMiddleware())
s.Engine.Use(authorizeMiddleware(s, client))
s.Engine.Use(parameterHandlerMiddleware())
func (s *AppServer) Init(client *redis.Client) {
s.Engine.Use(middleware.ThumbMiddleware())
s.Engine.Use(middleware.ParameterHandlerMiddleware())
s.Engine.Use(errorHandler)
// 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir)
@@ -65,14 +54,30 @@ func (s *AppServer) Init(debug bool, client *redis.Client) {
func (s *AppServer) Run(db *gorm.DB) error {
// 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)
err := db.Where("name", "system").First(&sysConfig).Error
if err != nil {
return err
return fmt.Errorf("failed to load system config: %v", err)
}
err = utils.JsonDecode(sysConfig.Value, &s.SysConfig)
if err != nil {
return fmt.Errorf("failed to decode system config: %v", err)
}
// 统计安装信息
go func() {
info, err := host.Info()
if err == nil {
apiURL := fmt.Sprintf("%s/api/installs/push", types.GeekAPIURL)
timestamp := time.Now().Unix()
product := "geekai-plus"
signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp)
sign := utils.Sha256(signStr)
resp, err := req.C().R().SetBody(map[string]interface{}{"product": product, "device_id": info.HostID, "timestamp": timestamp, "sign": sign}).Post(apiURL)
if err == nil {
logger.Debugf("register install info success: %v", resp.String())
}
}
}()
logger.Infof("http://%s", s.Config.Listen)
return s.Engine.Run(s.Config.Listen)
}
@@ -90,286 +95,3 @@ func errorHandler(c *gin.Context) {
//加载完 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) {
var tokenString string
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
if isAdminApi { // 后台管理 API
tokenString = c.GetHeader(types.AdminAuthHeader)
} else if c.Request.URL.Path == "/api/chat/new" {
tokenString = c.Query("token")
} else {
tokenString = c.GetHeader(types.UserAuthHeader)
}
if tokenString == "" {
if needLogin(c) {
resp.NotAuth(c, "You should put Authorization in request headers")
c.Abort()
return
} else { // 直接放行
c.Next()
return
}
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok && needLogin(c) {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
if isAdminApi {
return []byte(s.Config.AdminSession.SecretKey), nil
} else {
return []byte(s.Config.Session.SecretKey), nil
}
})
if err != nil && needLogin(c) {
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 && needLogin(c) {
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() && needLogin(c) {
resp.NotAuth(c, "Token is expired")
c.Abort()
return
}
key := fmt.Sprintf("users/%v", claims["user_id"])
if isAdminApi {
key = fmt.Sprintf("admin/%v", claims["user_id"])
}
if _, err := client.Get(context.Background(), key).Result(); err != nil && needLogin(c) {
resp.NotAuth(c, "Token is not found in redis")
c.Abort()
return
}
c.Set(types.LoginUserID, claims["user_id"])
}
}
func needLogin(c *gin.Context) bool {
if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/user/logout" ||
c.Request.URL.Path == "/api/user/resetPass" ||
c.Request.URL.Path == "/api/admin/login" ||
c.Request.URL.Path == "/api/admin/logout" ||
c.Request.URL.Path == "/api/admin/login/captcha" ||
c.Request.URL.Path == "/api/user/register" ||
c.Request.URL.Path == "/api/chat/history" ||
c.Request.URL.Path == "/api/chat/detail" ||
c.Request.URL.Path == "/api/chat/list" ||
c.Request.URL.Path == "/api/role/list" ||
c.Request.URL.Path == "/api/model/list" ||
c.Request.URL.Path == "/api/mj/imgWall" ||
c.Request.URL.Path == "/api/mj/client" ||
c.Request.URL.Path == "/api/mj/notify" ||
c.Request.URL.Path == "/api/invite/hits" ||
c.Request.URL.Path == "/api/sd/imgWall" ||
c.Request.URL.Path == "/api/sd/client" ||
c.Request.URL.Path == "/api/dall/imgWall" ||
c.Request.URL.Path == "/api/dall/client" ||
c.Request.URL.Path == "/api/product/list" ||
c.Request.URL.Path == "/api/menu/list" ||
c.Request.URL.Path == "/api/markMap/client" ||
c.Request.URL.Path == "/api/payment/alipay/notify" ||
c.Request.URL.Path == "/api/payment/hupipay/notify" ||
c.Request.URL.Path == "/api/payment/payjs/notify" ||
c.Request.URL.Path == "/api/payment/wechat/notify" ||
c.Request.URL.Path == "/api/payment/doPay" ||
c.Request.URL.Path == "/api/payment/payWays" ||
c.Request.URL.Path == "/api/suno/client" ||
c.Request.URL.Path == "/api/suno/detail" ||
c.Request.URL.Path == "/api/suno/play" ||
c.Request.URL.Path == "/api/download" ||
c.Request.URL.Path == "/api/video/client" ||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
strings.HasPrefix(c.Request.URL.Path, "/api/user/clogin") ||
strings.HasPrefix(c.Request.URL.Path, "/api/config/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/function/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/sms/") ||
strings.HasPrefix(c.Request.URL.Path, "/api/captcha/") ||
strings.HasPrefix(c.Request.URL.Path, "/static/") {
return false
}
return true
}
// 统一参数处理
func parameterHandlerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// GET 参数处理
params := c.Request.URL.Query()
for key, values := range params {
for i, value := range values {
params[key][i] = strings.TrimSpace(value)
}
}
// update get parameters
c.Request.URL.RawQuery = params.Encode()
// skip file upload requests
contentType := c.Request.Header.Get("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
c.Next()
return
}
if strings.Contains(contentType, "application/json") {
// process POST JSON request body
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Next()
return
}
// 还原请求体
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 将请求体解析为 JSON
var jsonData map[string]interface{}
if err := c.ShouldBindJSON(&jsonData); err != nil {
c.Next()
return
}
// 对 JSON 数据中的字符串值去除两端空格
trimJSONStrings(jsonData)
// 更新请求体
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
}
c.Next()
}
}
// 递归对 JSON 数据中的字符串值去除两端空格
func trimJSONStrings(data interface{}) {
switch v := data.(type) {
case map[string]interface{}:
for key, value := range v {
switch valueType := value.(type) {
case string:
v[key] = strings.TrimSpace(valueType)
case map[string]interface{}, []interface{}:
trimJSONStrings(value)
}
}
case []interface{}:
for i, value := range v {
switch valueType := value.(type) {
case string:
v[i] = strings.TrimSpace(valueType)
case map[string]interface{}, []interface{}:
trimJSONStrings(value)
}
}
}
}
// 静态资源中间件
func staticResourceMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
url := c.Request.URL.String()
// 拦截生成缩略图请求
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
r := strings.SplitAfter(url, "imageView2")
size := strings.Split(r[1], "/")
if len(size) != 8 {
c.String(http.StatusNotFound, "invalid thumb args")
return
}
with := utils.IntValue(size[3], 0)
height := utils.IntValue(size[5], 0)
quality := utils.IntValue(size[7], 75)
// 打开图片文件
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
file, err := os.Open(filePath)
if err != nil {
c.String(http.StatusNotFound, "Image not found")
return
}
defer file.Close()
// 解码图片
img, _, err := image.Decode(file)
// for .webp image
if err != nil {
img, err = webp.Decode(file)
}
if err != nil {
c.String(http.StatusInternalServerError, "Error decoding image")
return
}
var newImg image.Image
if height == 0 || with == 0 {
// 固定宽度,高度自适应
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
} else {
// 生成缩略图
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
}
var buffer bytes.Buffer
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
if err != nil {
logger.Error(err)
c.String(http.StatusInternalServerError, err.Error())
return
}
// 设置图片缓存有效期为一年 (365天)
c.Header("Cache-Control", "max-age=31536000, public")
// 直接输出图像数据流
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
c.Abort() // 中断请求
}
c.Next()
}
}
+97 -2
View File
@@ -11,10 +11,12 @@ import (
"bytes"
"geekai/core/types"
logger2 "geekai/logger"
"geekai/store/model"
"geekai/utils"
"os"
"github.com/BurntSushi/toml"
"gorm.io/gorm"
)
var logger = logger2.GetLogger()
@@ -30,7 +32,6 @@ func NewDefaultConfig() *types.AppConfig {
SecretKey: utils.RandString(64),
MaxAge: 86400,
},
ApiConfig: types.ApiConfig{},
OSS: types.OSSConfig{
Active: "local",
Local: types.LocalStorageConfig{
@@ -38,7 +39,6 @@ func NewDefaultConfig() *types.AppConfig {
BasePath: "./static/upload",
},
},
AlipayConfig: types.AlipayConfig{Enabled: false, SandBox: false},
}
}
@@ -74,3 +74,98 @@ func SaveConfig(config *types.AppConfig) error {
return os.WriteFile(config.Path, buf.Bytes(), 0644)
}
func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
// 加载系统配置
var sysConfig model.Config
var baseConfig types.BaseConfig
db.Where("name", "system").First(&sysConfig)
err := utils.JsonDecode(sysConfig.Value, &baseConfig)
if err != nil {
logger.Error("load system config error: ", err)
}
// 加载验证码配置
var captchaConfig types.CaptchaConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyCaptcha).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &captchaConfig)
if err != nil {
logger.Error("load geek service config error: ", err)
}
// 加载微信登录配置
var wxLoginConfig types.WxLoginConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyWxLogin).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &wxLoginConfig)
if err != nil {
logger.Error("load wx login config error: ", err)
}
// 加载短信配置
var smsConfig types.SMSConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeySms).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &smsConfig)
if err != nil {
logger.Error("load sms config error: ", err)
}
// 加载 OSS 配置
var ossConfig types.OSSConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyOss).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &ossConfig)
if err != nil {
logger.Error("load oss config error: ", err)
}
// 加载 SMTP 配置
var smtpConfig types.SmtpConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeySmtp).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &smtpConfig)
if err != nil {
logger.Error("load smtp config error: ", err)
}
// 加载支付配置
var paymentConfig types.PaymentConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyPayment).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &paymentConfig)
if err != nil {
logger.Error("load payment config error: ", err)
}
// 加载文本审查配置
var moderationConfig types.ModerationConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyModeration).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &moderationConfig)
if err != nil {
logger.Error("load moderation config error: ", err)
}
// 加载即梦AI配置
var jimengConfig types.JimengConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyJimeng).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &jimengConfig)
if err != nil {
logger.Error("load jimeng config error: ", err)
}
return &types.SystemConfig{
Base: baseConfig,
SMS: smsConfig,
OSS: ossConfig,
SMTP: smtpConfig,
Payment: paymentConfig,
Captcha: captchaConfig,
WxLogin: wxLoginConfig,
Moderation: moderationConfig,
Jimeng: jimengConfig,
}
}
+112
View File
@@ -0,0 +1,112 @@
package middleware
import (
"context"
"fmt"
"geekai/core/types"
logger2 "geekai/logger"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt"
)
var logger = logger2.GetLogger()
// 前端用户授权验证
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader(types.UserAuthHeader)
if tokenString == "" {
resp.NotAuth(c, "无效的授权令牌")
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("不支持的令牌签名方法: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err != nil {
resp.NotAuth(c, fmt.Sprintf("解析授权令牌失败: %v", err))
c.Abort()
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
resp.NotAuth(c, "令牌无效")
c.Abort()
return
}
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
if expr > 0 && int64(expr) < time.Now().Unix() {
resp.NotAuth(c, "令牌过期")
c.Abort()
return
}
key := fmt.Sprintf("users/%v", claims["user_id"])
if _, err := redis.Get(context.Background(), key).Result(); err != nil {
resp.NotAuth(c, "当前用户已退出登录")
c.Abort()
return
}
c.Set(types.LoginUserID, claims["user_id"])
}
}
// 管理后台用户授权验证
func AdminAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader(types.AdminAuthHeader)
if tokenString == "" {
resp.NotAuth(c, "无效的授权令牌")
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("不支持的令牌签名方法: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})
if err != nil {
resp.NotAuth(c, fmt.Sprintf("解析授权令牌失败: %v", err))
c.Abort()
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
resp.NotAuth(c, "令牌无效")
c.Abort()
return
}
expr := utils.IntValue(utils.InterfaceToString(claims["expired"]), 0)
if expr > 0 && int64(expr) < time.Now().Unix() {
resp.NotAuth(c, "令牌过期")
c.Abort()
return
}
key := fmt.Sprintf("admin/%v", claims["user_id"])
if _, err := redis.Get(context.Background(), key).Result(); err != nil {
resp.NotAuth(c, "当前用户已退出登录")
c.Abort()
return
}
c.Set(types.AdminUserID, claims["user_id"])
}
}
+80
View File
@@ -0,0 +1,80 @@
package middleware
import (
"bytes"
"geekai/utils"
"io"
"strings"
"github.com/gin-gonic/gin"
)
// 统一参数处理
func ParameterHandlerMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// GET 参数处理
params := c.Request.URL.Query()
for key, values := range params {
for i, value := range values {
params[key][i] = strings.TrimSpace(value)
}
}
// update get parameters
c.Request.URL.RawQuery = params.Encode()
// skip file upload requests
contentType := c.Request.Header.Get("Content-Type")
if strings.Contains(contentType, "multipart/form-data") {
c.Next()
return
}
if strings.Contains(contentType, "application/json") {
// process POST JSON request body
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.Next()
return
}
// 还原请求体
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 将请求体解析为 JSON
var jsonData map[string]any
if err := c.ShouldBindJSON(&jsonData); err != nil {
c.Next()
return
}
// 对 JSON 数据中的字符串值去除两端空格
trimJSONStrings(jsonData)
// 更新请求体
c.Request.Body = io.NopCloser(bytes.NewBufferString(utils.JsonEncode(jsonData)))
}
c.Next()
}
}
// 递归对 JSON 数据中的字符串值去除两端空格
func trimJSONStrings(data any) {
switch v := data.(type) {
case map[string]any:
for key, value := range v {
switch valueType := value.(type) {
case string:
v[key] = strings.TrimSpace(valueType)
case map[string]any, []any:
trimJSONStrings(value)
}
}
case []any:
for i, value := range v {
switch valueType := value.(type) {
case string:
v[i] = strings.TrimSpace(valueType)
case map[string]any, []any:
trimJSONStrings(value)
}
}
}
}
+43
View File
@@ -0,0 +1,43 @@
package middleware
import (
"context"
"fmt"
"geekai/core/types"
"geekai/utils"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
)
// RateLimitEvery 使用 Redis 做固定间隔限流:在 interval 内仅允许一次请求
// Key 优先使用登录用户ID,若没有则退化为 route + IP
func RateLimitEvery(redisClient *redis.Client, interval time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
keyID := ""
if userID, ok := c.Get(types.LoginUserID); ok {
keyID = fmt.Sprintf("user:%s", utils.InterfaceToString(userID))
} else {
keyID = fmt.Sprintf("ip:%s", c.ClientIP())
}
fullPath := c.FullPath()
if fullPath == "" {
fullPath = c.Request.URL.Path
}
key := fmt.Sprintf("rl:%s:%s", fullPath, keyID)
okSet, err := redisClient.SetNX(context.Background(), key, 1, interval).Result()
if err != nil {
// Redis 异常时放行,避免误伤可用性
return
}
if !okSet {
c.JSON(http.StatusTooManyRequests, types.BizVo{Code: types.Failed, Message: "请求过于频繁,请稍后重试"})
c.Abort()
return
}
}
}
+78
View File
@@ -0,0 +1,78 @@
package middleware
import (
"bytes"
"geekai/utils"
"image"
"image/jpeg"
"net/http"
"os"
"strings"
"github.com/gin-gonic/gin"
"github.com/nfnt/resize"
"golang.org/x/image/webp"
)
// 缩略图中间件
func ThumbMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
url := c.Request.URL.String()
// 拦截生成缩略图请求
if strings.HasPrefix(url, "/static/") && strings.Contains(url, "?imageView2") {
r := strings.SplitAfter(url, "imageView2")
size := strings.Split(r[1], "/")
if len(size) != 8 {
c.String(http.StatusNotFound, "invalid thumb args")
return
}
with := utils.IntValue(size[3], 0)
height := utils.IntValue(size[5], 0)
quality := utils.IntValue(size[7], 75)
// 打开图片文件
filePath := strings.TrimLeft(c.Request.URL.Path, "/")
file, err := os.Open(filePath)
if err != nil {
c.String(http.StatusNotFound, "Image not found")
return
}
defer file.Close()
// 解码图片
img, _, err := image.Decode(file)
// for .webp image
if err != nil {
img, err = webp.Decode(file)
}
if err != nil {
c.String(http.StatusInternalServerError, "Error decoding image")
return
}
var newImg image.Image
if height == 0 || with == 0 {
// 固定宽度,高度自适应
newImg = resize.Resize(uint(with), uint(height), img, resize.Lanczos3)
} else {
// 生成缩略图
newImg = resize.Thumbnail(uint(with), uint(height), img, resize.Lanczos3)
}
var buffer bytes.Buffer
err = jpeg.Encode(&buffer, newImg, &jpeg.Options{Quality: quality})
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
// 设置图片缓存有效期为一年 (365天)
c.Header("Cache-Control", "max-age=31536000, public")
// 直接输出图像数据流
c.Data(http.StatusOK, "image/jpeg", buffer.Bytes())
c.Abort() // 中断请求
}
c.Next()
}
}
+26 -26
View File
@@ -9,19 +9,20 @@ package types
// ApiRequest API 请求实体
type ApiRequest struct {
Model string `json:"model,omitempty"` // 兼容百度文心一言
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens,omitempty"` // 兼容百度文心一言
Stream bool `json:"stream"`
Messages []interface{} `json:"messages,omitempty"`
Prompt []interface{} `json:"prompt,omitempty"` // 兼容 ChatGLM
Tools []Tool `json:"tools,omitempty"`
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台
Model string `json:"model,omitempty"`
Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens,omitempty"`
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型
Stream bool `json:"stream,omitempty"`
Messages []any `json:"messages,omitempty"`
Tools []Tool `json:"tools,omitempty"`
Functions []any `json:"functions,omitempty"` // 兼容中转平台
ResponseFormat any `json:"response_format,omitempty"` // 响应格式
ToolChoice string `json:"tool_choice,omitempty"`
Input map[string]interface{} `json:"input,omitempty"` //兼容阿里通义千问
Parameters map[string]interface{} `json:"parameters,omitempty"` //兼容阿里通义千问
Input map[string]any `json:"input,omitempty"` //兼容阿里通义千问
Parameters map[string]any `json:"parameters,omitempty"` //兼容阿里通义千问
}
type Message struct {
@@ -40,26 +41,17 @@ type ChoiceItem struct {
}
type Delta struct {
Role string `json:"role"`
Name string `json:"name"`
Content interface{} `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FunctionCall struct {
Role string `json:"role"`
Name string `json:"name"`
Content any `json:"content"`
ReasoningContent string `json:"reasoning_content,omitempty"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FunctionCall struct {
Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"`
} `json:"function_call,omitempty"`
}
// ChatSession 聊天会话对象
type ChatSession struct {
SessionId string `json:"session_id"`
UserId uint `json:"user_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
Model ChatModel `json:"model"` // GPT 模型
Tools string `json:"tools"` // 函数
}
type ChatModel struct {
Id uint `json:"id"`
Name string `json:"name"`
@@ -67,6 +59,8 @@ type ChatModel struct {
Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Description string `json:"description"` //模型描述
Category string `json:"category"` //模型类别
Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id"` // 绑定 API KEY
}
@@ -93,6 +87,7 @@ const (
PowerInvite = PowerType(4) // 邀请奖励
PowerRedeem = PowerType(5) // 众筹
PowerGift = PowerType(6) // 系统赠送
PowerSignIn = PowerType(7) // 每日签到
)
func (t PowerType) String() string {
@@ -105,7 +100,12 @@ func (t PowerType) String() string {
return "退款"
case PowerRedeem:
return "兑换"
case PowerGift:
return "赠送"
case PowerInvite:
return "邀请"
case PowerSignIn:
return "签到"
}
return "其他"
}
+3 -1
View File
@@ -17,15 +17,17 @@ var ErrConClosed = errors.New("connection Closed")
// WsClient websocket client
type WsClient struct {
Id string
Conn *websocket.Conn
lock sync.Mutex
mt int
Closed bool
}
func NewWsClient(conn *websocket.Conn) *WsClient {
func NewWsClient(conn *websocket.Conn, id string) *WsClient {
return &WsClient{
Conn: conn,
Id: id,
lock: sync.Mutex{},
mt: 2, // fixed bug for 'Invalid UTF-8 in text frame'
Closed: false,
+73 -128
View File
@@ -12,95 +12,22 @@ import (
)
type AppConfig struct {
Path string `toml:"-"`
Listen string
Session Session
AdminSession Session
ProxyURL string
MysqlDns string // mysql 连接地址
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
ApiConfig ApiConfig // ChatPlus API authorization configs
SMS SMSConfig // send mobile message config
OSS OSSConfig // OSS config
XXLConfig XXLConfig
AlipayConfig AlipayConfig // 支付宝支付渠道配置
HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置
SmtpConfig SmtpConfig // 邮件发送配置
JPayConfig JPayConfig // payjs 支付配置
WechatPayConfig WechatPayConfig // 微信支付渠道配置
TikaHost string // TiKa 服务器地址
}
type SmtpConfig struct {
UseTls bool // 是否使用 TLS 发送
Host string
Port int
AppName string // 应用名称
From string // 发件人邮箱地址
Password string // 发件人邮箱密码
}
type ApiConfig struct {
ApiURL string
AppId string
Token string
}
type AlipayConfig struct {
Enabled bool // 是否启用该支付通道
SandBox bool // 是否沙盒环境
AppId string // 应用 ID
UserId string // 支付宝用户 ID
PrivateKey string // 用户私钥文件路径
PublicKey string // 用户公钥文件路径
AlipayPublicKey string // 支付宝公钥文件路径
RootCert string // Root 秘钥路径
NotifyURL string // 异步通知回调
ReturnURL string // 支付成功返回地址
}
type WechatPayConfig struct {
Enabled bool // 是否启用该支付通道
AppId string // 公众号的APPID,如:wxd678efh567hg6787
MchId string // 直连商户的商户号,由微信支付生成并下发
SerialNo string // 商户证书的证书序列号
PrivateKey string // 用户私钥文件路径
ApiV3Key string // API V3 秘钥
NotifyURL string // 异步通知回调
ReturnURL string // 支付成功返回地址
}
type HuPiPayConfig struct { //虎皮椒第四方支付配置
Enabled bool // 是否启用该支付通道
Name string // 支付名称,如:wechat/alipay
AppId string // App ID
AppSecret string // app 密钥
ApiURL string // 支付网关
NotifyURL string // 异步通知回调
ReturnURL string // 支付成功返回地址
}
// JPayConfig PayJs 支付配置
type JPayConfig struct {
Enabled bool
Name string // 支付名称,默认 wechat
AppId string // 商户 ID
PrivateKey string // 私钥
ApiURL string // API 网关
NotifyURL string // 异步回调地址
ReturnURL string // 支付成功返回地址
}
type XXLConfig struct { // XXL 任务调度配置
Enabled bool
ServerAddr string
ExecutorIp string
ExecutorPort string
AccessToken string
RegistryKey string
Path string `toml:"-"`
Listen string
Session Session
AdminSession Session
ProxyURL string
MysqlDns string // mysql 连接地址
StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息
SMS SMSConfig // send mobile message config
OSS OSSConfig // OSS config
SmtpConfig SmtpConfig // 邮件发送配置
AlipayConfig AlipayConfig // 支付宝支付渠道配置
GeekPayConfig EpayConfig // GEEK 支付配置
WechatPayConfig WxPayConfig // 微信支付渠道配置
TikaHost string // TiKa 服务器地址
}
type RedisConfig struct {
@@ -110,49 +37,32 @@ type RedisConfig struct {
DB int
}
// LicenseKey 存储许可证书的 KEY
const LicenseKey = "Geek-AI-License"
type License struct {
Key string `json:"key"` // 许可证书密钥
MachineId string `json:"machine_id"` // 机器码
ExpiredAt int64 `json:"expired_at"` // 过期时间
IsActive bool `json:"is_active"` // 是否激活
Configs LicenseConfig `json:"configs"`
}
type LicenseConfig struct {
UserNum int `json:"user_num"` // 用户数量
DeCopy bool `json:"de_copy"` // 去版权
}
func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
type SystemConfig struct {
Title string `json:"title,omitempty"` // 网站标题
Slogan string `json:"slogan,omitempty"` // 网站 slogan
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
Logo string `json:"logo,omitempty"` // 形 Logo
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
DailyPower int `json:"daily_power,omitempty"` // 每日赠送算力
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
type BaseConfig struct {
Title string `json:"title,omitempty"` // 网站标题
Slogan string `json:"slogan,omitempty"` // 网站 slogan
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
Logo string `json:"logo,omitempty"` // 形 Logo
BarLogo string `json:"bar_logo,omitempty"` // 条形 Logo
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间,单位:分钟
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
DallPower int `json:"dall_power,omitempty"` // DALL-E-3消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力
MjPower int `json:"mj_power,omitempty"` // MJ消耗算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力
WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
@@ -162,10 +72,45 @@ type SystemConfig struct {
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
Copyright string `json:"copyright"` // 版权信息
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
IndexPage string `json:"index_page"` // 首页显示的页面
Copyright string `json:"copyright"` // 版权信息
ICP string `json:"icp"` // ICP 备案号
GaBeian string `json:"ga_beian"` // 公安备案号
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
EnableMobileSite bool `json:"enable_mobile_site"` // 是否开启手机站点
}
type SystemConfig struct {
Base BaseConfig
Payment PaymentConfig
OSS OSSConfig
SMS SMSConfig
SMTP SmtpConfig
Captcha CaptchaConfig
WxLogin WxLoginConfig
Jimeng JimengConfig
Moderation ModerationConfig
}
// 配置键名常量
const (
ConfigKeySystem = "system"
ConfigKeyNotice = "notice"
ConfigKeyAgreement = "agreement"
ConfigKeyPrivacy = "privacy"
ConfigKeyMarkMap = "mark_map"
ConfigKeyCaptcha = "captcha"
ConfigKeyWxLogin = "wx_login"
ConfigKeySms = "sms"
ConfigKeySmtp = "smtp"
ConfigKeyOss = "oss"
ConfigKeyPayment = "payment"
ConfigKeyModeration = "moderation"
ConfigKeyAI3D = "ai3d"
ConfigKeyJimeng = "jimeng"
)
+33
View File
@@ -0,0 +1,33 @@
package types
import "os"
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// GeekAI 增值服务
var GeekAPIURL = "https://sapi.geekai.me"
func init() {
if os.Getenv("GEEK_API_URL") != "" {
GeekAPIURL = os.Getenv("GEEK_API_URL")
}
}
// CaptchaConfig 行为验证码配置
type CaptchaConfig struct {
ApiKey string `json:"api_key,omitempty"`
Type string `json:"type,omitempty"` // 验证码类型, 可选值: "dot" 或 "slide"
Enabled bool `json:"enabled,omitempty"`
}
// WxLoginConfig 微信登录配置
type WxLoginConfig struct {
ApiKey string `json:"api_key,omitempty"`
NotifyURL string `json:"notify_url,omitempty"` // 登录成功回调 URL
Enabled bool `json:"enabled,omitempty"` // 是否启用微信登录
}
+66
View File
@@ -0,0 +1,66 @@
package types
// JimengConfig 即梦AI配置
type JimengConfig struct {
// 即梦AI的AccessKey和SecretKey
AccessKey string `json:"access_key,omitempty"`
SecretKey string `json:"secret_key,omitempty"`
// 火山引擎大模型专用的验证方式
ApiKey string `json:"api_key,omitempty"`
// 算力配置
Powers map[string]int `json:"powers,omitempty"`
}
// JMTaskStatus 任务状态
type JMTaskStatus string
const (
JMTaskStatusSubmited = JMTaskStatus("submited") // 任务已提交
JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务排队中
JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
JMTaskStatusDone = JMTaskStatus("done") // 处理完成
JMTaskStatusNotFound = JMTaskStatus("not_found") // 任务未找到
JMTaskStatusSuccess = JMTaskStatus("success") // 任务成功
JMTaskStatusFailed = JMTaskStatus("failed") // 任务失败
JMTaskStatusExpired = JMTaskStatus("expired") // 任务过期
)
// JMTaskType 任务类型
type JMTaskType string
const (
JMTaskTypeImage = JMTaskType("image") // 文生图
JMTaskTypeVideo = JMTaskType("video") // 图生图
JMTaskTypeVirtualHuman = JMTaskType("virtual_human") // 图像编辑
JMTaskTypeActionTransfer = JMTaskType("action_transfer") // 图像特效
)
// JimengTaskRequest 即梦AI任务请求
type JimengTaskRequest struct {
TaskType JMTaskType `json:"type"` // 任务类型
ReqKey string `json:"req_key"` // 请求Key
Action string `json:"action"` // 请求Action
Power int `json:"power"` // 消耗算力
// 公共参数
Prompt string `json:"prompt,omitempty"`
ImageUrls []string `json:"image_urls,omitempty"`
// 图片生成参数
Size string `json:"size,omitempty"`
UsePreLLM bool `json:"use_pre_llm,omitempty"`
Scale float64 `json:"scale,omitempty"`
ForceSingle bool `json:"force_single,omitempty"`
// 视频生成参数
Duration int `json:"duration,omitempty"` // 视频时长,单位:秒
TemplateId string `json:"template_id,omitempty"` // 运镜模板ID
AspectRatio string `json:"aspect_ratio,omitempty"`
CameraStrength string `json:"camera_strength,omitempty"` // 运镜强度
// 数字人视频生成参数
AudioURL string `json:"audio_url,omitempty"` // 音频URL
RecognizeKey string `json:"recognize_key,omitempty"` // 识别主体请求Key
// 视频动作迁移参数
VideoURL string `json:"video_url,omitempty"` // 动作视频URL
}
+1 -1
View File
@@ -16,7 +16,7 @@ type MKey interface {
string | int | uint
}
type MValue interface {
*WsClient | *ChatSession | context.CancelFunc | []Message
*WsClient | context.CancelFunc | []any
}
type LMap[K MKey, T MValue] struct {
lock sync.RWMutex
+73
View File
@@ -0,0 +1,73 @@
package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// 文本审查
type ModerationConfig struct {
Enable bool `json:"enable,omitempty"` // 是否启用文本审查
Active string `json:"active,omitempty"`
EnableGuide bool `json:"enable_guide,omitempty"` // 是否启用模型引导提示词
GuidePrompt string `json:"guide_prompt,omitempty"` // 模型引导提示词
Gitee ModerationGiteeConfig `json:"gitee,omitempty"`
Baidu ModerationBaiduConfig `json:"baidu,omitempty"`
Tencent ModerationTencentConfig `json:"tencent,omitempty"`
}
const (
ModerationGitee = "gitee"
ModerationBaidu = "baidu"
ModerationTencent = "tencent"
)
// GiteeAI 文本审查配置
type ModerationGiteeConfig struct {
ApiKey string `json:"api_key,omitempty"`
Model string `json:"model,omitempty"` // 文本审核模型
}
// 百度文本审查配置
type ModerationBaiduConfig struct {
AccessKey string `json:"access_key,omitempty"`
SecretKey string `json:"secret_key,omitempty"`
}
// 腾讯云文本审查配置
type ModerationTencentConfig struct {
AccessKey string `json:"access_key,omitempty"`
SecretKey string `json:"secret_key,omitempty"`
}
type ModerationResult struct {
Flagged bool `json:"flagged,omitempty"`
Categories map[string]bool `json:"categories,omitempty"`
CategoryScores map[string]float64 `json:"category_scores,omitempty"`
}
var ModerationCategories = map[string]string{
"politic": "内容涉及人物、事件或敏感的政治观点",
"porn": "明确的色情内容",
"insult": "具有侮辱、攻击性语言、人身攻击或冒犯性表达",
"violence": "包含暴力、血腥、攻击行为或煽动暴力的言论",
"illegal": "涉及违法活动的内容,如诈骗、赌博等",
"terror": "宣扬恐怖主义、极端暴力或煽动恐怖行为的内容",
"ad": "垃圾广告或未经许可的推广内容",
"spam": "无意义重复内容或诱导性信息",
"abuse": "人身攻击、恶意辱骂或侮辱性言论",
"polity": "涉及国家政治、领导人或政策的违规讨论内容",
}
// 敏感词来源
const (
ModerationSourceChat = "chat"
ModerationSourceMJ = "mj"
ModerationSourceDalle = "dalle"
ModerationSourceSD = "sd"
ModerationSourceSuno = "suno"
ModerationSourceVideo = "video"
ModerationSourceJiMeng = "jimeng"
)
+18 -7
View File
@@ -11,14 +11,25 @@ type OrderStatus int
const (
OrderNotPaid = OrderStatus(0)
OrderScanned = OrderStatus(1) // 已扫码
OrderPaidSuccess = OrderStatus(2)
OrderPaidSuccess = OrderStatus(2) // 已支付
OrderPaidFailed = OrderStatus(3) // 已关闭
)
type OrderRemark struct {
Days int `json:"days"` // 有效期
Power int `json:"power"` // 增加算力点数
Name string `json:"name"` // 产品名称
Price float64 `json:"price"`
Discount float64 `json:"discount"`
Days int `json:"days"` // 有效期
Power int `json:"power"` // 增加算力点数
Name string `json:"name"` // 产品名称
Price float64 `json:"price"`
}
// PayChannel 支付渠道
var PayChannel = map[string]string{
"alipay": "支付宝商号",
"wxpay": "微信商号",
"epay": "易支付",
}
var PayWays = map[string]string{
"alipay": "支付宝",
"wxpay": "微信支付",
}
+24 -26
View File
@@ -8,41 +8,39 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type OSSConfig struct {
Active string
Local LocalStorageConfig
Minio MiniOssConfig
QiNiu QiNiuOssConfig
AliYun AliYunOssConfig
Active string `json:"active,omitempty"`
Local LocalStorageConfig `json:"local,omitempty"`
Minio MiniOssConfig `json:"minio,omitempty"`
QiNiu QiNiuOssConfig `json:"qiniu,omitempty"`
AliYun AliYunOssConfig `json:"aliyun,omitempty"`
}
type MiniOssConfig struct {
Endpoint string
AccessKey string
AccessSecret string
Bucket string
SubDir string
UseSSL bool
Domain string
Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret,omitempty"`
Bucket string `json:"bucket,omitempty"`
UseSSL bool `json:"use_ssl,omitempty"`
Domain string `json:"domain,omitempty"`
}
type QiNiuOssConfig struct {
Zone string
AccessKey string
AccessSecret string
Bucket string
SubDir string
Domain string
Zone string `json:"zone,omitempty"`
AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret,omitempty"`
Bucket string `json:"bucket,omitempty"`
Domain string `json:"domain,omitempty"`
}
type AliYunOssConfig struct {
Endpoint string
AccessKey string
AccessSecret string
Bucket string
SubDir string
Domain string
Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret,omitempty"`
Bucket string `json:"bucket,omitempty"`
Domain string `json:"domain,omitempty"`
}
type LocalStorageConfig struct {
BasePath string
BaseURL string
BasePath string `json:"base_path,omitempty"`
BaseURL string `json:"base_url,omitempty"`
}
+60
View File
@@ -0,0 +1,60 @@
package types
type PaymentConfig struct {
Alipay AlipayConfig `json:"alipay,omitempty"` // 支付宝支付渠道配置
Epay EpayConfig `json:"epay,omitempty"` // 易支付配置
WxPay WxPayConfig `json:"wxpay,omitempty"` // 微信支付渠道配置
}
// AlipayConfig 支付宝支付配置
type AlipayConfig struct {
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
SandBox bool `json:"sandbox,omitempty"` // 是否沙盒环境
AppId string `json:"app_id,omitempty"` // 应用 ID
PrivateKey string `json:"private_key,omitempty"` // 应用私钥
AlipayPublicKey string `json:"alipay_public_key,omitempty"` // 支付宝公钥
Domain string `json:"domain,omitempty"` // 支付回调域名
}
func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
return c.AppId == other.AppId &&
c.PrivateKey == other.PrivateKey &&
c.AlipayPublicKey == other.AlipayPublicKey &&
c.Domain == other.Domain
}
// WxPayConfig 微信支付配置
type WxPayConfig struct {
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
AppId string `json:"app_id,omitempty"` // 公众号的APPID,如:wxd678efh567hg6787
MchId string `json:"mch_id,omitempty"` // 直连商户的商户号,由微信支付生成并下发
SerialNo string `json:"serial_no,omitempty"` // 商户证书的证书序列号
PrivateKey string `json:"private_key,omitempty"` // 商户证书私钥
ApiV3Key string `json:"api_v3_key,omitempty"` // API V3 秘钥
Domain string `json:"domain,omitempty"` // 支付回调域名
}
func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
return c.AppId == other.AppId &&
c.MchId == other.MchId &&
c.SerialNo == other.SerialNo &&
c.PrivateKey == other.PrivateKey &&
c.ApiV3Key == other.ApiV3Key &&
c.Domain == other.Domain
}
// EpayConfig 易支付配置
type EpayConfig struct {
Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
AppId string `json:"app_id,omitempty"` // 商户 ID
PrivateKey string `json:"private_key,omitempty"` // 私钥
ApiURL string `json:"api_url,omitempty"` // z支付 API 网关
Domain string `json:"domain,omitempty"` // 支付回调域名
}
func (c *EpayConfig) Equal(other *EpayConfig) bool {
return c.AppId == other.AppId &&
c.PrivateKey == other.PrivateKey &&
c.ApiURL == other.ApiURL &&
c.Domain == other.Domain
}
+1
View File
@@ -8,6 +8,7 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
const LoginUserID = "LOGIN_USER_ID"
const AdminUserID = "ADMIN_USER_ID"
const LoginUserCache = "LOGIN_USER_CACHE"
const UserAuthHeader = "Authorization"
+11 -14
View File
@@ -8,26 +8,23 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type SMSConfig struct {
Active string
Ali SmsConfigAli
Bao SmsConfigBao
Active string `json:"active,omitempty"`
Ali SmsConfigAli `json:"aliyun,omitempty"`
Bao SmsConfigBao `json:"bao,omitempty"`
}
// SmsConfigAli 阿里云短信平台配置
type SmsConfigAli struct {
AccessKey string
AccessSecret string
Product string
Domain string
Sign string // 短信签名
CodeTempId string // 验证码短信模板 ID
AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret,omitempty"`
Sign string `json:"sign,omitempty"` // 短信签名
CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板 ID
}
// SmsConfigBao 短信宝平台配置
type SmsConfigBao struct {
Username string //短信宝平台注册的用户名
Password string //短信宝平台注册的密码
Domain string //域名
Sign string // 短信签名
CodeTemplate string // 验证码短信模板 匹配
Username string `json:"username,omitempty"` //短信宝平台注册的用户名
Password string `json:"password,omitempty"` //短信宝平台注册的密码
Sign string `json:"sign,omitempty"` // 短信签名
CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配
}
+26
View File
@@ -0,0 +1,26 @@
package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type SmtpConfig struct {
UseTls bool `json:"use_tls,omitempty"` // 是否使用 TLS 发送
Host string `json:"host,omitempty"` // 邮件服务器地址
Port int `json:"port,omitempty"` // 邮件服务器端口
AppName string `json:"app_name,omitempty"` // 应用名称
From string `json:"from,omitempty"` // 发件人邮箱地址
Password string `json:"password,omitempty"` // 发件人邮箱密码
}
func (s *SmtpConfig) Equal(other *SmtpConfig) bool {
return s.UseTls == other.UseTls &&
s.Host == other.Host &&
s.Port == other.Port &&
s.AppName == other.AppName &&
s.From == other.From &&
s.Password == other.Password
}
+73 -37
View File
@@ -24,28 +24,29 @@ const (
// MjTask MidJourney 任务
type MjTask struct {
Id uint `json:"id"`
TaskId string `json:"task_id"`
ImgArr []string `json:"img_arr"`
Type TaskType `json:"type"`
UserId int `json:"user_id"`
Prompt string `json:"prompt,omitempty"`
NegPrompt string `json:"neg_prompt,omitempty"`
Params string `json:"full_prompt"`
Index int `json:"index,omitempty"`
MessageId string `json:"message_id,omitempty"`
MessageHash string `json:"message_hash,omitempty"`
RetryCount int `json:"retry_count"`
ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道
Mode string `json:"mode"` // 绘画模式,relax, fast, turbo
Id uint `json:"id"` // 任务ID
TaskId string `json:"task_id"` // 中转任务ID
ImgArr []string `json:"img_arr"`
Type TaskType `json:"type"`
UserId int `json:"user_id"`
Prompt string `json:"prompt,omitempty"`
NegPrompt string `json:"neg_prompt,omitempty"`
Params string `json:"full_prompt"`
Index int `json:"index,omitempty"`
MessageId string `json:"message_id,omitempty"`
MessageHash string `json:"message_hash,omitempty"`
ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道
Mode string `json:"mode"` // 绘画模式,relax, fast, turbo
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type SdTask struct {
Id int `json:"id"` // job 数据库ID
Type TaskType `json:"type"`
UserId int `json:"user_id"`
Params SdTaskParams `json:"params"`
RetryCount int `json:"retry_count"`
Id int `json:"id"` // job 数据库ID
Type TaskType `json:"type"`
UserId int `json:"user_id"`
Params SdTaskParams `json:"params"`
RetryCount int `json:"retry_count"`
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type SdTaskParams struct {
@@ -69,15 +70,17 @@ type SdTaskParams struct {
// DallTask DALL-E task
type DallTask struct {
JobId uint `json:"job_id"`
UserId uint `json:"user_id"`
Prompt string `json:"prompt"`
N int `json:"n"`
Quality string `json:"quality"`
Size string `json:"size"`
Style string `json:"style"`
Power int `json:"power"`
ModelId uint `json:"model_id"`
ModelName string `json:"model_name"`
ModelValue string `json:"model_value"`
Image []string `json:"image,omitempty"`
Id uint `json:"id"`
UserId uint `json:"user_id"`
Prompt string `json:"prompt"`
AspectRatio string `json:"aspect_ratio"`
Size string `json:"size"`
Power int `json:"power"`
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type SunoTask struct {
@@ -88,7 +91,8 @@ type SunoTask struct {
Title string `json:"title"`
RefTaskId string `json:"ref_task_id,omitempty"`
RefSongId string `json:"ref_song_id,omitempty"`
Prompt string `json:"prompt"` // 提示词/歌词
Prompt string `json:"prompt"` // 提示词
Lyrics string `json:"lyrics,omitempty"` // 歌词
Tags string `json:"tags"`
Model string `json:"model"`
Instrumental bool `json:"instrumental"` // 是否纯音乐
@@ -101,19 +105,21 @@ const (
VideoLuma = "luma"
VideoRunway = "runway"
VideoCog = "cog"
VideoKeLing = "keling"
)
type VideoTask struct {
Id uint `json:"id"`
Channel string `json:"channel"`
UserId int `json:"user_id"`
Type string `json:"type"`
TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词
Params VideoParams `json:"params"`
Id uint `json:"id"`
Channel string `json:"channel"`
UserId int `json:"user_id"`
Type string `json:"type"`
TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词
Params interface{} `json:"params"`
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type VideoParams struct {
type LumaVideoParams struct {
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
Loop bool `json:"loop"` // 是否循环参考图
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
@@ -123,3 +129,33 @@ type VideoParams struct {
Style string `json:"style"` // 风格
Duration int `json:"duration"` // 视频时长(秒)
}
type KeLingVideoParams struct {
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
Model string `json:"model"` // 模型: default/anime
Prompt string `json:"prompt"` // 视频描述
NegPrompt string `json:"negative_prompt"` // 负面提示词
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
Mode string `json:"mode"` // 生成模式: std/pro
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
Duration string `json:"duration"` // 视频时长: 5/10
CameraControl CameraControl `json:"camera_control"` // 摄像机控制
Image string `json:"image"` // 参考图片URL(image2video)
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
}
// CameraControl 摄像机控制
type CameraControl struct {
Type string `json:"type"` // 控制类型: simple/down_back/forward_up/right_turn_forward/left_turn_forward
Config CameraConfig `json:"config"` // 控制参数(仅simple类型时使用)
}
// CameraConfig 摄像机参数
type CameraConfig struct {
Horizontal int `json:"horizontal"` // 水平移动(-10到10)
Vertical int `json:"vertical"` // 垂直移动(-10到10)
Pan int `json:"pan"` // 左右旋转(-10到10)
Tilt int `json:"tilt"` // 上下旋转(-10到10)
Roll int `json:"roll"` // 横向翻转(-10到10)
Zoom int `json:"zoom"` // 镜头缩放(-10到10)
}
+45
View File
@@ -0,0 +1,45 @@
package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import "sync"
// UserLockManager 提供基于用户ID的TryLock功能,确保同一用户并发请求串行化
type UserLockManager struct {
mu sync.Mutex
locks map[uint]bool
}
func NewUserLockManager() *UserLockManager {
return &UserLockManager{mu: sync.Mutex{}, locks: make(map[uint]bool)}
}
// TryLock 尝试为指定用户加锁。若已被占用返回 false
func (m *UserLockManager) TryLock(userId uint) bool {
if userId == 0 {
return true
}
m.mu.Lock()
defer m.mu.Unlock()
if m.locks[userId] {
return false
}
m.locks[userId] = true
return true
}
// Unlock 释放指定用户的锁
func (m *UserLockManager) Unlock(userId uint) {
if userId == 0 {
return
}
m.mu.Lock()
delete(m.locks, userId)
m.mu.Unlock()
}
+36 -8
View File
@@ -17,21 +17,49 @@ type BizVo struct {
Data interface{} `json:"data,omitempty"`
}
// WsMessage Websocket message
type WsMessage struct {
Type WsMsgType `json:"type"` // 消息类别,start, end, img
Content interface{} `json:"content"`
// ReplyMessage 对话回复消息结构
type ReplyMessage struct {
Channel WsChannel `json:"channel"` // 消息频道,目前只有 chat
ClientId string `json:"clientId"` // 客户端ID
Type WsMsgType `json:"type"` // 消息类别
Body interface{} `json:"body"`
}
type WsMsgType string
type WsChannel string
const (
WsStart = WsMsgType("start")
WsMiddle = WsMsgType("middle")
WsEnd = WsMsgType("end")
WsErr = WsMsgType("error")
MsgTypeText = WsMsgType("text") // 输出内容
MsgTypeEnd = WsMsgType("end")
MsgTypeErr = WsMsgType("error")
MsgTypePing = WsMsgType("ping") // 心跳消息
ChPing = WsChannel("ping")
ChChat = WsChannel("chat")
ChMj = WsChannel("mj")
ChSd = WsChannel("sd")
ChDall = WsChannel("dall")
ChSuno = WsChannel("suno")
ChLuma = WsChannel("luma")
ChKeLing = WsChannel("keling")
)
// InputMessage 对话输入消息结构
type InputMessage struct {
Channel WsChannel `json:"channel"` // 消息频道
Type WsMsgType `json:"type"` // 消息类别
Body interface{} `json:"body"`
}
type ChatMessage struct {
Tools []int `json:"tools,omitempty"` // 允许调用工具列表
Stream bool `json:"stream,omitempty"` // 是否采用流式输出
RoleId int `json:"role_id"`
ModelId int `json:"model_id"`
ChatId string `json:"chat_id"`
Content string `json:"content"`
}
type BizCode int
const (
+1 -1
View File
@@ -4,7 +4,7 @@ build_name: runner-build
build_log: runner-build-errors.log
valid_ext: .go, .tpl, .tmpl, .html
no_rebuild_ext: .tpl, .tmpl, .html, .js, .vue
ignored: assets, tmp, web, .git, .idea, test, data
ignored: assets, tmp, web, .git, .idea, test, data, static
build_delay: 600
colors: 1
log_color_main: cyan
+8 -6
View File
@@ -18,20 +18,22 @@ require (
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
github.com/qiniu/go-sdk/v7 v7.17.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/volcengine/volc-sdk-golang v1.0.23
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/xxl-job/xxl-job-executor-go v1.2.0
require (
github.com/go-pay/gopay v1.5.101
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/google/go-tika v0.3.1
github.com/microcosm-cc/bluemonday v1.0.26
github.com/sashabaranov/go-openai v1.38.1
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.3.1
github.com/syndtr/goleveldb v1.0.0
github.com/volcengine/volcengine-go-sdk v1.1.34
golang.org/x/image v0.15.0
)
@@ -43,12 +45,13 @@ require (
github.com/go-pay/util v0.0.2 // indirect
github.com/go-pay/xlog v0.0.2 // indirect
github.com/go-pay/xtime v0.0.2 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
@@ -61,7 +64,6 @@ require (
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-basic/ipv4 v1.0.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
@@ -71,7 +73,7 @@ require (
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/jmespath/go-jmespath v0.4.0 // 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
@@ -113,7 +115,7 @@ require (
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
go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.23.0
golang.org/x/sys v0.20.0 // indirect
gorm.io/gorm v1.25.1
+85 -8
View File
@@ -1,3 +1,5 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
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=
@@ -6,6 +8,7 @@ github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiw
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
@@ -13,11 +16,13 @@ github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZx
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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
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=
@@ -28,6 +33,8 @@ github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0
github.com/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/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/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
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/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
@@ -39,8 +46,6 @@ 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-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
@@ -80,13 +85,33 @@ github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/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 v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
@@ -113,8 +138,11 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.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/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
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=
@@ -125,6 +153,7 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
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.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
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=
@@ -177,6 +206,7 @@ github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 h1:IFhPCcB0/H
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/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
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=
@@ -192,6 +222,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
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/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
@@ -204,6 +236,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
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=
@@ -226,8 +259,10 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ=
github.com/xxl-job/xxl-job-executor-go v1.2.0/go.mod h1:bUFhz/5Irp9zkdYk5MxhQcDDT6LlZrI8+rv5mHtQ1mo=
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
github.com/volcengine/volcengine-go-sdk v1.1.34 h1:ha90JycCCTJNCse0UDziBgBsuX98ITOrkwYlDWcm7NI=
github.com/volcengine/volcengine-go-sdk v1.1.34/go.mod h1:oxoVo+A17kvkwPkIeIHPVLjSw7EQAm+l/Vau1YGHN+A=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -242,8 +277,8 @@ go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
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/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
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=
@@ -257,15 +292,23 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -276,12 +319,15 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -320,16 +366,41 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
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-20190902080502-41f04d3bba15/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=
@@ -342,6 +413,10 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
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.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
@@ -351,4 +426,6 @@ gorm.io/driver/mysql v1.4.7/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8o
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=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+49 -35
View File
@@ -11,6 +11,7 @@ import (
"context"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
logger2 "geekai/logger"
@@ -19,9 +20,10 @@ import (
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -29,7 +31,7 @@ import (
var logger = logger2.GetLogger()
const SuperManagerID = 1
const SuperUsername = "admin"
type ManagerHandler struct {
handler.BaseHandler
@@ -45,6 +47,26 @@ func NewAdminHandler(app *core.AppServer, db *gorm.DB, client *redis.Client, cap
}
}
// RegisterRoutes 注册路由
func (h *ManagerHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/")
// 公开接口,不需要授权
group.POST("login", h.Login)
group.GET("logout", h.Logout)
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("session", h.Session)
group.GET("list", h.List)
group.POST("save", h.Save)
group.POST("enable", h.Enable)
group.GET("remove", h.Remove)
group.POST("resetPass", h.ResetPass)
}
}
// Login 登录
func (h *ManagerHandler) Login(c *gin.Context) {
var data struct {
@@ -59,19 +81,6 @@ func (h *ManagerHandler) Login(c *gin.Context) {
return
}
if h.App.SysConfig.EnabledVerify {
var check bool
if data.X != 0 {
check = h.captcha.SlideCheck(data)
} else {
check = h.captcha.Check(data)
}
if !check {
resp.ERROR(c, "请先完人机验证")
return
}
}
var manager model.AdminUser
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
if res.Error != nil {
@@ -85,7 +94,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
}
// 超级管理员默认是ID:1
if manager.Id != SuperManagerID && manager.Status == false {
if manager.Username != SuperUsername && !manager.Status {
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
return
}
@@ -116,7 +125,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
IsSuperAdmin bool `json:"is_super_admin"`
Token string `json:"token"`
}{
IsSuperAdmin: manager.Id == 1,
IsSuperAdmin: manager.Username == SuperUsername,
Token: tokenString,
}
@@ -135,16 +144,15 @@ func (h *ManagerHandler) Logout(c *gin.Context) {
// Session 会话检测
func (h *ManagerHandler) Session(c *gin.Context) {
id := h.GetLoginUserId(c)
key := fmt.Sprintf("admin/%d", id)
if _, err := h.redis.Get(context.Background(), key).Result(); err != nil {
resp.NotAuth(c)
id := h.GetAdminId(c)
if id == 0 {
resp.NotAuth(c, "当前用户已退出登录")
return
}
var manager model.AdminUser
res := h.DB.Where("id", id).First(&manager)
if res.Error != nil {
resp.NotAuth(c)
err := h.DB.Where("id", id).First(&manager).Error
if err != nil {
resp.NotAuth(c, "当前用户已退出登录")
return
}
@@ -219,12 +227,19 @@ func (h *ManagerHandler) Remove(c *gin.Context) {
return
}
if id == SuperManagerID {
var user model.AdminUser
res := h.DB.Where("id", id).First(&user)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
if user.Username == SuperUsername {
resp.ERROR(c, "超级管理员不能删除")
return
}
res := h.DB.Where("id", id).Delete(&model.AdminUser{})
res = h.DB.Where("id", id).Delete(&model.AdminUser{})
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
@@ -255,8 +270,14 @@ func (h *ManagerHandler) Enable(c *gin.Context) {
// ResetPass 重置密码
func (h *ManagerHandler) ResetPass(c *gin.Context) {
id := h.GetLoginUserId(c)
if id != SuperManagerID {
id := h.GetAdminId(c)
var user model.AdminUser
res := h.DB.Where("id", id).First(&user)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
if user.Username != SuperUsername {
resp.ERROR(c, "只有超级管理员能够进行该操作")
return
}
@@ -270,13 +291,6 @@ func (h *ManagerHandler) ResetPass(c *gin.Context) {
return
}
var user model.AdminUser
res := h.DB.Where("id", data.Id).First(&user)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
password := utils.GenPassword(data.Password, user.Salt)
user.Password = password
res = h.DB.Updates(&user)
+20 -6
View File
@@ -10,12 +10,14 @@ package admin
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"strings"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -29,6 +31,20 @@ func NewApiKeyHandler(app *core.AppServer, db *gorm.DB) *ApiKeyHandler {
return &ApiKeyHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}}
}
// RegisterRoutes 注册路由
func (h *ApiKeyHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/apikey/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.POST("save", h.Save)
group.POST("set", h.Set)
group.GET("remove", h.Remove)
}
}
func (h *ApiKeyHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
@@ -71,20 +87,18 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
resp.SUCCESS(c, keyVo)
}
// List 获取 API KEY 列表
func (h *ApiKeyHandler) List(c *gin.Context) {
status := h.GetBool(c, "status")
t := h.GetTrim(c, "type")
platform := h.GetTrim(c, "platform")
t := c.Query("type")
session := h.DB.Session(&gorm.Session{})
if status {
session = session.Where("enabled", true)
}
if t != "" {
session = session.Where("type", t)
}
if platform != "" {
session = session.Where("platform", platform)
types := strings.Split(t, "|")
session = session.Where("type IN ?", types)
}
var items []model.ApiKey
@@ -10,6 +10,7 @@ package admin
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
@@ -22,22 +23,37 @@ import (
"gorm.io/gorm"
)
type ChatRoleHandler struct {
type ChatAppHandler struct {
handler.BaseHandler
}
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
return &ChatRoleHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
return &ChatAppHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatAppHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/role/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.POST("save", h.Save)
group.POST("sort", h.Sort)
group.POST("set", h.Set)
group.GET("remove", h.Remove)
}
}
// Save 创建或者更新某个角色
func (h *ChatRoleHandler) Save(c *gin.Context) {
var data vo.ChatRole
func (h *ChatAppHandler) Save(c *gin.Context) {
var data vo.ChatApp
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var role model.ChatRole
var role model.ChatApp
err := utils.CopyObject(data, &role)
if err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -64,9 +80,9 @@ func (h *ChatRoleHandler) Save(c *gin.Context) {
resp.SUCCESS(c, data)
}
func (h *ChatRoleHandler) List(c *gin.Context) {
var items []model.ChatRole
var roles = make([]vo.ChatRole, 0)
func (h *ChatAppHandler) List(c *gin.Context) {
var items []model.ChatApp
var roles = make([]vo.ChatApp, 0)
res := h.DB.Order("sort_num ASC").Find(&items)
if res.Error != nil {
resp.ERROR(c, "No data found")
@@ -75,13 +91,18 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
// initialize model mane for role
modelIds := make([]int, 0)
typeIds := make([]int, 0)
for _, v := range items {
if v.ModelId > 0 {
modelIds = append(modelIds, v.ModelId)
modelIds = append(modelIds, int(v.ModelId))
}
if v.Tid > 0 {
typeIds = append(typeIds, int(v.Tid))
}
}
modelNameMap := make(map[int]string)
typeNameMap := make(map[int]string)
if len(modelIds) > 0 {
var models []model.ChatModel
tx := h.DB.Where("id IN ?", modelIds).Find(&models)
@@ -91,15 +112,25 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
}
}
}
if len(typeIds) > 0 {
var appTypes []model.AppType
tx := h.DB.Where("id IN ?", typeIds).Find(&appTypes)
if tx.Error == nil {
for _, m := range appTypes {
typeNameMap[int(m.Id)] = m.Name
}
}
}
for _, v := range items {
var role vo.ChatRole
var role vo.ChatApp
err := utils.CopyObject(v, &role)
if err == nil {
role.Id = v.Id
role.CreatedAt = v.CreatedAt.Unix()
role.UpdatedAt = v.UpdatedAt.Unix()
role.ModelName = modelNameMap[role.ModelId]
role.ModelName = modelNameMap[int(role.ModelId)]
role.TypeName = typeNameMap[int(role.Tid)]
roles = append(roles, role)
}
}
@@ -108,7 +139,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
}
// Sort 更新角色排序
func (h *ChatRoleHandler) Sort(c *gin.Context) {
func (h *ChatAppHandler) Sort(c *gin.Context) {
var data struct {
Ids []uint `json:"ids"`
Sorts []int `json:"sorts"`
@@ -120,7 +151,7 @@ func (h *ChatRoleHandler) Sort(c *gin.Context) {
}
for index, id := range data.Ids {
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
err := h.DB.Model(&model.ChatApp{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]).Error
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -130,7 +161,7 @@ func (h *ChatRoleHandler) Sort(c *gin.Context) {
resp.SUCCESS(c)
}
func (h *ChatRoleHandler) Set(c *gin.Context) {
func (h *ChatAppHandler) Set(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Filed string `json:"filed"`
@@ -142,7 +173,7 @@ func (h *ChatRoleHandler) Set(c *gin.Context) {
return
}
err := h.DB.Model(&model.ChatRole{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
err := h.DB.Model(&model.ChatApp{}).Where("id = ?", data.Id).Update(data.Filed, data.Value).Error
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -150,16 +181,15 @@ func (h *ChatRoleHandler) Set(c *gin.Context) {
resp.SUCCESS(c)
}
func (h *ChatRoleHandler) Remove(c *gin.Context) {
func (h *ChatAppHandler) 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{})
res := h.DB.Where("id", id).Delete(&model.ChatApp{})
if res.Error != nil {
logger.Error("error with update database", res.Error)
resp.ERROR(c, "删除失败!")
return
}
+165
View File
@@ -0,0 +1,165 @@
package admin
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ChatAppTypeHandler struct {
handler.BaseHandler
}
func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler {
return &ChatAppTypeHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatAppTypeHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/app/type/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.POST("save", h.Save)
group.GET("remove", h.Remove)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
}
}
// Save 创建或更新App类型
func (h *ChatAppTypeHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Icon string `json:"icon"`
SortNum int `json:"sort_num"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Id == 0 { // for add
err := h.DB.Where("name", data.Name).First(&model.AppType{}).Error
if err == nil {
resp.ERROR(c, "当前分类已经存在")
return
}
err = h.DB.Create(&model.AppType{
Name: data.Name,
Icon: data.Icon,
Enabled: data.Enabled,
SortNum: data.SortNum,
}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
} else { // for update
err := h.DB.Model(&model.AppType{}).Where("id", data.Id).Updates(map[string]interface{}{
"name": data.Name,
"icon": data.Icon,
"enabled": data.Enabled,
}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
}
resp.SUCCESS(c)
}
// List 获取App类型列表
func (h *ChatAppTypeHandler) List(c *gin.Context) {
var items []model.AppType
var appTypes = make([]vo.AppType, 0)
err := h.DB.Order("sort_num ASC").Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
for _, v := range items {
var appType vo.AppType
err = utils.CopyObject(v, &appType)
if err != nil {
continue
}
appType.Id = v.Id
appType.CreatedAt = v.CreatedAt.Unix()
appTypes = append(appTypes, appType)
}
resp.SUCCESS(c, appTypes)
}
// Remove 删除App类型
func (h *ChatAppTypeHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id <= 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.DB.Where("id", id).Delete(&model.AppType{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// Enable 启用|禁用
func (h *ChatAppTypeHandler) 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
}
err := h.DB.Model(&model.AppType{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// Sort 更新排序
func (h *ChatAppTypeHandler) 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 {
err := h.DB.Model(&model.AppType{}).Where("id", id).Update("sort_num", data.Sorts[index]).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
}
resp.SUCCESS(c)
}
+43 -18
View File
@@ -9,12 +9,14 @@ package admin
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@@ -27,16 +29,31 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB) *ChatHandler {
return &ChatHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/chat/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("list", h.List)
group.POST("message", h.Messages)
group.GET("history", h.History)
group.GET("remove", h.RemoveChat)
group.GET("message/remove", h.RemoveMessage)
}
}
type chatItemVo struct {
Username string `json:"username"`
UserId uint `json:"user_id"`
ChatId string `json:"chat_id"`
Title string `json:"title"`
Role vo.ChatRole `json:"role"`
Model string `json:"model"`
Token int `json:"token"`
CreatedAt int64 `json:"created_at"`
MsgNum int `json:"msg_num"` // 消息数量
Username string `json:"username"`
UserId uint `json:"user_id"`
ChatId string `json:"chat_id"`
Title string `json:"title"`
Role vo.ChatApp `json:"role"`
Model string `json:"model"`
Token int `json:"token"`
CreatedAt int64 `json:"created_at"`
MsgNum int `json:"msg_num"` // 消息数量
}
func (h *ChatHandler) List(c *gin.Context) {
@@ -86,7 +103,7 @@ func (h *ChatHandler) List(c *gin.Context) {
}
var messages []model.ChatMessage
var users []model.User
var roles []model.ChatRole
var roles []model.ChatApp
h.DB.Where("chat_id IN ?", chatIds).Find(&messages)
h.DB.Where("id IN ?", userIds).Find(&users)
h.DB.Where("id IN ?", roleIds).Find(&roles)
@@ -94,7 +111,7 @@ func (h *ChatHandler) List(c *gin.Context) {
tokenMap := make(map[string]int)
userMap := make(map[uint]string)
msgMap := make(map[string]int)
roleMap := make(map[uint]vo.ChatRole)
roleMap := make(map[uint]vo.ChatApp)
for _, msg := range messages {
tokenMap[msg.ChatId] += msg.Tokens
msgMap[msg.ChatId] += 1
@@ -103,7 +120,7 @@ func (h *ChatHandler) List(c *gin.Context) {
userMap[user.Id] = user.Username
}
for _, r := range roles {
var roleVo vo.ChatRole
var roleVo vo.ChatApp
err := utils.CopyObject(r, &roleVo)
if err != nil {
continue
@@ -189,7 +206,7 @@ func (h *ChatHandler) Messages(c *gin.Context) {
}
for _, item := range items {
list = append(list, chatMessageVo{
Id: item.Id,
Id: uint(item.Id),
UserId: item.UserId,
Username: userMap[item.UserId],
Content: item.Content,
@@ -208,20 +225,28 @@ func (h *ChatHandler) Messages(c *gin.Context) {
func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID
var items []model.ChatMessage
var messages = make([]vo.HistoryMessage, 0)
var messages = make([]vo.ChatMessage, 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
var v vo.ChatMessage
err := utils.CopyObject(item, &v)
if err != nil {
continue
}
// 解析内容
var content vo.MsgContent
err = utils.JsonDecode(item.Content, &content)
if err != nil {
content.Text = item.Content
}
v.Content = content
v.CreatedAt = item.CreatedAt.Unix()
v.UpdatedAt = item.UpdatedAt.Unix()
if err == nil {
messages = append(messages, v)
}
messages = append(messages, v)
}
}
+75 -17
View File
@@ -8,7 +8,9 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
@@ -28,21 +30,41 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
return &ChatModelHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatModelHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/model/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.POST("save", h.Save)
group.POST("set", h.Set)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
group.POST("batch-remove", h.BatchRemove)
}
}
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"`
Open bool `json:"open"`
Platform string `json:"platform"`
Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id,omitempty"`
CreatedAt int64 `json:"created_at"`
Id uint `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"`
Open bool `json:"open"`
Platform string `json:"platform"`
Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度
Desc string `json:"desc"` //模型描述
Tag string `json:"tag"` //模型标签
Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id,omitempty"`
CreatedAt int64 `json:"created_at"`
Type string `json:"type"`
Options map[string]string `json:"options"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -58,14 +80,16 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
item.Name = data.Name
item.Value = data.Value
item.Enabled = data.Enabled
item.SortNum = data.SortNum
item.Open = data.Open
item.Power = data.Power
item.MaxTokens = data.MaxTokens
item.MaxContext = data.MaxContext
item.Desc = data.Desc
item.Tag = data.Tag
item.Temperature = data.Temperature
item.KeyId = data.KeyId
item.KeyId = uint(data.KeyId)
item.Type = data.Type
item.Options = utils.JsonEncode(data.Options)
var res *gorm.DB
if data.Id > 0 {
res = h.DB.Save(&item)
@@ -94,12 +118,16 @@ func (h *ChatModelHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{})
enable := h.GetBool(c, "enable")
name := h.GetTrim(c, "name")
modelType := h.GetTrim(c, "type")
if enable {
session = session.Where("enabled", enable)
}
if name != "" {
session = session.Where("name LIKE ?", name+"%")
}
if modelType != "" {
session = session.Where("type", modelType)
}
var items []model.ChatModel
var cms = make([]vo.ChatModel, 0)
res := session.Order("sort_num ASC").Find(&items)
@@ -111,7 +139,7 @@ func (h *ChatModelHandler) List(c *gin.Context) {
// initialize key name
keyIds := make([]int, 0)
for _, v := range items {
keyIds = append(keyIds, v.KeyId)
keyIds = append(keyIds, int(v.KeyId))
}
var keys []model.ApiKey
keyMap := make(map[uint]string)
@@ -191,3 +219,33 @@ func (h *ChatModelHandler) Remove(c *gin.Context) {
}
resp.SUCCESS(c)
}
// BatchRemove 批量删除模型
func (h *ChatModelHandler) BatchRemove(c *gin.Context) {
var data struct {
Ids []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if len(data.Ids) == 0 {
resp.ERROR(c, "请选择要删除的模型")
return
}
// 执行批量删除
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.ChatModel{}).Error
if err != nil {
logger.Error("批量删除模型失败:", err)
resp.ERROR(c, "批量删除失败:"+err.Error())
return
}
resp.SUCCESS(c, gin.H{
"message": fmt.Sprintf("成功删除 %d 个模型", len(data.Ids)),
"deleted_count": len(data.Ids),
})
}
+335 -86
View File
@@ -9,42 +9,202 @@ package admin
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/store"
"geekai/service/oss"
"geekai/service/payment"
"geekai/service/sms"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/host"
"gorm.io/gorm"
)
type ConfigHandler struct {
handler.BaseHandler
levelDB *store.LevelDB
licenseService *service.LicenseService
sysConfig *types.SystemConfig
alipayService *payment.AlipayService
wxpayService *payment.WxPayService
epayService *payment.EPayService
smsManager *sms.SmsManager
uploaderManager *oss.UploaderManager
smtpService *service.SmtpService
captchaService *service.CaptchaService
wxLoginService *service.WxLoginService
}
func NewConfigHandler(app *core.AppServer, db *gorm.DB, levelDB *store.LevelDB, licenseService *service.LicenseService) *ConfigHandler {
func NewConfigHandler(
app *core.AppServer,
db *gorm.DB,
sysConfig *types.SystemConfig,
alipayService *payment.AlipayService,
wxpayService *payment.WxPayService,
epayService *payment.EPayService,
smsManager *sms.SmsManager,
uploaderManager *oss.UploaderManager,
smtpService *service.SmtpService,
captchaService *service.CaptchaService,
wxLoginService *service.WxLoginService,
) *ConfigHandler {
return &ConfigHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
levelDB: levelDB,
licenseService: licenseService,
BaseHandler: handler.BaseHandler{App: app, DB: db},
sysConfig: sysConfig,
alipayService: alipayService,
wxpayService: wxpayService,
epayService: epayService,
smsManager: smsManager,
uploaderManager: uploaderManager,
smtpService: smtpService,
captchaService: captchaService,
wxLoginService: wxLoginService,
}
}
func (h *ConfigHandler) Update(c *gin.Context) {
// RegisterRoutes 注册路由
func (h *ConfigHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/config")
// 需要管理员登录的接口
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
rg.POST("update/base", h.UpdateBase)
rg.POST("update/power", h.UpdatePower)
rg.POST("update/notice", h.UpdateNotice)
rg.POST("update/agreement", h.UpdateAgreement)
rg.POST("update/privacy", h.UpdatePrivacy)
rg.POST("update/mark_map", h.UpdateMarkMap)
rg.POST("update/captcha", h.UpdateCaptcha)
rg.POST("update/wx_login", h.UpdateWxLogin)
rg.POST("update/payment", h.UpdatePayment)
rg.POST("update/sms", h.UpdateSms)
rg.POST("update/oss", h.UpdateOss)
rg.POST("update/smtp", h.UpdateStmp)
rg.GET("get", h.Get)
}
}
// UpdateBase 更新基础配置
func (h *ConfigHandler) UpdateBase(c *gin.Context) {
var data types.BaseConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeySystem, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
h.sysConfig.Base = data
resp.SUCCESS(c, data)
}
// UpdatePower 更新系统配置
func (h *ConfigHandler) UpdatePower(c *gin.Context) {
var data struct {
Key string `json:"key"`
Config struct {
types.SystemConfig
Content string `json:"content,omitempty"`
Updated bool `json:"updated,omitempty"`
} `json:"config"`
ConfigBak types.SystemConfig `json:"config_bak,omitempty"`
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
h.sysConfig.Base.InitPower = data.InitPower
h.sysConfig.Base.DailyPower = data.DailyPower
h.sysConfig.Base.InvitePower = data.InvitePower
h.sysConfig.Base.MjPower = data.MjPower
h.sysConfig.Base.MjActionPower = data.MjActionPower
h.sysConfig.Base.SdPower = data.SdPower
h.sysConfig.Base.SunoPower = data.SunoPower
h.sysConfig.Base.LumaPower = data.LumaPower
h.sysConfig.Base.KeLingPowers = data.KeLingPowers
err := h.Update(types.ConfigKeySystem, h.sysConfig.Base)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, h.sysConfig.Base)
}
// UpdateNotice 更新公告配置
func (h *ConfigHandler) UpdateNotice(c *gin.Context) {
var data struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyNotice, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, data)
}
// UpdateAgreement 更新用户协议配置
func (h *ConfigHandler) UpdateAgreement(c *gin.Context) {
var data struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyAgreement, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, data)
}
// UpdatePrivacy 更新隐私政策配置
func (h *ConfigHandler) UpdatePrivacy(c *gin.Context) {
var data struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyPrivacy, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, data)
}
// UpdateMarkMap 更新思维导图配置
func (h *ConfigHandler) UpdateMarkMap(c *gin.Context) {
var data struct {
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&data); err != nil {
@@ -52,57 +212,176 @@ func (h *ConfigHandler) Update(c *gin.Context) {
return
}
// ONLY authorized user can change the copyright
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy {
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权")
err := h.Update(types.ConfigKeyMarkMap, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
value := utils.JsonEncode(&data.Config)
config := model.Config{Key: data.Key, Config: value}
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 = value
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)
}
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)
resp.SUCCESS(c, data)
}
// Get 获取指定的系统配置
func (h *ConfigHandler) Get(c *gin.Context) {
key := c.Query("key")
// UpdateCaptcha 更新行为验证码配置
func (h *ConfigHandler) UpdateCaptcha(c *gin.Context) {
var data types.CaptchaConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyCaptcha, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
h.captchaService.UpdateConfig(data)
resp.SUCCESS(c, data)
}
// UpdatePayment 更新支付配置
func (h *ConfigHandler) UpdatePayment(c *gin.Context) {
var data types.PaymentConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyPayment, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 如果启用状态发生改变,则需要更新支付服务配置
if data.WxPay.Enabled {
err = h.wxpayService.UpdateConfig(&data.WxPay)
if err != nil {
resp.ERROR(c, err.Error())
return
}
}
if data.Epay.Enabled {
h.epayService.UpdateConfig(&data.Epay)
}
if data.Alipay.Enabled {
err = h.alipayService.UpdateConfig(&data.Alipay)
if err != nil {
resp.ERROR(c, err.Error())
return
}
}
h.sysConfig.Payment = data
resp.SUCCESS(c, data)
}
// UpdateSms 更新短信配置
func (h *ConfigHandler) UpdateSms(c *gin.Context) {
var data types.SMSConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeySms, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 更新服务配置
h.smsManager.UpdateConfig(data)
resp.SUCCESS(c, data)
}
// UpdateOss 更新 Oss 配置
func (h *ConfigHandler) UpdateOss(c *gin.Context) {
var data types.OSSConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyOss, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 更新服务配置
h.uploaderManager.UpdateConfig(data)
h.sysConfig.OSS = data
resp.SUCCESS(c, data)
}
// UpdateStmp 更新 Stmp 配置
func (h *ConfigHandler) UpdateStmp(c *gin.Context) {
var data types.SmtpConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeySmtp, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 更新服务配置
h.smtpService.UpdateConfig(&data)
h.sysConfig.SMTP = data
resp.SUCCESS(c, data)
}
// UpdateWxLogin 更新微信登录配置
func (h *ConfigHandler) UpdateWxLogin(c *gin.Context) {
var data types.WxLoginConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.Update(types.ConfigKeyWxLogin, data)
if err != nil {
resp.ERROR(c, err.Error())
return
}
h.wxLoginService.UpdateConfig(data)
h.sysConfig.WxLogin = data
resp.SUCCESS(c, data)
}
// Update 更新系统配置
func (h *ConfigHandler) Update(name string, value any) error {
var config model.Config
res := h.DB.Where("marker", key).First(&config)
err := h.DB.Where("name", name).First(&config).Error
if err != nil { // 不存在则创建
config.Name = name
config.Value = utils.JsonEncode(value)
return h.DB.Create(&config).Error
} else { // 存在则更新
config.Value = utils.JsonEncode(value)
return h.DB.Updates(&config).Error
}
}
// Get 获取指定名称的系统配置
func (h *ConfigHandler) Get(c *gin.Context) {
name := c.Query("key")
var config model.Config
res := h.DB.Where("name", name).First(&config)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
var value map[string]interface{}
err := utils.JsonDecode(config.Config, &value)
var value map[string]any
err := utils.JsonDecode(config.Value, &value)
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -110,33 +389,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
resp.SUCCESS(c, value)
}
// Active 激活系统
func (h *ConfigHandler) Active(c *gin.Context) {
var data struct {
License string `json:"license"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
info, err := host.Info()
if err != nil {
resp.ERROR(c, err.Error())
return
}
err = h.licenseService.ActiveLicense(data.License, info.HostID)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, info.HostID)
}
// GetLicense 获取 License 信息
func (h *ConfigHandler) GetLicense(c *gin.Context) {
license := h.licenseService.GetLicense()
resp.SUCCESS(c, license)
}
+158 -36
View File
@@ -13,10 +13,11 @@ import (
"geekai/handler"
"geekai/store/model"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"time"
)
type DashboardHandler struct {
@@ -27,46 +28,161 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
return &DashboardHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *DashboardHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/dashboard/")
group.GET("stats", h.Stats)
}
// statsVo 增加 recentOrders、recentUsers 字段
// 最近订单
type OrderBrief struct {
OrderNo string `json:"order_no"`
Amount float64 `json:"amount"`
CreatedAt time.Time `json:"created_at"`
}
// 最近用户
type UserBrief struct {
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
LastActive time.Time `json:"last_active"`
}
type statsVo struct {
Users int64 `json:"users"`
Chats int64 `json:"chats"`
Tokens int `json:"tokens"`
Income float64 `json:"income"`
Chart map[string]map[string]float64 `json:"chart"`
Users int64 `json:"users"`
Chats int64 `json:"chats"`
Tokens int `json:"tokens"`
Income float64 `json:"income"`
Chart map[string]map[string]float64 `json:"chart"`
TodayUsers int64 `json:"todayUsers"`
TodayChats int64 `json:"todayChats"`
TodayTokens int `json:"todayTokens"`
TodayIncome float64 `json:"todayIncome"`
TodayOrders int64 `json:"todayOrders"`
TodayImageJobs int64 `json:"todayImageJobs"`
TodayVideoJobs int64 `json:"todayVideoJobs"`
TodayMusicJobs int64 `json:"todayMusicJobs"`
Orders int64 `json:"orders"`
ImageJobs int64 `json:"imageJobs"`
VideoJobs int64 `json:"videoJobs"`
MusicJobs int64 `json:"musicJobs"`
RecentOrders []OrderBrief `json:"recentOrders"`
RecentUsers []UserBrief `json:"recentUsers"`
}
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
// 总用户数
h.DB.Model(&model.User{}).Count(&stats.Users)
// 今日新增用户
h.DB.Model(&model.User{}).Where("created_at > ?", zeroTime).Count(&stats.TodayUsers)
// 总对话数
h.DB.Model(&model.ChatItem{}).Count(&stats.Chats)
// 今日新增对话
h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&stats.TodayChats)
// 总算力消耗
var powerLogs []model.PowerLog
h.DB.Where("mark = ?", types.PowerSub).Find(&powerLogs)
for _, item := range powerLogs {
stats.Tokens += item.Amount
}
// 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
// 今日算力消耗
var todayPowerLogs []model.PowerLog
h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
for _, item := range todayPowerLogs {
stats.TodayTokens += item.Amount
}
// tokens took stats
var historyMessages []model.ChatMessage
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages)
for _, item := range historyMessages {
stats.Tokens += item.Tokens
}
// 订单收入
var orders []model.Order
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&orders)
for _, item := range orders {
// 总收入
var allOrders []model.Order
h.DB.Where("status = ?", types.OrderPaidSuccess).Find(&allOrders)
for _, item := range allOrders {
stats.Income += item.Amount
}
// 今日收入
var todayOrders []model.Order
h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Find(&todayOrders)
for _, item := range todayOrders {
stats.TodayIncome += item.Amount
}
// 订单总数
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Count(&stats.Orders)
// 今日订单数
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders)
// 图片生成任务统计
var mjJobs, sdJobs, dallJobs, jimengImageJobs int64
h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs)
h.DB.Model(&model.SdJob{}).Count(&sdJobs)
h.DB.Model(&model.DallJob{}).Count(&dallJobs)
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Count(&jimengImageJobs)
stats.ImageJobs = mjJobs + sdJobs + dallJobs + jimengImageJobs
logger.Info("stats.ImageJobs", stats.ImageJobs)
// 今日图片生成任务统计
var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs)
h.DB.Model(&model.SdJob{}).Where("created_at > ?", zeroTime).Count(&todaySdJobs)
h.DB.Model(&model.DallJob{}).Where("created_at > ?", zeroTime).Count(&todayDallJobs)
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_image", "image_to_image", "image_edit", "image_effects"}).Where("created_at > ?", zeroTime).Count(&todayJimengImageJobs)
stats.TodayImageJobs = todayMjJobs + todaySdJobs + todayDallJobs + todayJimengImageJobs
// 视频生成任务统计
var videoJobs, jimengVideoJobs int64
h.DB.Model(&model.VideoJob{}).Count(&videoJobs)
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Count(&jimengVideoJobs)
stats.VideoJobs = videoJobs + jimengVideoJobs
// 今日视频生成任务统计
var todayVideoJobs, todayJimengVideoJobs int64
h.DB.Model(&model.VideoJob{}).Where("created_at > ?", zeroTime).Count(&todayVideoJobs)
h.DB.Model(&model.JimengJob{}).Where("type IN ?", []string{"text_to_video", "image_to_video"}).Where("created_at > ?", zeroTime).Count(&todayJimengVideoJobs)
stats.TodayVideoJobs = todayVideoJobs + todayJimengVideoJobs
// 音乐生成任务统计
h.DB.Model(&model.SunoJob{}).Count(&stats.MusicJobs)
// 今日音乐生成任务统计
h.DB.Model(&model.SunoJob{}).Where("created_at > ?", zeroTime).Count(&stats.TodayMusicJobs)
// recentOrders: 最近10条已支付订单
var orderList []model.Order
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Order("created_at desc").Limit(10).Find(&orderList)
for _, o := range orderList {
stats.RecentOrders = append(stats.RecentOrders, OrderBrief{
OrderNo: o.OrderNo,
Amount: o.Amount,
CreatedAt: o.CreatedAt,
})
}
// recentUsers: 最近10个注册用户
var userList []model.User
h.DB.Model(&model.User{}).Order("created_at desc").Limit(10).Find(&userList)
for _, u := range userList {
lastActive := u.UpdatedAt
if lastActive.IsZero() {
lastActive = u.CreatedAt
}
stats.RecentUsers = append(stats.RecentUsers, UserBrief{
Nickname: u.Nickname,
Avatar: u.Avatar,
LastActive: lastActive,
})
}
// 统计7天的订单的图表
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
var statsChart = make(map[string]map[string]float64)
@@ -81,23 +197,29 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
// 统计用户7天增加的曲线
var users []model.User
res = h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users)
if res.Error == nil {
err := h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users).Error
if err == nil {
for _, item := range users {
userStatistic[item.CreatedAt.Format("2006-01-02")] += 1
}
}
// 统计7天Token 消耗
res = h.DB.Where("created_at > ?", startDate).Find(&historyMessages)
for _, item := range historyMessages {
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
// 统计7天算力消耗
var chartPowerLogs []model.PowerLog
err = h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", startDate).Find(&chartPowerLogs).Error
if err == nil {
for _, item := range chartPowerLogs {
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Amount)
}
}
// 统计最近7天的订单
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
for _, item := range orders {
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
var orders []model.Order
err = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders).Error
if err == nil {
for _, item := range orders {
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
}
}
statsChart["users"] = userStatistic
+16 -1
View File
@@ -9,6 +9,7 @@ package admin
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
@@ -30,6 +31,21 @@ func NewFunctionHandler(app *core.AppServer, db *gorm.DB) *FunctionHandler {
return &FunctionHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *FunctionHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/function/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.POST("save", h.Save)
group.POST("set", h.Set)
group.GET("remove", h.Remove)
group.GET("token", h.GenToken)
}
}
func (h *FunctionHandler) Save(c *gin.Context) {
var data vo.Function
if err := c.ShouldBindJSON(&data); err != nil {
@@ -119,7 +135,6 @@ func (h *FunctionHandler) GenToken(c *gin.Context) {
})
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil {
logger.Error("error with generate token", err)
resp.ERROR(c)
return
}
+267
View File
@@ -0,0 +1,267 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ImageHandler struct {
handler.BaseHandler
userService *service.UserService
uploader *oss.UploaderManager
}
func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *ImageHandler {
return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
}
// RegisterRoutes 注册路由
func (h *ImageHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/image/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("list/mj", h.MjList)
group.POST("list/sd", h.SdList)
group.POST("list/dall", h.DallList)
group.GET("remove", h.Remove)
}
}
type imageQuery struct {
Prompt string `json:"prompt"`
Username string `json:"username"`
CreatedAt []string `json:"created_at"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// MjList Midjourney 任务列表
func (h *ImageHandler) MjList(c *gin.Context) {
var data imageQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.MidJourneyJob{}).Count(&total)
var list []model.MidJourneyJob
var items = make([]vo.MidJourneyJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.MidJourneyJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// SdList Stable Diffusion 任务列表
func (h *ImageHandler) SdList(c *gin.Context) {
var data imageQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.SdJob{}).Count(&total)
var list []model.SdJob
var items = make([]vo.SdJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.SdJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// DallList DALL-E 任务列表
func (h *ImageHandler) DallList(c *gin.Context) {
var data imageQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.DallJob{}).Count(&total)
var list []model.DallJob
var items = make([]vo.DallJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.DallJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
func (h *ImageHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
tab := c.Query("tab")
tx := h.DB.Begin()
var md, remark, imgURL string
var power, userId, progress int
switch tab {
case "mj":
var job model.MidJourneyJob
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
resp.ERROR(c, "记录不存在")
return
}
tx.Delete(&job)
md = "mid-journey"
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
imgURL = job.ImgURL
case "sd":
var job model.SdJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = "stable-diffusion"
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
imgURL = job.ImgURL
case "dall":
var job model.DallJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = "dall-e-3"
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
imgURL = job.ImgURL
default:
resp.ERROR(c, types.InvalidArgs)
return
}
if progress != 100 {
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund,
Model: md,
Remark: remark,
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image
err := h.uploader.GetUploadHandler().Delete(imgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
resp.SUCCESS(c)
}
+279
View File
@@ -0,0 +1,279 @@
package admin
import (
"fmt"
"strconv"
"geekai/core"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/jimeng"
"geekai/service/oss"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AdminJimengHandler 管理后台即梦AI处理器
type AdminJimengHandler struct {
handler.BaseHandler
jimengClient *jimeng.Client
userService *service.UserService
uploader *oss.UploaderManager
}
// NewAdminJimengHandler 创建管理后台即梦AI处理器
func NewAdminJimengHandler(app *core.AppServer, db *gorm.DB, jimengClient *jimeng.Client, userService *service.UserService, uploader *oss.UploaderManager) *AdminJimengHandler {
return &AdminJimengHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
jimengClient: jimengClient,
userService: userService,
uploader: uploader,
}
}
// RegisterRoutes 注册即梦AI管理后台路由
func (h *AdminJimengHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/jimeng/")
rg.GET("/jobs", h.Jobs)
rg.GET("/jobs/:id", h.JobDetail)
rg.POST("/jobs/remove", h.BatchRemove)
rg.GET("/stats", h.Stats)
rg.POST("/config/update", h.UpdateConfig)
}
// Jobs 获取任务列表
func (h *AdminJimengHandler) Jobs(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
userId := h.GetInt(c, "user_id", 0)
taskType := h.GetTrim(c, "type")
status := h.GetTrim(c, "status")
var tasks []model.JimengJob
var total int64
session := h.DB.Model(&model.JimengJob{})
// 构建查询条件
if userId > 0 {
session = session.Where("user_id = ?", userId)
}
if taskType != "" {
session = session.Where("type = ?", taskType)
}
if status != "" {
session = session.Where("status = ?", status)
}
// 获取总数
err := session.Count(&total).Error
if err != nil {
resp.ERROR(c, "获取任务数量失败")
return
}
// 获取数据
offset := (page - 1) * pageSize
err = session.Order("created_at DESC").Offset(offset).Limit(pageSize).Find(&tasks).Error
if err != nil {
resp.ERROR(c, "获取任务列表失败")
return
}
resp.SUCCESS(c, gin.H{
"jobs": tasks,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// JobDetail 获取任务详情
func (h *AdminJimengHandler) JobDetail(c *gin.Context) {
idStr := c.Param("id")
jobId, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
resp.ERROR(c, "参数错误")
return
}
var job model.JimengJob
err = h.DB.Where("id = ?", jobId).First(&job).Error
if err != nil {
resp.ERROR(c, "任务不存在")
return
}
resp.SUCCESS(c, job)
}
// BatchRemove 批量删除任务
func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
var req struct {
JobIds []uint `json:"job_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, "参数错误")
return
}
var deletedCount int64 = 0
for _, jobId := range req.JobIds {
var job model.JimengJob
err := h.DB.Where("id = ?", jobId).First(&job).Error
if err != nil {
continue // 跳过不存在的
}
tx := h.DB.Begin()
if job.Status != types.JMTaskStatusSuccess && job.Power > 0 {
remark := fmt.Sprintf("任务未成功,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "jimeng",
Remark: remark,
})
if err != nil {
tx.Rollback()
continue
}
}
err = tx.Where("id = ?", jobId).Delete(&model.JimengJob{}).Error
if err != nil {
tx.Rollback()
continue
}
tx.Commit()
deletedCount++
if job.ImgURL != "" {
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
}
if job.VideoURL != "" {
err = h.uploader.GetUploadHandler().Delete(job.VideoURL)
if err != nil {
logger.Error("remove video failed: ", err)
}
}
}
resp.SUCCESS(c, gin.H{
"message": "批量删除成功",
"deleted_count": deletedCount,
})
}
// Stats 获取统计信息
func (h *AdminJimengHandler) Stats(c *gin.Context) {
type StatResult struct {
Status types.JMTaskStatus `json:"status"`
Count int64 `json:"count"`
}
var stats []StatResult
err := h.DB.Model(&model.JimengJob{}).
Select("status, COUNT(*) as count").
Group("status").
Find(&stats).Error
if err != nil {
resp.ERROR(c, "获取统计信息失败")
return
}
// 整理统计数据
result := gin.H{
"totalTasks": int64(0),
"completedTasks": int64(0),
"processingTasks": int64(0),
"failedTasks": int64(0),
"pendingTasks": int64(0),
}
for _, stat := range stats {
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
switch stat.Status {
case types.JMTaskStatusInQueue:
result["pendingTasks"] = stat.Count
case types.JMTaskStatusSuccess:
result["completedTasks"] = stat.Count
case types.JMTaskStatusGenerating:
result["processingTasks"] = stat.Count
case types.JMTaskStatusFailed:
result["failedTasks"] = stat.Count
}
}
resp.SUCCESS(c, result)
}
// UpdateConfig 更新即梦AI配置
func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
var req types.JimengConfig
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, "参数错误")
return
}
// 验证必填字段
if req.AccessKey == "" {
resp.ERROR(c, "AccessKey不能为空")
return
}
if req.SecretKey == "" {
resp.ERROR(c, "SecretKey不能为空")
return
}
// 验证算力配置
if len(req.Powers) == 0 {
resp.ERROR(c, "请至少配置一个模型的积分")
return
}
for key, val := range req.Powers {
if val <= 0 {
resp.ERROR(c, fmt.Sprintf("模型 %s 的积分必须大于0", key))
return
}
}
// 保存配置
tx := h.DB.Begin()
value := utils.JsonEncode(&req)
var exist model.Config
tx.Where("name", types.ConfigKeyJimeng).First(&exist)
if exist.Id > 0 {
exist.Value = value
err := tx.Updates(&exist).Error
if err != nil {
resp.ERROR(c, "更新配置失败: "+err.Error())
return
}
} else {
exist.Name = types.ConfigKeyJimeng
exist.Value = value
err := tx.Create(&exist).Error
if err != nil {
resp.ERROR(c, "创建配置失败: "+err.Error())
return
}
}
// 更新服务中的客户端配置
err := h.jimengClient.UpdateConfig(req)
if err != nil {
resp.ERROR(c, err.Error())
tx.Rollback()
return
}
tx.Commit()
h.App.SysConfig.Jimeng = req
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
}
+215
View File
@@ -0,0 +1,215 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type MediaHandler struct {
handler.BaseHandler
userService *service.UserService
uploader *oss.UploaderManager
}
func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *MediaHandler {
return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
}
// RegisterRoutes 注册路由
func (h *MediaHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/media/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("suno", h.SunoList)
group.POST("videos", h.Videos)
group.GET("remove", h.Remove)
}
}
type mediaQuery struct {
Type string `json:"type"` // 任务类型 luma, keling
Prompt string `json:"prompt"`
Username string `json:"username"`
CreatedAt []string `json:"created_at"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// SunoList Suno 任务列表
func (h *MediaHandler) SunoList(c *gin.Context) {
var data mediaQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.SunoJob{}).Count(&total)
var list []model.SunoJob
var items = make([]vo.SunoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.SunoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// Videos 视频任务列表
func (h *MediaHandler) Videos(c *gin.Context) {
var data mediaQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{}).Where("type", data.Type)
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.VideoJob{}).Count(&total)
var list []model.VideoJob
var items = make([]vo.VideoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.VideoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
if job.VideoURL == "" {
job.VideoURL = job.WaterURL
}
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
func (h *MediaHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
tab := c.Query("tab")
tx := h.DB.Begin()
var md, remark, fileURL string
var power, userId, progress int
switch tab {
case "suno":
var job model.SunoJob
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
resp.ERROR(c, "记录不存在")
return
}
tx.Delete(&job)
md = "suno"
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
fileURL = job.AudioURL
case "luma":
case "keling":
var job model.VideoJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = job.Type
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
fileURL = job.VideoURL
if fileURL == "" {
fileURL = job.WaterURL
}
default:
resp.ERROR(c, types.InvalidArgs)
return
}
if progress != 100 {
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund,
Model: md,
Remark: remark,
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image
err := h.uploader.GetUploadHandler().Delete(fileURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
resp.SUCCESS(c)
}
+10
View File
@@ -27,6 +27,16 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *MenuHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/menu/")
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)
}
func (h *MenuHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
+333
View File
@@ -0,0 +1,333 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service/moderation"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ModerationHandler struct {
handler.BaseHandler
sysConfig *types.SystemConfig
moderationManager *moderation.ServiceManager
}
func NewModerationHandler(app *core.AppServer, db *gorm.DB, sysConfig *types.SystemConfig, moderationManager *moderation.ServiceManager) *ModerationHandler {
return &ModerationHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, sysConfig: sysConfig, moderationManager: moderationManager}
}
// RegisterRoutes 注册路由
func (h *ModerationHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/moderation/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("list", h.List)
group.GET("remove", h.Remove)
group.POST("batch-remove", h.BatchRemove)
group.GET("source-list", h.GetSourceList)
group.POST("config", h.UpdateModeration)
group.POST("test", h.TestModeration)
}
}
// List 获取文本审核记录列表
func (h *ModerationHandler) List(c *gin.Context) {
var data struct {
Username string `json:"username"`
Source string `json:"source"`
StartDate string `json:"start_date"`
EndDate string `json:"end_date"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
// 构建查询条件
if data.Username != "" {
// 通过用户名查找用户ID
var user model.User
if err := h.DB.Where("username LIKE ?", "%"+data.Username+"%").First(&user).Error; err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Source != "" {
session = session.Where("source", data.Source)
}
if data.StartDate != "" && data.EndDate != "" {
startTime := data.StartDate + " 00:00:00"
endTime := data.EndDate + " 23:59:59"
session = session.Where("created_at >= ? AND created_at <= ?", startTime, endTime)
}
// 统计总数
var total int64
session.Model(&model.Moderation{}).Count(&total)
// 分页
page := data.Page
pageSize := data.PageSize
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
offset := (page - 1) * pageSize
session = session.Offset(offset).Limit(pageSize)
// 查询数据
var items []model.Moderation
err := session.Order("id DESC").Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 获取用户信息
userIds := make([]uint, 0)
for _, item := range items {
userIds = append(userIds, item.UserId)
}
var users []model.User
if len(userIds) > 0 {
h.DB.Where("id IN ?", userIds).Find(&users)
}
userMap := make(map[uint]string)
for _, user := range users {
userMap[user.Id] = user.Username
}
// 转换为响应数据
list := make([]map[string]any, 0)
for _, item := range items {
var moderation types.ModerationResult
err := utils.JsonDecode(item.Result, &moderation)
if err != nil {
continue
}
var result []string
for value, label := range types.ModerationCategories {
if moderation.Categories[value] {
result = append(result, label)
}
}
list = append(list, map[string]any{
"id": item.Id,
"user_id": item.UserId,
"username": userMap[item.UserId],
"source": item.Source,
"input": item.Input,
"output": item.Output,
"result": result,
"created_at": item.CreatedAt.Unix(),
})
}
resp.SUCCESS(c, map[string]any{
"items": list,
"total": total,
"page": page,
"page_size": pageSize,
})
}
func (h *ModerationHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id <= 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
err := h.DB.Where("id", id).Delete(&model.Moderation{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// BatchRemove 批量删除文本审核记录
func (h *ModerationHandler) BatchRemove(c *gin.Context) {
var data struct {
Ids []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if len(data.Ids) == 0 {
resp.ERROR(c, "请选择要删除的记录")
return
}
err := h.DB.Where("id IN ?", data.Ids).Delete(&model.Moderation{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// 获取 source 列表
func (h *ModerationHandler) GetSourceList(c *gin.Context) {
sources := []gin.H{
{
"id": types.ModerationSourceChat,
"name": "AI对话",
},
{
"id": types.ModerationSourceMJ,
"name": "Midjourney 绘图",
},
{
"id": types.ModerationSourceDalle,
"name": "Dalle 绘图",
},
{
"id": types.ModerationSourceSD,
"name": "StableDiffusion 绘图",
},
{
"id": types.ModerationSourceSuno,
"name": "Suno 音乐",
},
{
"id": types.ModerationSourceVideo,
"name": "视频生成",
},
{
"id": types.ModerationSourceJiMeng,
"name": "即梦AI",
},
}
resp.SUCCESS(c, sources)
}
// UpdateModeration 更新文本审查配置
func (h *ModerationHandler) UpdateModeration(c *gin.Context) {
var data types.ModerationConfig
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var config model.Config
err := h.DB.Where("name", types.ConfigKeyModeration).First(&config).Error
if err != nil {
config.Name = types.ConfigKeyModeration
config.Value = utils.JsonEncode(data)
err = h.DB.Create(&config).Error
} else {
config.Value = utils.JsonEncode(data)
err = h.DB.Updates(&config).Error
}
if err != nil {
resp.ERROR(c, err.Error())
return
}
h.moderationManager.UpdateConfig(data)
h.sysConfig.Moderation = data
resp.SUCCESS(c, data)
}
// 测试结果类型,用于前端显示
type ModerationTestResult struct {
IsAbnormal bool `json:"isAbnormal"`
Details []ModerationTestDetail `json:"details"`
}
type ModerationTestDetail struct {
Category string `json:"category"`
Description string `json:"description"`
Confidence string `json:"confidence"`
IsCategory bool `json:"isCategory"`
}
// TestModeration 测试文本审查服务
func (h *ModerationHandler) TestModeration(c *gin.Context) {
var data struct {
Text string `json:"text"`
Service string `json:"service"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Text == "" {
resp.ERROR(c, "测试文本不能为空")
return
}
// 检查是否启用了文本审查
if !h.sysConfig.Moderation.Enable {
resp.ERROR(c, "文本审查服务未启用")
return
}
// 获取当前激活的审核服务
service := h.moderationManager.GetService()
// 执行文本审核
result, err := service.Moderate(data.Text)
if err != nil {
resp.ERROR(c, "审核服务调用失败: "+err.Error())
return
}
// 转换为前端需要的格式
testResult := ModerationTestResult{
IsAbnormal: result.Flagged,
Details: make([]ModerationTestDetail, 0),
}
// 构建详细信息
for category, description := range types.ModerationCategories {
score := result.CategoryScores[category]
isCategory := result.Categories[category]
testResult.Details = append(testResult.Details, ModerationTestDetail{
Category: category,
Description: description,
Confidence: fmt.Sprintf("%.2f", score),
IsCategory: isCategory,
})
}
resp.SUCCESS(c, testResult)
}
+34 -3
View File
@@ -15,6 +15,7 @@ import (
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -28,6 +29,14 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
return &OrderHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *OrderHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/order/")
group.POST("list", h.List)
group.GET("remove", h.Remove)
group.GET("clear", h.Clear)
}
func (h *OrderHandler) List(c *gin.Context) {
var data struct {
OrderNo string `json:"order_no"`
@@ -67,6 +76,16 @@ func (h *OrderHandler) List(c *gin.Context) {
order.Id = item.Id
order.CreatedAt = item.CreatedAt.Unix()
order.UpdatedAt = item.UpdatedAt.Unix()
payChannel, ok := types.PayChannel[item.Channel]
if !ok {
payChannel = item.Channel
}
payWays, ok := types.PayWays[item.PayWay]
if !ok {
payWays = item.PayWay
}
order.ChannelName = payChannel
order.PayName = payWays
list = append(list, order)
} else {
logger.Error(err)
@@ -92,7 +111,7 @@ func (h *OrderHandler) Remove(c *gin.Context) {
return
}
err := h.DB.Unscoped().Where("id = ?", id).Delete(&model.Order{}).Error
err := h.DB.Where("id = ?", id).Delete(&model.Order{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -102,8 +121,20 @@ func (h *OrderHandler) Remove(c *gin.Context) {
}
func (h *OrderHandler) Clear(c *gin.Context) {
err := h.DB.Unscoped().Where("status <> ?", 2).Where("pay_time", 0).Delete(&model.Order{}).Error
var orders []model.Order
err := h.DB.Where("status <> ?", 2).Where("pay_time", 0).Find(&orders).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
deleteIds := make([]uint, 0)
for _, order := range orders {
// 只删除超时的未支付订单
if time.Now().After(order.CreatedAt.Add(time.Minute * time.Duration(h.App.SysConfig.Base.OrderPayTimeout))) {
deleteIds = append(deleteIds, order.Id)
}
}
err = h.DB.Where("id IN ?", deleteIds).Delete(&model.Order{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
+13
View File
@@ -28,9 +28,16 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
return &PowerLogHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *PowerLogHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/powerLog/")
group.POST("list", h.List)
}
func (h *PowerLogHandler) List(c *gin.Context) {
var data struct {
Username string `json:"username"`
UserId uint `json:"userid"`
Type int `json:"type"`
Model string `json:"model"`
Date []string `json:"date"`
@@ -49,6 +56,12 @@ func (h *PowerLogHandler) List(c *gin.Context) {
if data.Type > 0 {
session = session.Where("type", data.Type)
}
if data.UserId > 0 {
session = session.Where("user_id", data.UserId)
}
if data.Username != "" {
session = session.Where("username", data.Username)
}
if len(data.Date) == 2 {
start := data.Date[0] + " 00:00:00"
end := data.Date[1] + " 00:00:00"
+16 -9
View File
@@ -15,9 +15,10 @@ import (
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type ProductHandler struct {
@@ -28,14 +29,22 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
return &ProductHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ProductHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/product/")
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
}
func (h *ProductHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Discount float64 `json:"discount"`
Enabled bool `json:"enabled"`
Days int `json:"days"`
Power int `json:"power"`
CreatedAt int64 `json:"created_at"`
}
@@ -45,12 +54,10 @@ func (h *ProductHandler) Save(c *gin.Context) {
}
item := model.Product{
Name: data.Name,
Price: data.Price,
Discount: data.Discount,
Days: data.Days,
Power: data.Power,
Enabled: data.Enabled}
Name: data.Name,
Price: data.Price,
Power: data.Power,
Enabled: data.Enabled}
item.Id = data.Id
if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0)
+80 -15
View File
@@ -8,6 +8,8 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"encoding/csv"
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/handler"
@@ -27,6 +29,16 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *RedeemHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/redeem/")
group.GET("list", h.List)
group.POST("create", h.Create)
group.POST("set", h.Set)
group.GET("remove", h.Remove)
group.POST("export", h.Export)
}
func (h *RedeemHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
@@ -35,12 +47,10 @@ func (h *RedeemHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{})
if code != "" {
session.Where("code LIKE ?", "%"+code+"%")
session = session.Where("code LIKE ?", "%"+code+"%")
}
if status == 0 {
session.Where("redeem_at = ?", 0)
} else if status == 1 {
session.Where("redeem_at > ?", 0)
if status >= 0 {
session = session.Where("redeemed_at", status)
}
var total int64
@@ -80,6 +90,65 @@ func (h *RedeemHandler) List(c *gin.Context) {
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, items))
}
// Export 导出 CVS 文件
func (h *RedeemHandler) Export(c *gin.Context) {
var data struct {
Status int `json:"status"`
Ids []int `json:"ids"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
}
session := h.DB.Session(&gorm.Session{})
if data.Status >= 0 {
session = session.Where("redeemed_at", data.Status)
}
if len(data.Ids) > 0 {
session = session.Where("id IN ?", data.Ids)
}
var items []model.Redeem
err := session.Order("id DESC").Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 设置响应头,告诉浏览器这是一个附件,需要下载
c.Header("Prompt-Disposition", "attachment; filename=output.csv")
c.Header("Prompt-Type", "text/csv")
// 创建一个 CSV writer
writer := csv.NewWriter(c.Writer)
// 写入 CSV 文件的标题行
headers := []string{"名称", "兑换码", "算力", "创建时间"}
if err := writer.Write(headers); err != nil {
resp.ERROR(c, err.Error())
return
}
// 写入数据行
records := make([][]string, 0)
for _, item := range items {
records = append(records, []string{item.Name, item.Code, fmt.Sprintf("%d", item.Power), item.CreatedAt.Format("2006-01-02 15:04:05")})
}
for _, record := range records {
if err := writer.Write(record); err != nil {
resp.ERROR(c, err.Error())
return
}
}
// 确保所有数据都已写入响应
writer.Flush()
if err := writer.Error(); err != nil {
resp.ERROR(c, err.Error())
return
}
}
func (h *RedeemHandler) Create(c *gin.Context) {
var data struct {
Name string `json:"name"`
@@ -146,19 +215,15 @@ func (h *RedeemHandler) Set(c *gin.Context) {
}
func (h *RedeemHandler) Remove(c *gin.Context) {
var data struct {
Id uint
}
if err := c.ShouldBindJSON(&data); err != nil {
id := h.GetInt(c, "id", 0)
if id <= 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.Id > 0 {
err := h.DB.Where("id", data.Id).Delete(&model.Redeem{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
err := h.DB.Where("id", id).Delete(&model.Redeem{}).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
+28 -2
View File
@@ -9,13 +9,15 @@ package admin
import (
"geekai/core"
"geekai/core/middleware"
"geekai/handler"
"geekai/service/oss"
"geekai/store/model"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type UploadHandler struct {
@@ -27,15 +29,39 @@ func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderMan
return &UploadHandler{BaseHandler: handler.BaseHandler{DB: db, App: app}, uploaderManager: manager}
}
// RegisterRoutes 注册路由
func (h *UploadHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/upload")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("", h.Upload)
}
}
func (h *UploadHandler) Upload(c *gin.Context) {
// 判断文件大小
f, err := c.FormFile("file")
if err != nil {
resp.ERROR(c, err.Error())
return
}
if h.App.SysConfig.Base.MaxFileSize > 0 && f.Size > int64(h.App.SysConfig.Base.MaxFileSize)*1024*1024 {
resp.ERROR(c, "文件大小超过限制")
return
}
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil {
resp.ERROR(c, err.Error())
return
}
userId := 0
res := h.DB.Create(&model.File{
UserId: userId,
UserId: uint(userId),
Name: file.Name,
ObjKey: file.ObjKey,
URL: file.URL,
+81 -18
View File
@@ -10,9 +10,9 @@ package admin
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
@@ -20,16 +20,34 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
)
type UserHandler struct {
handler.BaseHandler
licenseService *service.LicenseService
redis *redis.Client
}
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *UserHandler {
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService}
func NewUserHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client) *UserHandler {
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, redis: redisCli}
}
// RegisterRoutes 注册路由
func (h *UserHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/user/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.POST("save", h.Save)
group.GET("remove", h.Remove)
group.GET("loginLog", h.LoginLog)
group.GET("genLoginLink", h.GenLoginLink)
group.POST("resetPass", h.ResetPass)
}
}
// List 用户列表
@@ -37,6 +55,8 @@ func (h *UserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
username := h.GetTrim(c, "username")
mobile := h.GetTrim(c, "mobile")
email := h.GetTrim(c, "email")
offset := (page - 1) * pageSize
var items []model.User
@@ -47,6 +67,12 @@ func (h *UserHandler) List(c *gin.Context) {
if username != "" {
session = session.Where("username LIKE ?", "%"+username+"%")
}
if mobile != "" {
session = session.Where("mobile LIKE ?", "%"+mobile+"%")
}
if email != "" {
session = session.Where("email LIKE ?", "%"+email+"%")
}
session.Model(&model.User{}).Count(&total)
res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
@@ -73,6 +99,8 @@ func (h *UserHandler) Save(c *gin.Context) {
Id uint `json:"id"`
Password string `json:"password"`
Username string `json:"username"`
Mobile string `json:"mobile"`
Email string `json:"email"`
ChatRoles []string `json:"chat_roles"`
ChatModels []int `json:"chat_models"`
ExpiredTime string `json:"expired_time"`
@@ -84,13 +112,6 @@ func (h *UserHandler) Save(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs)
return
}
// 检测最大注册人数
var totalUser int64
h.DB.Model(&model.User{}).Count(&totalUser)
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
return
}
var user = model.User{}
var res *gorm.DB
var userVo vo.User
@@ -102,6 +123,8 @@ func (h *UserHandler) Save(c *gin.Context) {
}
var oldPower = user.Power
user.Username = data.Username
user.Email = data.Email
user.Mobile = data.Mobile
user.Status = data.Status
user.Vip = data.Vip
user.Power = data.Power
@@ -109,7 +132,8 @@ func (h *UserHandler) Save(c *gin.Context) {
user.ChatModels = utils.JsonEncode(data.ChatModels)
user.ExpiredTime = utils.Str2stamp(data.ExpiredTime)
res = h.DB.Select("username", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user)
if res.Error != nil {
logger.Error("error with update database", res.Error)
resp.ERROR(c, res.Error.Error())
@@ -135,6 +159,13 @@ func (h *UserHandler) Save(c *gin.Context) {
CreatedAt: time.Now(),
})
}
// 如果禁用了用户,则将用户踢下线
if user.Status == false {
key := fmt.Sprintf("users/%v", user.Id)
if _, err := h.redis.Del(c, key).Result(); err != nil {
logger.Error("error with delete session: ", err)
}
}
} else {
// 检查用户是否已经存在
h.DB.Where("username", data.Username).First(&user)
@@ -147,19 +178,18 @@ func (h *UserHandler) Save(c *gin.Context) {
u := model.User{
Username: data.Username,
Password: utils.GenPassword(data.Password, salt),
Mobile: data.Mobile,
Email: data.Email,
Avatar: "/images/avatar/user.png",
Salt: salt,
Power: data.Power,
Status: true,
ChatRoles: utils.JsonEncode(data.ChatRoles),
ChatConfig: "{}",
ChatModels: utils.JsonEncode(data.ChatModels),
ExpiredTime: utils.Str2stamp(data.ExpiredTime),
}
if h.licenseService.GetLicense().Configs.DeCopy {
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
} else {
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
}
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
res = h.DB.Create(&u)
_ = utils.CopyObject(u, &userVo)
userVo.Id = u.Id
@@ -262,7 +292,7 @@ func (h *UserHandler) Remove(c *gin.Context) {
}
}
if err != nil {
resp.ERROR(c, "删除失败")
resp.ERROR(c, err.Error())
tx.Rollback()
return
}
@@ -295,3 +325,36 @@ func (h *UserHandler) LoginLog(c *gin.Context) {
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs))
}
// GenLoginLink 生成登录链接
func (h *UserHandler) GenLoginLink(c *gin.Context) {
id := c.Query("id")
if id == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.User
if err := h.DB.Where("id = ?", id).First(&user).Error; err != nil {
resp.ERROR(c, "用户不存在")
return
}
// 创建 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
sessionKey := fmt.Sprintf("users/%d", user.Id)
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
resp.ERROR(c, "error with save token: "+err.Error())
return
}
resp.SUCCESS(c, tokenString)
}
+13 -4
View File
@@ -8,16 +8,17 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"fmt"
"geekai/core"
"geekai/core/types"
logger2 "geekai/logger"
"geekai/store/model"
"geekai/utils"
"errors"
"fmt"
"gorm.io/gorm"
"strings"
"gorm.io/gorm"
"github.com/gin-gonic/gin"
)
@@ -69,6 +70,14 @@ func (h *BaseHandler) GetLoginUserId(c *gin.Context) uint {
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
}
func (h *BaseHandler) GetAdminId(c *gin.Context) uint {
userId, ok := c.Get(types.AdminUserID)
if !ok {
return 0
}
return uint(utils.IntValue(utils.InterfaceToString(userId), 0))
}
func (h *BaseHandler) IsLogin(c *gin.Context) bool {
return h.GetLoginUserId(c) > 0
}
@@ -85,7 +94,7 @@ func (h *BaseHandler) GetLoginUser(c *gin.Context) (model.User, error) {
}
var user model.User
res := h.DB.First(&user, userId)
res := h.DB.Where("id", userId).First(&user)
// 更新缓存
if res.Error == nil {
c.Set(types.LoginUserCache, user)
+41 -4
View File
@@ -8,23 +8,45 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
)
// 今日头条函数实现
type CaptchaHandler struct {
App *core.AppServer
service *service.CaptchaService
}
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler {
return &CaptchaHandler{service: s}
func NewCaptchaHandler(app *core.AppServer, s *service.CaptchaService, sysConfig *types.SystemConfig) *CaptchaHandler {
return &CaptchaHandler{App: app, service: s}
}
// RegisterRoutes 注册路由
func (h *CaptchaHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/captcha/")
// 无需授权的接口
group.GET("get", h.Get)
group.POST("check", h.Check)
group.GET("slide/get", h.SlideGet)
group.POST("slide/check", h.SlideCheck)
group.GET("config", h.GetConfig)
}
func (h *CaptchaHandler) GetConfig(c *gin.Context) {
resp.SUCCESS(c, gin.H{"enabled": h.service.GetConfig().Enabled, "type": h.service.GetConfig().Type})
}
func (h *CaptchaHandler) Get(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
data, err := h.service.Get()
if err != nil {
resp.ERROR(c, err.Error())
@@ -36,6 +58,11 @@ func (h *CaptchaHandler) Get(c *gin.Context) {
// Check verify the captcha data
func (h *CaptchaHandler) Check(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
var data struct {
Key string `json:"key"`
Dots string `json:"dots"`
@@ -55,6 +82,11 @@ func (h *CaptchaHandler) Check(c *gin.Context) {
// SlideGet 获取滑动验证图片
func (h *CaptchaHandler) SlideGet(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
data, err := h.service.SlideGet()
if err != nil {
resp.ERROR(c, err.Error())
@@ -66,6 +98,11 @@ func (h *CaptchaHandler) SlideGet(c *gin.Context) {
// SlideCheck 滑动验证结果校验
func (h *CaptchaHandler) SlideCheck(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
var data struct {
Key string `json:"key"`
X int `json:"x"`
+138
View File
@@ -0,0 +1,138 @@
package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ChatAppHandler struct {
BaseHandler
}
func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
return &ChatAppHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatAppHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/app/")
group.GET("list", h.List)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.GET("list/user", h.ListByUser)
group.POST("update", h.UpdateApp)
}
}
// List 获取用户聊天应用列表
func (h *ChatAppHandler) List(c *gin.Context) {
tid := h.GetInt(c, "tid", 0)
var roles []model.ChatApp
session := h.DB.Where("enable", true)
if tid > 0 {
session = session.Where("tid", tid)
}
err := session.Order("sort_num ASC").Find(&roles).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
var roleVos = make([]vo.ChatApp, 0)
for _, r := range roles {
var v vo.ChatApp
err := utils.CopyObject(r, &v)
if err == nil {
v.Id = r.Id
roleVos = append(roleVos, v)
}
}
resp.SUCCESS(c, roleVos)
}
// ListByUser 获取用户添加的角色列表
func (h *ChatAppHandler) ListByUser(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
var roles []model.ChatApp
session := h.DB.Where("enable", true)
// 如果用户没登录,则获取所有角色
if userId > 0 {
var user model.User
h.DB.First(&user, userId)
var roleKeys []string
if user.ChatRoles != "" {
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
if err != nil {
resp.ERROR(c, "角色解析失败!")
return
}
}
// 保证用户至少有一个角色可用
if len(roleKeys) > 0 {
session = session.Where("marker IN ?", roleKeys)
}
}
if id > 0 {
session = session.Or("id", id)
}
res := session.Order("sort_num ASC").Find(&roles)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
var roleVos = make([]vo.ChatApp, 0)
for _, r := range roles {
var v vo.ChatApp
err := utils.CopyObject(r, &v)
if err == nil {
v.Id = r.Id
roleVos = append(roleVos, v)
}
}
resp.SUCCESS(c, roleVos)
}
// UpdateApp 更新用户聊天应用
func (h *ChatAppHandler) UpdateApp(c *gin.Context) {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
var data struct {
Keys []string `json:"keys"`
}
if err = c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
+50
View File
@@ -0,0 +1,50 @@
package handler
import (
"geekai/core"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ChatAppTypeHandler struct {
BaseHandler
}
func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler {
return &ChatAppTypeHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatAppTypeHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/app/type/")
group.GET("list", h.List)
}
// List 获取App类型列表
func (h *ChatAppTypeHandler) List(c *gin.Context) {
var items []model.AppType
var appTypes = make([]vo.AppType, 0)
err := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
for _, v := range items {
var appType vo.AppType
err = utils.CopyObject(v, &appType)
if err != nil {
continue
}
appType.Id = v.Id
appType.CreatedAt = v.CreatedAt.Unix()
appTypes = append(appTypes, appType)
}
resp.SUCCESS(c, appTypes)
}
+838
View File
@@ -0,0 +1,838 @@
package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/sora"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"io"
"net/http"
"net/url"
"os"
"path"
"strings"
"time"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/sashabaranov/go-openai"
"gorm.io/gorm"
)
const (
ChatEventStart = "start"
ChatEventEnd = "end"
ChatEventComplete = "complete"
ChatEventError = "error"
ChatEventMessageDelta = "message_delta"
ChatEventTitle = "title"
)
type ChatInput struct {
UserId uint `json:"user_id"`
RoleId uint `json:"role_id"`
ModelId uint `json:"model_id"`
ChatId string `json:"chat_id"`
Prompt string `json:"prompt"`
Tools []uint `json:"tools"`
Stream bool `json:"stream"`
Files []vo.File `json:"files"`
ChatModel model.ChatModel `json:"chat_model,omitempty"`
ChatRole model.ChatApp `json:"chat_role,omitempty"`
LastMsgId uint `json:"last_msg_id,omitempty"` // 最后的消息ID,用于重新生成答案的时候过滤上下文
}
type ChatHandler struct {
BaseHandler
redis *redis.Client
uploadManager *oss.UploaderManager
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
userService *service.UserService
moderationManager *moderation.ServiceManager
userLocks *types.UserLockManager
soraService *sora.SoraService
}
func NewChatHandler(app *core.AppServer,
db *gorm.DB,
redis *redis.Client,
manager *oss.UploaderManager,
userService *service.UserService,
soraService *sora.SoraService,
moderationManager *moderation.ServiceManager) *ChatHandler {
return &ChatHandler{
BaseHandler: BaseHandler{App: app, DB: db},
redis: redis,
uploadManager: manager,
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
userService: userService,
moderationManager: moderationManager,
userLocks: types.NewUserLockManager(),
soraService: soraService,
}
}
// RegisterRoutes 注册路由
func (h *ChatHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/chat/")
// 其他接口需要用户授权
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.Any("message", h.Chat)
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)
group.POST("tts", h.TextToSpeech)
}
}
// Chat 处理聊天请求
func (h *ChatHandler) Chat(c *gin.Context) {
// 设置SSE响应头
c.Header("Prompt-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("X-Accel-Buffering", "no")
var input ChatInput
if err := c.ShouldBindJSON(&input); err != nil {
pushMessage(c, ChatEventError, types.InvalidArgs)
c.Abort()
return
}
// 用户级并发锁,确保同一用户同时只有一个对话请求
if !h.userLocks.TryLock(input.UserId) {
pushMessage(c, ChatEventError, "您有一个对话请求正在进行中,请稍后再试或先停止当前生成!")
c.Abort()
return
}
defer h.userLocks.Unlock(input.UserId)
ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel()
// 这里做个全局的异常处理,防止整个请求异常,导致 SSE 连接断开
defer func() {
if err := recover(); err != nil {
logger.Errorf("chat handler error: %v", err)
pushMessage(c, ChatEventError, err)
c.Abort()
}
}()
// 使用旧的聊天数据覆盖模型和角色ID
var chat model.ChatItem
h.DB.Where("chat_id", input.ChatId).First(&chat)
if chat.Id > 0 {
input.ModelId = chat.ModelId
input.RoleId = chat.RoleId
}
// 验证聊天角色
var chatRole model.ChatApp
err := h.DB.First(&chatRole, input.RoleId).Error
if err != nil || !chatRole.Enable {
pushMessage(c, ChatEventError, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!")
return
}
input.ChatRole = chatRole
// 获取模型信息
var chatModel model.ChatModel
err = h.DB.Where("id", input.ModelId).First(&chatModel).Error
if err != nil || !chatModel.Enabled {
pushMessage(c, ChatEventError, "当前AI模型暂未启用,请更换模型后再发起对话!")
return
}
input.ChatModel = chatModel
// 发送消息
err = h.sendMessage(ctx, input, c)
if err != nil {
pushMessage(c, ChatEventError, err.Error())
return
}
pushMessage(c, ChatEventEnd, "对话完成")
}
func pushMessage(c *gin.Context, msgType string, content interface{}) {
c.SSEvent("message", map[string]interface{}{
"type": msgType,
"body": content,
})
c.Writer.Flush()
}
func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.Context) error {
var user model.User
res := h.DB.Model(&model.User{}).First(&user, input.UserId)
if res.Error != nil {
return errors.New("未授权用户,您正在进行非法操作!")
}
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 {
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
}
if userVo.Power < input.ChatModel.Power {
return fmt.Errorf("您的算力不足,请购买算力。")
}
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
return errors.New("您的账号已经过期,请联系管理员!")
}
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
promptTokens, _ := utils.CalcTokens(input.Prompt, input.ChatModel.Value)
if promptTokens > input.ChatModel.MaxContext {
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
}
var req = types.ApiRequest{
Model: input.ChatModel.Value,
Stream: input.Stream,
Temperature: input.ChatModel.Temperature,
}
// 兼容 OpenAI 模型
if strings.HasPrefix(input.ChatModel.Value, "o1-") ||
strings.HasPrefix(input.ChatModel.Value, "o3-") ||
strings.HasPrefix(input.ChatModel.Value, "gpt") {
req.MaxCompletionTokens = input.ChatModel.MaxTokens
} else {
req.MaxTokens = input.ChatModel.MaxTokens
}
if len(input.Tools) > 0 && !strings.HasPrefix(input.ChatModel.Value, "o1-") {
var items []model.Function
res = h.DB.Where("enabled", true).Where("id IN ?", input.Tools).Find(&items)
if res.Error == nil {
var tools = make([]types.Tool, 0)
for _, v := range items {
var parameters map[string]interface{}
err = utils.JsonDecode(v.Parameters, &parameters)
if err != nil {
continue
}
tool := types.Tool{
Type: "function",
Function: types.Function{
Name: v.Name,
Description: v.Description,
Parameters: parameters,
},
}
if v, ok := parameters["required"]; v == nil || !ok {
tool.Function.Parameters["required"] = []string{}
}
tools = append(tools, tool)
}
if len(tools) > 0 {
req.Tools = tools
req.ToolChoice = "auto"
}
}
}
// 加载聊天上下文
chatCtx := make([]any, 0)
messages := make([]any, 0)
if h.App.SysConfig.Base.EnableContext {
_ = utils.JsonDecode(input.ChatRole.Context, &messages)
if h.App.SysConfig.Base.ContextDeep > 0 {
var historyMessages []model.ChatMessage
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
if input.LastMsgId > 0 { // 重新生成和编辑逻辑
var lastMessage model.ChatMessage
err = dbSession.Where("id < ?", input.LastMsgId).Where("type", types.ReplyMsg).Order("id DESC").First(&lastMessage).Error
if err != nil {
input.LastMsgId = 0
} else {
input.LastMsgId = lastMessage.Id
}
dbSession = dbSession.Where("id < ?", input.LastMsgId)
// 删除对应的聊天记录
h.DB.Debug().Where("chat_id", input.ChatId).Where("id > ?", input.LastMsgId).Delete(&model.ChatMessage{})
}
err = dbSession.Limit(h.App.SysConfig.Base.ContextDeep).Order("id DESC").Find(&historyMessages).Error
if err == nil {
for i := len(historyMessages) - 1; i >= 0; i-- {
msg := historyMessages[i]
ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.ReplyMsg {
ms.Role = "assistant"
}
chatCtx = append(chatCtx, ms)
}
}
}
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
// MaxContextLength = Response + Tool + Prompt + Context
tokens := req.MaxTokens // 最大响应长度
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
tokens += tks + promptTokens
for i := len(messages) - 1; i >= 0; i-- {
v := messages[i]
tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
// 上下文 token 超出了模型的最大上下文长度
if tokens+tks >= input.ChatModel.MaxContext {
break
}
// 上下文的深度超出了模型的最大上下文深度
if len(chatCtx) >= h.App.SysConfig.Base.ContextDeep {
break
}
tokens += tks
chatCtx = append(chatCtx, v)
}
}
reqMgs := make([]any, 0)
// 添加引导提示词,防止模型生成违规内容
if h.App.SysConfig.Moderation.EnableGuide {
reqMgs = append(reqMgs, map[string]any{
"role": "system",
"content": h.App.SysConfig.Moderation.GuidePrompt,
})
}
for i := len(chatCtx) - 1; i >= 0; i-- {
reqMgs = append(reqMgs, chatCtx[i])
}
fileContents := make([]string, 0) // 文件内容
var finalPrompt = input.Prompt
imgList := make([]any, 0)
for _, file := range input.Files {
logger.Debugf("detected file: %+v", file.URL)
// 处理图片
if isImageURL(file.URL) || isVideoURL(file.URL) {
imgList = append(imgList, gin.H{
"type": "image_url",
"image_url": gin.H{
"url": file.URL,
},
})
} else if isTextURL(file.URL) {
// 处理文件,提取文件内容
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
if err != nil {
logger.Error("error with read file: ", err)
continue
} else {
fileContents = append(fileContents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
logger.Debugf("fileContents: %s", fileContents)
}
}
}
if len(fileContents) > 0 {
finalPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML)\n\n %s\n\n 问题:%s", strings.Join(fileContents, "\n"), input.Prompt)
tokens, _ := utils.CalcTokens(finalPrompt, req.Model)
if tokens > input.ChatModel.MaxContext {
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
}
} else {
finalPrompt = input.Prompt
}
if len(imgList) > 0 {
imgList = append(imgList, map[string]any{
"type": "text",
"text": input.Prompt,
})
req.Messages = append(reqMgs, map[string]any{
"role": "user",
"content": imgList,
})
} else {
req.Messages = append(reqMgs, map[string]any{
"role": "user",
"content": finalPrompt,
})
}
return h.sendOpenAiMessage(req, userVo, ctx, input, c)
}
// 判断一个 URL 是否图片链接
func isImageURL(url string) bool {
// 检查是否是有效的URL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return false
}
// 检查文件扩展名
ext := strings.ToLower(path.Ext(url))
validImageExts := map[string]bool{
".jpg": true,
".jpeg": true,
".png": true,
".gif": true,
".bmp": true,
".webp": true,
".svg": true,
".ico": true,
}
return validImageExts[ext]
}
// 判断是个链接是否是文本链接
func isTextURL(url string) bool {
// 检查是否是有效的URL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return false
}
// 检查文件扩展名
ext := strings.ToLower(path.Ext(url))
validTextExts := map[string]bool{
".txt": true,
".doc": true,
".docx": true,
".pdf": true,
}
return validTextExts[ext]
}
// 判断是个链接是否是视频
func isVideoURL(url string) bool {
// 检查是否是有效的URL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return false
}
// 检查文件扩展名
ext := strings.ToLower(path.Ext(url))
validVideoExts := map[string]bool{
".mp4": true,
".avi": true,
".mov": true,
".wmv": true,
}
return validVideoExts[ext]
}
// Tokens 统计 token 数量
func (h *ChatHandler) Tokens(c *gin.Context) {
var data struct {
Text string `json:"text"`
Model string `json:"model"`
ChatId string `json:"chat_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
if data.Text == "" && data.ChatId != "" {
var item model.ChatMessage
userId, _ := c.Get(types.LoginUserID)
res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).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.ReqCancelFunc.Has(sessionId) {
h.ReqCancelFunc.Get(sessionId)()
h.ReqCancelFunc.Delete(sessionId)
}
resp.SUCCESS(c, types.OkMsg)
}
// 发送请求到 OpenAI 服务器
// useOwnApiKey: 是否使用了用户自己的 API KEY
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input ChatInput, apiKey *model.ApiKey) (*http.Response, error) {
// if the chat model bind a KEY, use it directly
if input.ChatModel.KeyId > 0 {
h.DB.Where("id", input.ChatModel.KeyId).Where("enabled", true).Find(apiKey)
} else { // use the last unused key
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
}
if apiKey.Id == 0 {
return nil, errors.New("no available key, please import key")
}
logger.Debugf("对话请求消息体:%+v", req)
var apiURL string
p, _ := url.Parse(apiKey.ApiURL)
// 如果设置的是 BASE_URL 没有路径,则添加 /v1/chat/completions
if p.Path == "" {
apiURL = fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
} else {
apiURL = apiKey.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")
if len(apiKey.ProxyURL) > 5 { // 使用代理
proxy, _ := url.Parse(apiKey.ProxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
} else {
client = http.DefaultClient
}
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
// 更新API KEY 最后使用时间
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
return client.Do(request)
}
// 扣减用户算力
func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens int, replyTokens int) {
power := 1
if input.ChatModel.Power > 0 {
power = input.ChatModel.Power
}
err := h.userService.DecreasePower(userVo.Id, power, model.PowerLog{
Type: types.PowerConsume,
Model: input.ChatModel.Value,
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", input.ChatModel.Name, promptTokens, replyTokens),
})
if err != nil {
logger.Error(err)
}
}
func (h *ChatHandler) saveChatHistory(
c *gin.Context,
req types.ApiRequest,
usage Usage,
message types.Message,
input ChatInput,
userVo vo.User,
promptCreatedAt time.Time,
replyCreatedAt time.Time) {
// 文本审核
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(usage.Content)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
logger.Debugf("moderationResult: %+v", moderationResult)
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: userVo.Id,
Source: types.ModerationSourceChat,
Input: usage.Prompt,
Output: usage.Content,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
pushMessage(c, ChatEventError, "很抱歉,内容触发敏感词预警,AI 无法回答!!!")
// 更新用户算力
if input.ChatModel.Power > 0 {
h.subUserPower(userVo, input, 0, 0)
}
return
}
}
// 追加聊天记录
// for prompt
var promptTokens, replyTokens, totalTokens int
if usage.PromptTokens > 0 {
promptTokens = usage.PromptTokens
} else {
promptTokens, _ = utils.CalcTokens(usage.Content, req.Model)
}
historyUserMsg := model.ChatMessage{
UserId: userVo.Id,
ChatId: input.ChatId,
RoleId: input.RoleId,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: utils.JsonEncode(vo.MsgContent{
Text: usage.Prompt,
Files: input.Files,
}),
Tokens: promptTokens,
TotalTokens: promptTokens,
UseContext: true,
Model: req.Model,
}
historyUserMsg.CreatedAt = promptCreatedAt
historyUserMsg.UpdatedAt = promptCreatedAt
err := h.DB.Save(&historyUserMsg).Error
if err != nil {
logger.Error("failed to save prompt history message: ", err)
}
// for reply
// 计算本次对话消耗的总 token 数量
if usage.CompletionTokens > 0 {
replyTokens = usage.CompletionTokens
totalTokens = usage.TotalTokens
} else {
replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
totalTokens = replyTokens + getTotalTokens(req)
}
// 如果是 Sora 相关模型,则下载视频
files := make([]vo.File, 0)
if strings.HasPrefix(req.Model, "sora") {
video, err := h.soraService.DownloadVideoURL(message.Content)
if err == nil {
files = append(files, *video)
}
}
historyReplyMsg := model.ChatMessage{
UserId: userVo.Id,
ChatId: input.ChatId,
RoleId: input.RoleId,
Type: types.ReplyMsg,
Icon: input.ChatRole.Icon,
Content: utils.JsonEncode(vo.MsgContent{
Text: message.Content,
Files: files,
}),
Tokens: replyTokens,
TotalTokens: totalTokens,
UseContext: true,
Model: req.Model,
}
historyReplyMsg.CreatedAt = replyCreatedAt
historyReplyMsg.UpdatedAt = replyCreatedAt
err = h.DB.Create(&historyReplyMsg).Error
if err != nil {
logger.Error("failed to save reply history message: ", err)
}
// 发送完整聊天记录给前端
var messageVo vo.ChatMessage
err = utils.CopyObject(historyReplyMsg, &messageVo)
if err == nil {
// 解析内容
var content vo.MsgContent
err = utils.JsonDecode(historyReplyMsg.Content, &content)
if err != nil {
content.Text = historyReplyMsg.Content
}
content.Files = files
messageVo.Content = content
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
pushMessage(c, ChatEventComplete, messageVo)
}
// 更新用户算力
if input.ChatModel.Power > 0 {
h.subUserPower(userVo, input, promptTokens, replyTokens)
}
// 保存当前会话
var chatItem model.ChatItem
err = h.DB.Where("chat_id = ?", input.ChatId).First(&chatItem).Error
if err != nil {
chatItem.ChatId = input.ChatId
chatItem.UserId = userVo.Id
chatItem.RoleId = input.RoleId
chatItem.ModelId = input.ModelId
if utf8.RuneCountInString(usage.Prompt) > 30 {
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
} else {
chatItem.Title = usage.Prompt
}
chatItem.Model = req.Model
err = h.DB.Create(&chatItem).Error
if err != nil {
logger.Error("failed to save chat item: ", err)
}
}
}
// TextToSpeech 文本生成语音
func (h *ChatHandler) TextToSpeech(c *gin.Context) {
var data struct {
ModelId int `json:"model_id"`
Text string `json:"text"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
if _, err := os.Stat(audioFile); err != nil {
resp.ERROR(c, err.Error())
return
}
if err := os.MkdirAll(audioFile, 0755); err != nil {
resp.ERROR(c, err.Error())
return
}
audioFile = fmt.Sprintf("%s/%s.mp3", audioFile, textHash)
if _, err := os.Stat(audioFile); err == nil {
// 设置响应头
c.Header("Prompt-Type", "audio/mpeg")
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
c.File(audioFile)
return
}
// 查询模型
var chatModel model.ChatModel
err := h.DB.Where("id", data.ModelId).First(&chatModel).Error
if err != nil {
resp.ERROR(c, "找不到语音模型")
return
}
// 调用 DeepSeek 的 API 接口
var apiKey model.ApiKey
if chatModel.KeyId > 0 {
h.DB.Where("id", chatModel.KeyId).First(&apiKey)
}
if apiKey.Id == 0 {
h.DB.Where("type", "tts").Where("enabled", true).First(&apiKey)
}
if apiKey.Id == 0 {
resp.ERROR(c, "no TTS API key, please import key")
return
}
logger.Debugf("chatModel: %+v, apiKey: %+v", chatModel, apiKey)
// 调用 openai tts api
config := openai.DefaultConfig(apiKey.Value)
config.BaseURL = apiKey.ApiURL + "/v1"
client := openai.NewClientWithConfig(config)
voice := openai.VoiceAlloy
var options map[string]string
err = utils.JsonDecode(chatModel.Options, &options)
if err == nil {
voice = openai.SpeechVoice(options["voice"])
}
req := openai.CreateSpeechRequest{
Model: openai.SpeechModel(chatModel.Value),
Input: data.Text,
Voice: voice,
}
audioData, err := client.CreateSpeech(context.Background(), req)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 先将音频数据读取到内存
audioBytes, err := io.ReadAll(audioData)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 保存到音频文件
err = os.WriteFile(audioFile, audioBytes, 0644)
if err != nil {
logger.Error("failed to save audio file: ", err)
}
// 设置响应头
c.Header("Prompt-Type", "audio/mpeg")
c.Header("Prompt-Disposition", "attachment; filename=speech.mp3")
// 直接写入完整的音频数据到响应
_, err = c.Writer.Write(audioBytes)
if err != nil {
logger.Error("写入音频数据到响应失败:", err)
}
}
@@ -1,4 +1,4 @@
package chatimpl
package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
@@ -20,6 +20,7 @@ import (
// List 获取会话列表
func (h *ChatHandler) List(c *gin.Context) {
logger.Info(h.GetLoginUserId(c))
if !h.IsLogin(c) {
resp.SUCCESS(c)
return
@@ -28,31 +29,40 @@ func (h *ChatHandler) List(c *gin.Context) {
userId := h.GetLoginUserId(c)
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
}
h.DB.Debug().Where("user_id", userId).Order("id DESC").Find(&chats)
if len(chats) == 0 {
resp.SUCCESS(c, items)
return
}
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)
}
}
}
var roleIds = make([]uint, 0)
var modelValues = make([]string, 0)
for _, chat := range chats {
roleIds = append(roleIds, chat.RoleId)
modelValues = append(modelValues, chat.Model)
}
var roles []model.ChatApp
var models []model.ChatModel
roleMap := make(map[uint]model.ChatApp)
modelMap := make(map[string]model.ChatModel)
h.DB.Where("id IN ?", roleIds).Find(&roles)
h.DB.Where("value IN ?", modelValues).Find(&models)
for _, role := range roles {
roleMap[role.Id] = role
}
for _, m := range models {
modelMap[m.Value] = m
}
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
item.ModelId = modelMap[chat.Model].Id
items = append(items, item)
}
}
resp.SUCCESS(c, items)
}
@@ -95,8 +105,6 @@ func (h *ChatHandler) Clear(c *gin.Context) {
var chatIds = make([]string, 0)
for _, chat := range chats {
chatIds = append(chatIds, chat.ChatId)
// 清空会话上下文
h.ChatContexts.Delete(chat.ChatId)
}
err = h.DB.Transaction(func(tx *gorm.DB) error {
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
@@ -124,20 +132,28 @@ func (h *ChatHandler) Clear(c *gin.Context) {
func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID
var items []model.ChatMessage
var messages = make([]vo.HistoryMessage, 0)
var messages = make([]vo.ChatMessage, 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
var v vo.ChatMessage
err := utils.CopyObject(item, &v)
if err != nil {
continue
}
// 解析内容
var content vo.MsgContent
err = utils.JsonDecode(item.Content, &content)
if err != nil {
content.Text = item.Content
}
v.Content = content
v.CreatedAt = item.CreatedAt.Unix()
v.UpdatedAt = item.UpdatedAt.Unix()
if err == nil {
messages = append(messages, v)
}
messages = append(messages, v)
}
}
@@ -170,10 +186,6 @@ func (h *ChatHandler) Remove(c *gin.Context) {
return
}
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
// 清空会话上下文
h.ChatContexts.Delete(chatId)
resp.SUCCESS(c, types.OkMsg)
}
@@ -193,7 +205,7 @@ func (h *ChatHandler) Detail(c *gin.Context) {
}
// 填充角色名称
var role model.ChatRole
var role model.ChatApp
res = h.DB.Where("id", chatItem.RoleId).First(&role)
if res.Error != nil {
resp.ERROR(c, "Role not found")
+16 -12
View File
@@ -26,33 +26,37 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
return &ChatModelHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ChatModelHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/model/")
group.GET("list", h.List)
}
// List 模型列表
func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel
var chatModels = make([]vo.ChatModel, 0)
var res *gorm.DB
session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
t := c.Query("type")
if t != "" {
session = session.Where("type", t)
}
// 如果用户没有登录,则加载所有开放模型
if !h.IsLogin(c) {
res = session.Where("open", true).Order("sort_num ASC").Find(&items)
} else {
session = session.Where("type", "chat")
}
session = session.Where("open", true)
if h.IsLogin(c) && t == "chat" {
user, _ := h.GetLoginUser(c)
var models []int
err := utils.JsonDecode(user.ChatModels, &models)
if err != nil {
resp.ERROR(c, "当前用户没有订阅任何模型")
return
}
// 查询用户有权限访问的模型以及所有开放的模型
res = h.DB.Where("enabled = ?", true).Where(
h.DB.Where("id IN ?", models).Or("open", true),
).Order("sort_num ASC").Find(&items)
if err == nil {
session = session.Or("id IN ?", models)
}
}
res := session.Order("sort_num ASC").Find(&items)
if res.Error == nil {
for _, item := range items {
var cm vo.ChatModel
+257
View File
@@ -0,0 +1,257 @@
package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"io"
"strings"
"time"
"github.com/gin-gonic/gin"
req2 "github.com/imroc/req/v3"
)
type Usage struct {
Prompt string `json:"prompt,omitempty"`
Content string `json:"content,omitempty"`
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}
type OpenAIResVo struct {
Id string `json:"id"`
Object string `json:"object"`
Created int `json:"created"`
Model string `json:"model"`
SystemFingerprint string `json:"system_fingerprint"`
Choices []struct {
Index int `json:"index"`
Message struct {
Role string `json:"role"`
Content string `json:"content"`
} `json:"message"`
Logprobs interface{} `json:"logprobs"`
FinishReason string `json:"finish_reason"`
} `json:"choices"`
Usage Usage `json:"usage"`
}
// OPenAI 消息发送实现
func (h *ChatHandler) sendOpenAiMessage(
req types.ApiRequest,
userVo vo.User,
ctx context.Context,
input ChatInput,
c *gin.Context) error {
promptCreatedAt := time.Now() // 记录提问时间
start := time.Now()
var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, input, &apiKey)
logger.Info("HTTP请求完成,耗时:", time.Since(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", input.Prompt)
} else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
}
return err
} else {
defer response.Body.Close()
}
if response.StatusCode != 200 {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求 OpenAI API 失败:%d, %v", response.StatusCode, string(body))
}
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
replyCreatedAt := time.Now() // 记录回复时间
// 循环读取 Chunk 消息
var message = types.Message{Role: "assistant"}
var contents = make([]string, 0)
var function model.Function
var toolCall = false
var arguments = make([]string, 0)
var reasoning = false
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 { // 数据解析出错
return errors.New(line)
}
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue
}
if responseBody.Choices[0].Delta.Content == nil &&
responseBody.Choices[0].Delta.ToolCalls == nil &&
responseBody.Choices[0].Delta.ReasoningContent == "" {
continue
}
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
pushMessage(c, "text", "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
break
}
var tool types.ToolCall
if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
tool = responseBody.Choices[0].Delta.ToolCalls[0]
if toolCall && tool.Function.Name == "" {
arguments = append(arguments, tool.Function.Arguments)
continue
}
}
// 兼容 Function Call
fun := responseBody.Choices[0].Delta.FunctionCall
if fun.Name != "" {
tool = *new(types.ToolCall)
tool.Function.Name = fun.Name
} else if toolCall {
arguments = append(arguments, fun.Arguments)
continue
}
if !utils.IsEmptyValue(tool) {
res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
if res.Error == nil {
toolCall = true
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
pushMessage(c, "text", callMsg)
contents = append(contents, callMsg)
}
continue
}
if responseBody.Choices[0].FinishReason == "tool_calls" ||
responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
break
}
// output stopped
if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else { // 正常输出结果
// 兼容思考过程
if responseBody.Choices[0].Delta.ReasoningContent != "" {
reasoningContent := responseBody.Choices[0].Delta.ReasoningContent
if !reasoning {
reasoningContent = fmt.Sprintf("<think>%s", reasoningContent)
reasoning = true
}
pushMessage(c, "text", reasoningContent)
contents = append(contents, reasoningContent)
} else if responseBody.Choices[0].Delta.Content != "" {
finalContent := responseBody.Choices[0].Delta.Content
if reasoning {
finalContent = fmt.Sprintf("</think>%s", responseBody.Choices[0].Delta.Content)
reasoning = false
}
contents = append(contents, utils.InterfaceToString(finalContent))
pushMessage(c, "text", finalContent)
}
}
} // end for
if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", input.Prompt)
} else {
logger.Error("信息读取出错:", err)
}
}
if toolCall { // 调用函数完成任务
params := make(map[string]any)
_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
params["user_id"] = userVo.Id
var apiRes types.BizVo
r, err := req2.C().R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", function.Token).
SetBody(params).Post(function.Action)
errMsg := ""
if err != nil {
errMsg = err.Error()
} else {
all, _ := io.ReadAll(r.Body)
err = json.Unmarshal(all, &apiRes)
if err != nil {
errMsg = err.Error()
} else if apiRes.Code != types.Success {
errMsg = apiRes.Message
}
}
if errMsg != "" {
errMsg = "调用函数工具出错:" + errMsg
contents = append(contents, errMsg)
} else {
errMsg = utils.InterfaceToString(apiRes.Data)
contents = append(contents, errMsg)
// 扣减算力
if function.Power > 0 {
h.userService.DecreasePower(userVo.Id, function.Power, model.PowerLog{
Type: types.PowerConsume,
Model: function.Name,
Remark: fmt.Sprintf("函数调用,%s(%s)", function.Label, function.Name),
})
}
}
pushMessage(c, "text", errMsg)
}
// 消息发送成功
if len(contents) > 0 {
usage := Usage{
Prompt: input.Prompt,
Content: strings.Join(contents, ""),
PromptTokens: 0,
CompletionTokens: 0,
TotalTokens: 0,
}
message.Content = usage.Content
h.saveChatHistory(c, req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
}
} else { // 非流式输出
var respVo OpenAIResVo
body, err := io.ReadAll(response.Body)
if err != nil {
return fmt.Errorf("读取响应失败:%v", body)
}
err = json.Unmarshal(body, &respVo)
if err != nil {
return fmt.Errorf("解析响应失败:%v", body)
}
content := respVo.Choices[0].Message.Content
pushMessage(c, "text", content)
respVo.Usage.Prompt = input.Prompt
respVo.Usage.Content = content
h.saveChatHistory(c, req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
}
return nil
}
-91
View File
@@ -1,91 +0,0 @@
package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"geekai/core"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ChatRoleHandler struct {
BaseHandler
}
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// List 获取用户聊天应用列表
func (h *ChatRoleHandler) List(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
var roles []model.ChatRole
query := h.DB.Where("enable", true)
if userId > 0 {
var user model.User
h.DB.First(&user, userId)
var roleKeys []string
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
if err != nil {
resp.ERROR(c, "角色解析失败!")
return
}
query = query.Where("marker IN ?", roleKeys)
}
if id > 0 {
query = query.Or("id", id)
}
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
var roleVos = make([]vo.ChatRole, 0)
for _, r := range roles {
var v vo.ChatRole
err := utils.CopyObject(r, &v)
if err == nil {
v.Id = r.Id
roleVos = append(roleVos, v)
}
}
resp.SUCCESS(c, roleVos)
}
// UpdateRole 更新用户聊天角色
func (h *ChatRoleHandler) UpdateRole(c *gin.Context) {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
var data struct {
Keys []string `json:"keys"`
}
if err = c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
-622
View File
@@ -1,622 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/handler"
logger2 "geekai/logger"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"html/template"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"unicode/utf8"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"gorm.io/gorm"
)
var logger = logger2.GetLogger()
type ChatHandler struct {
handler.BaseHandler
redis *redis.Client
uploadManager *oss.UploaderManager
licenseService *service.LicenseService
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
ChatContexts *types.LMap[string, []types.Message] // 聊天上下文 Map [chatId] => []Message
userService *service.UserService
}
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler {
return &ChatHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
redis: redis,
uploadManager: manager,
licenseService: licenseService,
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
ChatContexts: types.NewLMap[string, []types.Message](),
userService: userService,
}
}
// 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)
tools := c.Query("tools")
client := types.NewWsClient(ws)
var chatRole model.ChatRole
res := h.DB.First(&chatRole, roleId)
if res.Error != nil || !chatRole.Enable {
utils.ReplyMessage(client, "当前聊天角色不存在或者未启用,连接已关闭!!!")
c.Abort()
return
}
// if the role bind a model_id, use role's bind model_id
if chatRole.ModelId > 0 {
modelId = chatRole.ModelId
}
// 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 := &types.ChatSession{
SessionId: sessionId,
ClientIP: c.ClientIP(),
UserId: h.GetLoginUserId(c),
Tools: tools,
}
// 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,
Name: chatModel.Name,
Value: chatModel.Value,
Power: chatModel.Power,
MaxTokens: chatModel.MaxTokens,
MaxContext: chatModel.MaxContext,
Temperature: chatModel.Temperature,
KeyId: chatModel.KeyId}
logger.Infof("New websocket connected, IP: %s", c.ClientIP())
go func() {
for {
_, msg, err := client.Receive()
if err != nil {
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
client.Close()
cancelFunc := h.ReqCancelFunc.Get(sessionId)
if cancelFunc != nil {
cancelFunc()
h.ReqCancelFunc.Delete(sessionId)
}
return
}
var message types.WsMessage
err = utils.JsonDecode(string(msg), &message)
if err != nil {
continue
}
// 心跳消息
if message.Type == "heartbeat" {
logger.Debug("收到 Chat 心跳消息:", message.Content)
continue
}
logger.Info("Receive a message: ", message.Content)
ctx, cancel := context.WithCancel(context.Background())
h.ReqCancelFunc.Put(sessionId, cancel)
// 回复消息
err = h.sendMessage(ctx, session, chatRole, utils.InterfaceToString(message.Content), client)
if err != nil {
logger.Error(err)
utils.ReplyMessage(client, err.Error())
} else {
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
logger.Infof("回答完毕: %v", message.Content)
}
}
}()
}
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error {
if !h.App.Debug {
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 {
return errors.New("未授权用户,您正在进行非法操作!")
}
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 {
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
}
if userVo.Power < session.Model.Power {
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power)
}
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
return errors.New("您的账号已经过期,请联系管理员!")
}
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value)
if promptTokens > session.Model.MaxContext {
return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
}
var req = types.ApiRequest{
Model: session.Model.Value,
Stream: true,
}
req.Temperature = session.Model.Temperature
req.MaxTokens = session.Model.MaxTokens
if session.Tools != "" {
toolIds := strings.Split(session.Tools, ",")
var items []model.Function
res = h.DB.Where("enabled", true).Where("id IN ?", toolIds).Find(&items)
if res.Error == nil {
var tools = make([]types.Tool, 0)
for _, v := range items {
var parameters map[string]interface{}
err = utils.JsonDecode(v.Parameters, &parameters)
if err != nil {
continue
}
tool := types.Tool{
Type: "function",
Function: types.Function{
Name: v.Name,
Description: v.Description,
Parameters: parameters,
},
}
if v, ok := parameters["required"]; v == nil || !ok {
tool.Function.Parameters["required"] = []string{}
}
tools = append(tools, tool)
}
if len(tools) > 0 {
req.Tools = tools
req.ToolChoice = "auto"
}
}
}
// 加载聊天上下文
chatCtx := make([]types.Message, 0)
messages := make([]types.Message, 0)
if h.App.SysConfig.EnableContext {
if h.ChatContexts.Has(session.ChatId) {
messages = h.ChatContexts.Get(session.ChatId)
} else {
_ = utils.JsonDecode(role.Context, &messages)
if h.App.SysConfig.ContextDeep > 0 {
var historyMessages []model.ChatMessage
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages)
if res.Error == nil {
for i := len(historyMessages) - 1; i >= 0; i-- {
msg := historyMessages[i]
ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.ReplyMsg {
ms.Role = "assistant"
}
chatCtx = append(chatCtx, ms)
}
}
}
}
// 计算当前请求的 token 总长度,确保不会超出最大上下文长度
// MaxContextLength = Response + Tool + Prompt + Context
tokens := req.MaxTokens // 最大响应长度
tks, _ := utils.CalcTokens(utils.JsonEncode(req.Tools), req.Model)
tokens += tks + promptTokens
for i := len(messages) - 1; i >= 0; i-- {
v := messages[i]
tks, _ := utils.CalcTokens(v.Content, req.Model)
// 上下文 token 超出了模型的最大上下文长度
if tokens+tks >= session.Model.MaxContext {
break
}
// 上下文的深度超出了模型的最大上下文深度
if len(chatCtx) >= h.App.SysConfig.ContextDeep {
break
}
tokens += tks
chatCtx = append(chatCtx, v)
}
logger.Debugf("聊天上下文:%+v", chatCtx)
}
reqMgs := make([]interface{}, 0)
for _, m := range chatCtx {
reqMgs = append(reqMgs, m)
}
fullPrompt := prompt
text := prompt
// extract files in prompt
files := utils.ExtractFileURLs(prompt)
logger.Debugf("detected FILES: %+v", files)
// 如果不是逆向模型,则提取文件内容
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" ||
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") ||
strings.HasSuffix(session.Model.Value, "claude-3")) {
contents := make([]string, 0)
var file model.File
for _, v := range files {
h.DB.Where("url = ?", v).First(&file)
content, err := utils.ReadFileContent(v, h.App.Config.TikaHost)
if err != nil {
logger.Error("error with read file: ", err)
} else {
contents = append(contents, fmt.Sprintf("%s 文件内容:%s", file.Name, content))
}
text = strings.Replace(text, v, "", 1)
}
if len(contents) > 0 {
fullPrompt = fmt.Sprintf("请根据提供的文件内容信息回答问题(其中Excel 已转成 HTML)\n\n %s\n\n 问题:%s", strings.Join(contents, "\n"), text)
}
tokens, _ := utils.CalcTokens(fullPrompt, req.Model)
if tokens > session.Model.MaxContext {
return fmt.Errorf("文件的长度超出模型允许的最大上下文长度,请减少文件内容数量或文件大小。")
}
}
logger.Debug("最终Prompt", fullPrompt)
// extract images from prompt
imgURLs := utils.ExtractImgURLs(prompt)
logger.Debugf("detected IMG: %+v", imgURLs)
var content interface{}
if len(imgURLs) > 0 {
data := make([]interface{}, 0)
for _, v := range imgURLs {
text = strings.Replace(text, v, "", 1)
data = append(data, gin.H{
"type": "image_url",
"image_url": gin.H{
"url": v,
},
})
}
data = append(data, gin.H{
"type": "text",
"text": strings.TrimSpace(text),
})
content = data
} else {
content = fullPrompt
}
req.Messages = append(reqMgs, map[string]interface{}{
"role": "user",
"content": content,
})
logger.Debugf("%+v", req.Messages)
return h.sendOpenAiMessage(chatCtx, req, userVo, ctx, session, role, prompt, ws)
}
// Tokens 统计 token 数量
func (h *ChatHandler) Tokens(c *gin.Context) {
var data struct {
Text string `json:"text"`
Model string `json:"model"`
ChatId string `json:"chat_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
if data.Text == "" && data.ChatId != "" {
var item model.ChatMessage
userId, _ := c.Get(types.LoginUserID)
res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).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.ReqCancelFunc.Has(sessionId) {
h.ReqCancelFunc.Get(sessionId)()
h.ReqCancelFunc.Delete(sessionId)
}
resp.SUCCESS(c, types.OkMsg)
}
// 发送请求到 OpenAI 服务器
// useOwnApiKey: 是否使用了用户自己的 API KEY
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) {
// if the chat model bind a KEY, use it directly
if session.Model.KeyId > 0 {
h.DB.Where("id", session.Model.KeyId).Find(apiKey)
}
// use the last unused key
if apiKey.Id == 0 {
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
}
if apiKey.Id == 0 {
return nil, errors.New("no available key, please import key")
}
// ONLY allow apiURL in blank list
err := h.licenseService.IsValidApiURL(apiKey.ApiURL)
if err != nil {
return nil, err
}
logger.Debugf(utils.JsonEncode(req))
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.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")
if len(apiKey.ProxyURL) > 5 { // 使用代理
proxy, _ := url.Parse(apiKey.ProxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
} else {
client = http.DefaultClient
}
logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
// 更新API KEY 最后使用时间
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
return client.Do(request)
}
// 扣减用户算力
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) {
power := 1
if session.Model.Power > 0 {
power = session.Model.Power
}
err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{
Type: types.PowerConsume,
Model: session.Model.Value,
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens),
})
if err != nil {
logger.Error(err)
}
}
func (h *ChatHandler) saveChatHistory(
req types.ApiRequest,
prompt string,
contents []string,
message types.Message,
chatCtx []types.Message,
session *types.ChatSession,
role model.ChatRole,
userVo vo.User,
promptCreatedAt time.Time,
replyCreatedAt time.Time) {
if message.Role == "" {
message.Role = "assistant"
}
message.Content = strings.Join(contents, "")
useMsg := types.Message{Role: "user", Content: prompt}
// 更新上下文消息,如果是调用函数则不需要更新上下文
if h.App.SysConfig.EnableContext {
chatCtx = append(chatCtx, useMsg) // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
h.ChatContexts.Put(session.ChatId, chatCtx)
}
// 追加聊天记录
// for prompt
promptToken, err := utils.CalcTokens(prompt, req.Model)
if err != nil {
logger.Error(err)
}
historyUserMsg := model.ChatMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.PromptMsg,
Icon: userVo.Avatar,
Content: template.HTMLEscapeString(prompt),
Tokens: promptToken,
UseContext: true,
Model: req.Model,
}
historyUserMsg.CreatedAt = promptCreatedAt
historyUserMsg.UpdatedAt = promptCreatedAt
err = h.DB.Save(&historyUserMsg).Error
if err != nil {
logger.Error("failed to save prompt history message: ", err)
}
// for reply
// 计算本次对话消耗的总 token 数量
replyTokens, _ := utils.CalcTokens(message.Content, req.Model)
totalTokens := replyTokens + getTotalTokens(req)
historyReplyMsg := model.ChatMessage{
UserId: userVo.Id,
ChatId: session.ChatId,
RoleId: role.Id,
Type: types.ReplyMsg,
Icon: role.Icon,
Content: message.Content,
Tokens: totalTokens,
UseContext: true,
Model: req.Model,
}
historyReplyMsg.CreatedAt = replyCreatedAt
historyReplyMsg.UpdatedAt = replyCreatedAt
err = h.DB.Create(&historyReplyMsg).Error
if err != nil {
logger.Error("failed to save reply history message: ", err)
}
// 更新用户算力
if session.Model.Power > 0 {
h.subUserPower(userVo, session, promptToken, replyTokens)
}
// 保存当前会话
var chatItem model.ChatItem
err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error
if err != nil {
chatItem.ChatId = session.ChatId
chatItem.UserId = userVo.Id
chatItem.RoleId = role.Id
chatItem.ModelId = session.Model.Id
if utf8.RuneCountInString(prompt) > 30 {
chatItem.Title = string([]rune(prompt)[:30]) + "..."
} else {
chatItem.Title = prompt
}
chatItem.Model = req.Model
err = h.DB.Create(&chatItem).Error
if err != nil {
logger.Error("failed to save chat item: ", err)
}
}
}
// 将AI回复消息中生成的图片链接下载到本地
func (h *ChatHandler) extractImgUrl(text string) string {
pattern := `!\[([^\]]*)]\(([^)]+)\)`
re := regexp.MustCompile(pattern)
matches := re.FindAllStringSubmatch(text, -1)
// 下载图片并替换链接地址
for _, match := range matches {
imageURL := match[2]
logger.Debug(imageURL)
// 对于相同地址的图片,已经被替换了,就不再重复下载了
if !strings.Contains(text, imageURL) {
continue
}
newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false)
if err != nil {
logger.Error("error with download image: ", err)
continue
}
text = strings.ReplaceAll(text, imageURL, newImgURL)
}
return text
}
-188
View File
@@ -1,188 +0,0 @@
package chatimpl
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
req2 "github.com/imroc/req/v3"
"io"
"strings"
"time"
)
// OPenAI 消息发送实现
func (h *ChatHandler) sendOpenAiMessage(
chatCtx []types.Message,
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 = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey)
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start))
if err != nil {
if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt)
} else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
}
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 function model.Function
var toolCall = false
var arguments = make([]string, 0)
scanner := bufio.NewScanner(response.Body)
var isNew = true
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 { // 数据解析出错
return errors.New(line)
}
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue
}
if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil {
continue
}
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
utils.ReplyMessage(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
break
}
var tool types.ToolCall
if len(responseBody.Choices[0].Delta.ToolCalls) > 0 {
tool = responseBody.Choices[0].Delta.ToolCalls[0]
if toolCall && tool.Function.Name == "" {
arguments = append(arguments, tool.Function.Arguments)
continue
}
}
// 兼容 Function Call
fun := responseBody.Choices[0].Delta.FunctionCall
if fun.Name != "" {
tool = *new(types.ToolCall)
tool.Function.Name = fun.Name
} else if toolCall {
arguments = append(arguments, fun.Arguments)
continue
}
if !utils.IsEmptyValue(tool) {
res := h.DB.Where("name = ?", tool.Function.Name).First(&function)
if res.Error == nil {
toolCall = true
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsMiddle, Content: callMsg})
contents = append(contents, callMsg)
}
continue
}
if responseBody.Choices[0].FinishReason == "tool_calls" ||
responseBody.Choices[0].FinishReason == "function_call" { // 函数调用完毕
break
}
// output stopped
if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了
} else {
content := responseBody.Choices[0].Delta.Content
contents = append(contents, utils.InterfaceToString(content))
if isNew {
utils.ReplyChunkMessage(ws, types.WsMessage{Type: types.WsStart})
isNew = false
}
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 toolCall { // 调用函数完成任务
params := make(map[string]interface{})
_ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
params["user_id"] = userVo.Id
var apiRes types.BizVo
r, err := req2.C().R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", function.Token).
SetBody(params).
SetSuccessResult(&apiRes).Post(function.Action)
errMsg := ""
if err != nil {
errMsg = err.Error()
} else if r.IsErrorState() {
errMsg = r.Status
}
if errMsg != "" || apiRes.Code != types.Success {
msg := "调用函数工具出错:" + apiRes.Message + errMsg
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: msg,
})
contents = append(contents, msg)
} else {
utils.ReplyChunkMessage(ws, types.WsMessage{
Type: types.WsMiddle,
Content: apiRes.Data,
})
contents = append(contents, utils.InterfaceToString(apiRes.Data))
}
}
// 消息发送成功
if len(contents) > 0 {
h.saveChatHistory(req, prompt, contents, message, chatCtx, session, role, userVo, promptCreatedAt, replyCreatedAt)
}
} else {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求 OpenAI API 失败:%s", body)
}
return nil
}
+13 -13
View File
@@ -9,7 +9,6 @@ package handler
import (
"geekai/core"
"geekai/service"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
@@ -20,25 +19,32 @@ import (
type ConfigHandler struct {
BaseHandler
licenseService *service.LicenseService
}
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler {
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService}
func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ConfigHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/config/")
// 无需授权的接口
group.GET("get", h.Get)
}
// Get 获取指定的系统配置
func (h *ConfigHandler) Get(c *gin.Context) {
key := c.Query("key")
var config model.Config
res := h.DB.Where("marker", key).First(&config)
res := h.DB.Where("name", key).First(&config)
if res.Error != nil {
resp.ERROR(c, res.Error.Error())
return
}
var value map[string]interface{}
err := utils.JsonDecode(config.Config, &value)
var value map[string]any
err := utils.JsonDecode(config.Value, &value)
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -46,9 +52,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
resp.SUCCESS(c, value)
}
// License 获取 License 配置
func (h *ConfigHandler) License(c *gin.Context) {
license := h.licenseService.GetLicense()
resp.SUCCESS(c, license.Configs)
}
+119 -104
View File
@@ -10,34 +10,35 @@ package handler
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/dalle"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
)
type DallJobHandler struct {
BaseHandler
redis *redis.Client
dallService *dalle.Service
uploader *oss.UploaderManager
userService *service.UserService
dallService *dalle.Service
uploader *oss.UploaderManager
userService *service.UserService
moderationManager *moderation.ServiceManager
}
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService) *DallJobHandler {
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *DallJobHandler {
return &DallJobHandler{
dallService: service,
uploader: manager,
userService: userService,
dallService: service,
uploader: manager,
userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{
App: app,
DB: db,
@@ -45,82 +46,91 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service,
}
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *DallJobHandler) 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)
c.Abort()
return
// RegisterRoutes 注册路由
func (h *DallJobHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/dall/")
// 公开接口,不需要授权
group.GET("imgWall", h.ImgWall)
group.GET("models", h.GetModels)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("image", h.Image)
group.GET("jobs", h.JobList)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
}
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
logger.Info("Invalid user ID")
c.Abort()
return
}
client := types.NewWsClient(ws)
h.dallService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
go func() {
for {
_, msg, err := client.Receive()
if err != nil {
client.Close()
h.dallService.Clients.Delete(uint(userId))
return
}
var message types.WsMessage
err = utils.JsonDecode(string(msg), &message)
if err != nil {
continue
}
// 心跳消息
if message.Type == "heartbeat" {
logger.Debug("收到 DallE 心跳消息:", message.Content)
continue
}
}
}()
}
func (h *DallJobHandler) preCheck(c *gin.Context) bool {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return false
}
if user.Power < h.App.SysConfig.DallPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false
}
return true
}
// Image 创建一个绘画任务
func (h *DallJobHandler) Image(c *gin.Context) {
if !h.preCheck(c) {
return
}
var data types.DallTask
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
// 文本审核
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceDalle,
Input: data.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,提示词未通过文本审核,请重新输入!")
return
}
}
var chatModel model.ChatModel
if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
resp.ERROR(c, "模型不存在")
return
}
// 检查用户剩余算力
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
if user.Power < chatModel.Power {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return
}
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
task := types.DallTask{
UserId: uint(userId),
ModelId: chatModel.Id,
ModelName: chatModel.Name,
ModelValue: chatModel.Value,
Image: data.Image,
Prompt: data.Prompt,
AspectRatio: data.AspectRatio,
Size: data.Size,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
Power: chatModel.Power,
}
job := model.DallJob{
UserId: uint(userId),
Prompt: data.Prompt,
Power: h.App.SysConfig.DallPower,
UserId: uint(userId),
Prompt: data.Prompt,
Power: chatModel.Power,
TaskInfo: utils.JsonEncode(task),
}
res := h.DB.Create(&job)
if res.Error != nil {
@@ -128,19 +138,18 @@ func (h *DallJobHandler) Image(c *gin.Context) {
return
}
h.dallService.PushTask(types.DallTask{
JobId: job.Id,
UserId: uint(userId),
Prompt: data.Prompt,
Quality: data.Quality,
Size: data.Size,
Style: data.Style,
Power: job.Power,
})
task.Id = job.Id
h.dallService.PushTask(task)
client := h.dallService.Clients.Get(job.UserId)
if client != nil {
_ = client.Send([]byte("Task Updated"))
// 扣减算力
err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
Type: types.PowerConsume,
Model: chatModel.Name,
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
})
if err != nil {
resp.ERROR(c, "error with decrease power: "+err.Error())
return
}
resp.SUCCESS(c)
}
@@ -228,30 +237,14 @@ func (h *DallJobHandler) Remove(c *gin.Context) {
}
// 删除任务
tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
err := h.DB.Delete(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := h.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "dall-e-3",
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
@@ -273,3 +266,25 @@ func (h *DallJobHandler) Publish(c *gin.Context) {
resp.SUCCESS(c)
}
func (h *DallJobHandler) GetModels(c *gin.Context) {
var models []model.ChatModel
err := h.DB.Where("type", "img").Where("enabled", true).Find(&models).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
var modelVos []vo.ChatModel
for _, v := range models {
var modelVo vo.ChatModel
err := utils.CopyObject(v, &modelVo)
if err != nil {
continue
}
modelVo.Id = v.Id
modelVos = append(modelVos, modelVo)
}
resp.SUCCESS(c, modelVos)
}
+72 -45
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/service/dalle"
"geekai/service/oss"
"geekai/store/model"
@@ -29,9 +30,9 @@ import (
type FunctionHandler struct {
BaseHandler
config types.ApiConfig
uploadManager *oss.UploaderManager
dallService *dalle.Service
userService *service.UserService
}
func NewFunctionHandler(
@@ -39,18 +40,30 @@ func NewFunctionHandler(
db *gorm.DB,
config *types.AppConfig,
manager *oss.UploaderManager,
dallService *dalle.Service) *FunctionHandler {
dallService *dalle.Service,
userService *service.UserService) *FunctionHandler {
return &FunctionHandler{
BaseHandler: BaseHandler{
App: server,
DB: db,
},
config: config.ApiConfig,
uploadManager: manager,
dallService: dallService,
userService: userService,
}
}
// RegisterRoutes 注册路由
func (h *FunctionHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/function/")
group.GET("list", h.List)
// 需要用户授权的接口
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
}
type resVo struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
@@ -102,21 +115,18 @@ func (h *FunctionHandler) WeiBo(c *gin.Context) {
return
}
if h.config.Token == "" {
resp.ERROR(c, "无效的 API Token")
return
}
url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL)
url := fmt.Sprintf("%s/api/weibo/fetch", types.GeekAPIURL)
var res resVo
r, err := req.C().R().
SetHeader("AppId", h.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
SetHeader("Authorization", "Bearer geekai-plus").
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err))
if err != nil {
resp.ERROR(c, fmt.Sprintf("%v", err))
return
}
if r.IsErrorState() {
resp.ERROR(c, fmt.Sprintf("error http code status: %v", r.Status))
}
if res.Code != types.Success {
resp.ERROR(c, res.Message)
@@ -138,19 +148,17 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
return
}
if h.config.Token == "" {
resp.ERROR(c, "无效的 API Token")
return
}
url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL)
url := fmt.Sprintf("%s/api/zaobao/fetch", types.GeekAPIURL)
var res resVo
r, err := req.C().R().
SetHeader("AppId", h.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
SetHeader("Authorization", "Bearer geekai-plus").
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
resp.ERROR(c, fmt.Sprintf("%v%v", err, r.Err))
if err != nil {
resp.ERROR(c, fmt.Sprintf("%v", err))
return
}
if r.IsErrorState() {
resp.ERROR(c, fmt.Sprintf("%v", r.Err))
return
}
@@ -164,7 +172,7 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
for _, v := range res.Data.Items {
builder = append(builder, v.Title)
}
builder = append(builder, fmt.Sprintf("%s", res.Data.Title))
builder = append(builder, res.Data.Title)
resp.SUCCESS(c, strings.Join(builder, "\n\n"))
}
@@ -181,48 +189,67 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
return
}
var chatModel model.ChatModel
res := h.DB.Where("type = ?", "img").Where("enabled", true).First(&chatModel)
if res.Error != nil {
resp.ERROR(c, "没有找到可用的AI绘图模型!")
return
}
logger.Debugf("绘画参数:%+v", params)
var user model.User
res := h.DB.Where("id = ?", params["user_id"]).First(&user)
res = h.DB.Where("id = ?", params["user_id"]).First(&user)
if res.Error != nil {
resp.ERROR(c, "当前用户不存在!")
return
}
if user.Power < h.App.SysConfig.DallPower {
resp.ERROR(c, "创建 DALL-E 绘图任务失败,算力不足")
if user.Power < chatModel.Power {
resp.ERROR(c, "创建绘图任务失败,算力不足")
return
}
// create dall task
prompt := utils.InterfaceToString(params["prompt"])
job := model.DallJob{
UserId: user.Id,
Prompt: prompt,
Power: h.App.SysConfig.DallPower,
task := types.DallTask{
UserId: user.Id,
Prompt: prompt,
ModelId: chatModel.Id,
ModelValue: chatModel.Value,
ModelName: chatModel.Name,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
Power: chatModel.Power,
}
res = h.DB.Create(&job)
if res.Error != nil {
resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+res.Error.Error())
job := model.DallJob{
UserId: user.Id,
Prompt: prompt,
Power: chatModel.Power,
TaskInfo: utils.JsonEncode(task),
}
err := h.DB.Create(&job).Error
if err != nil {
resp.ERROR(c, "创建绘图任务失败:"+err.Error())
return
}
content, err := h.dallService.Image(types.DallTask{
JobId: job.Id,
UserId: user.Id,
Prompt: job.Prompt,
N: 1,
Quality: "standard",
Size: "1024x1024",
Style: "vivid",
Power: job.Power,
}, true)
task.Id = job.Id
content, err := h.dallService.Image(task, true)
if err != nil {
resp.ERROR(c, "任务执行失败:"+err.Error())
return
}
// 扣减算力
err = h.userService.DecreasePower(user.Id, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: task.ModelName,
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)),
})
if err != nil {
resp.ERROR(c, "扣减算力失败:"+err.Error())
return
}
resp.SUCCESS(c, content)
}
+134 -14
View File
@@ -8,14 +8,18 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"strings"
)
// InviteHandler 用户邀请
@@ -27,6 +31,23 @@ func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
return &InviteHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *InviteHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/invite/")
// 公开接口,不需要授权
group.GET("hits", h.Hits)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.GET("code", h.Code)
group.GET("list", h.List)
group.GET("stats", h.Stats)
group.GET("rules", h.Rules)
}
}
// Code 获取当前用户邀请码
func (h *InviteHandler) Code(c *gin.Context) {
userId := h.GetLoginUserId(c)
@@ -65,21 +86,34 @@ func (h *InviteHandler) List(c *gin.Context) {
var total int64
session.Model(&model.InviteLog{}).Count(&total)
var items []model.InviteLog
var list = make([]vo.InviteLog, 0)
offset := (page - 1) * pageSize
res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items)
if res.Error == nil {
for _, item := range items {
var v vo.InviteLog
err := utils.CopyObject(item, &v)
if err == nil {
v.Id = item.Id
v.CreatedAt = item.CreatedAt.Unix()
list = append(list, v)
} else {
logger.Error(err)
}
err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
userIds := make([]uint, 0)
for _, item := range items {
userIds = append(userIds, item.UserId)
}
userMap := make(map[uint]model.User)
var users []model.User
h.DB.Model(&model.User{}).Where("id IN (?)", userIds).Find(&users)
for _, user := range users {
userMap[user.Id] = user
}
var list = make([]vo.InviteLog, 0)
for _, item := range items {
var v vo.InviteLog
err := utils.CopyObject(item, &v)
if err != nil {
continue
}
v.CreatedAt = item.CreatedAt.Unix()
v.Avatar = userMap[item.UserId].Avatar
list = append(list, v)
}
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
}
@@ -90,3 +124,89 @@ func (h *InviteHandler) Hits(c *gin.Context) {
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
resp.SUCCESS(c)
}
// Stats 获取邀请统计
func (h *InviteHandler) Stats(c *gin.Context) {
userId := h.GetLoginUserId(c)
// 获取邀请码
var inviteCode model.InviteCode
res := h.DB.Where("user_id = ?", userId).First(&inviteCode)
if res.Error != nil {
resp.ERROR(c, "邀请码不存在")
return
}
// 统计累计邀请数
var totalInvite int64
h.DB.Model(&model.InviteLog{}).Where("inviter_id = ?", userId).Count(&totalInvite)
// 统计今日邀请数
today := time.Now().Format("2006-01-02")
var todayInvite int64
h.DB.Model(&model.InviteLog{}).Where("inviter_id = ? AND DATE(created_at) = ?", userId, today).Count(&todayInvite)
// 获取系统配置中的邀请奖励
var config model.Config
var invitePower int = 200 // 默认值
if h.DB.Where("name = ?", "system").First(&config).Error == nil {
var configMap map[string]any
if utils.JsonDecode(config.Value, &configMap) == nil {
if power, ok := configMap["invite_power"].(float64); ok {
invitePower = int(power)
}
}
}
// 计算获得奖励总数
rewardTotal := int(totalInvite) * invitePower
// 构建邀请链接
inviteLink := fmt.Sprintf("%s/register?invite=%s", h.App.Config.StaticUrl, inviteCode.Code)
stats := vo.InviteStats{
InviteCount: int(totalInvite),
RewardTotal: rewardTotal,
TodayInvite: int(todayInvite),
InviteCode: inviteCode.Code,
InviteLink: inviteLink,
}
resp.SUCCESS(c, stats)
}
// Rules 获取奖励规则
func (h *InviteHandler) Rules(c *gin.Context) {
// 获取系统配置中的邀请奖励
var config model.Config
var invitePower int = 200 // 默认值
if h.DB.Where("name = ?", "system").First(&config).Error == nil {
var configMap map[string]interface{}
if utils.JsonDecode(config.Value, &configMap) == nil {
if power, ok := configMap["invite_power"].(float64); ok {
invitePower = int(power)
}
}
}
rules := []vo.RewardRule{
{
Id: 1,
Title: "好友注册",
Desc: "好友通过邀请链接成功注册",
Icon: "icon-user-fill",
Color: "#1989fa",
Reward: invitePower,
},
{
Id: 2,
Title: "好友首次充值",
Desc: "好友首次充值任意金额",
Icon: "icon-money",
Color: "#07c160",
Reward: invitePower * 2, // 假设首次充值奖励是注册奖励的2倍
},
}
resp.SUCCESS(c, rules)
}
+356
View File
@@ -0,0 +1,356 @@
package handler
import (
"errors"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/jimeng"
"geekai/service/moderation"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// JimengHandler 即梦AI处理器
type JimengHandler struct {
BaseHandler
jimengService *jimeng.Service
userService *service.UserService
moderationManager *moderation.ServiceManager
}
// NewJimengHandler 创建即梦AI处理器
func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *gorm.DB, userService *service.UserService, moderationManager *moderation.ServiceManager) *JimengHandler {
return &JimengHandler{
BaseHandler: BaseHandler{App: app, DB: db},
jimengService: jimengService,
userService: userService,
moderationManager: moderationManager,
}
}
// RegisterRoutes 注册路由,新增统一任务接口
func (h *JimengHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/jimeng/")
group.GET("power-config", h.GetPowerConfig)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("task", h.CreateTask)
group.POST("jobs", h.Jobs)
group.GET("remove", h.Remove)
group.GET("retry", h.Retry)
}
}
// CreateTask 统一任务创建接口
func (h *JimengHandler) CreateTask(c *gin.Context) {
var req types.JimengTaskRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 文本审核
if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceJiMeng,
Input: req.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return
}
}
if req.Prompt == "" && len(req.ImageUrls) == 0 {
resp.ERROR(c, "提示词和图片不能同时为空")
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
// 获取算力消耗
powerCost, err := h.getTaskPower(req)
if err != nil {
resp.ERROR(c, "计算任务消耗积分失败: "+err.Error())
return
}
if user.Power < powerCost {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return
}
req.Power = powerCost
job, err := h.jimengService.CreateTask(user.Id, &req)
if err != nil {
logger.Errorf("create jimeng task failed: %v", err)
resp.ERROR(c, "创建任务失败")
return
}
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume,
Model: job.ReqKey,
Remark: h.getTaskRemark(req, job.Id),
})
resp.SUCCESS(c)
}
func (h *JimengHandler) getTaskRemark(req types.JimengTaskRequest, jobId uint) string {
remark := fmt.Sprintf("即梦任务%s,任务ID%d", req.ReqKey, jobId)
perUnit, ok := h.App.SysConfig.Jimeng.Powers[req.ReqKey]
if !ok || perUnit <= 0 {
return remark // Fallback if power not found or invalid
}
switch req.TaskType {
case types.JMTaskTypeImage:
remark = fmt.Sprintf("即梦图片生成,任务ID%d%d积分/张", jobId, perUnit)
case types.JMTaskTypeVideo:
seconds := 0
if perUnit > 0 {
seconds = req.Power / perUnit
}
remark = fmt.Sprintf("即梦视频生成,任务ID%d%d积分/秒, %d秒", jobId, perUnit, seconds)
case types.JMTaskTypeVirtualHuman:
seconds := 0
if perUnit > 0 {
seconds = req.Power / perUnit
}
remark = fmt.Sprintf("即梦数字人视频生成,任务ID%d,%d积分/秒, %d秒", jobId, perUnit, seconds)
case types.JMTaskTypeActionTransfer:
seconds := 0
if perUnit > 0 {
seconds = req.Power / perUnit
}
remark = fmt.Sprintf("即梦视频动作迁移,任务ID%d%d积分/秒, %d秒", jobId, perUnit, seconds)
}
return remark
}
// Jobs 获取任务列表
func (h *JimengHandler) Jobs(c *gin.Context) {
userId := h.GetLoginUserId(c)
var req struct {
Page int `json:"page"`
PageSize int `json:"page_size"`
Filter string `json:"filter"`
Ids []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var jobs []model.JimengJob
var total int64
query := h.DB.Model(&model.JimengJob{}).Where("user_id = ?", userId)
switch req.Filter {
case "image":
query = query.Where("type = ?", types.JMTaskTypeImage)
case "video":
query = query.Where("type = ?", types.JMTaskTypeVideo)
case "virtual_human":
query = query.Where("type = ?", types.JMTaskTypeVirtualHuman)
case "action_transfer":
query = query.Where("type = ?", types.JMTaskTypeActionTransfer)
}
if len(req.Ids) > 0 {
query = query.Where("id IN (?)", req.Ids)
}
// 统计总数
if err := query.Count(&total).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
// 分页查询
offset := (req.Page - 1) * req.PageSize
if err := query.Order("updated_at DESC").Offset(offset).Limit(req.PageSize).Find(&jobs).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
// 填充 VO
var jobVos []vo.JimengJob
for _, job := range jobs {
var jobVo vo.JimengJob
err := utils.CopyObject(job, &jobVo)
if err != nil {
continue
}
jobVo.CreatedAt = job.CreatedAt.Unix()
jobVos = append(jobVos, jobVo)
}
resp.SUCCESS(c, vo.NewPage(total, req.Page, req.PageSize, jobVos))
}
// Remove 删除任务
func (h *JimengHandler) Remove(c *gin.Context) {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
jobId := h.GetInt(c, "id", 0)
if jobId == 0 {
resp.ERROR(c, "参数错误")
return
}
// 获取任务,判断状态
job, err := h.jimengService.GetJob(uint(jobId))
if err != nil {
resp.ERROR(c, "任务不存在")
return
}
if job.UserId != user.Id {
resp.ERROR(c, "无权限操作")
return
}
// 正在运行中的任务不能删除
if job.Status == types.JMTaskStatusGenerating || job.Status == types.JMTaskStatusInQueue {
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
return
}
if err := h.DB.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
logger.Errorf("delete jimeng job failed: %v", err)
resp.ERROR(c, "删除任务失败")
return
}
resp.SUCCESS(c, gin.H{})
}
// Retry 重试任务
func (h *JimengHandler) Retry(c *gin.Context) {
userId := h.GetLoginUserId(c)
jobId := h.GetInt(c, "id", 0)
if jobId == 0 {
resp.ERROR(c, "参数错误")
return
}
// 检查任务是否存在且属于当前用户
job, err := h.jimengService.GetJob(uint(jobId))
if err != nil {
resp.ERROR(c, "任务不存在")
return
}
if job.UserId != userId {
resp.ERROR(c, "无权限操作")
return
}
// 只有失败的任务才能重试
if job.Status != types.JMTaskStatusFailed {
resp.ERROR(c, "只有失败的任务才能重试")
return
}
// 重置任务状态
if err := h.jimengService.UpdateJobStatus(uint(jobId), types.JMTaskStatusInQueue, ""); err != nil {
logger.Errorf("reset job status failed: %v", err)
resp.ERROR(c, "重置任务状态失败")
return
}
// 重新推送到队列
if err := h.jimengService.PushTaskToQueue(uint(jobId)); err != nil {
logger.Errorf("push retry task to queue failed: %v", err)
resp.ERROR(c, "推送重试任务失败")
return
}
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
}
func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
logger.Debugf("getTaskPower req: %+v", req)
config := h.App.SysConfig.Jimeng
basePower, ok := config.Powers[req.ReqKey]
if !ok || basePower <= 0 {
return 0, errors.New("未配置模型积分或配置不合法")
}
switch req.TaskType {
case types.JMTaskTypeImage:
return basePower, nil
case types.JMTaskTypeVideo:
if req.Duration == 0 {
return 0, errors.New("视频时长不能为0")
}
return basePower * req.Duration, nil
case types.JMTaskTypeVirtualHuman:
if req.AudioURL == "" {
return 0, errors.New("音频URL不能为空")
}
audioDuration, err := utils.AudioDurationFromURL(req.AudioURL)
if err != nil {
return 0, err
}
seconds := int(audioDuration.Seconds())
if seconds <= 0 {
return 0, errors.New("音频时长无效")
}
return basePower * seconds, nil
case types.JMTaskTypeActionTransfer:
if req.VideoURL == "" {
return 0, errors.New("视频URL不能为空")
}
videoDuration, err := utils.VideoDurationMP4FromURL(req.VideoURL)
if err != nil {
return 0, err
}
seconds := int(videoDuration.Seconds())
if seconds <= 0 {
return 0, errors.New("视频时长无效")
}
return basePower * seconds, nil
default:
return 0, errors.New("任务类型不支持")
}
}
// GetPowerConfig 获取即梦各任务类型算力消耗配置
func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
config := h.App.SysConfig.Jimeng
resp.SUCCESS(c, gin.H{
"powers": config.Powers,
})
}
+42 -167
View File
@@ -8,24 +8,17 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// MarkMapHandler 生成思维导图
@@ -43,69 +36,46 @@ func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.Us
}
}
func (h *MarkMapHandler) 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)
// RegisterRoutes 注册路由
func (h *MarkMapHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/markMap/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("gen", h.Generate)
}
}
// Generate 生成思维导图
func (h *MarkMapHandler) Generate(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
ModelId int `json:"model_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
modelId := h.GetInt(c, "model_id", 0)
userId := h.GetInt(c, "user_id", 0)
client := types.NewWsClient(ws)
h.clients.Put(userId, client)
go func() {
for {
_, msg, err := client.Receive()
if err != nil {
client.Close()
h.clients.Delete(userId)
return
}
var message types.WsMessage
err = utils.JsonDecode(string(msg), &message)
if err != nil {
continue
}
// 心跳消息
if message.Type == "heartbeat" {
logger.Debug("收到 MarkMap 心跳消息:", message.Content)
continue
}
// change model
if message.Type == "model_id" {
modelId = utils.IntValue(utils.InterfaceToString(message.Content), 0)
continue
}
logger.Info("Receive a message: ", message.Content)
err = h.sendMessage(client, utils.InterfaceToString(message.Content), modelId, userId)
if err != nil {
logger.Error(err)
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsErr, Content: err.Error()})
}
}
}()
}
func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, modelId int, userId int) error {
userId := h.GetLoginUserId(c)
var user model.User
res := h.DB.Model(&model.User{}).First(&user, userId)
if res.Error != nil {
return fmt.Errorf("error with query user info: %v", res.Error)
err := h.DB.Where("id", userId).First(&user, userId).Error
if err != nil {
resp.ERROR(c, "error with query user info")
return
}
var chatModel model.ChatModel
res = h.DB.Where("id", modelId).First(&chatModel)
if res.Error != nil {
return fmt.Errorf("error with query chat model: %v", res.Error)
err = h.DB.Where("id", data.ModelId).First(&chatModel).Error
if err != nil {
resp.ERROR(c, "error with query chat model")
return
}
if user.Power < chatModel.Power {
return fmt.Errorf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power)
resp.ERROR(c, fmt.Sprintf("您当前剩余算力(%d)已不足以支付当前模型算力(%d)!", user.Power, chatModel.Power))
return
}
messages := make([]interface{}, 0)
@@ -127,63 +97,13 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
### 支付宝
### 微信
另外,除此之外不要任何解释性语句。
请直接生成结果,不要任何解释性语句。
`})
messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图,要求结构清晰,有条理", prompt)})
var req = types.ApiRequest{
Model: chatModel.Value,
Stream: true,
Messages: messages,
}
var apiKey model.ApiKey
response, err := h.doRequest(req, chatModel, &apiKey)
messages = append(messages, types.Message{Role: "user", Content: fmt.Sprintf("请生成一份有关【%s】一份思维导图,要求结构清晰,有条理", data.Prompt)})
content, err := utils.SendOpenAIMessage(h.DB, messages, data.ModelId)
if err != nil {
return fmt.Errorf("请求 OpenAI API 失败: %s", err)
}
defer response.Body.Close()
contentType := response.Header.Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
// 循环读取 Chunk 消息
scanner := bufio.NewScanner(response.Body)
var isNew = true
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 { // 数据解析出错
return fmt.Errorf("error with decode data: %v", line)
}
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue
}
if responseBody.Choices[0].FinishReason == "stop" {
break
}
if isNew {
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsStart})
isNew = false
}
utils.ReplyChunkMessage(client, types.WsMessage{
Type: types.WsMiddle,
Content: utils.InterfaceToString(responseBody.Choices[0].Delta.Content),
})
} // end for
utils.ReplyChunkMessage(client, types.WsMessage{Type: types.WsEnd})
} else {
body, _ := io.ReadAll(response.Body)
return fmt.Errorf("请求 OpenAI API 失败:%s", string(body))
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API 失败: %s", err))
return
}
// 扣减算力
@@ -194,55 +114,10 @@ func (h *MarkMapHandler) sendMessage(client *types.WsClient, prompt string, mode
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
})
if err != nil {
return err
resp.ERROR(c, "error with save power log, "+err.Error())
return
}
}
return nil
}
func (h *MarkMapHandler) doRequest(req types.ApiRequest, chatModel model.ChatModel, apiKey *model.ApiKey) (*http.Response, error) {
session := h.DB.Session(&gorm.Session{})
// if the chat model bind a KEY, use it directly
if chatModel.KeyId > 0 {
session = session.Where("id", chatModel.KeyId)
} else { // use the last unused key
session = session.Where("type", "chat").
Where("enabled", true).Order("last_used_at ASC")
}
res := session.First(apiKey)
if res.Error != nil {
return nil, errors.New("no available key, please import key")
}
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
// 更新 API KEY 的最后使用时间
h.DB.Model(apiKey).UpdateColumn("last_used_at", time.Now().Unix())
// 创建 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.Header.Set("Content-Type", "application/json")
if len(apiKey.ProxyURL) > 5 { // 使用代理
proxy, _ := url.Parse(apiKey.ProxyURL)
client = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(proxy),
},
}
} else {
client = http.DefaultClient
}
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", apiKey.Value))
logger.Debugf("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, req.Model)
return client.Do(request)
resp.SUCCESS(c, content)
}
+26 -1
View File
@@ -13,6 +13,7 @@ import (
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@@ -25,6 +26,13 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *MenuHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/menu/")
group.GET("list", h.List)
group.GET("list/all", h.ListAll)
}
// List 数据列表
func (h *MenuHandler) List(c *gin.Context) {
index := h.GetBool(c, "index")
@@ -33,7 +41,7 @@ func (h *MenuHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{})
session = session.Where("enabled", true)
if index {
session = session.Where("id IN ?", h.App.SysConfig.IndexNavs)
session = session.Where("id IN ?", h.App.SysConfig.Base.IndexNavs)
}
res := session.Order("sort_num ASC").Find(&items)
if res.Error == nil {
@@ -47,3 +55,20 @@ func (h *MenuHandler) List(c *gin.Context) {
}
resp.SUCCESS(c, list)
}
// ListAll 获取所有的菜单列表
func (h *MenuHandler) ListAll(c *gin.Context) {
var items []model.Menu
var list = make([]vo.Menu, 0)
res := h.DB.Order("sort_num ASC").Find(&items)
if res.Error == nil {
for _, item := range items {
var product vo.Menu
err := utils.CopyObject(item, &product)
if err == nil {
list = append(list, product)
}
}
}
resp.SUCCESS(c, list)
}
+114 -131
View File
@@ -8,40 +8,41 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"encoding/base64"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/mj"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
)
type MidJourneyHandler struct {
BaseHandler
mjService *mj.Service
snowflake *service.Snowflake
uploader *oss.UploaderManager
userService *service.UserService
mjService *mj.Service
snowflake *service.Snowflake
uploader *oss.UploaderManager
userService *service.UserService
moderationManager *moderation.ServiceManager
}
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService) *MidJourneyHandler {
func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, service *mj.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *MidJourneyHandler {
return &MidJourneyHandler{
snowflake: snowflake,
mjService: service,
uploader: manager,
userService: userService,
snowflake: snowflake,
mjService: service,
uploader: manager,
userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{
App: app,
DB: db,
@@ -49,6 +50,25 @@ func NewMidJourneyHandler(app *core.AppServer, db *gorm.DB, snowflake *service.S
}
}
// RegisterRoutes 注册路由
func (h *MidJourneyHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/mj/")
// 公开接口,不需要授权
group.GET("imgWall", h.ImgWall)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("image", h.Image)
group.POST("upscale", h.Upscale)
group.POST("variation", h.Variation)
group.GET("jobs", h.JobList)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
}
}
func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
user, err := h.GetLoginUser(c)
if err != nil {
@@ -56,7 +76,7 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
return false
}
if user.Power < h.App.SysConfig.MjPower {
if user.Power < h.App.SysConfig.Base.MjPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false
}
@@ -65,27 +85,6 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
}
// 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)
c.Abort()
return
}
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
logger.Info("Invalid user ID")
c.Abort()
return
}
client := types.NewWsClient(ws)
h.mjService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
// Image 创建一个绘画任务
func (h *MidJourneyHandler) Image(c *gin.Context) {
var data struct {
@@ -114,6 +113,29 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return
}
// 文本审核
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceMJ,
Input: data.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return
}
}
var params = ""
if data.Rate != "" && !strings.Contains(params, "--ar") {
params += " --ar " + data.Rate
@@ -175,13 +197,25 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
resp.ERROR(c, "error with generate task id: "+err.Error())
return
}
task := types.MjTask{
TaskId: taskId,
Type: types.TaskType(data.TaskType),
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
Params: params,
UserId: userId,
ImgArr: data.ImgArr,
Mode: h.App.SysConfig.Base.MjMode,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
}
job := model.MidJourneyJob{
Type: data.TaskType,
UserId: userId,
UserId: uint(userId),
TaskId: taskId,
TaskInfo: utils.JsonEncode(task),
Progress: 0,
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
Power: h.App.SysConfig.MjPower,
Power: h.App.SysConfig.Base.MjPower,
CreatedAt: time.Now(),
}
opt := "绘图"
@@ -198,22 +232,8 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return
}
h.mjService.PushTask(types.MjTask{
Id: job.Id,
TaskId: taskId,
Type: types.TaskType(data.TaskType),
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
Params: params,
UserId: userId,
ImgArr: data.ImgArr,
Mode: h.App.SysConfig.MjMode,
})
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
task.Id = job.Id
h.mjService.PushTask(task)
// update user's power
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
@@ -251,35 +271,31 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, _ := h.snowflake.Next(true)
job := model.MidJourneyJob{
Type: types.TaskUpscale.String(),
ReferenceId: data.MessageId,
UserId: userId,
TaskId: taskId,
Progress: 0,
Power: h.App.SysConfig.MjActionPower,
CreatedAt: time.Now(),
}
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return
}
h.mjService.PushTask(types.MjTask{
Id: job.Id,
task := types.MjTask{
Type: types.TaskUpscale,
UserId: userId,
ChannelId: data.ChannelId,
Index: data.Index,
MessageId: data.MessageId,
MessageHash: data.MessageHash,
Mode: h.App.SysConfig.MjMode,
})
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
Mode: h.App.SysConfig.Base.MjMode,
}
job := model.MidJourneyJob{
Type: types.TaskUpscale.String(),
UserId: uint(userId),
TaskId: taskId,
TaskInfo: utils.JsonEncode(task),
Progress: 0,
Power: h.App.SysConfig.Base.MjActionPower,
CreatedAt: time.Now(),
}
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return
}
task.Id = job.Id
h.mjService.PushTask(task)
// update user's power
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
@@ -310,36 +326,32 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, _ := h.snowflake.Next(true)
job := model.MidJourneyJob{
Type: types.TaskVariation.String(),
ChannelId: data.ChannelId,
ReferenceId: data.MessageId,
UserId: userId,
TaskId: taskId,
Progress: 0,
Power: h.App.SysConfig.MjActionPower,
CreatedAt: time.Now(),
}
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return
}
h.mjService.PushTask(types.MjTask{
Id: job.Id,
task := types.MjTask{
Type: types.TaskVariation,
UserId: userId,
Index: data.Index,
ChannelId: data.ChannelId,
MessageId: data.MessageId,
MessageHash: data.MessageHash,
Mode: h.App.SysConfig.MjMode,
})
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
Mode: h.App.SysConfig.Base.MjMode,
}
job := model.MidJourneyJob{
Type: types.TaskVariation.String(),
ChannelId: data.ChannelId,
UserId: uint(userId),
TaskId: taskId,
TaskInfo: utils.JsonEncode(task),
Progress: 0,
Power: h.App.SysConfig.Base.MjActionPower,
CreatedAt: time.Now(),
}
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return
}
task.Id = job.Id
h.mjService.PushTask(task)
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
@@ -420,14 +432,6 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
if err != nil {
continue
}
if item.Progress < 100 && item.ImgURL == "" && item.OrgURL != "" {
image, err := utils.DownloadImage(item.OrgURL, h.App.Config.ProxyURL)
if err == nil {
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
}
}
jobs = append(jobs, job)
}
return nil, vo.NewPage(total, page, pageSize, jobs)
@@ -443,40 +447,19 @@ func (h *MidJourneyHandler) Remove(c *gin.Context) {
return
}
// remove job recode
tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
// remove job
err := h.DB.Delete(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "mid-journey",
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
client := h.mjService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
resp.SUCCESS(c)
}
@@ -485,7 +468,7 @@ func (h *MidJourneyHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0)
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
err := h.DB.Model(&model.MidJourneyJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -9,17 +9,19 @@ package handler
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type NetHandler struct {
@@ -31,6 +33,22 @@ func NewNetHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManage
return &NetHandler{BaseHandler: BaseHandler{App: app, DB: db}, uploaderManager: manager}
}
// RegisterRoutes 注册路由
func (h *NetHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/upload")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("", h.Upload)
group.POST("list", h.List)
group.GET("remove", h.Remove)
}
// 公开接口,不需要授权
h.App.Engine.GET("/api/download", h.Download)
}
func (h *NetHandler) Upload(c *gin.Context) {
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil {
@@ -46,7 +64,7 @@ func (h *NetHandler) Upload(c *gin.Context) {
userId := h.GetLoginUserId(c)
res := h.DB.Create(&model.File{
UserId: int(userId),
UserId: uint(userId),
Name: file.Name,
ObjKey: file.ObjKey,
URL: file.URL,
@@ -64,7 +82,9 @@ func (h *NetHandler) Upload(c *gin.Context) {
func (h *NetHandler) List(c *gin.Context) {
var data struct {
Urls []string `json:"urls,omitempty"`
Urls []string `json:"urls,omitempty"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@@ -79,21 +99,32 @@ func (h *NetHandler) List(c *gin.Context) {
if len(data.Urls) > 0 {
session = session.Where("url IN ?", data.Urls)
}
session.Find(&items)
if len(items) > 0 {
for _, v := range items {
var file vo.File
err := utils.CopyObject(v, &file)
if err != nil {
logger.Error(err)
continue
}
file.CreatedAt = v.CreatedAt.Unix()
files = append(files, file)
}
// 统计总数
var total int64
session.Model(&model.File{}).Count(&total)
if data.Page > 0 && data.PageSize > 0 {
offset := (data.Page - 1) * data.PageSize
session = session.Offset(offset).Limit(data.PageSize)
}
err := session.Order("id desc").Find(&items).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, files)
for _, v := range items {
var file vo.File
err := utils.CopyObject(v, &file)
if err != nil {
logger.Error(err)
continue
}
file.CreatedAt = v.CreatedAt.Unix()
files = append(files, file)
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, files))
}
// Remove remove files
@@ -130,7 +161,15 @@ func (h *NetHandler) Download(c *gin.Context) {
return
}
// 使用http.Get下载文件
r, err := http.Get(fileUrl)
req, err := http.NewRequest("GET", fileUrl, nil)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 模拟浏览器 UA
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
client := &http.Client{}
r, err := client.Do(req)
if err != nil {
resp.ERROR(c, err.Error())
return
@@ -143,6 +182,5 @@ func (h *NetHandler) Download(c *gin.Context) {
}
c.Status(http.StatusOK)
// 将下载的文件内容写入响应
_, _ = io.Copy(c.Writer, r.Body)
}
+26 -12
View File
@@ -9,12 +9,12 @@ package handler
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -28,6 +28,18 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
return &OrderHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *OrderHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/order/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.GET("list", h.List)
group.GET("query", h.Query)
}
}
// List 订单列表
func (h *OrderHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
@@ -48,10 +60,21 @@ func (h *OrderHandler) List(c *gin.Context) {
order.Id = item.Id
order.CreatedAt = item.CreatedAt.Unix()
order.UpdatedAt = item.UpdatedAt.Unix()
payChannel, ok := types.PayChannel[item.Channel]
if !ok {
payChannel = item.PayWay
}
payWays, ok := types.PayWays[item.PayWay]
if !ok {
payWays = item.PayWay
}
order.ChannelName = payChannel
order.PayName = payWays
list = append(list, order)
} else {
logger.Error(err)
}
}
}
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
@@ -72,17 +95,8 @@ func (h *OrderHandler) Query(c *gin.Context) {
return
}
counter := 0
for {
time.Sleep(time.Second)
var item model.Order
h.DB.Where("order_no = ?", orderNo).First(&item)
if counter >= 15 || item.Status == types.OrderPaidSuccess || item.Status != order.Status {
order.Status = item.Status
break
}
counter++
}
var item model.Order
h.DB.Where("order_no = ?", orderNo).First(&item)
resp.SUCCESS(c, gin.H{"status": order.Status})
}
+363 -513
View File
@@ -9,19 +9,16 @@ package handler
import (
"embed"
"encoding/base64"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/payment"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/shopspring/decimal"
"math"
"net/http"
"net/url"
"sync"
"time"
@@ -34,520 +31,371 @@ type PayWay struct {
Value string `json:"value"`
}
var (
PayWayAlipay = PayWay{Name: "支付宝", Value: "alipay"}
PayWayXunHu = PayWay{Name: "虎皮椒", Value: "hupi"}
PayWayJs = PayWay{Name: "PayJS", Value: "payjs"}
PayWayWechat = PayWay{Name: "微信支付", Value: "wechat"}
)
// PaymentHandler 支付服务回调 handler
type PaymentHandler struct {
BaseHandler
alipayService *payment.AlipayService
huPiPayService *payment.HuPiPayService
jsPayService *payment.JPayService
wechatPayService *payment.WechatPayService
snowflake *service.Snowflake
fs embed.FS
lock sync.Mutex
signKey string // 用来签名的随机秘钥
alipayService *payment.AlipayService
epayService *payment.EPayService
wxpayService *payment.WxPayService
snowflake *service.Snowflake
userService *service.UserService
fs embed.FS
lock sync.Mutex
config *types.PaymentConfig
}
func NewPaymentHandler(
server *core.AppServer,
alipayService *payment.AlipayService,
huPiPayService *payment.HuPiPayService,
jsPayService *payment.JPayService,
wechatPayService *payment.WechatPayService,
geekPayService *payment.EPayService,
wxpayService *payment.WxPayService,
db *gorm.DB,
userService *service.UserService,
snowflake *service.Snowflake,
fs embed.FS) *PaymentHandler {
fs embed.FS,
sysConfig *types.SystemConfig) *PaymentHandler {
return &PaymentHandler{
alipayService: alipayService,
huPiPayService: huPiPayService,
jsPayService: jsPayService,
wechatPayService: wechatPayService,
snowflake: snowflake,
fs: fs,
lock: sync.Mutex{},
alipayService: alipayService,
epayService: geekPayService,
wxpayService: wxpayService,
snowflake: snowflake,
userService: userService,
fs: fs,
lock: sync.Mutex{},
BaseHandler: BaseHandler{
App: server,
DB: db,
},
signKey: utils.RandString(32),
config: &sysConfig.Payment,
}
}
func (h *PaymentHandler) DoPay(c *gin.Context) {
orderNo := h.GetTrim(c, "order_no")
payWay := h.GetTrim(c, "pay_way")
t := h.GetInt(c, "t", 0)
sign := h.GetTrim(c, "sign")
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, payWay, t, h.signKey)
newSign := utils.Sha256(signStr)
if newSign != sign {
resp.ERROR(c, "订单签名错误!")
return
// RegisterRoutes 注册路由
func (h *PaymentHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/payment/")
// 支付回调接口(公开)
rg.POST("notify/alipay", h.AlipayNotify)
rg.GET("notify/epay", h.EPayNotify)
rg.POST("notify/wxpay", h.WxpayNotify)
// 需要用户登录的接口
rg.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
rg.POST("create", h.CreateOrder)
}
// 检查二维码是否过期
if time.Now().Unix()-int64(t) > int64(h.App.SysConfig.OrderPayTimeout) {
resp.ERROR(c, "支付二维码已过期,请重新生成!")
return
}
if orderNo == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
var order model.Order
res := h.DB.Where("order_no = ?", orderNo).First(&order)
if res.Error != nil {
resp.ERROR(c, "Order not found")
return
}
// fix: 这里先检查一下订单状态,如果已经支付了,就直接返回
if order.Status == types.OrderPaidSuccess {
resp.ERROR(c, "订单已支付成功,无需重复支付!")
return
}
// 更新扫码状态
h.DB.Model(&order).UpdateColumn("status", types.OrderScanned)
if payWay == "alipay" { // 支付宝
amount := fmt.Sprintf("%.2f", order.Amount)
uri, err := h.alipayService.PayUrlMobile(order.OrderNo, amount, order.Subject)
if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error())
return
}
c.Redirect(302, uri)
return
} else if payWay == "hupi" { // 虎皮椒支付
params := payment.HuPiPayReq{
Version: "1.1",
TradeOrderId: orderNo,
TotalFee: fmt.Sprintf("%f", order.Amount),
Title: order.Subject,
NotifyURL: h.App.Config.HuPiPayConfig.NotifyURL,
WapName: "极客学长",
}
r, err := h.huPiPayService.Pay(params)
if err != nil {
resp.ERROR(c, err.Error())
return
}
c.Redirect(302, r.URL)
}
resp.ERROR(c, "Invalid operations")
}
// PayQrcode 生成支付 URL 二维码
func (h *PaymentHandler) PayQrcode(c *gin.Context) {
var data struct {
PayWay string `json:"pay_way"` // 支付方式
ProductId uint `json:"product_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var product model.Product
res := h.DB.First(&product, data.ProductId)
if res.Error != nil {
resp.ERROR(c, "Product not found")
return
}
orderNo, err := h.snowflake.Next(false)
if err != nil {
resp.ERROR(c, "error with generate trade no: "+err.Error())
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
var payWay string
var notifyURL string
switch data.PayWay {
case "hupi":
payWay = PayWayXunHu.Value
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
break
case "payjs":
payWay = PayWayJs.Value
notifyURL = h.App.Config.JPayConfig.NotifyURL
break
case "alipay":
payWay = PayWayAlipay.Value
notifyURL = h.App.Config.AlipayConfig.NotifyURL
break
default:
payWay = PayWayWechat.Value
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
}
// 创建订单
remark := types.OrderRemark{
Days: product.Days,
Power: product.Power,
Name: product.Name,
Price: product.Price,
Discount: product.Discount,
}
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
order := model.Order{
UserId: user.Id,
Username: user.Username,
ProductId: product.Id,
OrderNo: orderNo,
Subject: product.Name,
Amount: amount,
Status: types.OrderNotPaid,
PayWay: payWay,
Remark: utils.JsonEncode(remark),
}
res = h.DB.Create(&order)
if res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "error with create order: "+res.Error.Error())
return
}
// PayJs 单独处理,只能用官方生成的二维码
if data.PayWay == "payjs" {
params := payment.JPayReq{
TotalFee: int(math.Ceil(order.Amount * 100)),
OutTradeNo: order.OrderNo,
Subject: product.Name,
func (h *PaymentHandler) StartSyncOrders() {
go func() {
for {
err := h.SyncOrders()
if err != nil {
logger.Error(err)
}
time.Sleep(time.Second * 5)
}
r := h.jsPayService.Pay(params)
if r.IsOK() {
resp.SUCCESS(c, gin.H{"order_no": order.OrderNo, "image": r.Qrcode})
return
} else {
resp.ERROR(c, "error with generating payment qrcode: "+r.ReturnMsg)
return
}
}
var logo string
if data.PayWay == "alipay" {
logo = "res/img/alipay.jpg"
} else if data.PayWay == "hupi" {
if h.App.Config.HuPiPayConfig.Name == "wechat" {
logo = "res/img/wechat-pay.jpg"
} else {
logo = "res/img/alipay.jpg"
}
} else if data.PayWay == "wechat" {
logo = "res/img/wechat-pay.jpg"
}
file, err := h.fs.Open(logo)
if err != nil {
resp.ERROR(c, "error with open qrcode log file: "+err.Error())
return
}
parse, err := url.Parse(notifyURL)
if err != nil {
resp.ERROR(c, err.Error())
return
}
timestamp := time.Now().Unix()
signStr := fmt.Sprintf("%s-%s-%d-%s", orderNo, data.PayWay, timestamp, h.signKey)
sign := utils.Sha256(signStr)
var imageURL string
if data.PayWay == "wechat" {
payUrl, err := h.wechatPayService.PayUrlNative(order.OrderNo, int(math.Floor(order.Amount*100)), product.Name)
if err != nil {
resp.ERROR(c, "error with generating wechat payment qrcode: "+err.Error())
return
} else {
imageURL = payUrl
}
} else {
imageURL = fmt.Sprintf("%s://%s/api/payment/doPay?order_no=%s&pay_way=%s&t=%d&sign=%s", parse.Scheme, parse.Host, orderNo, data.PayWay, timestamp, sign)
}
imgData, err := utils.GenQrcode(imageURL, 400, file)
if err != nil {
resp.ERROR(c, err.Error())
return
}
imgDataBase64 := base64.StdEncoding.EncodeToString(imgData)
resp.SUCCESS(c, gin.H{"order_no": orderNo, "image": fmt.Sprintf("data:image/jpg;base64, %s", imgDataBase64), "url": imageURL})
}()
}
// Mobile 移动端支付
func (h *PaymentHandler) Mobile(c *gin.Context) {
var data struct {
PayWay string `json:"pay_way"` // 支付方式
ProductId uint `json:"product_id"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var product model.Product
res := h.DB.First(&product, data.ProductId)
if res.Error != nil {
resp.ERROR(c, "Product not found")
return
}
orderNo, err := h.snowflake.Next(false)
// SyncOrders 同步订单状态
func (h *PaymentHandler) SyncOrders() error {
defer func() {
if err := recover(); err != nil {
logger.Errorf("同步订单状态发生异常: %v", err)
}
}()
var orders []model.Order
err := h.DB.Where("status", types.OrderNotPaid).Where("checked", false).Find(&orders).Error
if err != nil {
resp.ERROR(c, "error with generate trade no: "+err.Error())
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
amount, _ := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Float64()
var payWay string
var notifyURL, returnURL string
var payURL string
switch data.PayWay {
case "hupi":
payWay = PayWayXunHu.Name
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
parse, _ := url.Parse(h.App.Config.HuPiPayConfig.ReturnURL)
baseURL := fmt.Sprintf("%s://%s", parse.Scheme, parse.Host)
params := payment.HuPiPayReq{
Version: "1.1",
TradeOrderId: orderNo,
TotalFee: fmt.Sprintf("%f", amount),
Title: product.Name,
NotifyURL: notifyURL,
ReturnURL: returnURL,
CallbackURL: returnURL,
WapName: "极客学长",
WapUrl: baseURL,
Type: "WAP",
}
r, err := h.huPiPayService.Pay(params)
if err != nil {
errMsg := "error with generating Pay Hupi URL: " + err.Error()
logger.Error(errMsg)
resp.ERROR(c, errMsg)
return
}
payURL = r.URL
case "payjs":
payWay = PayWayJs.Name
notifyURL = h.App.Config.JPayConfig.NotifyURL
returnURL = h.App.Config.JPayConfig.ReturnURL
totalFee := decimal.NewFromFloat(product.Price).Sub(decimal.NewFromFloat(product.Discount)).Mul(decimal.NewFromInt(100)).IntPart()
params := url.Values{}
params.Add("total_fee", fmt.Sprintf("%d", totalFee))
params.Add("out_trade_no", orderNo)
params.Add("body", product.Name)
params.Add("notify_url", notifyURL)
params.Add("auto", "0")
payURL = h.jsPayService.PayH5(params)
case "alipay":
payWay = PayWayAlipay.Name
payURL, err = h.alipayService.PayUrlMobile(orderNo, fmt.Sprintf("%.2f", amount), product.Name)
if err != nil {
errMsg := "error with generating Alipay URL: " + err.Error()
resp.ERROR(c, errMsg)
return
}
case "wechat":
payWay = PayWayWechat.Name
payURL, err = h.wechatPayService.PayUrlH5(orderNo, int(amount*100), product.Name, c.ClientIP())
if err != nil {
errMsg := "error with generating Wechat URL: " + err.Error()
logger.Error(errMsg)
resp.ERROR(c, errMsg)
return
}
default:
resp.ERROR(c, "Unsupported pay way: "+data.PayWay)
return
}
// 创建订单
remark := types.OrderRemark{
Days: product.Days,
Power: product.Power,
Name: product.Name,
Price: product.Price,
Discount: product.Discount,
}
order := model.Order{
UserId: user.Id,
Username: user.Username,
ProductId: product.Id,
OrderNo: orderNo,
Subject: product.Name,
Amount: amount,
Status: types.OrderNotPaid,
PayWay: payWay,
Remark: utils.JsonEncode(remark),
}
res = h.DB.Create(&order)
if res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "error with create order: "+res.Error.Error())
return
}
resp.SUCCESS(c, gin.H{"url": payURL, "order_no": orderNo})
}
// 异步通知回调公共逻辑
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
var order model.Order
res := h.DB.Where("order_no = ?", orderNo).First(&order)
if res.Error != nil {
err := fmt.Errorf("error with fetch order: %v", res.Error)
logger.Error(err)
return err
}
for _, order := range orders {
time.Sleep(time.Second * 1)
//超时15分钟的订单,直接标记为已关闭
if time.Now().After(order.CreatedAt.Add(time.Minute * 5)) {
h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true)
logger.Errorf("订单超时:%v", order)
continue
}
// 查询订单状态
var res payment.OrderInfo
switch order.Channel {
case payment.PayChannelEpay:
res, err = h.epayService.Query(order.OrderNo)
if err != nil {
logger.Errorf("error with query order info: %v", err)
continue
}
// 微信支付
case payment.PayChannelWX:
res, err = h.wxpayService.Query(order.OrderNo)
logger.Debugf("微信支付订单状态:%+v", res)
if err != nil {
logger.Errorf("error with query order info: %v", err)
continue
}
case payment.PayChannelAL:
res, err = h.alipayService.Query(order.OrderNo)
logger.Debugf("支付宝支付订单状态:%+v", res)
if err != nil {
logger.Errorf("error with query order info: %v", err)
continue
}
}
// 订单已关闭
if res.Closed() {
h.DB.Model(&model.Order{}).Where("id", order.Id).Updates(map[string]any{
"checked": true,
"status": types.OrderPaidFailed,
})
logger.Errorf("订单已关闭:%v", order)
continue
}
// 订单未支付,不处理,继续轮询
if !res.Success() {
continue
}
// 订单支付成功
err = h.paySuccess(res)
if err != nil {
logger.Errorf("error with deal order: %v", err)
continue
}
}
return nil
}
func (h *PaymentHandler) CreateOrder(c *gin.Context) {
var data struct {
PayWay string `json:"pay_way,omitempty"` // 支付方式:支付宝,微信
Pid int `json:"pid,omitempty"`
Device string `json:"device,omitempty"`
Domain string `json:"domain,omitempty"` // 支付回调域名
Channel string `json:"channel,omitempty"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var product model.Product
err := h.DB.Where("id", data.Pid).First(&product).Error
if err != nil {
resp.ERROR(c, "Product not found")
return
}
orderNo, err := h.snowflake.Next(false)
if err != nil {
resp.ERROR(c, "error with generate trade no: "+err.Error())
return
}
userId := h.GetLoginUserId(c)
var user model.User
err = h.DB.Where("id", userId).First(&user).Error
if err != nil {
resp.NotAuth(c)
return
}
amount := product.Price
var payURL, notifyURL string
switch data.PayWay {
case "wxpay":
logger.Debugf("微信支付,%+v", data)
data.Channel = payment.PayChannelWX
// 优先使用微信官方支付
if h.config.WxPay.Enabled {
data.Channel = "wxpay"
if h.config.WxPay.Domain != "" {
data.Domain = h.config.WxPay.Domain
}
notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain)
payURL, err = h.wxpayService.Pay(payment.PayRequest{
OutTradeNo: orderNo,
TotalFee: fmt.Sprintf("%d", int(amount*100)),
Subject: product.Name,
NotifyURL: notifyURL,
ClientIP: c.ClientIP(),
Device: data.Device,
PayWay: payment.PayWayWX,
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
} else if h.config.Epay.Enabled { // 聚合支付
logger.Debugf("聚合支付%+v", data)
data.Channel = payment.PayChannelEpay
if h.config.Epay.Domain != "" {
data.Domain = h.config.Epay.Domain
}
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
params := payment.PayRequest{
OutTradeNo: orderNo,
Subject: product.Name,
TotalFee: fmt.Sprintf("%f", amount),
ClientIP: c.ClientIP(),
Device: data.Device,
PayWay: payment.PayWayWX,
NotifyURL: notifyURL,
}
r, err := h.epayService.Pay(params)
logger.Debugf("请求支付结果,%+v", r)
if err != nil {
resp.ERROR(c, err.Error())
return
} else {
payURL = r
}
} else {
resp.ERROR(c, "系统没有配置可用的支付渠道!")
return
}
case "alipay":
if h.config.Alipay.Enabled {
logger.Debugf("支付宝,%+v", data)
data.Channel = payment.PayChannelAL
if h.config.Alipay.Domain != "" { // 用于本地调试支付
data.Domain = h.config.Alipay.Domain
}
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Domain)
money := fmt.Sprintf("%.2f", amount)
payURL, err = h.alipayService.Pay(payment.PayRequest{
Device: data.Device,
OutTradeNo: orderNo,
Subject: product.Name,
TotalFee: money,
NotifyURL: notifyURL,
})
if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error())
return
}
} else if h.config.Epay.Enabled { // 聚合支付
logger.Debugf("聚合支付,%+v", data)
data.Channel = payment.PayChannelEpay
if h.config.Epay.Domain != "" {
data.Domain = h.config.Epay.Domain
}
notifyURL = fmt.Sprintf("%s/api/payment/notify/epay", data.Domain)
params := payment.PayRequest{
OutTradeNo: orderNo,
Subject: product.Name,
TotalFee: fmt.Sprintf("%f", amount),
ClientIP: c.ClientIP(),
Device: data.Device,
PayWay: data.PayWay,
NotifyURL: notifyURL,
}
r, err := h.epayService.Pay(params)
if err != nil {
resp.ERROR(c, err.Error())
return
} else {
payURL = r
}
} else {
resp.ERROR(c, "系统没有配置可用的支付渠道!")
return
}
default:
resp.ERROR(c, "不支持的支付渠道")
return
}
// 创建订单
remark := types.OrderRemark{
Power: product.Power,
Name: product.Name,
Price: product.Price,
}
order := model.Order{
UserId: user.Id,
Username: user.Username,
OrderNo: orderNo,
Subject: product.Name,
Amount: amount,
Status: types.OrderNotPaid,
PayWay: data.PayWay,
Channel: data.Channel,
Remark: utils.JsonEncode(remark),
}
err = h.DB.Create(&order).Error
if err != nil {
resp.ERROR(c, "error with create order: "+err.Error())
return
}
resp.SUCCESS(c, gin.H{"pay_url": payURL, "order_no": orderNo})
}
// 支付成功处理
func (h *PaymentHandler) paySuccess(info payment.OrderInfo) error {
h.lock.Lock()
defer h.lock.Unlock()
var order model.Order
err := h.DB.Where("order_no", info.OutTradeNo).First(&order).Error
if err != nil {
return fmt.Errorf("error with fetch order: %v", err)
}
// 已支付订单,直接返回
if order.Status == types.OrderPaidSuccess {
return nil
}
var user model.User
res = h.DB.First(&user, order.UserId)
if res.Error != nil {
err := fmt.Errorf("error with fetch user info: %v", res.Error)
logger.Error(err)
return err
err = h.DB.First(&user, order.UserId).Error
if err != nil {
return fmt.Errorf("error with fetch user info: %v", err)
}
var remark types.OrderRemark
err := utils.JsonDecode(order.Remark, &remark)
err = utils.JsonDecode(order.Remark, &remark)
if err != nil {
err := fmt.Errorf("error with decode order remark: %v", err)
logger.Error(err)
return err
return fmt.Errorf("error with decode order remark: %v", err)
}
var opt string
var power int
if remark.Days > 0 { // VIP 充值
if user.ExpiredTime >= time.Now().Unix() {
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
opt = "VIP充值,VIP 没到期,只延期不增加算力"
} else {
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
user.Power += h.App.SysConfig.VipMonthPower
power = h.App.SysConfig.VipMonthPower
opt = "VIP充值"
}
user.Vip = true
} else { // 充值点卡,直接增加次数即可
user.Power += remark.Power
opt = "点卡充值"
power = remark.Power
}
// 更新用户信息
res = h.DB.Updates(&user)
if res.Error != nil {
err := fmt.Errorf("error with update user info: %v", res.Error)
logger.Error(err)
// 增加用户算力
err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{
Type: types.PowerRecharge,
Model: order.Subject,
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
CreatedAt: time.Now(),
})
if err != nil {
return err
}
// 更新订单状态
order.PayTime = time.Now().Unix()
order.PayTime = utils.Str2stamp(info.PayTime)
order.Status = types.OrderPaidSuccess
order.TradeNo = tradeNo
res = h.DB.Updates(&order)
if res.Error != nil {
err := fmt.Errorf("error with update order info: %v", res.Error)
logger.Error(err)
return err
order.TradeNo = info.TradeId
order.Checked = true
err = h.DB.Debug().Updates(&order).Error
if err != nil {
return fmt.Errorf("error with update order info: %v", err)
}
// 更新产品销量
h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).UpdateColumn("sales", gorm.Expr("sales + ?", 1))
// 记录算力充值日志
if power > 0 {
h.DB.Create(&model.PowerLog{
UserId: user.Id,
Username: user.Username,
Type: types.PowerRecharge,
Amount: power,
Balance: user.Power,
Mark: types.PowerAdd,
Model: order.PayWay,
Remark: fmt.Sprintf("%s,金额:%f,订单号:%s", opt, order.Amount, order.OrderNo),
CreatedAt: time.Now(),
})
err = h.DB.Model(&model.Product{}).Where("id = ?", order.ProductId).
UpdateColumn("sales", gorm.Expr("sales + ?", 1)).Error
if err != nil {
return fmt.Errorf("error with update product sales: %v", err)
}
return nil
}
// GetPayWays 获取支付方式
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
data := gin.H{}
if h.App.Config.AlipayConfig.Enabled {
data["alipay"] = gin.H{"name": "alipay"}
}
if h.App.Config.HuPiPayConfig.Enabled {
data["hupi"] = gin.H{"name": h.App.Config.HuPiPayConfig.Name}
}
if h.App.Config.JPayConfig.Enabled {
data["payjs"] = gin.H{"name": h.App.Config.JPayConfig.Name}
}
if h.App.Config.WechatPayConfig.Enabled {
data["wechat"] = gin.H{"name": "wechat"}
}
resp.SUCCESS(c, data)
}
// HuPiPayNotify 虎皮椒支付异步回调
func (h *PaymentHandler) HuPiPayNotify(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
c.String(http.StatusOK, "fail")
return
}
orderNo := c.Request.Form.Get("trade_order_id")
tradeNo := c.Request.Form.Get("open_order_id")
logger.Infof("收到虎皮椒订单支付回调,订单 NO:%s,交易流水号:%s", orderNo, tradeNo)
if err = h.huPiPayService.Check(tradeNo); err != nil {
logger.Error("订单校验失败:", err)
c.String(http.StatusOK, "fail")
return
}
err = h.notify(orderNo, tradeNo)
if err != nil {
c.String(http.StatusOK, "fail")
return
}
c.String(http.StatusOK, "success")
}
// AlipayNotify 支付宝支付回调
func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
err := c.Request.ParseForm()
@@ -556,18 +404,17 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
return
}
// TODO:验证交易签名
res := h.alipayService.TradeVerify(c.Request)
logger.Infof("验证支付结果:%+v", res)
if !res.Success() {
logger.Error("订单校验失败:", res.Message)
orderInfo, err := h.alipayService.Query(c.Request.Form.Get("out_trade_no"))
logger.Infof("收到支付宝商号订单支付回调:%+v", orderInfo)
if !orderInfo.Success() {
logger.Errorf("订单校验失败:%v", err)
c.String(http.StatusOK, "fail")
return
}
tradeNo := c.Request.Form.Get("trade_no")
err = h.notify(res.OutTradeNo, tradeNo)
err = h.paySuccess(orderInfo)
if err != nil {
logger.Error(err)
c.String(http.StatusOK, "fail")
return
}
@@ -575,60 +422,63 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
c.String(http.StatusOK, "success")
}
// PayJsNotify PayJs 支付异步回调
func (h *PaymentHandler) PayJsNotify(c *gin.Context) {
// EPayNotify 易支付支付异步回调
func (h *PaymentHandler) EPayNotify(c *gin.Context) {
var params = make(map[string]string)
for k := range c.Request.URL.Query() {
params[k] = c.Query(k)
}
logger.Infof("收到易支付订单支付回调:%+v", params)
// 检查支付状态, 如果未支付,则返回成功
if params["trade_status"] != "TRADE_SUCCESS" {
c.String(http.StatusOK, "success")
return
}
sign := h.epayService.Sign(params)
if sign != c.Query("sign") {
logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign"))
c.String(http.StatusOK, "fail")
return
}
// 查询订单状态
order, err := h.epayService.Query(params["out_trade_no"])
if err != nil {
logger.Error(err)
c.String(http.StatusOK, "fail")
return
}
err = h.paySuccess(order)
if err != nil {
logger.Error(err)
c.String(http.StatusOK, "fail")
return
}
c.String(http.StatusOK, "success")
}
// WxpayNotify 微信商户支付异步回调
func (h *PaymentHandler) WxpayNotify(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
c.String(http.StatusOK, "fail")
return
}
orderNo := c.Request.Form.Get("out_trade_no")
returnCode := c.Request.Form.Get("return_code")
logger.Infof("收到PayJs订单支付回调,订单 NO:%s,支付结果代码:%v", orderNo, returnCode)
// 支付失败
if returnCode != "1" {
orderInfo, err := h.wxpayService.TradeVerify(c.Request)
logger.Infof("收到微信商号订单支付回调:%+v", orderInfo)
if err != nil {
logger.Errorf("订单校验失败:%v", err)
c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL"})
return
}
// 校验订单支付状态
tradeNo := c.Request.Form.Get("payjs_order_id")
err = h.jsPayService.TradeVerify(tradeNo)
if err != nil {
logger.Error("订单校验失败:", err)
c.String(http.StatusOK, "fail")
return
}
err = h.notify(orderNo, tradeNo)
if err != nil {
c.String(http.StatusOK, "fail")
return
}
c.String(http.StatusOK, "success")
}
// WechatPayNotify 微信商户支付异步回调
func (h *PaymentHandler) WechatPayNotify(c *gin.Context) {
err := c.Request.ParseForm()
if err != nil {
c.String(http.StatusOK, "fail")
return
}
result := h.wechatPayService.TradeVerify(c.Request)
if !result.Success() {
logger.Error("订单校验失败:", err)
c.JSON(http.StatusBadRequest, gin.H{
"code": "FAIL",
"message": err.Error(),
})
return
}
err = h.notify(result.OutTradeNo, result.TradeId)
err = h.paySuccess(orderInfo)
if err != nil {
logger.Error(err)
c.String(http.StatusOK, "fail")
return
}
+56
View File
@@ -9,11 +9,13 @@ package handler
import (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
@@ -27,6 +29,18 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *PowerLogHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/powerLog/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("list", h.List)
group.GET("stats", h.Stats)
}
}
func (h *PowerLogHandler) List(c *gin.Context) {
var data struct {
Model string `json:"model"`
@@ -72,3 +86,45 @@ func (h *PowerLogHandler) List(c *gin.Context) {
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
}
// Stats 获取用户算力统计
func (h *PowerLogHandler) Stats(c *gin.Context) {
userId := h.GetLoginUserId(c)
if userId == 0 {
resp.NotAuth(c)
return
}
// 获取用户信息(包含余额)
var user model.User
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
resp.ERROR(c, "用户不存在")
return
}
// 计算总消费(所有支出记录)
var totalConsume int64
h.DB.Model(&model.PowerLog{}).
Where("user_id", userId).
Where("mark", types.PowerSub).
Select("COALESCE(SUM(amount), 0)").
Scan(&totalConsume)
// 计算今日消费
today := time.Now().Format("2006-01-02")
var todayConsume int64
h.DB.Model(&model.PowerLog{}).
Where("user_id", userId).
Where("mark", types.PowerSub).
Where("DATE(created_at) = ?", today).
Select("COALESCE(SUM(amount), 0)").
Scan(&todayConsume)
stats := map[string]interface{}{
"total": totalConsume,
"today": todayConsume,
"balance": user.Power,
}
resp.SUCCESS(c, stats)
}
+7
View File
@@ -13,6 +13,7 @@ import (
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
@@ -25,6 +26,12 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
return &ProductHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
// RegisterRoutes 注册路由
func (h *ProductHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/product/")
group.GET("list", h.List)
}
// List 模型列表
func (h *ProductHandler) List(c *gin.Context) {
var items []model.Product
+146
View File
@@ -0,0 +1,146 @@
package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// 提示词生成 handler
// 使用 AI 生成绘画指令,歌词,视频生成指令等
type PromptHandler struct {
BaseHandler
userService *service.UserService
}
func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService) *PromptHandler {
return &PromptHandler{
BaseHandler: BaseHandler{
App: app,
DB: db,
},
userService: userService,
}
}
// RegisterRoutes 注册路由
func (h *PromptHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/prompt/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)).Use(middleware.RateLimitEvery(h.App.Redis, 30*time.Second))
{
group.POST("lyric", h.Lyric)
group.POST("image", h.Image)
group.POST("video", h.Video)
group.POST("meta", h.MetaPrompt)
}
}
// Lyric 生成歌词
func (h *PromptHandler) Lyric(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, content)
}
// Image 生成 AI 绘画提示词
func (h *PromptHandler) Image(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, strings.Trim(content, `"`))
}
// Video 生成视频提示词
func (h *PromptHandler) Video(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, strings.Trim(content, `"`))
}
// MetaPrompt 生成元提示词
func (h *PromptHandler) MetaPrompt(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
messages := make([]interface{}, 0)
messages = append(messages, types.Message{
Role: "system",
Content: service.MetaPromptTemplate,
})
messages = append(messages, types.Message{
Role: "user",
Content: "Task, Goal, or the Role to actor is:\n" + data.Prompt,
})
content, err := utils.SendOpenAIMessage(h.DB, messages, 0)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, strings.Trim(content, `"`))
}
func (h *PromptHandler) getPromptModel() string {
if h.App.SysConfig.Base.AssistantModelId > 0 {
var chatModel model.ChatModel
h.DB.Where("id", h.App.SysConfig.Base.AssistantModelId).First(&chatModel)
return chatModel.Value
}
return "gpt-4o"
}
+234
View File
@@ -0,0 +1,234 @@
package handler
import (
"encoding/json"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"io"
"net/http"
"regexp"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// OpenAI Realtime API Relay Server
type RealtimeHandler struct {
BaseHandler
userService *service.UserService
}
func NewRealtimeHandler(server *core.AppServer, db *gorm.DB, userService *service.UserService) *RealtimeHandler {
return &RealtimeHandler{BaseHandler: BaseHandler{App: server, DB: db}, userService: userService}
}
// RegisterRoutes 注册路由
func (h *RealtimeHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/realtime/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.Any("", h.Connection)
group.POST("voice", h.VoiceChat)
}
}
func (h *RealtimeHandler) Connection(c *gin.Context) {
// 获取客户端请求中指定的子协议
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
md := c.Query("model")
userId := h.GetLoginUserId(c)
var user model.User
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
c.Abort()
return
}
// 将 HTTP 协议升级为 Websocket 协议
subProtocols := strings.Split(clientProtocols, ",")
ws, err := (&websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
Subprotocols: subProtocols,
}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
c.Abort()
return
}
defer ws.Close()
// 目前只针对 VIP 用户可以访问
if !user.Vip {
sendError(ws, "当前功能只针对 VIP 用户开放")
c.Abort()
return
}
var apiKey model.ApiKey
h.DB.Where("type", "realtime").Where("enabled", true).Order("last_used_at ASC").First(&apiKey)
if apiKey.Id == 0 {
sendError(ws, "管理员未配置 Realtime API KEY")
c.Abort()
return
}
apiURL := fmt.Sprintf("%s/v1/realtime?model=%s", apiKey.ApiURL, md)
// 连接到真实的后端服务器,传入相同的子协议
headers := http.Header{}
// 修正子协议内容
subProtocols[1] = "openai-insecure-api-key." + apiKey.Value
if clientProtocols != "" {
headers.Set("Sec-WebSocket-Protocol", strings.Join(subProtocols, ","))
}
backendConn, _, err := websocket.DefaultDialer.Dial(apiURL, headers)
if err != nil {
sendError(ws, "桥接后端 API 失败:"+err.Error())
c.Abort()
return
}
defer backendConn.Close()
// 确保协议一致性,如果失败返回
if ws.Subprotocol() != backendConn.Subprotocol() {
sendError(ws, "Websocket 子协议不匹配")
c.Abort()
return
}
// 更新API KEY 最后使用时间
h.DB.Model(&model.ApiKey{}).Where("id", apiKey.Id).UpdateColumn("last_used_at", time.Now().Unix())
// 开始双向转发
errorChan := make(chan error, 2)
go relay(ws, backendConn, errorChan)
go relay(backendConn, ws, errorChan)
// 等待其中一个连接关闭
err = <-errorChan
logger.Infof("Relay ended: %v", err)
}
func relay(src, dst *websocket.Conn, errorChan chan error) {
for {
messageType, message, err := src.ReadMessage()
if err != nil {
errorChan <- err
return
}
err = dst.WriteMessage(messageType, message)
if err != nil {
errorChan <- err
return
}
}
}
func sendError(ws *websocket.Conn, message string) {
err := ws.WriteJSON(map[string]string{"event_id": "event_01", "type": "error", "error": message})
if err != nil {
logger.Error(err)
}
}
// OpenAI 实时语音对话,一次性对话
func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
var apiKey model.ApiKey
err := h.DB.Session(&gorm.Session{}).Where("type", "realtime").Where("enabled", true).First(&apiKey).Error
if err != nil {
resp.ERROR(c, fmt.Sprintf("error with fetch OpenAI API KEY%v", err))
return
}
// 检查用户是否还有算力
userId := h.GetLoginUserId(c)
var user model.User
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
resp.ERROR(c, fmt.Sprintf("error with fetch user%v", err))
return
}
if user.Power < h.App.SysConfig.Base.AdvanceVoicePower {
resp.ERROR(c, "当前用户算力不足,无法使用该功能")
return
}
var response utils.OpenAIResponse
client := req.C()
if len(apiKey.ProxyURL) > 5 {
client.SetProxyURL(apiKey.ApiURL)
}
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, "advanced-voice")
r, err := client.R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{
Model: "advanced-voice",
Temperature: 0.9,
MaxTokens: 1024,
Stream: false,
Messages: []interface{}{types.Message{
Role: "user",
Content: "实时语音通话",
}},
}).Post(apiURL)
if err != nil {
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API失败:%v", err))
return
}
if r.IsErrorState() {
resp.ERROR(c, fmt.Sprintf("请求 OpenAI API失败:%v", r.Status))
return
}
body, _ := io.ReadAll(r.Body)
err = json.Unmarshal(body, &response)
if err != nil {
resp.ERROR(c, fmt.Sprintf("解析API数据失败:%v, %s", err, string(body)))
}
// 更新 API KEY 的最后使用时间
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
// 扣减算力
err = h.userService.DecreasePower(userId, h.App.SysConfig.Base.AdvanceVoicePower, model.PowerLog{
Type: types.PowerConsume,
Model: "advanced-voice",
Remark: "实时语音通话",
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
logger.Infof("Response: %v", response.Choices[0].Message.Content)
// 提取链接
re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
links := re.FindAllStringSubmatch(response.Choices[0].Message.Content, -1)
var url = ""
if len(links) > 0 {
url = links[0][2]
}
resp.SUCCESS(c, url)
}
+16 -3
View File
@@ -10,14 +10,16 @@ package handler
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"sync"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type RedeemHandler struct {
@@ -30,6 +32,17 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB, userService *service.Use
return &RedeemHandler{BaseHandler: BaseHandler{App: app, DB: db}, userService: userService}
}
// RegisterRoutes 注册路由
func (h *RedeemHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/redeem/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("verify", h.Verify)
}
}
func (h *RedeemHandler) Verify(c *gin.Context) {
var data struct {
Code string `json:"code"`
@@ -61,7 +74,7 @@ func (h *RedeemHandler) Verify(c *gin.Context) {
}
tx := h.DB.Begin()
err := h.userService.IncreasePower(int(userId), item.Power, model.PowerLog{
err := h.userService.IncreasePower(userId, item.Power, model.PowerLog{
Type: types.PowerRedeem,
Model: "兑换码",
Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
+88 -93
View File
@@ -10,8 +10,10 @@ package handler
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/sd"
"geekai/store"
@@ -19,11 +21,8 @@ import (
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"net/http"
"time"
"github.com/gorilla/websocket"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
@@ -31,12 +30,13 @@ import (
type SdJobHandler struct {
BaseHandler
redis *redis.Client
sdService *sd.Service
uploader *oss.UploaderManager
snowflake *service.Snowflake
leveldb *store.LevelDB
userService *service.UserService
redis *redis.Client
sdService *sd.Service
uploader *oss.UploaderManager
snowflake *service.Snowflake
leveldb *store.LevelDB
userService *service.UserService
moderationManager *moderation.ServiceManager
}
func NewSdJobHandler(app *core.AppServer,
@@ -45,13 +45,15 @@ func NewSdJobHandler(app *core.AppServer,
manager *oss.UploaderManager,
snowflake *service.Snowflake,
userService *service.UserService,
levelDB *store.LevelDB) *SdJobHandler {
levelDB *store.LevelDB,
moderationManager *moderation.ServiceManager) *SdJobHandler {
return &SdJobHandler{
sdService: service,
uploader: manager,
snowflake: snowflake,
leveldb: levelDB,
userService: userService,
sdService: service,
uploader: manager,
snowflake: snowflake,
leveldb: levelDB,
userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{
App: app,
DB: db,
@@ -59,25 +61,21 @@ func NewSdJobHandler(app *core.AppServer,
}
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *SdJobHandler) Client(c *gin.Context) {
ws, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool { return true }}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
c.Abort()
return
}
// RegisterRoutes 注册路由
func (h *SdJobHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/sd/")
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
logger.Info("Invalid user ID")
c.Abort()
return
}
// 公开接口,不需要授权
group.GET("imgWall", h.ImgWall)
client := types.NewWsClient(ws)
h.sdService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("image", h.Image)
group.GET("jobs", h.JobList)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
}
}
func (h *SdJobHandler) preCheck(c *gin.Context) bool {
@@ -87,7 +85,7 @@ func (h *SdJobHandler) preCheck(c *gin.Context) bool {
return false
}
if user.Power < h.App.SysConfig.SdPower {
if user.Power < h.App.SysConfig.Base.SdPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false
}
@@ -108,6 +106,29 @@ func (h *SdJobHandler) Image(c *gin.Context) {
return
}
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceSD,
Input: data.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return
}
}
if data.Width <= 0 {
data.Width = 512
}
@@ -126,6 +147,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
if data.Sampler == "" {
data.Sampler = "Euler a"
}
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, err := h.snowflake.Next(true)
@@ -133,32 +155,39 @@ func (h *SdJobHandler) Image(c *gin.Context) {
resp.ERROR(c, "error with generate task id: "+err.Error())
return
}
params := types.SdTaskParams{
TaskId: taskId,
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
Steps: data.Steps,
Sampler: data.Sampler,
FaceFix: data.FaceFix,
CfgScale: data.CfgScale,
Seed: data.Seed,
Height: data.Height,
Width: data.Width,
HdFix: data.HdFix,
HdRedrawRate: data.HdRedrawRate,
HdScale: data.HdScale,
HdScaleAlg: data.HdScaleAlg,
HdSteps: data.HdSteps,
task := types.SdTask{
Type: types.TaskImage,
Params: types.SdTaskParams{
TaskId: taskId,
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
Steps: data.Steps,
Sampler: data.Sampler,
FaceFix: data.FaceFix,
CfgScale: data.CfgScale,
Seed: data.Seed,
Height: data.Height,
Width: data.Width,
HdFix: data.HdFix,
HdRedrawRate: data.HdRedrawRate,
HdScale: data.HdScale,
HdScaleAlg: data.HdScaleAlg,
HdSteps: data.HdSteps,
},
UserId: userId,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
}
job := model.SdJob{
UserId: userId,
UserId: uint(userId),
Type: types.TaskImage.String(),
TaskId: params.TaskId,
Params: utils.JsonEncode(params),
TaskId: taskId,
Params: utils.JsonEncode(task.Params),
TaskInfo: utils.JsonEncode(task),
Prompt: data.Prompt,
Progress: 0,
Power: h.App.SysConfig.SdPower,
Power: h.App.SysConfig.Base.SdPower,
CreatedAt: time.Now(),
}
res := h.DB.Create(&job)
@@ -167,17 +196,8 @@ func (h *SdJobHandler) Image(c *gin.Context) {
return
}
h.sdService.PushTask(types.SdTask{
Id: int(job.Id),
Type: types.TaskImage,
Params: params,
UserId: userId,
})
client := h.sdService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
task.Id = int(job.Id)
h.sdService.PushTask(task)
// update user's power
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
@@ -260,15 +280,6 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
if err != nil {
continue
}
if item.Progress < 100 {
// 从 leveldb 中获取图片预览数据
var imageData string
err = h.leveldb.Get(item.TaskId, &imageData)
if err == nil {
job.ImgURL = "data:image/png;base64," + imageData
}
}
jobs = append(jobs, job)
}
@@ -286,30 +297,14 @@ func (h *SdJobHandler) Remove(c *gin.Context) {
}
// 删除任务
tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
err := h.DB.Delete(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%s Err: %s", job.TaskId, job.ErrMsg),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image
err := h.uploader.GetUploadHandler().Delete(job.ImgURL)
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
@@ -323,7 +318,7 @@ func (h *SdJobHandler) Publish(c *gin.Context) {
userId := h.GetLoginUserId(c)
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: int(userId)}).UpdateColumn("publish", action).Error
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
if err != nil {
resp.ERROR(c, err.Error())
return
+37 -20
View File
@@ -24,24 +24,31 @@ const CodeStorePrefix = "/verify/codes/"
type SmsHandler struct {
BaseHandler
redis *redis.Client
sms *sms.ServiceManager
smtp *service.SmtpService
captcha *service.CaptchaService
redis *redis.Client
sms *sms.SmsManager
smtp *service.SmtpService
captchaService *service.CaptchaService
}
func NewSmsHandler(
app *core.AppServer,
client *redis.Client,
sms *sms.ServiceManager,
sms *sms.SmsManager,
smtp *service.SmtpService,
captcha *service.CaptchaService) *SmsHandler {
return &SmsHandler{
redis: client,
sms: sms,
captcha: captcha,
smtp: smtp,
BaseHandler: BaseHandler{App: app}}
redis: client,
sms: sms,
captchaService: captcha,
smtp: smtp,
BaseHandler: BaseHandler{App: app}}
}
// RegisterRoutes 注册路由
func (h *SmsHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/sms/")
// 无需授权的接口
group.POST("code", h.SendCode)
}
// SendCode 发送验证码
@@ -56,12 +63,12 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs)
return
}
if h.App.SysConfig.EnabledVerify {
if h.captchaService.GetConfig().Enabled {
var check bool
if data.X != 0 {
check = h.captcha.SlideCheck(data)
check = h.captchaService.SlideCheck(data)
} else {
check = h.captcha.Check(data)
check = h.captchaService.Check(data)
}
if !check {
resp.ERROR(c, "请先完人机验证")
@@ -72,13 +79,27 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
code := utils.RandomNumber(6)
var err error
if strings.Contains(data.Receiver, "@") { // email
if !utils.Contains(h.App.SysConfig.RegisterWays, "email") {
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "email") {
resp.ERROR(c, "系统已禁用邮箱注册!")
return
}
// 检查邮箱后缀是否在白名单
if len(h.App.SysConfig.Base.EmailWhiteList) > 0 {
inWhiteList := false
for _, suffix := range h.App.SysConfig.Base.EmailWhiteList {
if strings.HasSuffix(data.Receiver, suffix) {
inWhiteList = true
break
}
}
if !inWhiteList {
resp.ERROR(c, "邮箱后缀不在白名单中")
return
}
}
err = h.smtp.SendVerifyCode(data.Receiver, code)
} else {
if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") {
if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "mobile") {
resp.ERROR(c, "系统已禁用手机号注册!")
return
}
@@ -97,9 +118,5 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
return
}
if h.App.Debug {
resp.SUCCESS(c, code)
} else {
resp.SUCCESS(c)
}
resp.SUCCESS(c)
}
+94 -105
View File
@@ -10,59 +10,60 @@ package handler
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/suno"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type SunoHandler struct {
BaseHandler
sunoService *suno.Service
uploader *oss.UploaderManager
userService *service.UserService
sunoService *suno.Service
uploader *oss.UploaderManager
userService *service.UserService
moderationManager *moderation.ServiceManager
}
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService) *SunoHandler {
func NewSunoHandler(app *core.AppServer, db *gorm.DB, service *suno.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *SunoHandler {
return &SunoHandler{
BaseHandler: BaseHandler{
App: app,
DB: db,
},
sunoService: service,
uploader: uploader,
userService: userService,
sunoService: service,
uploader: uploader,
userService: userService,
moderationManager: moderationManager,
}
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *SunoHandler) 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)
c.Abort()
return
}
// RegisterRoutes 注册路由
func (h *SunoHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/suno/")
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
logger.Info("Invalid user ID")
c.Abort()
return
}
// 公开接口,不需要授权
group.GET("play", h.Play)
client := types.NewWsClient(ws)
h.sunoService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("create", h.Create)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
group.POST("update", h.Update)
group.GET("detail", h.Detail)
}
}
func (h *SunoHandler) Create(c *gin.Context) {
@@ -86,6 +87,40 @@ func (h *SunoHandler) Create(c *gin.Context) {
return
}
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceSuno,
Input: data.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return
}
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
if user.Power < h.App.SysConfig.Base.SunoPower {
resp.ERROR(c, "您的算力不足,请充值后再试!")
return
}
// 歌曲拼接
if data.SongId != "" && data.Type == 3 {
var song model.SunoJob
@@ -100,20 +135,36 @@ func (h *SunoHandler) Create(c *gin.Context) {
data.Prompt = fmt.Sprintf("%s\n%s", song.Prompt, refSong.Prompt)
}
}
task := types.SunoTask{
UserId: int(h.GetLoginUserId(c)),
Type: data.Type,
Title: data.Title,
RefTaskId: data.RefTaskId,
RefSongId: data.RefSongId,
ExtendSecs: data.ExtendSecs,
Prompt: data.Prompt,
Lyrics: data.Lyrics,
Tags: data.Tags,
Model: data.Model,
Instrumental: data.Instrumental,
SongId: data.SongId,
AudioURL: data.AudioURL,
}
// 插入数据库
job := model.SunoJob{
UserId: int(h.GetLoginUserId(c)),
UserId: uint(task.UserId),
Prompt: data.Prompt,
Instrumental: data.Instrumental,
ModelName: data.Model,
TaskInfo: utils.JsonEncode(task),
Tags: data.Tags,
Title: data.Title,
Type: data.Type,
RefSongId: data.RefSongId,
RefTaskId: data.RefTaskId,
ExtendSecs: data.ExtendSecs,
Power: h.App.SysConfig.SunoPower,
Power: h.App.SysConfig.Base.SunoPower,
SongId: utils.RandString(32),
}
if data.Lyrics != "" {
@@ -126,25 +177,13 @@ func (h *SunoHandler) Create(c *gin.Context) {
}
// 创建任务
h.sunoService.PushTask(types.SunoTask{
Id: job.Id,
UserId: job.UserId,
Type: job.Type,
Title: job.Title,
RefTaskId: data.RefTaskId,
RefSongId: data.RefSongId,
ExtendSecs: data.ExtendSecs,
Prompt: job.Prompt,
Tags: data.Tags,
Model: data.Model,
Instrumental: data.Instrumental,
SongId: data.SongId,
AudioURL: data.AudioURL,
})
task.Id = job.Id
h.sunoService.PushTask(task)
// update user's power
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: job.ModelName,
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName),
CreatedAt: time.Now(),
})
@@ -153,10 +192,6 @@ func (h *SunoHandler) Create(c *gin.Context) {
return
}
client := h.sunoService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
}
resp.SUCCESS(c)
}
@@ -225,28 +260,19 @@ func (h *SunoHandler) Remove(c *gin.Context) {
resp.ERROR(c, err.Error())
return
}
// 删除任务
tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
// 只有失败或者已完成的任务可以删除
if !(job.Progress == service.FailTaskProgress || job.Progress == 100) {
resp.ERROR(c, "只有失败和超时(10分钟)的任务才能删除!")
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err := h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: job.ModelName,
Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID%sErr:%s", job.TaskId, job.ErrMsg),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
// 删除任务
err = h.DB.Delete(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
tx.Commit()
// 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
@@ -343,40 +369,3 @@ func (h *SunoHandler) Play(c *gin.Context) {
}
h.DB.Model(&model.SunoJob{}).Where("song_id", songId).UpdateColumn("play_times", gorm.Expr("play_times + ?", 1))
}
const genLyricTemplate = `
你是一位才华横溢的作曲家,拥有丰富的情感和细腻的笔触,你对文字有着独特的感悟力,能将各种情感和意境巧妙地融入歌词中。
请以【%s】为主题创作一首歌曲,歌曲时间不要太短,3分钟左右,不要输出任何解释性的内容。
输出格式如下:
歌曲名称
第一节:
{{歌词内容}}
副歌:
{{歌词内容}}
第二节:
{{歌词内容}}
副歌:
{{歌词内容}}
尾声:
{{歌词内容}}
`
// Lyric 生成歌词
func (h *SunoHandler) Lyric(c *gin.Context) {
var data struct {
Prompt string `json:"prompt"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(genLyricTemplate, data.Prompt), "gpt-4o-mini")
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, content)
}
+20 -5
View File
@@ -1,25 +1,40 @@
package handler
import (
"geekai/core"
"geekai/core/middleware"
"geekai/service"
"geekai/service/payment"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
)
type TestHandler struct {
App *core.AppServer
db *gorm.DB
snowflake *service.Snowflake
js *payment.JPayService
js *payment.EPayService
}
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.JPayService) *TestHandler {
return &TestHandler{db: db, snowflake: snowflake, js: js}
func NewTestHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, js *payment.EPayService) *TestHandler {
return &TestHandler{App: app, db: db, snowflake: snowflake, js: js}
}
// RegisterRoutes 注册路由
func (h *TestHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/test/")
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.Any("sse", h.PostTest, h.SseTest)
}
}
func (h *TestHandler) SseTest(c *gin.Context) {
//c.Header("Content-Type", "text/event-stream")
//c.Header("Body-Type", "text/event-stream")
//c.Header("Cache-Control", "no-cache")
//c.Header("Connection", "keep-alive")
//
+268 -240
View File
@@ -8,15 +8,17 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"context"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/store"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/imroc/req/v3"
"strings"
"time"
@@ -32,9 +34,11 @@ type UserHandler struct {
BaseHandler
searcher *xdb.Searcher
redis *redis.Client
licenseService *service.LicenseService
captcha *service.CaptchaService
levelDB *store.LevelDB
captchaService *service.CaptchaService
userService *service.UserService
wxLoginService *service.WxLoginService
ipSearcher *xdb.Searcher
}
func NewUserHandler(
@@ -42,16 +46,46 @@ func NewUserHandler(
db *gorm.DB,
searcher *xdb.Searcher,
client *redis.Client,
levelDB *store.LevelDB,
captcha *service.CaptchaService,
userService *service.UserService,
licenseService *service.LicenseService) *UserHandler {
wxLoginService *service.WxLoginService,
ipSearcher *xdb.Searcher) *UserHandler {
return &UserHandler{
BaseHandler: BaseHandler{DB: db, App: app},
searcher: searcher,
redis: client,
captcha: captcha,
licenseService: licenseService,
levelDB: levelDB,
captchaService: captcha,
userService: userService,
wxLoginService: wxLoginService,
ipSearcher: ipSearcher,
}
}
// RegisterRoutes 注册路由
func (h *UserHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/user/")
// 公开接口,不需要授权
group.POST("register", h.Register)
group.POST("login", h.Login)
group.POST("resetPass", h.ResetPass)
group.GET("login/qrcode", h.GetWxLoginQRCode)
group.POST("login/callback", h.WxLoginCallback)
group.GET("login/status", h.GetWxLoginState)
group.GET("logout", h.Logout)
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.GET("session", h.Session)
group.GET("profile", h.Profile)
group.POST("profile/update", h.ProfileUpdate)
group.POST("password", h.UpdatePass)
group.POST("bind/mobile", h.BindMobile)
group.POST("bind/email", h.BindEmail)
group.GET("signin", h.SignIn)
}
}
@@ -74,20 +108,27 @@ func (h *UserHandler) Register(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs)
return
}
// 人机验证
if h.captchaService.GetConfig().Enabled {
var check bool
if data.X != 0 {
check = h.captchaService.SlideCheck(data)
} else {
check = h.captchaService.Check(data)
}
if !check {
resp.ERROR(c, "请先完人机验证")
return
}
}
data.Password = strings.TrimSpace(data.Password)
if len(data.Password) < 8 {
resp.ERROR(c, "密码长度不能少于8个字符")
return
}
// 检测最大注册人数
var totalUser int64
h.DB.Model(&model.User{}).Count(&totalUser)
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
return
}
// 检查验证码
var key string
if data.RegWay == "email" {
@@ -106,25 +147,18 @@ func (h *UserHandler) Register(c *gin.Context) {
}
}
// 验证邀请码
inviteCode := model.InviteCode{}
if data.InviteCode != "" {
res := h.DB.Where("code = ?", data.InviteCode).First(&inviteCode)
if res.Error != nil {
resp.ERROR(c, "无效的邀请码")
return
}
}
// check if the username is existing
user := model.User{Username: data.Username, Password: data.Password}
var item model.User
session := h.DB.Session(&gorm.Session{})
if data.Mobile != "" {
session = session.Where("mobile = ?", data.Mobile)
data.Username = data.Mobile
user.Username = data.Mobile
user.Mobile = data.Mobile
} else if data.Email != "" {
session = session.Where("email = ?", data.Email)
data.Username = data.Email
user.Username = data.Email
user.Email = data.Email
} else if data.Username != "" {
session = session.Where("username = ?", data.Username)
}
@@ -134,87 +168,19 @@ func (h *UserHandler) Register(c *gin.Context) {
return
}
salt := utils.RandString(8)
user := model.User{
Username: data.Username,
Mobile: data.Mobile,
Email: data.Email,
Password: utils.GenPassword(data.Password, salt),
Avatar: "/images/avatar/user.png",
Salt: salt,
Status: true,
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
Power: h.App.SysConfig.InitPower,
}
// 被邀请人也获得赠送算力
if data.InviteCode != "" {
user.Power += h.App.SysConfig.InvitePower
}
if h.licenseService.GetLicense().Configs.DeCopy {
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
} else {
user.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
}
tx := h.DB.Begin()
if err := tx.Create(&user).Error; err != nil {
user, err := h.createNewUser(user, data.InviteCode)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 记录邀请关系
if data.InviteCode != "" {
// 增加邀请数量
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.InvitePower > 0 {
err := h.userService.IncreasePower(int(inviteCode.UserId), h.App.SysConfig.InvitePower, model.PowerLog{
Type: types.PowerInvite,
Model: "",
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
// 添加邀请记录
err := tx.Create(&model.InviteLog{
InviterId: inviteCode.UserId,
UserId: user.Id,
Username: user.Username,
InviteCode: inviteCode.Code,
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.InvitePower),
}).Error
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
_ = h.redis.Del(c, key) // 注册成功,删除短信验证码
// 自动登录创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id,
"expired": time.Now().Add(time.Second * time.Duration(h.App.Config.Session.MaxAge)).Unix(),
})
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
token, err := h.doLogin(&user, c.ClientIP())
if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error())
resp.ERROR(c, 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, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
}
// Login 用户登录
@@ -230,13 +196,12 @@ func (h *UserHandler) Login(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs)
return
}
if h.App.SysConfig.EnabledVerify {
if h.captchaService.GetConfig().Enabled {
var check bool
if data.X != 0 {
check = h.captcha.SlideCheck(data)
check = h.captchaService.SlideCheck(data)
} else {
check = h.captcha.Check(data)
check = h.captchaService.Check(data)
}
if !check {
resp.ERROR(c, "请先完人机验证")
@@ -257,40 +222,18 @@ func (h *UserHandler) Login(c *gin.Context) {
return
}
if user.Status == false {
if !user.Status {
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.Username,
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))
token, err := h.doLogin(&user, c.ClientIP())
if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error())
resp.ERROR(c, 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, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
}
// Logout 注 销
@@ -302,135 +245,168 @@ func (h *UserHandler) Logout(c *gin.Context) {
resp.SUCCESS(c)
}
// CLogin 第三方登录请求二维码
func (h *UserHandler) CLogin(c *gin.Context) {
returnURL := h.GetTrim(c, "return_url")
var res types.BizVo
apiURL := fmt.Sprintf("%s/api/clogin/request", h.App.Config.ApiConfig.ApiURL)
r, err := req.C().R().SetBody(gin.H{"login_type": "wx", "return_url": returnURL}).
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
SetSuccessResult(&res).
Post(apiURL)
// GetWxLoginQRCode 获取微信登录二维码URL
func (h *UserHandler) GetWxLoginQRCode(c *gin.Context) {
if !h.wxLoginService.GetConfig().Enabled {
resp.ERROR(c, "微信登录功能未启用")
return
}
if h.wxLoginService.GetConfig().ApiKey == "" {
resp.ERROR(c, "微信登录服务令牌未配置")
return
}
state := utils.RandString(32)
qrCodeURL, err := h.wxLoginService.GetLoginQrCodeUrl(state)
if err != nil {
resp.ERROR(c, err.Error())
return
}
if r.IsErrorState() {
resp.ERROR(c, "error with login http status: "+r.Status)
return
}
if res.Code != types.Success {
resp.ERROR(c, "error with http response: "+res.Message)
return
}
resp.SUCCESS(c, res.Data)
resp.SUCCESS(c, gin.H{
"url": qrCodeURL,
"state": state,
})
}
// CLoginCallback 第三方登录回调
func (h *UserHandler) CLoginCallback(c *gin.Context) {
loginType := c.Query("login_type")
code := c.Query("code")
userId := h.GetInt(c, "user_id", 0)
action := c.Query("action")
// 查询微信登录状态
func (h *UserHandler) GetWxLoginState(c *gin.Context) {
state := c.Query("state")
if state == "" {
resp.ERROR(c, "参数错误")
return
}
var res types.BizVo
apiURL := fmt.Sprintf("%s/api/clogin/info", h.App.Config.ApiConfig.ApiURL)
r, err := req.C().R().SetBody(gin.H{"login_type": loginType, "code": code}).
SetHeader("AppId", h.App.Config.ApiConfig.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.App.Config.ApiConfig.Token)).
SetSuccessResult(&res).
Post(apiURL)
status, err := h.wxLoginService.GetLoginStatus(state)
if err != nil {
resp.ERROR(c, err.Error())
return
}
if r.IsErrorState() {
resp.ERROR(c, "error with login http status: "+r.Status)
if status.Status != service.LoginStatusSuccess {
resp.SUCCESS(c, status)
return
}
if res.Code != types.Success {
resp.ERROR(c, "error with http response: "+res.Message)
return
}
// login successfully
data := res.Data.(map[string]interface{})
// 登录成功
var user model.User
if action == "bind" && userId > 0 {
err = h.DB.Where("openid", data["openid"]).First(&user).Error
if err == nil {
resp.ERROR(c, "该微信已经绑定其他账号,请先解绑")
return
}
err = h.DB.Where("id", userId).First(&user).Error
h.DB.Where("openid = ?", status.OpenID).First(&user)
if user.Id == 0 {
// 创建新用户
user, err = h.createNewUser(model.User{OpenId: status.OpenID}, "")
if err != nil {
resp.ERROR(c, "绑定用户不存在")
resp.ERROR(c, err.Error())
return
}
}
err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error
if err != nil {
resp.ERROR(c, "更新用户信息失败,"+err.Error())
return
}
resp.SUCCESS(c, gin.H{"token": ""})
token, err := h.doLogin(&user, c.ClientIP())
if err != nil {
resp.ERROR(c, err.Error())
return
}
session := gin.H{}
tx := h.DB.Where("openid", data["openid"]).First(&user)
if tx.Error != nil {
// create new user
var totalUser int64
h.DB.Model(&model.User{}).Count(&totalUser)
if h.licenseService.GetLicense().Configs.UserNum > 0 && int(totalUser) >= h.licenseService.GetLicense().Configs.UserNum {
resp.ERROR(c, "当前注册用户数已达上限,请请升级 License")
return
}
status.Status = service.LoginStatusExpired
h.wxLoginService.SetLoginStatus(state, *status)
salt := utils.RandString(8)
password := fmt.Sprintf("%d", utils.RandomNumber(8))
user = model.User{
Username: fmt.Sprintf("%s@%d", loginType, utils.RandomNumber(10)),
Password: utils.GenPassword(password, salt),
Avatar: fmt.Sprintf("%s", data["avatar"]),
Salt: salt,
Status: true,
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
ChatModels: utils.JsonEncode(h.App.SysConfig.DefaultModels), // 默认开通的模型
Power: h.App.SysConfig.InitPower,
OpenId: fmt.Sprintf("%s", data["openid"]),
Nickname: fmt.Sprintf("%s", data["nickname"]),
}
status.Status = service.LoginStatusSuccess
status.Token = token
resp.SUCCESS(c, status)
}
tx = h.DB.Create(&user)
if tx.Error != nil {
resp.ERROR(c, "保存数据失败")
logger.Error(tx.Error)
return
// createNewUser 创建新用户
func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
if user.OpenId != "" {
user.Platform = "wechat"
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8))
user.Password = "geekai123"
} else {
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
if user.Username == "" || user.Password == "" {
return user, fmt.Errorf("用户名或密码不能为空")
}
session["username"] = user.Username
session["password"] = password
} else { // login directly
// 更新最后登录时间和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.Username,
LoginIp: c.ClientIP(),
LoginAddress: utils.Ip2Region(h.searcher, c.ClientIP()),
})
}
salt := utils.RandString(8)
user.Salt = salt
user.Password = utils.GenPassword(user.Password, salt)
user.Avatar = "/images/avatar/user.png"
user.Status = true
user.ChatRoles = utils.JsonEncode([]string{"gpt"})
user.ChatConfig = "{}"
user.ChatModels = "{}"
user.Power = h.App.SysConfig.Base.InitPower
// 创建用户
tx := h.DB.Begin()
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return user, err
}
// 记录邀请关系
if code != "" {
var inviteCode model.InviteCode
err := h.DB.Where("code = ?", code).First(&inviteCode).Error
if err != nil {
logger.Error("无效的邀请码:", err.Error())
tx.Rollback()
return user, fmt.Errorf("无效的邀请码")
}
// 增加邀请数量
h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.Base.InvitePower > 0 {
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
Type: types.PowerInvite,
Model: "Invite",
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d,邀请码:%s,新用户:%s", h.App.SysConfig.Base.InvitePower, inviteCode.Code, user.Username),
})
if err != nil {
tx.Rollback()
return user, err
}
// 添加邀请记录
err = tx.Create(&model.InviteLog{
InviterId: inviteCode.UserId,
UserId: user.Id,
Username: user.Username,
InviteCode: inviteCode.Code,
Remark: fmt.Sprintf("奖励 %d 算力", h.App.SysConfig.Base.InvitePower),
}).Error
if err != nil {
tx.Rollback()
return user, err
}
}
}
tx.Commit()
return user, nil
}
// doLogin 执行登录操作
func (h *UserHandler) doLogin(user *model.User, ip string) (string, error) {
// 更新最后登录时间和IP
user.LastLoginIp = ip
user.LastLoginAt = time.Now().Unix()
err := h.DB.Model(user).Updates(user).Error
if err != nil {
return "", fmt.Errorf("failed to update user: %v", err)
}
// 记录登录日志
h.DB.Create(&model.UserLoginLog{
UserId: user.Id,
Username: user.Username,
LoginIp: ip,
LoginAddress: utils.Ip2Region(h.ipSearcher, ip),
})
// 创建 token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id,
@@ -438,17 +414,42 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) {
})
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error())
return
return "", fmt.Errorf("failed to generate token: %v", err)
}
// 保存到 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())
sessionKey := fmt.Sprintf("users/%d", user.Id)
if _, err = h.redis.Set(context.Background(), sessionKey, tokenString, 0).Result(); err != nil {
return "", fmt.Errorf("error with save token: %v", err)
}
return tokenString, nil
}
// WxLoginCallback 微信登录回调处理
func (h *UserHandler) WxLoginCallback(c *gin.Context) {
var data struct {
OpenID string `json:"openid"`
State string `json:"state"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session["token"] = tokenString
resp.SUCCESS(c, session)
if data.OpenID == "" || data.State == "" {
resp.ERROR(c, "参数错误")
return
}
// 设置登录状态
status := service.LoginStatus{
Status: service.LoginStatusSuccess,
OpenID: data.OpenID,
}
h.wxLoginService.SetLoginStatus(data.State, status)
resp.SUCCESS(c, status)
}
// Session 获取/验证会话
@@ -587,7 +588,7 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
session = session.Where("email", data.Email)
key = CodeStorePrefix + data.Email
} else if data.Type == "mobile" {
session = session.Where("mobile", data.Email)
session = session.Where("mobile", data.Mobile)
key = CodeStorePrefix + data.Mobile
} else {
resp.ERROR(c, "验证类别错误")
@@ -694,3 +695,30 @@ func (h *UserHandler) BindEmail(c *gin.Context) {
_ = h.redis.Del(c, key) // 删除短信验证码
resp.SUCCESS(c)
}
// SignIn 每日签到
func (h *UserHandler) SignIn(c *gin.Context) {
// 获取当前日期
date := time.Now().Format("2006-01-02")
// 检查是否已经签到
userId := h.GetLoginUserId(c)
key := fmt.Sprintf("signin/%d/%s", userId, date)
var signIn bool
err := h.levelDB.Get(key, &signIn)
if err == nil && signIn {
resp.ERROR(c, "今日已签到,请明日再来!")
return
}
// 签到
h.levelDB.Put(key, true)
if h.App.SysConfig.Base.DailyPower > 0 {
h.userService.IncreasePower(userId, h.App.SysConfig.Base.DailyPower, model.PowerLog{
Type: types.PowerSignIn,
Model: "SignIn",
Remark: fmt.Sprintf("每日签到奖励,金额:%d", h.App.SysConfig.Base.DailyPower),
})
}
resp.SUCCESS(c)
}
+212 -63
View File
@@ -10,58 +10,56 @@ package handler
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/video"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
)
type VideoHandler struct {
BaseHandler
videoService *video.Service
uploader *oss.UploaderManager
userService *service.UserService
videoService *video.Service
uploader *oss.UploaderManager
userService *service.UserService
moderationManager *moderation.ServiceManager
}
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService) *VideoHandler {
func NewVideoHandler(app *core.AppServer, db *gorm.DB, service *video.Service, uploader *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *VideoHandler {
return &VideoHandler{
BaseHandler: BaseHandler{
App: app,
DB: db,
},
videoService: service,
uploader: uploader,
userService: userService,
videoService: service,
uploader: uploader,
userService: userService,
moderationManager: moderationManager,
}
}
// Client WebSocket 客户端,用于通知任务状态变更
func (h *VideoHandler) 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)
c.Abort()
return
}
// RegisterRoutes 注册路由
func (h *VideoHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/video/")
userId := h.GetInt(c, "user_id", 0)
if userId == 0 {
logger.Info("Invalid user ID")
c.Abort()
return
// 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("luma/create", h.LumaCreate)
group.POST("keling/create", h.KeLingCreate)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
}
client := types.NewWsClient(ws)
h.videoService.Clients.Put(uint(userId), client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
}
func (h *VideoHandler) LumaCreate(c *gin.Context) {
@@ -77,25 +75,67 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs)
return
}
// 检查 Prompt 长度
if data.Prompt == "" {
resp.ERROR(c, "prompt is needed")
return
}
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceVideo,
Input: data.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return
}
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
if user.Power < h.App.SysConfig.Base.LumaPower {
resp.ERROR(c, "您的算力不足,请充值后再试!")
return
}
userId := int(h.GetLoginUserId(c))
params := types.VideoParams{
params := types.LumaVideoParams{
PromptOptimize: data.ExpandPrompt,
Loop: data.Loop,
StartImgURL: data.FirstFrameImg,
EndImgURL: data.EndFrameImg,
}
task := types.VideoTask{
UserId: userId,
Type: types.VideoLuma,
Prompt: data.Prompt,
Params: params,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
}
// 插入数据库
job := model.VideoJob{
UserId: userId,
Type: types.VideoLuma,
Prompt: data.Prompt,
Power: h.App.SysConfig.LumaPower,
Params: utils.JsonEncode(params),
UserId: uint(userId),
Type: types.VideoLuma,
Prompt: data.Prompt,
Power: h.App.SysConfig.Base.LumaPower,
TaskInfo: utils.JsonEncode(task),
}
tx := h.DB.Create(&job)
if tx.Error != nil {
@@ -104,16 +144,11 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
}
// 创建任务
h.videoService.PushTask(types.VideoTask{
Id: job.Id,
UserId: userId,
Type: types.VideoLuma,
Prompt: data.Prompt,
Params: params,
})
task.Id = job.Id
h.videoService.PushTask(task)
// update user's power
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "luma",
Remark: fmt.Sprintf("Luma 文生视频,任务ID%d", job.Id),
@@ -122,10 +157,102 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
client := h.videoService.Clients.Get(uint(job.UserId))
if client != nil {
_ = client.Send([]byte("Task Updated"))
func (h *VideoHandler) KeLingCreate(c *gin.Context) {
var data struct {
Channel string `json:"channel"`
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
Model string `json:"model"` // 模型: kling-v1-5,kling-v1-6
Prompt string `json:"prompt"` // 视频描述
NegPrompt string `json:"negative_prompt"` // 负面提示词
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
Mode string `json:"mode"` // 生成模式: std/pro
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
Duration string `json:"duration"` // 视频时长: 5/10
CameraControl types.CameraControl `json:"camera_control"` // 摄像机控制
Image string `json:"image"` // 参考图片URL(image2video)
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
// 计算当前任务所需算力
key := fmt.Sprintf("%s_%s_%s", data.Model, data.Mode, data.Duration)
power := h.App.SysConfig.Base.KeLingPowers[key]
if power == 0 {
resp.ERROR(c, "当前模型暂不支持")
return
}
if user.Power < power {
resp.ERROR(c, "您的算力不足,请充值后再试!")
return
}
if data.Prompt == "" {
resp.ERROR(c, "prompt is needed")
return
}
userId := int(h.GetLoginUserId(c))
params := types.KeLingVideoParams{
TaskType: data.TaskType,
Model: data.Model,
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
CfgScale: data.CfgScale,
Mode: data.Mode,
AspectRatio: data.AspectRatio,
Duration: data.Duration,
CameraControl: data.CameraControl,
Image: data.Image,
ImageTail: data.ImageTail,
}
task := types.VideoTask{
UserId: userId,
Type: types.VideoKeLing,
Prompt: data.Prompt,
Params: params,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
Channel: data.Channel,
}
// 插入数据库
job := model.VideoJob{
UserId: uint(userId),
Type: types.VideoKeLing,
Prompt: data.Prompt,
Power: power,
TaskInfo: utils.JsonEncode(task),
}
tx := h.DB.Create(&job)
if tx.Error != nil {
resp.ERROR(c, tx.Error.Error())
return
}
// 创建任务
task.Id = job.Id
h.videoService.PushTask(task)
// update user's power
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "keling",
Remark: fmt.Sprintf("keling 文生视频,任务ID%d", job.Id),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
@@ -136,14 +263,14 @@ func (h *VideoHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
all := h.GetBool(c, "all")
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId)
session := h.DB.Session(&gorm.Session{})
if t != "" {
session = session.Where("type", t)
}
if all {
session = session.Where("publish", 0).Where("progress", 100)
} else {
session = session.Where("user_id", h.GetLoginUserId(c))
session = session.Where("user_id", userId)
}
// 统计总数
var total int64
@@ -169,6 +296,36 @@ func (h *VideoHandler) List(c *gin.Context) {
continue
}
item.CreatedAt = v.CreatedAt.Unix()
if item.VideoURL == "" {
item.VideoURL = v.WaterURL
}
// 解析任务详情
if item.Type == types.VideoKeLing {
task := types.VideoTask{}
err = utils.JsonDecode(v.TaskInfo, &task)
if err != nil {
continue
}
var params types.KeLingVideoParams
err = utils.JsonDecode(utils.JsonEncode(task.Params), &params)
if err != nil {
continue
}
item.RawData = map[string]interface{}{
"task_type": params.TaskType,
"model": params.Model,
"cfg_scale": params.CfgScale,
"mode": params.Mode,
"aspect_ratio": params.AspectRatio,
"duration": params.Duration,
"model_name": fmt.Sprintf("%s_%s_%s", params.Model, params.Mode, params.Duration),
}
// 如果视频URL不为空,则设置为生成成功
if item.VideoURL != "" {
item.Progress = 100
}
}
items = append(items, item)
}
@@ -184,32 +341,24 @@ func (h *VideoHandler) Remove(c *gin.Context) {
resp.ERROR(c, err.Error())
return
}
// 只有失败或者超时的任务才能删除
if !(job.Progress == service.FailTaskProgress || time.Now().After(job.CreatedAt.Add(time.Minute*30))) {
resp.ERROR(c, "只有失败和超时(30分钟)的任务才能删除!")
return
}
// 删除任务
tx := h.DB.Begin()
if err := tx.Delete(&job).Error; err != nil {
tx.Rollback()
err = h.DB.Delete(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 如果任务未完成,或者任务失败,则恢复用户算力
if job.Progress != 100 {
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "luma",
Remark: fmt.Sprintf("Luma 任务失败,退回算力。任务ID%sErr:%s", job.TaskId, job.ErrMsg),
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
resp.SUCCESS(c)
}
func (h *VideoHandler) Publish(c *gin.Context) {
+4 -3
View File
@@ -8,11 +8,12 @@ package logger
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"os"
"strings"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
)
var logger *zap.Logger
@@ -23,7 +24,7 @@ func GetLogger() *zap.SugaredLogger {
return sugarLogger
}
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL")))
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("GEEKAI_LOG_LEVEL")))
encoder := getEncoder()
writerSyncer := getLogWriter()
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
+153 -245
View File
@@ -14,15 +14,17 @@ import (
"geekai/core/types"
"geekai/handler"
"geekai/handler/admin"
"geekai/handler/chatimpl"
logger2 "geekai/logger"
"geekai/service"
"geekai/service/dalle"
"geekai/service/jimeng"
"geekai/service/mj"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/payment"
"geekai/service/sd"
"geekai/service/sms"
"geekai/service/sora"
"geekai/service/suno"
"geekai/service/video"
"geekai/store"
@@ -30,7 +32,7 @@ import (
"log"
"os"
"os/signal"
"strconv"
"runtime/debug"
"syscall"
"time"
@@ -71,15 +73,16 @@ func main() {
if configFile == "" {
configFile = "config.toml"
}
debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
logger.Info("Loading config file: ", configFile)
if !debug {
defer func() {
if err := recover(); err != nil {
logger.Error("Panic Error:", err)
defer func() {
if err := recover(); err != nil {
logger.Error("Panic Error:", err)
// 打印堆栈信息
if os.Getenv("GEEKAI_DEBUG") == "true" {
debug.PrintStack()
}
}()
}
}
}()
app := fx.New(
// 初始化配置应用配置
@@ -89,16 +92,16 @@ func main() {
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)
s.Init(client)
}),
fx.Provide(func(db *gorm.DB) *types.SystemConfig {
return core.LoadSystemConfig(db)
}),
// 初始化数据库
@@ -126,9 +129,9 @@ func main() {
}),
// 创建控制器
fx.Provide(handler.NewChatRoleHandler),
fx.Provide(handler.NewChatAppHandler),
fx.Provide(handler.NewUserHandler),
fx.Provide(chatimpl.NewChatHandler),
fx.Provide(handler.NewChatHandler),
fx.Provide(handler.NewNetHandler),
fx.Provide(handler.NewSmsHandler),
fx.Provide(handler.NewRedeemHandler),
@@ -141,40 +144,35 @@ func main() {
fx.Provide(handler.NewProductHandler),
fx.Provide(handler.NewConfigHandler),
fx.Provide(handler.NewPowerLogHandler),
fx.Provide(handler.NewJimengHandler),
fx.Provide(service.NewMigrationService),
fx.Invoke(func(migrationService *service.MigrationService) {
migrationService.StartMigrate()
}),
// 管理后台控制器
fx.Provide(admin.NewConfigHandler),
fx.Provide(admin.NewAdminHandler),
fx.Provide(admin.NewApiKeyHandler),
fx.Provide(admin.NewUserHandler),
fx.Provide(admin.NewChatRoleHandler),
fx.Provide(admin.NewChatAppHandler),
fx.Provide(admin.NewRedeemHandler),
fx.Provide(admin.NewDashboardHandler),
fx.Provide(admin.NewChatModelHandler),
fx.Provide(admin.NewProductHandler),
fx.Provide(admin.NewOrderHandler),
fx.Provide(admin.NewChatHandler),
fx.Provide(admin.NewPowerLogHandler),
// 创建服务
fx.Provide(sms.NewSendServiceManager),
fx.Provide(func(config *types.AppConfig) *service.CaptchaService {
return service.NewCaptchaService(config.ApiConfig)
}),
fx.Provide(oss.NewUploaderManager),
fx.Provide(dalle.NewService),
fx.Invoke(func(s *dalle.Service) {
s.Run()
s.CheckTaskNotify()
s.DownloadImages()
s.CheckTaskStatus()
}),
fx.Provide(admin.NewAdminJimengHandler),
// 邮件服务
fx.Provide(service.NewSmtpService),
// License 服务
fx.Provide(service.NewLicenseService),
fx.Invoke(func(licenseService *service.LicenseService) {
licenseService.SyncLicense()
// Dalle 服务
fx.Provide(dalle.NewService),
fx.Invoke(func(s *dalle.Service) {
s.Run()
s.DownloadImages()
s.CheckTaskStatus()
}),
// MidJourney service pool
@@ -183,335 +181,233 @@ func main() {
fx.Invoke(func(s *mj.Service) {
s.Run()
s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadImages()
}),
// Sora service
fx.Provide(sora.NewSoraService),
// Stable Diffusion 机器人
fx.Provide(sd.NewService),
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
s.Run()
s.CheckTaskStatus()
s.CheckTaskNotify()
}),
fx.Provide(suno.NewService),
fx.Invoke(func(s *suno.Service) {
s.Run()
s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadFiles()
}),
fx.Provide(video.NewService),
fx.Invoke(func(s *video.Service) {
s.Run()
s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadFiles()
}),
fx.Provide(service.NewUserService),
fx.Provide(payment.NewAlipayService),
fx.Provide(payment.NewHuPiPay),
fx.Provide(payment.NewJPayService),
fx.Provide(payment.NewWechatService),
// 即梦AI 服务
fx.Provide(jimeng.NewClient),
fx.Provide(jimeng.NewService),
fx.Invoke(func(service *jimeng.Service) {
service.Start()
}),
fx.Provide(service.NewSnowflake),
fx.Provide(service.NewXXLJobExecutor),
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) {
if config.XXLConfig.Enabled {
go func() {
log.Fatal(exec.Run())
}()
}
// 创建短信服务
fx.Provide(sms.NewAliYunSmsService),
fx.Provide(sms.NewBaoSmsService),
fx.Provide(sms.NewSmsManager),
fx.Provide(service.NewCaptchaService),
fx.Provide(service.NewWxLoginService),
// 支付服务
fx.Provide(payment.NewAlipayService),
fx.Provide(payment.NewEPayService),
fx.Provide(payment.NewWxpayService),
// 文件上传服务
fx.Provide(oss.NewLocalStorage),
fx.Provide(oss.NewMiniOss),
fx.Provide(oss.NewQiNiuOss),
fx.Provide(oss.NewAliYunOss),
fx.Provide(oss.NewUploaderManager),
// 用户服务
fx.Provide(service.NewUserService),
// 文本审查服务
fx.Provide(moderation.NewGiteeAIModeration),
fx.Provide(moderation.NewBaiduAIModeration),
fx.Provide(moderation.NewTencentAIModeration),
fx.Provide(moderation.NewServiceManager),
fx.Provide(admin.NewModerationHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ModerationHandler) {
h.RegisterRoutes()
}),
// 注册路由
fx.Invoke(func(s *core.AppServer, h *handler.ChatRoleHandler) {
group := s.Engine.Group("/api/role/")
group.GET("list", h.List)
group.POST("update", h.UpdateRole)
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
h.RegisterRoutes()
}),
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.UpdatePass)
group.POST("bind/mobile", h.BindMobile)
group.POST("bind/email", h.BindEmail)
group.POST("resetPass", h.ResetPass)
group.GET("clogin", h.CLogin)
group.GET("clogin/callback", h.CLoginCallback)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *chatimpl.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.ChatHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
s.Engine.POST("/api/upload", h.Upload)
s.Engine.POST("/api/upload/list", h.List)
s.Engine.GET("/api/upload/remove", h.Remove)
s.Engine.GET("/api/download", h.Download)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
group := s.Engine.Group("/api/sms/")
group.POST("code", h.SendCode)
h.RegisterRoutes()
}),
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)
group.GET("slide/get", h.SlideGet)
group.POST("slide/check", h.SlideCheck)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
group := s.Engine.Group("/api/redeem/")
group.POST("verify", h.Verify)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
group := s.Engine.Group("/api/mj/")
group.Any("client", h.Client)
group.POST("image", h.Image)
group.POST("upscale", h.Upscale)
group.POST("variation", h.Variation)
group.GET("jobs", h.JobList)
group.GET("imgWall", h.ImgWall)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
group := s.Engine.Group("/api/sd")
group.Any("client", h.Client)
group.POST("image", h.Image)
group.GET("jobs", h.JobList)
group.GET("imgWall", h.ImgWall)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
group := s.Engine.Group("/api/config/")
group.GET("get", h.Get)
group.GET("license", h.License)
h.RegisterRoutes()
}),
// 管理后台控制器
// 管理后台路由注册
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
group := s.Engine.Group("/api/admin/")
group.POST("config/update", h.Update)
group.GET("config/get", h.Get)
group.POST("active", h.Active)
group.GET("config/get/license", h.GetLicense)
h.RegisterRoutes()
}),
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("list", h.List)
group.POST("save", h.Save)
group.POST("enable", h.Enable)
group.GET("remove", h.Remove)
group.POST("resetPass", h.ResetPass)
h.RegisterRoutes()
}),
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.POST("set", h.Set)
group.GET("remove", h.Remove)
h.RegisterRoutes()
}),
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)
h.RegisterRoutes()
}),
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.POST("set", h.Set)
group.GET("remove", h.Remove)
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
group := s.Engine.Group("/api/admin/redeem/")
group.GET("list", h.List)
group.POST("create", h.Create)
group.POST("set", h.Set)
group.POST("remove", h.Remove)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/")
group.GET("stats", h.Stats)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
group := s.Engine.Group("/api/model/")
group.GET("list", h.List)
h.RegisterRoutes()
}),
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("set", h.Set)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
group := s.Engine.Group("/api/payment/")
group.GET("doPay", h.DoPay)
group.GET("payWays", h.GetPayWays)
group.POST("qrcode", h.PayQrcode)
group.POST("mobile", h.Mobile)
group.POST("alipay/notify", h.AlipayNotify)
group.POST("hupipay/notify", h.HuPiPayNotify)
group.POST("payjs/notify", h.PayJsNotify)
group.POST("wechat/notify", h.WechatPayNotify)
h.RegisterRoutes()
h.StartSyncOrders()
}),
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
group := s.Engine.Group("/api/admin/product/")
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
group := s.Engine.Group("/api/admin/order/")
group.POST("list", h.List)
group.GET("remove", h.Remove)
group.GET("clear", h.Clear)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
group := s.Engine.Group("/api/order/")
group.GET("list", h.List)
group.GET("query", h.Query)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
group := s.Engine.Group("/api/product/")
group.GET("list", h.List)
h.RegisterRoutes()
}),
fx.Provide(handler.NewInviteHandler),
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
group := s.Engine.Group("/api/invite/")
group.GET("code", h.Code)
group.GET("list", h.List)
group.GET("hits", h.Hits)
h.RegisterRoutes()
}),
fx.Provide(admin.NewFunctionHandler),
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
group := s.Engine.Group("/api/admin/function/")
group.POST("save", h.Save)
group.POST("set", h.Set)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("token", h.GenToken)
h.RegisterRoutes()
}),
fx.Provide(admin.NewUploadHandler),
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
s.Engine.POST("/api/admin/upload", h.Upload)
h.RegisterRoutes()
}),
fx.Provide(handler.NewFunctionHandler),
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
group := s.Engine.Group("/api/function/")
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
group.GET("list", h.List)
h.RegisterRoutes()
}),
fx.Provide(admin.NewChatHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
group := s.Engine.Group("/api/admin/chat/")
group.POST("list", h.List)
group.POST("message", h.Messages)
group.GET("history", h.History)
group.GET("remove", h.RemoveChat)
group.GET("message/remove", h.RemoveMessage)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
group := s.Engine.Group("/api/powerLog/")
group.POST("list", h.List)
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
group := s.Engine.Group("/api/admin/powerLog/")
group.POST("list", h.List)
h.RegisterRoutes()
}),
fx.Provide(admin.NewMenuHandler),
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
group := s.Engine.Group("/api/admin/menu/")
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)
h.RegisterRoutes()
}),
fx.Provide(handler.NewMenuHandler),
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
group := s.Engine.Group("/api/menu/")
group.GET("list", h.List)
h.RegisterRoutes()
}),
fx.Provide(handler.NewMarkMapHandler),
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
group := s.Engine.Group("/api/markMap/")
group.Any("client", h.Client)
h.RegisterRoutes()
}),
fx.Provide(handler.NewDallJobHandler),
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
group := s.Engine.Group("/api/dall")
group.Any("client", h.Client)
group.POST("image", h.Image)
group.GET("jobs", h.JobList)
group.GET("imgWall", h.ImgWall)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
h.RegisterRoutes()
}),
fx.Provide(handler.NewSunoHandler),
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
group := s.Engine.Group("/api/suno")
group.Any("client", h.Client)
group.POST("create", h.Create)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
group.POST("update", h.Update)
group.GET("detail", h.Detail)
group.GET("play", h.Play)
group.POST("lyric", h.Lyric)
h.RegisterRoutes()
}),
fx.Provide(handler.NewVideoHandler),
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
group := s.Engine.Group("/api/video")
group.Any("client", h.Client)
group.POST("luma/create", h.LumaCreate)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.GET("publish", h.Publish)
h.RegisterRoutes()
}),
// 即梦AI 路由
fx.Invoke(func(s *core.AppServer, h *handler.JimengHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
h.RegisterRoutes()
}),
fx.Provide(admin.NewChatAppTypeHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
h.RegisterRoutes()
}),
fx.Provide(handler.NewChatAppTypeHandler),
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
h.RegisterRoutes()
}),
fx.Provide(handler.NewTestHandler),
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
group := s.Engine.Group("/api/test")
group.Any("sse", h.PostTest, h.SseTest)
h.RegisterRoutes()
}),
fx.Provide(handler.NewPromptHandler),
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() {
err := s.Run(db)
if err != nil {
log.Fatal(err)
logger.Error(err)
os.Exit(0)
}
}()
}),
@@ -527,6 +423,18 @@ func main() {
},
})
}),
fx.Provide(admin.NewImageHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
h.RegisterRoutes()
}),
fx.Provide(admin.NewMediaHandler),
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
h.RegisterRoutes()
}),
fx.Provide(handler.NewRealtimeHandler),
fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
h.RegisterRoutes()
}),
)
// 启动应用程序
go func() {
-38
View File
@@ -1,38 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDszCCApugAwIBAgIQICMRB0rBU2/rZJbfJGMYIzANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDYzNTQxWhcNMjQxMTA2MDYzNTQxWjCB
hDELMAkGA1UEBhMCQ04xHzAdBgNVBAoMFm1ib25meTkwMTVAc2FuZGJveC5jb20xDzANBgNVBAsM
BkFsaXBheTFDMEEGA1UEAww65pSv5LuY5a6dKOS4reWbvSnnvZHnu5zmioDmnK/mnInpmZDlhazl
j7gtMjA4ODcyMTAyMDc1MDU4MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsoKcw5
sxaiyV7mpWzDtnQ1K518eQLP0+dJlZAf06aBep/Aj9DIqrba/k7DHt8dKQvILMLAMpN1+2IRxbaO
yxMa/laj3lZ1eHrB6F077O3D62oHcE3noZtXL0N1zZAxpmkNmYIHeLZS2oLMS4ANu47O/wpDC7BV
HjdpZugtdPJ4mxdCpM9GDdLs7W4s5QI4PUPK4skFNMFoKI+0cYP/9ju87UP//IHC/K510GWNl+Gn
Cvgag3AmiIB0utJNsGhxm6zT1T9tUWjW9iz/BxBKiPatsCX9VpPQzGnW7ZonRQtiZSokIlP2IPvl
H5DcwpWUz3/LUY0SmKxnKOEYeOOqCW8CAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
DQEBCwUAA4IBAQAtgxF2EzjOndEFxBUD9tFwcSt6XKGggOp52oft1pvynPg4ALTLafOtfEPDrFBH
PwpYrSu9s9C8NJtaA2HrlCfBjIuwEFTXiN+HPvS0SwSPKt9AXEiTcOF8vDcGamEen8QI4fo5Jia7
2VRKkerkww5/+FzSaVO7ZUKuL80M1QJStmAZc8kPPwdYOTTW2bGf8BcmSDL6SPElBkt7tCCRd4sn
+jq4cZ0yb2i77rBZCwHcTvfTqIBblPwLv4uGvg3+83BxIB5w6Kqp06bKEAPmobFY5IVHa+ON0/qi
BXxXr+WQ3piKRVQEN64+PTAjSc67Ix1umvpLl3Ko6Ry7NJmpDcUn
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDszCCApugAwIBAgIQIBkIGbgVxq210KxLJ+YA/TANBgkqhkiG9w0BAQsFADCBhDELMAkGA1UE
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxJTAjBgNVBAsMHENlcnRpZmljYXRpb24gQXV0
aG9yaXR5IHRlc3QxNjA0BgNVBAMMLUFudCBGaW5hbmNpYWwgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
dHkgUjEgdGVzdDAeFw0xOTA4MTkxMTE2MDBaFw0yNDA4MDExMTE2MDBaMIGRMQswCQYDVQQGEwJD
TjEbMBkGA1UECgwSQW50IEZpbmFuY2lhbCB0ZXN0MSUwIwYDVQQLDBxDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eSB0ZXN0MT4wPAYDVQQDDDVBbnQgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y
aXR5IENsYXNzIDIgUjEgdGVzdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMh4FKYO
ZyRQHD6eFbPKZeSAnrfjfU7xmS9Yoozuu+iuqZlb6Z0SPLUqqTZAFZejOcmr07ln/pwZxluqplxC
5+B48End4nclDMlT5HPrDr3W0frs6Xsa2ZNcyil/iKNB5MbGll8LRAxntsKvZZj6vUTMb705gYgm
VUMILwi/ZxKTQqBtkT/kQQ5y6nOZsj7XI5rYdz6qqOROrpvS/d7iypdHOMIM9Iz9DlL1mrCykbBi
t25y+gTeXmuisHUwqaRpwtCGK4BayCqxRGbNipe6W73EK9lBrrzNtTr9NaysesT/v+l25JHCL9tG
wpNr1oWFzk4IHVOg0ORiQ6SUgxZUTYcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgTwMA0GCSqGSIb3
DQEBCwUAA4IBAQBWThEoIaQoBX2YeRY/I8gu6TYnFXtyuCljANnXnM38ft+ikhE5mMNgKmJYLHvT
yWWWgwHoSAWEuml7EGbE/2AK2h3k0MdfiWLzdmpPCRG/RJHk6UB1pMHPilI+c0MVu16OPpKbg5Vf
LTv7dsAB40AzKsvyYw88/Ezi1osTXo6QQwda7uefvudirtb8FcQM9R66cJxl3kt1FXbpYwheIm/p
j1mq64swCoIYu4NrsUYtn6CV542DTQMI5QdXkn+PzUUly8F6kDp+KpMNd0avfWNL5+O++z+F5Szy
1CPta1D7EQ/eYmMP+mOQ35oifWIoFCpN6qQVBS/Hob1J/UUyg7BW
-----END CERTIFICATE-----
-88
View File
@@ -1,88 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIBszCCAVegAwIBAgIIaeL+wBcKxnswDAYIKoEcz1UBg3UFADAuMQswCQYDVQQG
EwJDTjEOMAwGA1UECgwFTlJDQUMxDzANBgNVBAMMBlJPT1RDQTAeFw0xMjA3MTQw
MzExNTlaFw00MjA3MDcwMzExNTlaMC4xCzAJBgNVBAYTAkNOMQ4wDAYDVQQKDAVO
UkNBQzEPMA0GA1UEAwwGUk9PVENBMFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAE
MPCca6pmgcchsTf2UnBeL9rtp4nw+itk1Kzrmbnqo05lUwkwlWK+4OIrtFdAqnRT
V7Q9v1htkv42TsIutzd126NdMFswHwYDVR0jBBgwFoAUTDKxl9kzG8SmBcHG5Yti
W/CXdlgwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFEwysZfZ
MxvEpgXBxuWLYlvwl3ZYMAwGCCqBHM9VAYN1BQADSAAwRQIgG1bSLeOXp3oB8H7b
53W+CKOPl2PknmWEq/lMhtn25HkCIQDaHDgWxWFtnCrBjH16/W3Ezn7/U/Vjo5xI
pDoiVhsLwg==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIF0zCCA7ugAwIBAgIIH8+hjWpIDREwDQYJKoZIhvcNAQELBQAwejELMAkGA1UE
BhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNVBAsMF0NlcnRpZmlj
YXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5jaWFsIENlcnRpZmlj
YXRpb24gQXV0aG9yaXR5IFIxMB4XDTE4MDMyMTEzNDg0MFoXDTM4MDIyODEzNDg0
MFowejELMAkGA1UEBhMCQ04xFjAUBgNVBAoMDUFudCBGaW5hbmNpYWwxIDAeBgNV
BAsMF0NlcnRpZmljYXRpb24gQXV0aG9yaXR5MTEwLwYDVQQDDChBbnQgRmluYW5j
aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFIxMIICIjANBgkqhkiG9w0BAQEF
AAOCAg8AMIICCgKCAgEAtytTRcBNuur5h8xuxnlKJetT65cHGemGi8oD+beHFPTk
rUTlFt9Xn7fAVGo6QSsPb9uGLpUFGEdGmbsQ2q9cV4P89qkH04VzIPwT7AywJdt2
xAvMs+MgHFJzOYfL1QkdOOVO7NwKxH8IvlQgFabWomWk2Ei9WfUyxFjVO1LVh0Bp
dRBeWLMkdudx0tl3+21t1apnReFNQ5nfX29xeSxIhesaMHDZFViO/DXDNW2BcTs6
vSWKyJ4YIIIzStumD8K1xMsoaZBMDxg4itjWFaKRgNuPiIn4kjDY3kC66Sl/6yTl
YUz8AybbEsICZzssdZh7jcNb1VRfk79lgAprm/Ktl+mgrU1gaMGP1OE25JCbqli1
Pbw/BpPynyP9+XulE+2mxFwTYhKAwpDIDKuYsFUXuo8t261pCovI1CXFzAQM2w7H
DtA2nOXSW6q0jGDJ5+WauH+K8ZSvA6x4sFo4u0KNCx0ROTBpLif6GTngqo3sj+98
SZiMNLFMQoQkjkdN5Q5g9N6CFZPVZ6QpO0JcIc7S1le/g9z5iBKnifrKxy0TQjtG
PsDwc8ubPnRm/F82RReCoyNyx63indpgFfhN7+KxUIQ9cOwwTvemmor0A+ZQamRe
9LMuiEfEaWUDK+6O0Gl8lO571uI5onYdN1VIgOmwFbe+D8TcuzVjIZ/zvHrAGUcC
AwEAAaNdMFswCwYDVR0PBAQDAgEGMAwGA1UdEwQFMAMBAf8wHQYDVR0OBBYEFF90
tATATwda6uWx2yKjh0GynOEBMB8GA1UdIwQYMBaAFF90tATATwda6uWx2yKjh0Gy
nOEBMA0GCSqGSIb3DQEBCwUAA4ICAQCVYaOtqOLIpsrEikE5lb+UARNSFJg6tpkf
tJ2U8QF/DejemEHx5IClQu6ajxjtu0Aie4/3UnIXop8nH/Q57l+Wyt9T7N2WPiNq
JSlYKYbJpPF8LXbuKYG3BTFTdOVFIeRe2NUyYh/xs6bXGr4WKTXb3qBmzR02FSy3
IODQw5Q6zpXj8prYqFHYsOvGCEc1CwJaSaYwRhTkFedJUxiyhyB5GQwoFfExCVHW
05ZFCAVYFldCJvUzfzrWubN6wX0DD2dwultgmldOn/W/n8at52mpPNvIdbZb2F41
T0YZeoWnCJrYXjq/32oc1cmifIHqySnyMnavi75DxPCdZsCOpSAT4j4lAQRGsfgI
kkLPGQieMfNNkMCKh7qjwdXAVtdqhf0RVtFILH3OyEodlk1HYXqX5iE5wlaKzDop
PKwf2Q3BErq1xChYGGVS+dEvyXc/2nIBlt7uLWKp4XFjqekKbaGaLJdjYP5b2s7N
1dM0MXQ/f8XoXKBkJNzEiM3hfsU6DOREgMc1DIsFKxfuMwX3EkVQM1If8ghb6x5Y
jXayv+NLbidOSzk4vl5QwngO/JYFMkoc6i9LNwEaEtR9PhnrdubxmrtM+RjfBm02
77q3dSWFESFQ4QxYWew4pHE0DpWbWy/iMIKQ6UZ5RLvB8GEcgt8ON7BBJeMc+Dyi
kT9qhqn+lw==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIICiDCCAgygAwIBAgIIQX76UsB/30owDAYIKoZIzj0EAwMFADB6MQswCQYDVQQG
EwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UECwwXQ2VydGlmaWNh
dGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNpYWwgQ2VydGlmaWNh
dGlvbiBBdXRob3JpdHkgRTEwHhcNMTkwNDI4MTYyMDQ0WhcNNDkwNDIwMTYyMDQ0
WjB6MQswCQYDVQQGEwJDTjEWMBQGA1UECgwNQW50IEZpbmFuY2lhbDEgMB4GA1UE
CwwXQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxMTAvBgNVBAMMKEFudCBGaW5hbmNp
YWwgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgRTEwdjAQBgcqhkjOPQIBBgUrgQQA
IgNiAASCCRa94QI0vR5Up9Yr9HEupz6hSoyjySYqo7v837KnmjveUIUNiuC9pWAU
WP3jwLX3HkzeiNdeg22a0IZPoSUCpasufiLAnfXh6NInLiWBrjLJXDSGaY7vaokt
rpZvAdmjXTBbMAsGA1UdDwQEAwIBBjAMBgNVHRMEBTADAQH/MB0GA1UdDgQWBBRZ
4ZTgDpksHL2qcpkFkxD2zVd16TAfBgNVHSMEGDAWgBRZ4ZTgDpksHL2qcpkFkxD2
zVd16TAMBggqhkjOPQQDAwUAA2gAMGUCMQD4IoqT2hTUn0jt7oXLdMJ8q4vLp6sg
wHfPiOr9gxreb+e6Oidwd2LDnC4OUqCWiF8CMAzwKs4SnDJYcMLf2vpkbuVE4dTH
Rglz+HGcTLWsFs4KxLsq7MuU+vJTBUeDJeDjdA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIUEMdk6dVgOEIS2cCP0Q43P90Ps5YwDQYJKoZIhvcNAQEF
BQAwajELMAkGA1UEBhMCQ04xEzARBgNVBAoMCmlUcnVzQ2hpbmExHDAaBgNVBAsM
E0NoaW5hIFRydXN0IE5ldHdvcmsxKDAmBgNVBAMMH2lUcnVzQ2hpbmEgQ2xhc3Mg
MiBSb290IENBIC0gRzMwHhcNMTMwNDE4MDkzNjU2WhcNMzMwNDE4MDkzNjU2WjBq
MQswCQYDVQQGEwJDTjETMBEGA1UECgwKaVRydXNDaGluYTEcMBoGA1UECwwTQ2hp
bmEgVHJ1c3QgTmV0d29yazEoMCYGA1UEAwwfaVRydXNDaGluYSBDbGFzcyAyIFJv
b3QgQ0EgLSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOPPShpV
nJbMqqCw6Bz1kehnoPst9pkr0V9idOwU2oyS47/HjJXk9Rd5a9xfwkPO88trUpz5
4GmmwspDXjVFu9L0eFaRuH3KMha1Ak01citbF7cQLJlS7XI+tpkTGHEY5pt3EsQg
wykfZl/A1jrnSkspMS997r2Gim54cwz+mTMgDRhZsKK/lbOeBPpWtcFizjXYCqhw
WktvQfZBYi6o4sHCshnOswi4yV1p+LuFcQ2ciYdWvULh1eZhLxHbGXyznYHi0dGN
z+I9H8aXxqAQfHVhbdHNzi77hCxFjOy+hHrGsyzjrd2swVQ2iUWP8BfEQqGLqM1g
KgWKYfcTGdbPB1MCAwEAAaNjMGEwHQYDVR0OBBYEFG/oAMxTVe7y0+408CTAK8hA
uTyRMB8GA1UdIwQYMBaAFG/oAMxTVe7y0+408CTAK8hAuTyRMA8GA1UdEwEB/wQF
MAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IBAQBLnUTfW7hp
emMbuUGCk7RBswzOT83bDM6824EkUnf+X0iKS95SUNGeeSWK2o/3ALJo5hi7GZr3
U8eLaWAcYizfO99UXMRBPw5PRR+gXGEronGUugLpxsjuynoLQu8GQAeysSXKbN1I
UugDo9u8igJORYA+5ms0s5sCUySqbQ2R5z/GoceyI9LdxIVa1RjVX8pYOj8JFwtn
DJN3ftSFvNMYwRuILKuqUYSHc2GPYiHVflDh5nDymCMOQFcFG3WsEuB+EYQPFgIU
1DHmdZcz7Llx8UOZXX2JupWCYzK1XhJb+r4hK5ncf/w8qGtYlmyJpxk3hr1TfUJX
Yf4Zr0fJsGuv
-----END CERTIFICATE-----
-19
View File
@@ -1,19 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDmTCCAoGgAwIBAgIQICMRB2LW76yahgdg3IFNPDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UE
BhMCQ04xGzAZBgNVBAoMEkFudCBGaW5hbmNpYWwgdGVzdDElMCMGA1UECwwcQ2VydGlmaWNhdGlv
biBBdXRob3JpdHkgdGVzdDE+MDwGA1UEAww1QW50IEZpbmFuY2lhbCBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eSBDbGFzcyAyIFIxIHRlc3QwHhcNMjMxMTA3MDU0NjE5WhcNMjQxMTExMDU0NjE5WjBr
MQswCQYDVQQGEwJDTjEfMB0GA1UECgwWbWJvbmZ5OTAxNUBzYW5kYm94LmNvbTEPMA0GA1UECwwG
QWxpcGF5MSowKAYDVQQDDCEyMDg4NzIxMDIwNzUwNTgxLTkwMjEwMDAxMzE2NTgwMjMwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxihQPf1Q+g9ArgM46shVqL5sbRha/df95D1PsWyEq
ANmWmG4zZ+ksYDVQrc4KzhSRoi56sm/7TDFYTmM6bW99e/nKW58WxyZB4ie5qA3F4n17psPyDqb8
IokcQmCphSFDaXQD6AoXoLNtTM0vAI2cWxAgebZ/vsrdj5Ntjt+Rp3NYMCk1i5xovHcfILzLEGbX
QXoT9fo5AhHotTWa6xHVLPUGY9qwLzQxHzBmvy5ZMfnOfJkm/mDisTSqAUB59F3dzU/1ARVkEZ1w
Mgb4XohWBw6iurQfbMnH2mIomAAwwZVFv+sXDbL9yMbSMo/SjVsTQprn0Q0EnwLo7nmmOM6HAgMB
AAGjEjAQMA4GA1UdDwEB/wQEAwIE8DANBgkqhkiG9w0BAQsFAAOCAQEAn3Y4/C1h9R6ONsBqX3/q
XfHX7yX1FM0Y1x48X3/Yxk6HivAkTukhhhVYVKJsbrbzRqHDp9vhAP/FR6o6pAevaYMmLov0VMXU
7oAuetgkaYEYkDuNen5/Hpdhqi2vTtdT+q9w8zHJd6MDQ0aoHgIxpLKw5vof2R1N4fwSgNXMiXE5
kmllKQMem/+on2p+Sj80/2asxryHIGlH87qPzkffv+kIOkZthbTApTFLLjdVri2QHGe8/cc4xy01
/9iR3IUzNahotT41lJ4bMevBY7XMAS3n5ekyABN/9ZRJqhWdXgmFCRN/u56qd6lDgu7R2M2QUoyc
LuW5DfgRItKlmUB7sw==
-----END CERTIFICATE-----
-1
View File
@@ -1 +0,0 @@
MIIEpQIBAAKCAQEAsYoUD39UPoPQK4DOOrIVai+bG0YWv3X/eQ9T7FshKgDZlphuM2fpLGA1UK3OCs4UkaIuerJv+0wxWE5jOm1vfXv5ylufFscmQeInuagNxeJ9e6bD8g6m/CKJHEJgqYUhQ2l0A+gKF6CzbUzNLwCNnFsQIHm2f77K3Y+TbY7fkadzWDApNYucaLx3HyC8yxBm10F6E/X6OQIR6LU1musR1Sz1BmPasC80MR8wZr8uWTH5znyZJv5g4rE0qgFAefRd3c1P9QEVZBGdcDIG+F6IVgcOorq0H2zJx9piKJgAMMGVRb/rFw2y/cjG0jKP0o1bE0Ka59ENBJ8C6O55pjjOhwIDAQABAoIBAFetNfz1R7hbxjlFshMAkVzQR8wvT9qbvl+dtzdZRcaFhu89NecDIP7+QDYor0FcxoGpU0TazDyRQyk2BQD8vHt+9zv9BVLtZLJSqoWgPbUFBi1DjS8EF2ka8RVYnn35NhUhhd7L//ftL88Bh673mfembQ9srDjoEy1Z01feoABAnCMkNFl986DmEwnarvEufXSDIgeN4ioMxha4NvfIPuI0zpVdV1O9sv+SGC+VEWZBtN3GNsaf4zS/f8FVGvTiU/Abz0gSw/iwSPHclDWQDTN3yFHf/tfqlzh0mH0WfhnuOBFWXzK+R7fbnM+asI9ttvzRcfpzgRGXdPcNcOv/6cECgYEA3DVqpi1k8MYfJixju6SG5gfyhM4VFksFmCMaNPgtatDMBKLMTgV/Ej6LXREojcy29uZl83F09pVlpd41eG39ULIPktixA/BqErQ2UaWh6kOxifycpu22Jh0r09hax6UgVrcBrrnCJEjcFsuJlrZvXQSzc3PBxjWy5gjabS5h9iECgYEAzmVAIh2frF01Y95zsLueAhhZwCtPanm6kf7ivR4r1plIX3b2sNRhWGmEHFgaCE6Braa0ogQ73Hd26kw4ZW+D6QMGC/zjCBEzDLLf++SjdVUHiY5AR4WHqXzq1jdAlsVyo9R661oAOp3lhiJVGLNXkHyEfEVPHsaxJh4osYSbX6cCgYEAx32Qx0i6eDFTyLZQB46uMrgiaVN04QRH5iJuvGvUYT8UhGKjaU8rZfDJOh+wOH2rhxMEaz1uc3C2bERY9mfWI4Ob/jFWc7YZsiYWS3Mcsuhubw4tMECLUg39RWZsHw8ls8kIuixIh6yFzhTH6YQOcRswIrhMZG8DScfdcSmiz2ECgYEAkWP1t5KSpkLKl11etcKUXfl1T8+yk9jIOowIgRw92WAFAWq2AH67TCKYM7dEL1HOO9tRJ0hAOt/U3ttuZtYVYBEHM26jJ02mXm2rJrA7DS4mrxmL4lYH6LbcXqZxU0Qnq4zEQgIWYzRTORf6Rfof1uJAGaJhR9bDd4yLMfGt2cUCgYEAo216Y61xOHUTA4AF1eekk+r+uOcQgQDvLXfs9FkDdJLk0mPG48/+eIYpPFnANJ/riF/DWOp8WGEe2IzA9yUFexzDbNQK8ha9kGcxaSAyiCwzjZ/t9/+hScDSV8kNqWSRSisu/YOFleEHbokT6mbLZ+gdqES8mUUanaEBzRQYGxo=
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

+24 -28
View File
@@ -8,35 +8,38 @@ package service
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"fmt"
"geekai/core/types"
"github.com/imroc/req/v3"
"time"
"github.com/imroc/req/v3"
)
type CaptchaService struct {
config types.ApiConfig
config types.CaptchaConfig
client *req.Client
}
func NewCaptchaService(config types.ApiConfig) *CaptchaService {
func NewCaptchaService(sysConfig *types.SystemConfig) *CaptchaService {
return &CaptchaService{
config: config,
config: sysConfig.Captcha,
client: req.C().SetTimeout(10 * time.Second),
}
}
func (s *CaptchaService) Get() (interface{}, error) {
if s.config.Token == "" {
return nil, errors.New("无效的 API Token")
}
func (s *CaptchaService) UpdateConfig(config types.CaptchaConfig) {
s.config = config
}
url := fmt.Sprintf("%s/api/captcha/get", s.config.ApiURL)
func (s *CaptchaService) GetConfig() types.CaptchaConfig {
return s.config
}
func (s *CaptchaService) Get() (any, error) {
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("AppId", s.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return nil, fmt.Errorf("请求 API 失败:%v", err)
@@ -49,12 +52,11 @@ func (s *CaptchaService) Get() (interface{}, error) {
return res.Data, nil
}
func (s *CaptchaService) Check(data interface{}) bool {
url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL)
func (s *CaptchaService) Check(data any) bool {
url := fmt.Sprintf("%s/api/captcha/check", types.GeekAPIURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("AppId", s.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetBodyJsonMarshal(data).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
@@ -68,16 +70,11 @@ func (s *CaptchaService) Check(data interface{}) bool {
return true
}
func (s *CaptchaService) SlideGet() (interface{}, error) {
if s.config.Token == "" {
return nil, errors.New("无效的 API Token")
}
url := fmt.Sprintf("%s/api/captcha/slide/get", s.config.ApiURL)
func (s *CaptchaService) SlideGet() (any, error) {
url := fmt.Sprintf("%s/api/captcha/slide/get", types.GeekAPIURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("AppId", s.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() {
return nil, fmt.Errorf("请求 API 失败:%v", err)
@@ -90,12 +87,11 @@ func (s *CaptchaService) SlideGet() (interface{}, error) {
return res.Data, nil
}
func (s *CaptchaService) SlideCheck(data interface{}) bool {
url := fmt.Sprintf("%s/api/captcha/slide/check", s.config.ApiURL)
func (s *CaptchaService) SlideCheck(data any) bool {
url := fmt.Sprintf("%s/api/captcha/slide/check", types.GeekAPIURL)
var res types.BizVo
r, err := s.client.R().
SetHeader("AppId", s.config.AppId).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetBodyJsonMarshal(data).
SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() {
@@ -8,7 +8,6 @@ package dalle
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"errors"
"fmt"
"geekai/core/types"
logger2 "geekai/logger"
@@ -17,9 +16,10 @@ import (
"geekai/store"
"geekai/store/model"
"geekai/utils"
"github.com/go-redis/redis/v8"
"time"
"github.com/go-redis/redis/v8"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
@@ -33,8 +33,6 @@ type Service struct {
db *gorm.DB
uploadManager *oss.UploaderManager
taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
userService *service.UserService
}
@@ -43,8 +41,6 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
httpClient: req.C().SetTimeout(time.Minute * 3),
db: db,
taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli),
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
Clients: types.NewLMap[uint, *types.WsClient](),
uploadManager: manager,
userService: userService,
}
@@ -53,10 +49,26 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
// PushTask push a new mj task in to task queue
func (s *Service) PushTask(task types.DallTask) {
logger.Infof("add a new DALL-E task to the task list: %+v", task)
s.taskQueue.RPush(task)
if err := s.taskQueue.RPush(task); err != nil {
logger.Errorf("push dall-e task to queue failed: %v", err)
}
}
func (s *Service) Run() {
// 将数据库中未提交的任务加载到队列
var jobs []model.DallJob
s.db.Where("progress", 0).Find(&jobs)
for _, v := range jobs {
var task types.DallTask
err := utils.JsonDecode(v.TaskInfo, &task)
if err != nil {
logger.Errorf("decode task info with error: %v", err)
continue
}
task.Id = v.Id
s.PushTask(task)
}
logger.Info("Starting DALL-E job consumer...")
go func() {
for {
@@ -67,80 +79,68 @@ func (s *Service) Run() {
continue
}
logger.Infof("handle a new DALL-E task: %+v", task)
_, err = s.Image(task, false)
if err != nil {
logger.Errorf("error with image task: %v", err)
s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
"progress": service.FailTaskProgress,
"err_msg": err.Error(),
})
s.notifyQueue.RPush(service.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})
}
go func() {
_, err = s.Image(task, false)
if err != nil {
logger.Errorf("error with image task: %v", err)
s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"progress": service.FailTaskProgress,
"err_msg": err.Error(),
})
}
}()
}
}()
}
type imgReq struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
N int `json:"n"`
Size string `json:"size"`
Quality string `json:"quality"`
Style string `json:"style"`
Model string `json:"model"`
Image []string `json:"image,omitempty"`
Prompt string `json:"prompt"`
AspectRatio string `json:"aspect_ratio,omitempty"`
Size string `json:"size,omitempty"`
ResponseFormat string `json:"response_format,omitempty"`
}
type imgRes struct {
Created int64 `json:"created"`
Data []struct {
RevisedPrompt string `json:"revised_prompt"`
Url string `json:"url"`
RevisedPrompt string `json:"revised_prompt,omitempty"`
Url string `json:"url,omitempty"`
B64Json string `json:"b64_json,omitempty"`
} `json:"data"`
}
type ErrRes struct {
Error struct {
Code interface{} `json:"code"`
Message string `json:"message"`
Param interface{} `json:"param"`
Type string `json:"type"`
Code any `json:"code"`
Message string `json:"message"`
Param any `json:"param"`
Type string `json:"type"`
} `json:"error"`
}
func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
logger.Debugf("绘画参数:%+v", task)
prompt := task.Prompt
// translate prompt
if utils.HasChinese(prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, prompt), "gpt-4o-mini")
if err == nil {
prompt = content
logger.Debugf("重写后提示词:%s", prompt)
}
}
var user model.User
s.db.Where("id", task.UserId).First(&user)
if user.Power < task.Power {
return "", errors.New("insufficient of power")
}
// 扣减算力
err := s.userService.DecreasePower(int(user.Id), task.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "dall-e-3",
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
})
if err != nil {
return "", fmt.Errorf("error with decrease power: %v", err)
var chatModel model.ChatModel
if task.ModelId > 0 {
s.db.Where("id", task.ModelId).First(&chatModel)
} else {
s.db.Where("value", task.ModelValue).First(&chatModel)
}
// get image generation API KEY
var apiKey model.ApiKey
err = s.db.Where("type", "dalle").
Where("enabled", true).
Order("last_used_at ASC").First(&apiKey).Error
session := s.db.Where("enabled", true)
if chatModel.KeyId > 0 {
session = session.Where("id = ?", chatModel.KeyId)
} else {
session = session.Where("type = ?", "dalle")
}
err := session.Order("last_used_at ASC").First(&apiKey).Error
if err != nil {
return "", fmt.Errorf("no available DALL-E api key: %v", err)
return "", fmt.Errorf("no available Image Generation api key: %v", err)
}
var res imgRes
@@ -150,92 +150,103 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
}
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
reqBody := imgReq{
Model: "dall-e-3",
Prompt: prompt,
N: 1,
Size: task.Size,
Style: task.Style,
Quality: task.Quality,
Model: chatModel.Value,
Prompt: task.Prompt,
AspectRatio: task.AspectRatio,
Size: task.Size,
ResponseFormat: "url",
}
// 图片编辑
if len(task.Image) > 0 {
reqBody.Image = task.Image
}
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
r, err := s.httpClient.R().SetHeader("Content-Type", "application/json").
r, err := s.httpClient.R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(reqBody).
SetErrorResult(&errRes).
SetSuccessResult(&res).
Post(apiURL)
if err != nil {
logger.Errorf("error with send request: %v", err)
return "", fmt.Errorf("error with send request: %v", err)
}
if r.IsErrorState() {
logger.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
return "", fmt.Errorf("error with send request, status: %s, %+v", r.Status, errRes.Error)
}
// update the api key last use time
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
// update task progress
err = s.db.Model(&model.DallJob{Id: task.JobId}).UpdateColumns(map[string]interface{}{
var imgURL string
var data = map[string]any{
"progress": 100,
"org_url": res.Data[0].Url,
"prompt": prompt,
}).Error
"prompt": task.Prompt,
}
// 如果返回的是base64,则需要上传到oss
if res.Data[0].B64Json != "" {
imgURL, err = s.uploadManager.GetUploadHandler().PutBase64(res.Data[0].B64Json)
if err != nil {
return "", fmt.Errorf("error with upload image: %v", err)
}
logger.Infof("upload image to oss: %s", imgURL)
data["img_url"] = imgURL
} else {
imgURL = res.Data[0].Url
}
data["org_url"] = imgURL
// update task progress
err = s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(data).Error
if err != nil {
return "", fmt.Errorf("err with update database: %v", err)
}
s.notifyQueue.RPush(service.NotifyMessage{UserId: int(task.UserId), JobId: int(task.JobId), Message: service.TaskStatusFailed})
var content string
if sync {
imgURL, err := s.downloadImage(task.JobId, int(task.UserId), res.Data[0].Url)
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n![](%s)\n", prompt, imgURL)
content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n![](%s)\n", task.Prompt, imgURL)
}
return content, nil
}
func (s *Service) CheckTaskNotify() {
go func() {
logger.Info("Running DALL-E task notify checking ...")
for {
var message service.NotifyMessage
err := s.notifyQueue.LPop(&message)
if err != nil {
continue
}
client := s.Clients.Get(uint(message.UserId))
if client == nil {
continue
}
err = client.Send([]byte(message.Message))
if err != nil {
continue
}
}
}()
}
func (s *Service) CheckTaskStatus() {
go func() {
logger.Info("Running DALL-E task status checking ...")
for {
// 检查未完成任务进度
var jobs []model.DallJob
res := s.db.Where("progress < ?", 100).Find(&jobs)
if res.Error != nil {
time.Sleep(5 * time.Second)
continue
}
s.db.Where("progress < ?", 100).Find(&jobs)
for _, job := range jobs {
// 超时的任务标记为失败
if time.Now().Sub(job.CreatedAt) > time.Minute*10 {
if time.Since(job.CreatedAt) > time.Minute*10 {
job.Progress = service.FailTaskProgress
job.ErrMsg = "任务超时"
s.db.Updates(&job)
}
}
// 找出失败的任务,并恢复其扣减算力
s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
for _, job := range jobs {
var task types.DallTask
err := utils.JsonDecode(job.TaskInfo, &task)
if err != nil {
continue
}
err = s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: task.ModelName,
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(time.Second * 10)
}
}()
@@ -257,7 +268,7 @@ func (s *Service) DownloadImages() {
}
logger.Infof("try to download image: %s", v.OrgURL)
imgURL, err := s.downloadImage(v.Id, int(v.UserId), v.OrgURL)
imgURL, err := s.downloadImage(v.Id, v.OrgURL)
if err != nil {
logger.Error("error with download image: %s, error: %v", imgURL, err)
continue
@@ -272,9 +283,9 @@ func (s *Service) DownloadImages() {
}()
}
func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string, error) {
func (s *Service) downloadImage(jobId uint, orgURL string) (string, error) {
// sava image
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false)
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, ".png", false)
if err != nil {
return "", err
}
@@ -284,6 +295,5 @@ func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string,
if res.Error != nil {
return "", err
}
s.notifyQueue.RPush(service.NotifyMessage{UserId: userId, JobId: int(jobId), Message: service.TaskStatusFinished})
return imgURL, nil
}

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