Compare commits

...

303 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 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 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 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 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
RockYang a77bebbc29 update iconfonts 2025-02-25 06:39:07 +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 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 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 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
572 changed files with 65740 additions and 42011 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 报告 🐛 name: Bug 报告 🐛
description: geekai 提交错误报告 description: chatgpt-plus 提交错误报告
labels: ['Bug'] labels: ['Bug']
body: body:
- type: checkboxes - type: checkboxes
+1 -1
View File
@@ -1,5 +1,5 @@
name: 功能优化 🚀 name: 功能优化 🚀
description: geekai 提交优化建议 description: chatgpt-plus 提交优化建议
labels: ['feature'] labels: ['feature']
body: body:
- type: checkboxes - type: checkboxes
+1
View File
@@ -14,3 +14,4 @@ logs
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
miniprogram
+114
View File
@@ -1,5 +1,119 @@
# 更新日志 # 更新日志
## 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 ## v4.1.8
- 功能优化:**UI 全新改版,支持主题切换**。 :rocket: :rocket: :rocket: - 功能优化:**UI 全新改版,支持主题切换**。 :rocket: :rocket: :rocket:
+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
+66 -81
View File
@@ -1,92 +1,77 @@
# GeekAI # 🚀 GeekAI-PLUS:一站式 AI 创意生产力平台
> 根据[《生成式人工智能服务管理暂行办法》](https://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 **重新定义 AI 创作体验,让每个人都能成为内容创作大师**
**GeekAI** 基于 AI 大语言模型 API 实现的 AI 助手全套开源解决方案,自带运营管理后台,开箱即用。集成了 OpenAI, Claude, 通义千问,KimiDeepSeekGitee AI 等多个平台的大语言模型。集成了 MidJourney 和 Stable Diffusion AI 绘画功能 基于 GeekAI 项目开发的高级版,增加了很多高级功能,比如思维导图,Dalle 绘画等。**高级版源码不会一次性开放,只提供镜像给大家免费使用**,源码会逐步逐步按照版同步迁移到[社区版(GeekAI](https://github.com/yangjian102621/geekai)。所以如果大家想要二次开发,请移步去社区版
主要特性: ## ✨ 核心特色
- 完整的开源系统,前端应用和后台管理系统皆可开箱即用。 ### 🎨 **全能 AI 创作矩阵**
- 基于 Websocket 实现,完美的打字机体验。
- 内置了各种预训练好的角色应用,比如小红书写手,英语翻译大师,苏格拉底,孔子,乔布斯,周报助手等。轻松满足你的各种聊天和应用需求。
- 支持 OpenAI, Claude, 通义千问,KimiDeepSeek 等多个大语言模型,**支持 Gitee AI Serverless 大模型 API**。
- 支持 Suno 文生音乐
- 支持 MidJourney / Stable Diffusion AI 绘画集成,文生图,图生图,换脸,融图。开箱即用。
- 支持使用个人微信二维码作为充值收费的支付渠道,无需企业支付通道。
- 已集成支付宝支付功能,微信支付,支持多种会员套餐和点卡购买功能。
- 集成插件 API 功能,可结合大语言模型的 function 功能开发各种强大的插件,已内置实现了微博热搜,今日头条,今日早报和 AI
绘画函数插件。
### 🚀 更多功能请查看 [GeekAI-PLUS](https://github.com/yangjian102621/geekai-plus) - **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话
- **图像生成**:整合 MidJourney、DALL-E、Stable Diffusion 三大主流 AI 绘画引擎
- **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐
- **视频制作**Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限
- **思维导图**:AI 辅助思维整理,复杂想法可视化呈现
- [x] 更友好的 UI 界面 ### 🏗️ **企业级技术架构**
- [x] 支持 Dall-E 文生图功能
- [x] 支持文生思维导图 - **高性能后端**Go + Gin + MySQL + Redis,支持高并发访问
- [x] 支持为模型绑定指定的 API KEY,支持为角色绑定指定的模型等功能 - **现代化前端**Vue3 + Element Plus + Vant,桌面移动双端适配
- [x] 支持网站 Logo 版权等信息的修改 - **智能缓存**:多层缓存策略,响应速度提升 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/plus/info/)。 请参考 [GeekAI 项目介绍](https://docs.geekai.me/info/)。
### 体验地址 ---
> 免费体验地址:[https://chat.geekai.me](https://chat.geekai.me) <br/> > **注意:请合法使用,禁止输出任何敏感、不友好或违规的内容!!!** _让 AI 成为你最强大的创作伙伴,开启无限创意可能!_
## 快速部署
请参考文档 [**GeekAI 快速部署**](https://docs.geekai.me/plus/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!!!)。**
![微信名片](https://docs.geekai.me/images/wx_card.png)
## 参与贡献
个人的力量始终有限,任何形式的贡献都是欢迎的,包括但不限于贡献代码,优化文档,提交 issue 和 PR 等。
#### 特此声明:由于个人时间有限,不接受在微信或者微信群给开发者提 Bug,有问题或者优化建议请提交 Issue 和 PR。非常感谢您的配合!
### Commit 类型
- feat: 新特性或功能
- fix: 缺陷修复
- docs: 文档更新
- style: 代码风格或者组件样式更新
- refactor: 代码重构,不引入新功能和缺陷修复
- opt: 性能优化
- chore: 一些不涉及到功能变动的小提交,比如修改文字表述,修改注释等
## 打赏
如果你觉得这个项目对你有帮助,并且情况允许的话,可以请作者喝杯咖啡,非常感谢你的支持~
![打赏](https://blog.img.r9it.com/image-f02ca9eccbe93c7b1193c2623e7336ea.png)
![Star History Chart](https://api.star-history.com/svg?repos=yangjian102621/geekai&type=Date)
+2
View File
@@ -16,6 +16,8 @@ tmp
bin bin
data data
config.toml config.toml
config.toml.*.bak
static/upload static/upload
static/audio
storage.json storage.json
res/certs/wechat/apiclient_key.pem res/certs/wechat/apiclient_key.pem
+2 -2
View File
@@ -3,11 +3,11 @@ NAME := geekai
all: amd64 arm64 all: amd64 arm64
amd64: 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 .PHONY: amd64
arm64: 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 .PHONY: arm64
clean: clean:
+1 -1
View File
@@ -1,6 +1,6 @@
Listen = "0.0.0.0:5678" Listen = "0.0.0.0:5678"
ProxyURL = "" # 如 http://127.0.0.1:7777 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" # 静态资源的目录 StaticDir = "./static" # 静态资源的目录
StaticUrl = "/static" # 静态资源访问 URL StaticUrl = "/static" # 静态资源访问 URL
TikaHost = "http://tika:9998" TikaHost = "http://tika:9998"
+15 -316
View File
@@ -8,93 +8,77 @@ package core
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"bytes"
"context"
"fmt" "fmt"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp"
"image"
"image/jpeg"
"io" "io"
"net/http" "net/http"
"os"
"runtime/debug" "runtime/debug"
"strings"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"github.com/imroc/req/v3" "github.com/imroc/req/v3"
"github.com/nfnt/resize"
"github.com/shirou/gopsutil/host" "github.com/shirou/gopsutil/host"
"golang.org/x/image/webp"
"gorm.io/gorm" "gorm.io/gorm"
) )
type AppServer struct { type AppServer struct {
Debug bool
Config *types.AppConfig Config *types.AppConfig
Engine *gin.Engine Engine *gin.Engine
SysConfig *types.SystemConfig // system config cache 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.SetMode(gin.ReleaseMode)
gin.DefaultWriter = io.Discard gin.DefaultWriter = io.Discard
return &AppServer{ return &AppServer{
Debug: false, Config: appConfig,
Config: appConfig, Redis: redis,
Engine: gin.Default(), Engine: gin.Default(),
SysConfig: sysConfig,
} }
} }
func (s *AppServer) Init(debug bool, client *redis.Client) { func (s *AppServer) Init(client *redis.Client) {
// 允许跨域请求 API s.Engine.Use(middleware.ThumbMiddleware())
s.Engine.Use(corsMiddleware()) s.Engine.Use(middleware.ParameterHandlerMiddleware())
s.Engine.Use(staticResourceMiddleware())
s.Engine.Use(authorizeMiddleware(s, client))
s.Engine.Use(parameterHandlerMiddleware())
s.Engine.Use(errorHandler) s.Engine.Use(errorHandler)
// 添加静态资源访问 // 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir) s.Engine.Static("/static", s.Config.StaticDir)
//启动服务
} }
func (s *AppServer) Run(db *gorm.DB) error { func (s *AppServer) Run(db *gorm.DB) error {
// load system configs // load system configs
var sysConfig model.Config var sysConfig model.Config
err := db.Where("marker", "system").First(&sysConfig).Error err := db.Where("name", "system").First(&sysConfig).Error
if err != nil { if err != nil {
return fmt.Errorf("failed to load system config: %v", err) return fmt.Errorf("failed to load system config: %v", err)
} }
err = utils.JsonDecode(sysConfig.Config, &s.SysConfig) err = utils.JsonDecode(sysConfig.Value, &s.SysConfig)
if err != nil { if err != nil {
return fmt.Errorf("failed to decode system config: %v", err) return fmt.Errorf("failed to decode system config: %v", err)
} }
logger.Infof("http://%s", s.Config.Listen)
// 统计安装信息 // 统计安装信息
go func() { go func() {
info, err := host.Info() info, err := host.Info()
if err == nil { if err == nil {
apiURL := fmt.Sprintf("%s/%s", s.Config.ApiConfig.ApiURL, "api/installs/push") apiURL := fmt.Sprintf("%s/api/installs/push", types.GeekAPIURL)
timestamp := time.Now().Unix() timestamp := time.Now().Unix()
product := "geekai-plus" product := "geekai-plus"
signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp) signStr := fmt.Sprintf("%s#%s#%d", product, info.HostID, timestamp)
sign := utils.Sha256(signStr) 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) resp, err := req.C().R().SetBody(map[string]interface{}{"product": product, "device_id": info.HostID, "timestamp": timestamp, "sign": sign}).Post(apiURL)
if err != nil { if err == nil {
logger.Errorf("register install info failed: %v", err)
} else {
logger.Debugf("register install info success: %v", resp.String()) logger.Debugf("register install info success: %v", resp.String())
} }
} }
}() }()
logger.Infof("http://%s", s.Config.Listen)
return s.Engine.Run(s.Config.Listen) return s.Engine.Run(s.Config.Listen)
} }
@@ -111,288 +95,3 @@ func errorHandler(c *gin.Context) {
//加载完 defer recover,继续后续接口调用 //加载完 defer recover,继续后续接口调用
c.Next() 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, Body-Length, Body-Type, Admin-Authorization,content-type")
// 允许浏览器(客户端)可以解析的头部 (重要)
c.Header("Access-Control-Expose-Headers", "Body-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) {
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
var tokenString string
isAdminApi := strings.Contains(c.Request.URL.Path, "/api/admin/")
if isAdminApi { // 后台管理 API
tokenString = c.GetHeader(types.AdminAuthHeader)
} else if clientProtocols != "" { // Websocket 连接
// 解析子协议内容
protocols := strings.Split(clientProtocols, ",")
if protocols[0] == "realtime" {
tokenString = strings.TrimSpace(protocols[1][25:])
} else if protocols[0] == "token" {
tokenString = strings.TrimSpace(protocols[1])
}
} 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/app/list" ||
c.Request.URL.Path == "/api/app/type/list" ||
c.Request.URL.Path == "/api/app/list/user" ||
c.Request.URL.Path == "/api/model/list" ||
c.Request.URL.Path == "/api/mj/imgWall" ||
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/dall/imgWall" ||
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/doPay" ||
c.Request.URL.Path == "/api/payment/payWays" ||
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/dall/models" ||
strings.HasPrefix(c.Request.URL.Path, "/api/test") ||
strings.HasPrefix(c.Request.URL.Path, "/api/payment/notify/") ||
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" "bytes"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" logger2 "geekai/logger"
"geekai/store/model"
"geekai/utils" "geekai/utils"
"os" "os"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"gorm.io/gorm"
) )
var logger = logger2.GetLogger() var logger = logger2.GetLogger()
@@ -30,7 +32,6 @@ func NewDefaultConfig() *types.AppConfig {
SecretKey: utils.RandString(64), SecretKey: utils.RandString(64),
MaxAge: 86400, MaxAge: 86400,
}, },
ApiConfig: types.ApiConfig{},
OSS: types.OSSConfig{ OSS: types.OSSConfig{
Active: "local", Active: "local",
Local: types.LocalStorageConfig{ Local: types.LocalStorageConfig{
@@ -38,7 +39,6 @@ func NewDefaultConfig() *types.AppConfig {
BasePath: "./static/upload", 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) 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()
}
}
+22 -27
View File
@@ -9,20 +9,20 @@ package types
// ApiRequest API 请求实体 // ApiRequest API 请求实体
type ApiRequest struct { type ApiRequest struct {
Model string `json:"model,omitempty"` Model string `json:"model,omitempty"`
Temperature float32 `json:"temperature"` Temperature float32 `json:"temperature"`
MaxTokens int `json:"max_tokens,omitempty"` MaxTokens int `json:"max_tokens,omitempty"`
MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型 MaxCompletionTokens int `json:"max_completion_tokens,omitempty"` // 兼容GPT O1 模型
Stream bool `json:"stream,omitempty"` Stream bool `json:"stream,omitempty"`
Messages []interface{} `json:"messages,omitempty"` Messages []any `json:"messages,omitempty"`
Tools []Tool `json:"tools,omitempty"` Tools []Tool `json:"tools,omitempty"`
Functions []interface{} `json:"functions,omitempty"` // 兼容中转平台 Functions []any `json:"functions,omitempty"` // 兼容中转平台
ResponseFormat interface{} `json:"response_format,omitempty"` // 响应格式 ResponseFormat any `json:"response_format,omitempty"` // 响应格式
ToolChoice string `json:"tool_choice,omitempty"` ToolChoice string `json:"tool_choice,omitempty"`
Input map[string]interface{} `json:"input,omitempty"` //兼容阿里通义千问 Input map[string]any `json:"input,omitempty"` //兼容阿里通义千问
Parameters map[string]interface{} `json:"parameters,omitempty"` //兼容阿里通义千问 Parameters map[string]any `json:"parameters,omitempty"` //兼容阿里通义千问
} }
type Message struct { type Message struct {
@@ -41,27 +41,17 @@ type ChoiceItem struct {
} }
type Delta struct { type Delta struct {
Role string `json:"role"` Role string `json:"role"`
Name string `json:"name"` Name string `json:"name"`
Content interface{} `json:"content"` Content any `json:"content"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"` ReasoningContent string `json:"reasoning_content,omitempty"`
FunctionCall struct { ToolCalls []ToolCall `json:"tool_calls,omitempty"`
FunctionCall struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Arguments string `json:"arguments,omitempty"` Arguments string `json:"arguments,omitempty"`
} `json:"function_call,omitempty"` } `json:"function_call,omitempty"`
} }
// ChatSession 聊天会话对象
type ChatSession struct {
UserId uint `json:"user_id"`
ClientIP string `json:"client_ip"` // 客户端 IP
ChatId string `json:"chat_id"` // 客户端聊天会话 ID, 多会话模式专用字段
Model ChatModel `json:"model"` // GPT 模型
Start int64 `json:"start"` // 开始请求时间戳
Tools []int `json:"tools"` // 工具函数列表
Stream bool `json:"stream"` // 是否采用流式输出
}
type ChatModel struct { type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@@ -69,6 +59,8 @@ type ChatModel struct {
Power int `json:"power"` Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度 MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度 MaxContext int `json:"max_context"` // 最大上下文长度
Description string `json:"description"` //模型描述
Category string `json:"category"` //模型类别
Temperature float32 `json:"temperature"` // 模型温度 Temperature float32 `json:"temperature"` // 模型温度
KeyId int `json:"key_id"` // 绑定 API KEY KeyId int `json:"key_id"` // 绑定 API KEY
} }
@@ -95,6 +87,7 @@ const (
PowerInvite = PowerType(4) // 邀请奖励 PowerInvite = PowerType(4) // 邀请奖励
PowerRedeem = PowerType(5) // 众筹 PowerRedeem = PowerType(5) // 众筹
PowerGift = PowerType(6) // 系统赠送 PowerGift = PowerType(6) // 系统赠送
PowerSignIn = PowerType(7) // 每日签到
) )
func (t PowerType) String() string { func (t PowerType) String() string {
@@ -111,6 +104,8 @@ func (t PowerType) String() string {
return "赠送" return "赠送"
case PowerInvite: case PowerInvite:
return "邀请" return "邀请"
case PowerSignIn:
return "签到"
} }
return "其他" return "其他"
} }
+66 -122
View File
@@ -17,87 +17,17 @@ type AppConfig struct {
Session Session Session Session
AdminSession Session AdminSession Session
ProxyURL string ProxyURL string
MysqlDns string // mysql 连接地址 MysqlDns string // mysql 连接地址
StaticDir string // 静态资源目录 StaticDir string // 静态资源目录
StaticUrl string // 静态资源 URL StaticUrl string // 静态资源 URL
Redis RedisConfig // redis 连接信息 Redis RedisConfig // redis 连接信息
ApiConfig ApiConfig // ChatPlus API authorization configs SMS SMSConfig // send mobile message config
SMS SMSConfig // send mobile message config OSS OSSConfig // OSS config
OSS OSSConfig // OSS config SmtpConfig SmtpConfig // 邮件发送配置
SmtpConfig SmtpConfig // 邮件发送配置 AlipayConfig AlipayConfig // 支付宝支付渠道配置
XXLConfig XXLConfig GeekPayConfig EpayConfig // GEEK 支付配置
AlipayConfig AlipayConfig // 支付宝支付渠道配置 WechatPayConfig WxPayConfig // 微信支付渠道配置
HuPiPayConfig HuPiPayConfig // 虎皮椒支付配置 TikaHost string // TiKa 服务器地址
GeekPayConfig GeekPayConfig // GEEK 支付配置
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 // 异步通知地址
}
type HuPiPayConfig struct { //虎皮椒第四方支付配置
Enabled bool // 是否启用该支付通道
AppId string // App ID
AppSecret string // app 密钥
ApiURL string // 支付网关
NotifyURL string // 异步通知地址
ReturnURL string // 同步通知地址
}
// GeekPayConfig GEEK支付配置
type GeekPayConfig struct {
Enabled bool
AppId string // 商户 ID
PrivateKey string // 私钥
ApiURL string // API 网关
NotifyURL string // 异步通知地址
ReturnURL string // 同步通知地址
Methods []string // 支付方式
}
type XXLConfig struct { // XXL 任务调度配置
Enabled bool
ServerAddr string
ExecutorIp string
ExecutorPort string
AccessToken string
RegistryKey string
} }
type RedisConfig struct { type RedisConfig struct {
@@ -107,50 +37,32 @@ type RedisConfig struct {
DB int 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 { func (c RedisConfig) Url() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port) return fmt.Sprintf("%s:%d", c.Host, c.Port)
} }
type SystemConfig struct { type BaseConfig struct {
Title string `json:"title,omitempty"` // 网站标题 Title string `json:"title,omitempty"` // 网站标题
Slogan string `json:"slogan,omitempty"` // 网站 slogan Slogan string `json:"slogan,omitempty"` // 网站 slogan
AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题 AdminTitle string `json:"admin_title,omitempty"` // 管理后台标题
Logo string `json:"logo,omitempty"` // 形 Logo Logo string `json:"logo,omitempty"` // 形 Logo
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值 BarLogo string `json:"bar_logo,omitempty"` // 条形 Logo
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
VipMonthPower int `json:"vip_month_power,omitempty"` // VIP 会员每月赠送的算力值
RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册 RegisterWays []string `json:"register_ways,omitempty"` // 注册方式:支持手机(mobile),邮箱注册(email),账号密码注册
EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册 EnabledRegister bool `json:"enabled_register,omitempty"` // 是否开放注册
OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间 OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间,单位:分钟
VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力
DallPower int `json:"dall_power,omitempty"` // DALL-E-3消耗算力 MjPower int `json:"mj_power,omitempty"` // MJ消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力 SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
AdvanceVoicePower int `json:"advance_voice_power,omitempty"` // 高级语音对话消耗算力 SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
PromptPower int `json:"prompt_power,omitempty"` // 生成提示词消耗算力 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"` // 微信客服地址 WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址
@@ -160,13 +72,45 @@ type SystemConfig struct {
SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词 SdNegPrompt string `json:"sd_neg_prompt"` // SD 默认反向提示词
MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo MjMode string `json:"mj_mode"` // midjourney 默认的API模式,relax, fast, turbo
IndexBgURL string `json:"index_bg_url"` // 前端首页背景图片 IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单 IndexPage string `json:"index_page"` // 首页显示的页面
Copyright string `json:"copyright"` // 版权信息 Copyright string `json:"copyright"` // 版权信息
MarkMapText string `json:"mark_map_text"` // 思维导入的默认文本 ICP string `json:"icp"` // ICP 备案号
GaBeian string `json:"ga_beian"` // 公安备案号
EnabledVerify bool `json:"enabled_verify"` // 是否启用验证码
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表 EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
TranslateModelId int `json:"translate_model_id"` // 用来做提示词翻译的模型 id 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 string | int | uint
} }
type MValue interface { type MValue interface {
*WsClient | *ChatSession | context.CancelFunc | []interface{} *WsClient | context.CancelFunc | []any
} }
type LMap[K MKey, T MValue] struct { type LMap[K MKey, T MValue] struct {
lock sync.RWMutex 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"
)
+12 -16
View File
@@ -11,29 +11,25 @@ type OrderStatus int
const ( const (
OrderNotPaid = OrderStatus(0) OrderNotPaid = OrderStatus(0)
OrderScanned = OrderStatus(1) // 已扫码 OrderPaidSuccess = OrderStatus(2) // 已支付
OrderPaidSuccess = OrderStatus(2) OrderPaidFailed = OrderStatus(3) // 已关闭
) )
type OrderRemark struct { type OrderRemark struct {
Days int `json:"days"` // 有效期 Days int `json:"days"` // 有效期
Power int `json:"power"` // 增加算力点数 Power int `json:"power"` // 增加算力点数
Name string `json:"name"` // 产品名称 Name string `json:"name"` // 产品名称
Price float64 `json:"price"` Price float64 `json:"price"`
Discount float64 `json:"discount"`
} }
var PayMethods = map[string]string{ // PayChannel 支付渠道
var PayChannel = map[string]string{
"alipay": "支付宝商号", "alipay": "支付宝商号",
"wechat": "微信商号", "wxpay": "微信商号",
"hupi": "虎皮椒", "epay": "易支付",
"geek": "易支付",
} }
var PayNames = map[string]string{
var PayWays = map[string]string{
"alipay": "支付宝", "alipay": "支付宝",
"wxpay": "微信支付", "wxpay": "微信支付",
"qqpay": "QQ钱包",
"jdpay": "京东支付",
"douyin": "抖音支付",
"paypal": "PayPal支付",
} }
+24 -26
View File
@@ -8,41 +8,39 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type OSSConfig struct { type OSSConfig struct {
Active string Active string `json:"active,omitempty"`
Local LocalStorageConfig Local LocalStorageConfig `json:"local,omitempty"`
Minio MiniOssConfig Minio MiniOssConfig `json:"minio,omitempty"`
QiNiu QiNiuOssConfig QiNiu QiNiuOssConfig `json:"qiniu,omitempty"`
AliYun AliYunOssConfig AliYun AliYunOssConfig `json:"aliyun,omitempty"`
} }
type MiniOssConfig struct { type MiniOssConfig struct {
Endpoint string Endpoint string `json:"endpoint,omitempty"`
AccessKey string AccessKey string `json:"access_key,omitempty"`
AccessSecret string AccessSecret string `json:"access_secret,omitempty"`
Bucket string Bucket string `json:"bucket,omitempty"`
SubDir string UseSSL bool `json:"use_ssl,omitempty"`
UseSSL bool Domain string `json:"domain,omitempty"`
Domain string
} }
type QiNiuOssConfig struct { type QiNiuOssConfig struct {
Zone string Zone string `json:"zone,omitempty"`
AccessKey string AccessKey string `json:"access_key,omitempty"`
AccessSecret string AccessSecret string `json:"access_secret,omitempty"`
Bucket string Bucket string `json:"bucket,omitempty"`
SubDir string Domain string `json:"domain,omitempty"`
Domain string
} }
type AliYunOssConfig struct { type AliYunOssConfig struct {
Endpoint string Endpoint string `json:"endpoint,omitempty"`
AccessKey string AccessKey string `json:"access_key,omitempty"`
AccessSecret string AccessSecret string `json:"access_secret,omitempty"`
Bucket string Bucket string `json:"bucket,omitempty"`
SubDir string Domain string `json:"domain,omitempty"`
Domain string
} }
type LocalStorageConfig struct { type LocalStorageConfig struct {
BasePath string BasePath string `json:"base_path,omitempty"`
BaseURL string 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 LoginUserID = "LOGIN_USER_ID"
const AdminUserID = "ADMIN_USER_ID"
const LoginUserCache = "LOGIN_USER_CACHE" const LoginUserCache = "LOGIN_USER_CACHE"
const UserAuthHeader = "Authorization" const UserAuthHeader = "Authorization"
+11 -14
View File
@@ -8,26 +8,23 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type SMSConfig struct { type SMSConfig struct {
Active string Active string `json:"active,omitempty"`
Ali SmsConfigAli Ali SmsConfigAli `json:"aliyun,omitempty"`
Bao SmsConfigBao Bao SmsConfigBao `json:"bao,omitempty"`
} }
// SmsConfigAli 阿里云短信平台配置 // SmsConfigAli 阿里云短信平台配置
type SmsConfigAli struct { type SmsConfigAli struct {
AccessKey string AccessKey string `json:"access_key,omitempty"`
AccessSecret string AccessSecret string `json:"access_secret,omitempty"`
Product string Sign string `json:"sign,omitempty"` // 短信签名
Domain string CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板 ID
Sign string // 短信签名
CodeTempId string // 验证码短信模板 ID
} }
// SmsConfigBao 短信宝平台配置 // SmsConfigBao 短信宝平台配置
type SmsConfigBao struct { type SmsConfigBao struct {
Username string //短信宝平台注册的用户名 Username string `json:"username,omitempty"` //短信宝平台注册的用户名
Password string //短信宝平台注册的密码 Password string `json:"password,omitempty"` //短信宝平台注册的密码
Domain string //域名 Sign string `json:"sign,omitempty"` // 短信签名
Sign string // 短信签名 CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配
CodeTemplate string // 验证码短信模板 匹配
} }
+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
}
+46 -20
View File
@@ -26,7 +26,6 @@ const (
type MjTask struct { type MjTask struct {
Id uint `json:"id"` // 任务ID Id uint `json:"id"` // 任务ID
TaskId string `json:"task_id"` // 中转任务ID TaskId string `json:"task_id"` // 中转任务ID
ClientId string `json:"client_id"`
ImgArr []string `json:"img_arr"` ImgArr []string `json:"img_arr"`
Type TaskType `json:"type"` Type TaskType `json:"type"`
UserId int `json:"user_id"` UserId int `json:"user_id"`
@@ -44,7 +43,6 @@ type MjTask struct {
type SdTask struct { type SdTask struct {
Id int `json:"id"` // job 数据库ID Id int `json:"id"` // job 数据库ID
Type TaskType `json:"type"` Type TaskType `json:"type"`
ClientId string `json:"client_id"`
UserId int `json:"user_id"` UserId int `json:"user_id"`
Params SdTaskParams `json:"params"` Params SdTaskParams `json:"params"`
RetryCount int `json:"retry_count"` RetryCount int `json:"retry_count"`
@@ -52,7 +50,6 @@ type SdTask struct {
} }
type SdTaskParams struct { type SdTaskParams struct {
ClientId string `json:"client_id"` // 客户端ID
TaskId string `json:"task_id"` TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词 Prompt string `json:"prompt"` // 提示词
NegPrompt string `json:"neg_prompt"` // 反向提示词 NegPrompt string `json:"neg_prompt"` // 反向提示词
@@ -73,22 +70,20 @@ type SdTaskParams struct {
// DallTask DALL-E task // DallTask DALL-E task
type DallTask struct { type DallTask struct {
ClientId string `json:"client_id"` ModelId uint `json:"model_id"`
ModelId uint `json:"model_id"` ModelName string `json:"model_name"`
ModelName string `json:"model_name"` ModelValue string `json:"model_value"`
Id uint `json:"id"` Image []string `json:"image,omitempty"`
UserId uint `json:"user_id"` Id uint `json:"id"`
Prompt string `json:"prompt"` UserId uint `json:"user_id"`
N int `json:"n"` Prompt string `json:"prompt"`
Quality string `json:"quality"` AspectRatio string `json:"aspect_ratio"`
Size string `json:"size"` Size string `json:"size"`
Style string `json:"style"` Power int `json:"power"`
Power int `json:"power"` TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
} }
type SunoTask struct { type SunoTask struct {
ClientId string `json:"client_id"`
Id uint `json:"id"` Id uint `json:"id"`
Channel string `json:"channel"` Channel string `json:"channel"`
UserId int `json:"user_id"` UserId int `json:"user_id"`
@@ -96,7 +91,8 @@ type SunoTask struct {
Title string `json:"title"` Title string `json:"title"`
RefTaskId string `json:"ref_task_id,omitempty"` RefTaskId string `json:"ref_task_id,omitempty"`
RefSongId string `json:"ref_song_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"` Tags string `json:"tags"`
Model string `json:"model"` Model string `json:"model"`
Instrumental bool `json:"instrumental"` // 是否纯音乐 Instrumental bool `json:"instrumental"` // 是否纯音乐
@@ -109,21 +105,21 @@ const (
VideoLuma = "luma" VideoLuma = "luma"
VideoRunway = "runway" VideoRunway = "runway"
VideoCog = "cog" VideoCog = "cog"
VideoKeLing = "keling"
) )
type VideoTask struct { type VideoTask struct {
ClientId string `json:"client_id"`
Id uint `json:"id"` Id uint `json:"id"`
Channel string `json:"channel"` Channel string `json:"channel"`
UserId int `json:"user_id"` UserId int `json:"user_id"`
Type string `json:"type"` Type string `json:"type"`
TaskId string `json:"task_id"` TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词 Prompt string `json:"prompt"` // 提示词
Params VideoParams `json:"params"` Params interface{} `json:"params"`
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
} }
type VideoParams struct { type LumaVideoParams struct {
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词 PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
Loop bool `json:"loop"` // 是否循环参考图 Loop bool `json:"loop"` // 是否循环参考图
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址 StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
@@ -133,3 +129,33 @@ type VideoParams struct {
Style string `json:"style"` // 风格 Style string `json:"style"` // 风格
Duration int `json:"duration"` // 视频时长(秒) 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()
}
+8 -7
View File
@@ -34,13 +34,14 @@ const (
MsgTypeErr = WsMsgType("error") MsgTypeErr = WsMsgType("error")
MsgTypePing = WsMsgType("ping") // 心跳消息 MsgTypePing = WsMsgType("ping") // 心跳消息
ChPing = WsChannel("ping") ChPing = WsChannel("ping")
ChChat = WsChannel("chat") ChChat = WsChannel("chat")
ChMj = WsChannel("mj") ChMj = WsChannel("mj")
ChSd = WsChannel("sd") ChSd = WsChannel("sd")
ChDall = WsChannel("dall") ChDall = WsChannel("dall")
ChSuno = WsChannel("suno") ChSuno = WsChannel("suno")
ChLuma = WsChannel("luma") ChLuma = WsChannel("luma")
ChKeLing = WsChannel("keling")
) )
// InputMessage 对话输入消息结构 // InputMessage 对话输入消息结构
+1 -1
View File
@@ -4,7 +4,7 @@ build_name: runner-build
build_log: runner-build-errors.log build_log: runner-build-errors.log
valid_ext: .go, .tpl, .tmpl, .html valid_ext: .go, .tpl, .tmpl, .html
no_rebuild_ext: .tpl, .tmpl, .html, .js, .vue 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 build_delay: 600
colors: 1 colors: 1
log_color_main: cyan log_color_main: cyan
+8 -11
View File
@@ -18,20 +18,22 @@ require (
github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480 github.com/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480
github.com/qiniu/go-sdk/v7 v7.17.1 github.com/qiniu/go-sdk/v7 v7.17.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 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 go.uber.org/zap v1.23.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.4.7 gorm.io/driver/mysql v1.4.7
) )
require github.com/xxl-job/xxl-job-executor-go v1.2.0
require ( require (
github.com/go-pay/gopay v1.5.101 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/google/go-tika v0.3.1
github.com/microcosm-cc/bluemonday v1.0.26 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/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.3.1 github.com/shopspring/decimal v1.3.1
github.com/syndtr/goleveldb v1.0.0 github.com/syndtr/goleveldb v1.0.0
github.com/volcengine/volcengine-go-sdk v1.1.34
golang.org/x/image v0.15.0 golang.org/x/image v0.15.0
) )
@@ -43,17 +45,13 @@ require (
github.com/go-pay/util v0.0.2 // indirect github.com/go-pay/util v0.0.2 // indirect
github.com/go-pay/xlog v0.0.2 // indirect github.com/go-pay/xlog v0.0.2 // indirect
github.com/go-pay/xtime 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/gorilla/css v1.0.0 // indirect
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99 // indirect
github.com/howeyc/fsnotify v0.9.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a // indirect
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect github.com/tklauser/numcpus v0.7.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
) )
require ( require (
@@ -66,7 +64,6 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gaukas/godicttls v0.0.3 // indirect github.com/gaukas/godicttls v0.0.3 // indirect
github.com/go-basic/ipv4 v1.0.0 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
@@ -76,7 +73,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/compress v1.16.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
@@ -118,7 +115,7 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/fx v1.19.3 go.uber.org/fx v1.19.3
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.23.0 golang.org/x/crypto v0.23.0
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
gorm.io/gorm v1.25.1 gorm.io/gorm v1.25.1
+85 -20
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 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
@@ -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/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/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 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 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.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/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 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -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/dlclark/regexp2 v1.8.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/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.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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 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-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-basic/ipv4 v1.0.0 h1:gjyFAa1USC1hhXTkPOwBWDPfMcUaIM+tvo1XzV9EZxs=
github.com/go-basic/ipv4 v1.0.0/go.mod h1:etLBnaxbidQfuqE6wgZQfs38nEWNmzALkxDZe4xY8Dg=
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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= 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 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt 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 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/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.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 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 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.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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA= github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
@@ -100,15 +125,11 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99 h1:A6qlLfihaWef15viqtecCz4XknZcgjgD7mEuhu7bHEc=
github.com/gravityblast/fresh v0.0.0-20240621171608-8d1fef547a99/go.mod h1:ukFDwXV66bGV7JnfyxFKuKiVp4zH4orBKXML+VCSrhI=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY=
github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I= github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI= github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
@@ -117,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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/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.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.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
@@ -129,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 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 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.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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -141,9 +166,6 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs= github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0 h1:LgmjED/yQILqmUED4GaXjrINWe7YJh4HM6z2EvEINPs=
github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs= github.com/lionsoul2014/ip2region/binding/golang v0.0.0-20230415042440-a5e3d8259ae0/go.mod h1:C5LA5UO2ZXJrLaPLYtE1wUJMiyd/nwWaCO5cw/2pSHs=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
@@ -177,10 +199,6 @@ github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:Ff
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU= github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a h1:Tg4E4cXPZSZyd3H1tJlYo6ZreXV0ZJvE/lorNqyw1AU=
github.com/pilu/config v0.0.0-20131214182432-3eb99e6c0b9a/go.mod h1:9Or9aIl95Kp43zONcHd5tLZGKXb9iLx0pZjau0uJ5zg=
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99 h1:+X7Gb40b5Bl3v5+3MiGK8Jhemjp65MHc+nkVCfq1Yfc=
github.com/pilu/fresh v0.0.0-20240621171608-8d1fef547a99/go.mod h1:2LLTtftTZSdAPR/iVyennXZDLZOYzyDn+T0qEKJ8eSw=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -188,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/pkoukk/tiktoken-go v0.1.1-0.20230418101013-cae809389480/go.mod h1:BijIqAP84FMYC4XbdJgjyMpiSjusU8x0Y0W9K2t0QtU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/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/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 h1:UoQv7fBKtzAiD1qZPIvTy62Se48YLKxcCYP9nAwWMa0=
github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w= github.com/qiniu/go-sdk/v7 v7.17.1/go.mod h1:nqoYCNo53ZlGA521RvRethvxUDvXKt4gtYXOwye868w=
@@ -203,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/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 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/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 h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
@@ -215,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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -237,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/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/xxl-job/xxl-job-executor-go v1.2.0 h1:MTl2DpwrK2+hNjRRks2k7vB3oy+3onqm9OaSarneeLQ= github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
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/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/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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
@@ -253,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/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 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 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.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 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 h1:OjGQ5KQDEUawVHxNwQgPpiypGHOxo2mNZsOqTak4fFY=
go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -268,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.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 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 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 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= 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 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= 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.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.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 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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-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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -287,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.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 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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-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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -302,7 +337,6 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -332,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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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-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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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-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 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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 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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@@ -354,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 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/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-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
@@ -363,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.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 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=
gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= 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= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+49 -35
View File
@@ -11,6 +11,7 @@ import (
"context" "context"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
logger2 "geekai/logger" logger2 "geekai/logger"
@@ -19,9 +20,10 @@ import (
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
@@ -29,7 +31,7 @@ import (
var logger = logger2.GetLogger() var logger = logger2.GetLogger()
const SuperManagerID = 1 const SuperUsername = "admin"
type ManagerHandler struct { type ManagerHandler struct {
handler.BaseHandler 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 登录 // Login 登录
func (h *ManagerHandler) Login(c *gin.Context) { func (h *ManagerHandler) Login(c *gin.Context) {
var data struct { var data struct {
@@ -59,19 +81,6 @@ func (h *ManagerHandler) Login(c *gin.Context) {
return 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 var manager model.AdminUser
res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager) res := h.DB.Model(&model.AdminUser{}).Where("username = ?", data.Username).First(&manager)
if res.Error != nil { if res.Error != nil {
@@ -85,7 +94,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
} }
// 超级管理员默认是ID:1 // 超级管理员默认是ID:1
if manager.Id != SuperManagerID && manager.Status == false { if manager.Username != SuperUsername && !manager.Status {
resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员") resp.ERROR(c, "该用户已被禁止登录,请联系超级管理员")
return return
} }
@@ -116,7 +125,7 @@ func (h *ManagerHandler) Login(c *gin.Context) {
IsSuperAdmin bool `json:"is_super_admin"` IsSuperAdmin bool `json:"is_super_admin"`
Token string `json:"token"` Token string `json:"token"`
}{ }{
IsSuperAdmin: manager.Id == 1, IsSuperAdmin: manager.Username == SuperUsername,
Token: tokenString, Token: tokenString,
} }
@@ -135,16 +144,15 @@ func (h *ManagerHandler) Logout(c *gin.Context) {
// Session 会话检测 // Session 会话检测
func (h *ManagerHandler) Session(c *gin.Context) { func (h *ManagerHandler) Session(c *gin.Context) {
id := h.GetLoginUserId(c) id := h.GetAdminId(c)
key := fmt.Sprintf("admin/%d", id) if id == 0 {
if _, err := h.redis.Get(context.Background(), key).Result(); err != nil { resp.NotAuth(c, "当前用户已退出登录")
resp.NotAuth(c)
return return
} }
var manager model.AdminUser var manager model.AdminUser
res := h.DB.Where("id", id).First(&manager) err := h.DB.Where("id", id).First(&manager).Error
if res.Error != nil { if err != nil {
resp.NotAuth(c) resp.NotAuth(c, "当前用户已退出登录")
return return
} }
@@ -219,12 +227,19 @@ func (h *ManagerHandler) Remove(c *gin.Context) {
return 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, "超级管理员不能删除") resp.ERROR(c, "超级管理员不能删除")
return return
} }
res := h.DB.Where("id", id).Delete(&model.AdminUser{}) res = h.DB.Where("id", id).Delete(&model.AdminUser{})
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, res.Error.Error()) resp.ERROR(c, res.Error.Error())
return return
@@ -255,8 +270,14 @@ func (h *ManagerHandler) Enable(c *gin.Context) {
// ResetPass 重置密码 // ResetPass 重置密码
func (h *ManagerHandler) ResetPass(c *gin.Context) { func (h *ManagerHandler) ResetPass(c *gin.Context) {
id := h.GetLoginUserId(c) id := h.GetAdminId(c)
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, "只有超级管理员能够进行该操作") resp.ERROR(c, "只有超级管理员能够进行该操作")
return return
} }
@@ -270,13 +291,6 @@ func (h *ManagerHandler) ResetPass(c *gin.Context) {
return 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) password := utils.GenPassword(data.Password, user.Salt)
user.Password = password user.Password = password
res = h.DB.Updates(&user) res = h.DB.Updates(&user)
+20 -2
View File
@@ -10,12 +10,14 @@ package admin
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"strings"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "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}} 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) { func (h *ApiKeyHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
@@ -71,16 +87,18 @@ func (h *ApiKeyHandler) Save(c *gin.Context) {
resp.SUCCESS(c, keyVo) resp.SUCCESS(c, keyVo)
} }
// List 获取 API KEY 列表
func (h *ApiKeyHandler) List(c *gin.Context) { func (h *ApiKeyHandler) List(c *gin.Context) {
status := h.GetBool(c, "status") status := h.GetBool(c, "status")
t := h.GetTrim(c, "type") t := c.Query("type")
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if status { if status {
session = session.Where("enabled", true) session = session.Where("enabled", true)
} }
if t != "" { if t != "" {
session = session.Where("type", t) types := strings.Split(t, "|")
session = session.Where("type IN ?", types)
} }
var items []model.ApiKey var items []model.ApiKey
+28 -13
View File
@@ -10,6 +10,7 @@ package admin
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/store/model" "geekai/store/model"
@@ -30,14 +31,29 @@ func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
return &ChatAppHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} 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 创建或者更新某个角色 // Save 创建或者更新某个角色
func (h *ChatAppHandler) Save(c *gin.Context) { func (h *ChatAppHandler) Save(c *gin.Context) {
var data vo.ChatRole var data vo.ChatApp
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
var role model.ChatRole var role model.ChatApp
err := utils.CopyObject(data, &role) err := utils.CopyObject(data, &role)
if err != nil { if err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -65,8 +81,8 @@ func (h *ChatAppHandler) Save(c *gin.Context) {
} }
func (h *ChatAppHandler) List(c *gin.Context) { func (h *ChatAppHandler) List(c *gin.Context) {
var items []model.ChatRole var items []model.ChatApp
var roles = make([]vo.ChatRole, 0) var roles = make([]vo.ChatApp, 0)
res := h.DB.Order("sort_num ASC").Find(&items) res := h.DB.Order("sort_num ASC").Find(&items)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "No data found") resp.ERROR(c, "No data found")
@@ -78,10 +94,10 @@ func (h *ChatAppHandler) List(c *gin.Context) {
typeIds := make([]int, 0) typeIds := make([]int, 0)
for _, v := range items { for _, v := range items {
if v.ModelId > 0 { if v.ModelId > 0 {
modelIds = append(modelIds, v.ModelId) modelIds = append(modelIds, int(v.ModelId))
} }
if v.Tid > 0 { if v.Tid > 0 {
typeIds = append(typeIds, v.Tid) typeIds = append(typeIds, int(v.Tid))
} }
} }
@@ -107,14 +123,14 @@ func (h *ChatAppHandler) List(c *gin.Context) {
} }
for _, v := range items { for _, v := range items {
var role vo.ChatRole var role vo.ChatApp
err := utils.CopyObject(v, &role) err := utils.CopyObject(v, &role)
if err == nil { if err == nil {
role.Id = v.Id role.Id = v.Id
role.CreatedAt = v.CreatedAt.Unix() role.CreatedAt = v.CreatedAt.Unix()
role.UpdatedAt = v.UpdatedAt.Unix() role.UpdatedAt = v.UpdatedAt.Unix()
role.ModelName = modelNameMap[role.ModelId] role.ModelName = modelNameMap[int(role.ModelId)]
role.TypeName = typeNameMap[role.Tid] role.TypeName = typeNameMap[int(role.Tid)]
roles = append(roles, role) roles = append(roles, role)
} }
} }
@@ -135,7 +151,7 @@ func (h *ChatAppHandler) Sort(c *gin.Context) {
} }
for index, id := range data.Ids { 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 { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -157,7 +173,7 @@ func (h *ChatAppHandler) Set(c *gin.Context) {
return 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 { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -172,9 +188,8 @@ func (h *ChatAppHandler) Remove(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
res := h.DB.Where("id", id).Delete(&model.ChatRole{}) res := h.DB.Where("id", id).Delete(&model.ChatApp{})
if res.Error != nil { if res.Error != nil {
logger.Error("error with update database", res.Error)
resp.ERROR(c, "删除失败!") resp.ERROR(c, "删除失败!")
return return
} }
@@ -2,12 +2,14 @@ package admin
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -20,6 +22,21 @@ func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler
return &ChatAppTypeHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} 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类型 // Save 创建或更新App类型
func (h *ChatAppTypeHandler) Save(c *gin.Context) { func (h *ChatAppTypeHandler) Save(c *gin.Context) {
var data struct { var data struct {
+43 -18
View File
@@ -9,12 +9,14 @@ package admin
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "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}} 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 { type chatItemVo struct {
Username string `json:"username"` Username string `json:"username"`
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
ChatId string `json:"chat_id"` ChatId string `json:"chat_id"`
Title string `json:"title"` Title string `json:"title"`
Role vo.ChatRole `json:"role"` Role vo.ChatApp `json:"role"`
Model string `json:"model"` Model string `json:"model"`
Token int `json:"token"` Token int `json:"token"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
MsgNum int `json:"msg_num"` // 消息数量 MsgNum int `json:"msg_num"` // 消息数量
} }
func (h *ChatHandler) List(c *gin.Context) { func (h *ChatHandler) List(c *gin.Context) {
@@ -86,7 +103,7 @@ func (h *ChatHandler) List(c *gin.Context) {
} }
var messages []model.ChatMessage var messages []model.ChatMessage
var users []model.User var users []model.User
var roles []model.ChatRole var roles []model.ChatApp
h.DB.Where("chat_id IN ?", chatIds).Find(&messages) h.DB.Where("chat_id IN ?", chatIds).Find(&messages)
h.DB.Where("id IN ?", userIds).Find(&users) h.DB.Where("id IN ?", userIds).Find(&users)
h.DB.Where("id IN ?", roleIds).Find(&roles) h.DB.Where("id IN ?", roleIds).Find(&roles)
@@ -94,7 +111,7 @@ func (h *ChatHandler) List(c *gin.Context) {
tokenMap := make(map[string]int) tokenMap := make(map[string]int)
userMap := make(map[uint]string) userMap := make(map[uint]string)
msgMap := make(map[string]int) msgMap := make(map[string]int)
roleMap := make(map[uint]vo.ChatRole) roleMap := make(map[uint]vo.ChatApp)
for _, msg := range messages { for _, msg := range messages {
tokenMap[msg.ChatId] += msg.Tokens tokenMap[msg.ChatId] += msg.Tokens
msgMap[msg.ChatId] += 1 msgMap[msg.ChatId] += 1
@@ -103,7 +120,7 @@ func (h *ChatHandler) List(c *gin.Context) {
userMap[user.Id] = user.Username userMap[user.Id] = user.Username
} }
for _, r := range roles { for _, r := range roles {
var roleVo vo.ChatRole var roleVo vo.ChatApp
err := utils.CopyObject(r, &roleVo) err := utils.CopyObject(r, &roleVo)
if err != nil { if err != nil {
continue continue
@@ -189,7 +206,7 @@ func (h *ChatHandler) Messages(c *gin.Context) {
} }
for _, item := range items { for _, item := range items {
list = append(list, chatMessageVo{ list = append(list, chatMessageVo{
Id: item.Id, Id: uint(item.Id),
UserId: item.UserId, UserId: item.UserId,
Username: userMap[item.UserId], Username: userMap[item.UserId],
Content: item.Content, Content: item.Content,
@@ -208,20 +225,28 @@ func (h *ChatHandler) Messages(c *gin.Context) {
func (h *ChatHandler) History(c *gin.Context) { func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID chatId := c.Query("chat_id") // 会话 ID
var items []model.ChatMessage 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) res := h.DB.Where("chat_id = ?", chatId).Find(&items)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "No history message") resp.ERROR(c, "No history message")
return return
} else { } else {
for _, item := range items { for _, item := range items {
var v vo.HistoryMessage var v vo.ChatMessage
err := utils.CopyObject(item, &v) 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.CreatedAt = item.CreatedAt.Unix()
v.UpdatedAt = item.UpdatedAt.Unix() v.UpdatedAt = item.UpdatedAt.Unix()
if err == nil { messages = append(messages, v)
messages = append(messages, v)
}
} }
} }
+74 -17
View File
@@ -8,7 +8,9 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/store/model" "geekai/store/model"
@@ -28,22 +30,41 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
return &ChatModelHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} 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) { func (h *ChatModelHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
SortNum int `json:"sort_num"` SortNum int `json:"sort_num"`
Open bool `json:"open"` Open bool `json:"open"`
Platform string `json:"platform"` Platform string `json:"platform"`
Power int `json:"power"` Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度 MaxTokens int `json:"max_tokens"` // 最大响应长度
MaxContext int `json:"max_context"` // 最大上下文长度 MaxContext int `json:"max_context"` // 最大上下文长度
Temperature float32 `json:"temperature"` // 模型温度 Desc string `json:"desc"` //模型描述
KeyId int `json:"key_id,omitempty"` Tag string `json:"tag"` //模型标签
CreatedAt int64 `json:"created_at"` Temperature float32 `json:"temperature"` // 模型温度
Type string `json:"type"` 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 { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -59,14 +80,16 @@ func (h *ChatModelHandler) Save(c *gin.Context) {
item.Name = data.Name item.Name = data.Name
item.Value = data.Value item.Value = data.Value
item.Enabled = data.Enabled item.Enabled = data.Enabled
item.SortNum = data.SortNum
item.Open = data.Open item.Open = data.Open
item.Power = data.Power item.Power = data.Power
item.MaxTokens = data.MaxTokens item.MaxTokens = data.MaxTokens
item.MaxContext = data.MaxContext item.MaxContext = data.MaxContext
item.Desc = data.Desc
item.Tag = data.Tag
item.Temperature = data.Temperature item.Temperature = data.Temperature
item.KeyId = data.KeyId item.KeyId = uint(data.KeyId)
item.Type = data.Type item.Type = data.Type
item.Options = utils.JsonEncode(data.Options)
var res *gorm.DB var res *gorm.DB
if data.Id > 0 { if data.Id > 0 {
res = h.DB.Save(&item) res = h.DB.Save(&item)
@@ -95,12 +118,16 @@ func (h *ChatModelHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
enable := h.GetBool(c, "enable") enable := h.GetBool(c, "enable")
name := h.GetTrim(c, "name") name := h.GetTrim(c, "name")
modelType := h.GetTrim(c, "type")
if enable { if enable {
session = session.Where("enabled", enable) session = session.Where("enabled", enable)
} }
if name != "" { if name != "" {
session = session.Where("name LIKE ?", name+"%") session = session.Where("name LIKE ?", name+"%")
} }
if modelType != "" {
session = session.Where("type", modelType)
}
var items []model.ChatModel var items []model.ChatModel
var cms = make([]vo.ChatModel, 0) var cms = make([]vo.ChatModel, 0)
res := session.Order("sort_num ASC").Find(&items) res := session.Order("sort_num ASC").Find(&items)
@@ -112,7 +139,7 @@ func (h *ChatModelHandler) List(c *gin.Context) {
// initialize key name // initialize key name
keyIds := make([]int, 0) keyIds := make([]int, 0)
for _, v := range items { for _, v := range items {
keyIds = append(keyIds, v.KeyId) keyIds = append(keyIds, int(v.KeyId))
} }
var keys []model.ApiKey var keys []model.ApiKey
keyMap := make(map[uint]string) keyMap := make(map[uint]string)
@@ -192,3 +219,33 @@ func (h *ChatModelHandler) Remove(c *gin.Context) {
} }
resp.SUCCESS(c) 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 -153
View File
@@ -9,42 +9,202 @@ package admin
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/service" "geekai/service"
"geekai/store" "geekai/service/oss"
"geekai/service/payment"
"geekai/service/sms"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/shirou/gopsutil/host"
"gorm.io/gorm" "gorm.io/gorm"
) )
type ConfigHandler struct { type ConfigHandler struct {
handler.BaseHandler handler.BaseHandler
levelDB *store.LevelDB sysConfig *types.SystemConfig
licenseService *service.LicenseService 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{ return &ConfigHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db}, BaseHandler: handler.BaseHandler{App: app, DB: db},
levelDB: levelDB, sysConfig: sysConfig,
licenseService: licenseService, 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 { var data struct {
Key string `json:"key"` InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
Config struct { DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
types.SystemConfig InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
Content string `json:"content,omitempty"` MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
Updated bool `json:"updated,omitempty"` MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
} `json:"config"` SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力
ConfigBak types.SystemConfig `json:"config_bak,omitempty"` 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 { if err := c.ShouldBindJSON(&data); err != nil {
@@ -52,57 +212,176 @@ func (h *ConfigHandler) Update(c *gin.Context) {
return return
} }
// ONLY authorized user can change the copyright err := h.Update(types.ConfigKeyMarkMap, data)
if (data.Key == "system" && data.Config.Copyright != data.ConfigBak.Copyright) && !h.licenseService.GetLicense().Configs.DeCopy { if err != nil {
resp.ERROR(c, "您无权修改版权信息,请先联系作者获取授权") resp.ERROR(c, err.Error())
return return
} }
value := utils.JsonEncode(&data.Config) resp.SUCCESS(c, data)
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)
} }
// Get 获取指定的系统配置 // UpdateCaptcha 更新行为验证码配置
func (h *ConfigHandler) Get(c *gin.Context) { func (h *ConfigHandler) UpdateCaptcha(c *gin.Context) {
key := c.Query("key") 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 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 { if res.Error != nil {
resp.ERROR(c, res.Error.Error()) resp.ERROR(c, res.Error.Error())
return return
} }
var value map[string]interface{} var value map[string]any
err := utils.JsonDecode(config.Config, &value) err := utils.JsonDecode(config.Value, &value)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -110,100 +389,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
resp.SUCCESS(c, value) 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)
}
// FixData 修复数据
func (h *ConfigHandler) FixData(c *gin.Context) {
resp.ERROR(c, "当前升级版本没有数据需要修正!")
return
//var fixed bool
//version := "data_fix_4.1.4"
//err := h.levelDB.Get(version, &fixed)
//if err == nil || fixed {
// resp.ERROR(c, "当前版本数据修复已完成,请不要重复执行操作")
// return
//}
//tx := h.DB.Begin()
//var users []model.User
//err = tx.Find(&users).Error
//if err != nil {
// resp.ERROR(c, err.Error())
// return
//}
//for _, user := range users {
// if user.Email != "" || user.Mobile != "" {
// continue
// }
// if utils.IsValidEmail(user.Username) {
// user.Email = user.Username
// } else if utils.IsValidMobile(user.Username) {
// user.Mobile = user.Username
// }
// err = tx.Save(&user).Error
// if err != nil {
// resp.ERROR(c, err.Error())
// tx.Rollback()
// return
// }
//}
//
//var orders []model.Order
//err = h.DB.Find(&orders).Error
//if err != nil {
// resp.ERROR(c, err.Error())
// return
//}
//for _, order := range orders {
// if order.PayWay == "支付宝" {
// order.PayWay = "alipay"
// order.PayType = "alipay"
// } else if order.PayWay == "微信支付" {
// order.PayWay = "wechat"
// order.PayType = "wxpay"
// } else if order.PayWay == "hupi" {
// order.PayType = "wxpay"
// }
// err = tx.Save(&order).Error
// if err != nil {
// resp.ERROR(c, err.Error())
// tx.Rollback()
// return
// }
//}
//tx.Commit()
//err = h.levelDB.Put(version, true)
//if err != nil {
// resp.ERROR(c, err.Error())
// return
//}
//resp.SUCCESS(c)
}
+158 -36
View File
@@ -13,10 +13,11 @@ import (
"geekai/handler" "geekai/handler"
"geekai/store/model" "geekai/store/model"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type DashboardHandler struct { 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}} 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 { type statsVo struct {
Users int64 `json:"users"` Users int64 `json:"users"`
Chats int64 `json:"chats"` Chats int64 `json:"chats"`
Tokens int `json:"tokens"` Tokens int `json:"tokens"`
Income float64 `json:"income"` Income float64 `json:"income"`
Chart map[string]map[string]float64 `json:"chart"` 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) { func (h *DashboardHandler) Stats(c *gin.Context) {
stats := statsVo{} stats := statsVo{}
// new users statistic
var userCount int64
now := time.Now() now := time.Now()
zeroTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) 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 var todayPowerLogs []model.PowerLog
res = h.DB.Model(&model.ChatItem{}).Where("created_at > ?", zeroTime).Count(&chatCount) h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", zeroTime).Find(&todayPowerLogs)
if res.Error == nil { for _, item := range todayPowerLogs {
stats.Chats = chatCount stats.TodayTokens += item.Amount
} }
// tokens took stats // 总收入
var historyMessages []model.ChatMessage var allOrders []model.Order
res = h.DB.Where("created_at > ?", zeroTime).Find(&historyMessages) h.DB.Where("status = ?", types.OrderPaidSuccess).Find(&allOrders)
for _, item := range historyMessages { for _, item := range allOrders {
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 {
stats.Income += item.Amount 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天的订单的图表 // 统计7天的订单的图表
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02") startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
var statsChart = make(map[string]map[string]float64) var statsChart = make(map[string]map[string]float64)
@@ -81,23 +197,29 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
// 统计用户7天增加的曲线 // 统计用户7天增加的曲线
var users []model.User var users []model.User
res = h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users) err := h.DB.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users).Error
if res.Error == nil { if err == nil {
for _, item := range users { for _, item := range users {
userStatistic[item.CreatedAt.Format("2006-01-02")] += 1 userStatistic[item.CreatedAt.Format("2006-01-02")] += 1
} }
} }
// 统计7天Token 消耗 // 统计7天算力消耗
res = h.DB.Where("created_at > ?", startDate).Find(&historyMessages) var chartPowerLogs []model.PowerLog
for _, item := range historyMessages { err = h.DB.Where("mark = ?", types.PowerSub).Where("created_at > ?", startDate).Find(&chartPowerLogs).Error
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens) if err == nil {
for _, item := range chartPowerLogs {
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Amount)
}
} }
// 统计最近7天的订单 // 统计最近7天的订单
res = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders) var orders []model.Order
for _, item := range orders { err = h.DB.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders).Error
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64() 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 statsChart["users"] = userStatistic
+16 -1
View File
@@ -9,6 +9,7 @@ package admin
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/store/model" "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}} 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) { func (h *FunctionHandler) Save(c *gin.Context) {
var data vo.Function var data vo.Function
if err := c.ShouldBindJSON(&data); err != nil { 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)) tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil { if err != nil {
logger.Error("error with generate token", err)
resp.ERROR(c) resp.ERROR(c)
return return
} }
+19 -6
View File
@@ -10,6 +10,7 @@ package admin
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/service" "geekai/service"
@@ -18,6 +19,7 @@ import (
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -32,6 +34,20 @@ func NewImageHandler(app *core.AppServer, db *gorm.DB, userService *service.User
return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager} 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 { type imageQuery struct {
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Username string `json:"username"` Username string `json:"username"`
@@ -189,11 +205,10 @@ func (h *ImageHandler) Remove(c *gin.Context) {
tx.Delete(&job) tx.Delete(&job)
md = "mid-journey" md = "mid-journey"
power = job.Power power = job.Power
userId = job.UserId userId = int(job.UserId)
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
imgURL = job.ImgURL imgURL = job.ImgURL
break
case "sd": case "sd":
var job model.SdJob var job model.SdJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil { if res := h.DB.Where("id", id).First(&job); res.Error != nil {
@@ -205,11 +220,10 @@ func (h *ImageHandler) Remove(c *gin.Context) {
tx.Delete(&job) tx.Delete(&job)
md = "stable-diffusion" md = "stable-diffusion"
power = job.Power power = job.Power
userId = job.UserId userId = int(job.UserId)
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
imgURL = job.ImgURL imgURL = job.ImgURL
break
case "dall": case "dall":
var job model.DallJob var job model.DallJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil { if res := h.DB.Where("id", id).First(&job); res.Error != nil {
@@ -225,14 +239,13 @@ func (h *ImageHandler) Remove(c *gin.Context) {
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
imgURL = job.ImgURL imgURL = job.ImgURL
break
default: default:
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
if progress != 100 { if progress != 100 {
err := h.userService.IncreasePower(userId, power, model.PowerLog{ err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund, Type: types.PowerRefund,
Model: md, Model: md,
Remark: remark, Remark: remark,
+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": "配置更新成功"})
}
+23 -8
View File
@@ -10,6 +10,7 @@ package admin
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/service" "geekai/service"
@@ -18,6 +19,7 @@ import (
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -32,7 +34,21 @@ func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.User
return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager} 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 mediaQuery struct {
Type string `json:"type"` // 任务类型 luma, keling
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Username string `json:"username"` Username string `json:"username"`
CreatedAt []string `json:"created_at"` CreatedAt []string `json:"created_at"`
@@ -84,15 +100,15 @@ func (h *MediaHandler) SunoList(c *gin.Context) {
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items)) resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
} }
// LumaList Luma 视频任务列表 // Videos 视频任务列表
func (h *MediaHandler) LumaList(c *gin.Context) { func (h *MediaHandler) Videos(c *gin.Context) {
var data mediaQuery var data mediaQuery
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{}).Where("type", data.Type)
if data.Username != "" { if data.Username != "" {
var user model.User var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error err := h.DB.Where("username", data.Username).First(&user).Error
@@ -148,12 +164,12 @@ func (h *MediaHandler) Remove(c *gin.Context) {
tx.Delete(&job) tx.Delete(&job)
md = "suno" md = "suno"
power = job.Power power = job.Power
userId = job.UserId userId = int(job.UserId)
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
fileURL = job.AudioURL fileURL = job.AudioURL
break
case "luma": case "luma":
case "keling":
var job model.VideoJob var job model.VideoJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil { if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在") resp.ERROR(c, "记录不存在")
@@ -164,21 +180,20 @@ func (h *MediaHandler) Remove(c *gin.Context) {
tx.Delete(&job) tx.Delete(&job)
md = job.Type md = job.Type
power = job.Power power = job.Power
userId = job.UserId userId = int(job.UserId)
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress progress = job.Progress
fileURL = job.VideoURL fileURL = job.VideoURL
if fileURL == "" { if fileURL == "" {
fileURL = job.WaterURL fileURL = job.WaterURL
} }
break
default: default:
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
if progress != 100 { if progress != 100 {
err := h.userService.IncreasePower(userId, power, model.PowerLog{ err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund, Type: types.PowerRefund,
Model: md, Model: md,
Remark: remark, Remark: remark,
+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}} 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) { func (h *MenuHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` 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)
}
+16 -8
View File
@@ -29,6 +29,14 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
return &OrderHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} 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) { func (h *OrderHandler) List(c *gin.Context) {
var data struct { var data struct {
OrderNo string `json:"order_no"` OrderNo string `json:"order_no"`
@@ -68,16 +76,16 @@ func (h *OrderHandler) List(c *gin.Context) {
order.Id = item.Id order.Id = item.Id
order.CreatedAt = item.CreatedAt.Unix() order.CreatedAt = item.CreatedAt.Unix()
order.UpdatedAt = item.UpdatedAt.Unix() order.UpdatedAt = item.UpdatedAt.Unix()
payMethod, ok := types.PayMethods[item.PayWay] payChannel, ok := types.PayChannel[item.Channel]
if !ok { if !ok {
payMethod = item.PayWay payChannel = item.Channel
} }
payName, ok := types.PayNames[item.PayType] payWays, ok := types.PayWays[item.PayWay]
if !ok { if !ok {
payName = item.PayWay payWays = item.PayWay
} }
order.PayMethod = payMethod order.ChannelName = payChannel
order.PayName = payName order.PayName = payWays
list = append(list, order) list = append(list, order)
} else { } else {
logger.Error(err) logger.Error(err)
@@ -121,8 +129,8 @@ func (h *OrderHandler) Clear(c *gin.Context) {
} }
deleteIds := make([]uint, 0) deleteIds := make([]uint, 0)
for _, order := range orders { for _, order := range orders {
// 只删除 15 分钟内的未支付订单 // 只删除超时的未支付订单
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) { if time.Now().After(order.CreatedAt.Add(time.Minute * time.Duration(h.App.SysConfig.Base.OrderPayTimeout))) {
deleteIds = append(deleteIds, order.Id) deleteIds = append(deleteIds, order.Id)
} }
} }
+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}} 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) { func (h *PowerLogHandler) List(c *gin.Context) {
var data struct { var data struct {
Username string `json:"username"` Username string `json:"username"`
UserId uint `json:"userid"`
Type int `json:"type"` Type int `json:"type"`
Model string `json:"model"` Model string `json:"model"`
Date []string `json:"date"` Date []string `json:"date"`
@@ -49,6 +56,12 @@ func (h *PowerLogHandler) List(c *gin.Context) {
if data.Type > 0 { if data.Type > 0 {
session = session.Where("type", data.Type) 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 { if len(data.Date) == 2 {
start := data.Date[0] + " 00:00:00" start := data.Date[0] + " 00:00:00"
end := data.Date[1] + " 00:00:00" end := data.Date[1] + " 00:00:00"
+16 -9
View File
@@ -15,9 +15,10 @@ import (
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type ProductHandler struct { 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}} 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) { func (h *ProductHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Price float64 `json:"price"` Price float64 `json:"price"`
Discount float64 `json:"discount"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Days int `json:"days"`
Power int `json:"power"` Power int `json:"power"`
CreatedAt int64 `json:"created_at"` CreatedAt int64 `json:"created_at"`
} }
@@ -45,12 +54,10 @@ func (h *ProductHandler) Save(c *gin.Context) {
} }
item := model.Product{ item := model.Product{
Name: data.Name, Name: data.Name,
Price: data.Price, Price: data.Price,
Discount: data.Discount, Power: data.Power,
Days: data.Days, Enabled: data.Enabled}
Power: data.Power,
Enabled: data.Enabled}
item.Id = data.Id item.Id = data.Id
if item.Id > 0 { if item.Id > 0 {
item.CreatedAt = time.Unix(data.CreatedAt, 0) item.CreatedAt = time.Unix(data.CreatedAt, 0)
+12 -2
View File
@@ -29,6 +29,16 @@ func NewRedeemHandler(app *core.AppServer, db *gorm.DB) *RedeemHandler {
return &RedeemHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} 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) { func (h *RedeemHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1) page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20) pageSize := h.GetInt(c, "page_size", 20)
@@ -106,8 +116,8 @@ func (h *RedeemHandler) Export(c *gin.Context) {
} }
// 设置响应头,告诉浏览器这是一个附件,需要下载 // 设置响应头,告诉浏览器这是一个附件,需要下载
c.Header("Content-Disposition", "attachment; filename=output.csv") c.Header("Prompt-Disposition", "attachment; filename=output.csv")
c.Header("Content-Type", "text/csv") c.Header("Prompt-Type", "text/csv")
// 创建一个 CSV writer // 创建一个 CSV writer
writer := csv.NewWriter(c.Writer) writer := csv.NewWriter(c.Writer)
+28 -2
View File
@@ -9,13 +9,15 @@ package admin
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/handler" "geekai/handler"
"geekai/service/oss" "geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type UploadHandler struct { 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} 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) { 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") file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
userId := 0 userId := 0
res := h.DB.Create(&model.File{ res := h.DB.Create(&model.File{
UserId: userId, UserId: uint(userId),
Name: file.Name, Name: file.Name,
ObjKey: file.ObjKey, ObjKey: file.ObjKey,
URL: file.URL, URL: file.URL,
+65 -18
View File
@@ -10,28 +10,44 @@ package admin
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/go-redis/redis/v8"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm" "gorm.io/gorm"
) )
type UserHandler struct { type UserHandler struct {
handler.BaseHandler handler.BaseHandler
licenseService *service.LicenseService redis *redis.Client
redis *redis.Client
} }
func NewUserHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService, redisCli *redis.Client) *UserHandler { func NewUserHandler(app *core.AppServer, db *gorm.DB, redisCli *redis.Client) *UserHandler {
return &UserHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, licenseService: licenseService, redis: redisCli} 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 用户列表 // List 用户列表
@@ -39,6 +55,8 @@ func (h *UserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1) page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20) pageSize := h.GetInt(c, "page_size", 20)
username := h.GetTrim(c, "username") username := h.GetTrim(c, "username")
mobile := h.GetTrim(c, "mobile")
email := h.GetTrim(c, "email")
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
var items []model.User var items []model.User
@@ -49,6 +67,12 @@ func (h *UserHandler) List(c *gin.Context) {
if username != "" { if username != "" {
session = session.Where("username LIKE ?", "%"+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) session.Model(&model.User{}).Count(&total)
res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items) res := session.Offset(offset).Limit(pageSize).Order("id DESC").Find(&items)
@@ -88,13 +112,6 @@ func (h *UserHandler) Save(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return 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 user = model.User{}
var res *gorm.DB var res *gorm.DB
var userVo vo.User var userVo vo.User
@@ -168,14 +185,11 @@ func (h *UserHandler) Save(c *gin.Context) {
Power: data.Power, Power: data.Power,
Status: true, Status: true,
ChatRoles: utils.JsonEncode(data.ChatRoles), ChatRoles: utils.JsonEncode(data.ChatRoles),
ChatConfig: "{}",
ChatModels: utils.JsonEncode(data.ChatModels), ChatModels: utils.JsonEncode(data.ChatModels),
ExpiredTime: utils.Str2stamp(data.ExpiredTime), ExpiredTime: utils.Str2stamp(data.ExpiredTime),
} }
if h.licenseService.GetLicense().Configs.DeCopy { u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
} else {
u.Nickname = fmt.Sprintf("极客学长@%d", utils.RandomNumber(6))
}
res = h.DB.Create(&u) res = h.DB.Create(&u)
_ = utils.CopyObject(u, &userVo) _ = utils.CopyObject(u, &userVo)
userVo.Id = u.Id userVo.Id = u.Id
@@ -311,3 +325,36 @@ func (h *UserHandler) LoginLog(c *gin.Context) {
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, logs)) 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)
}
+10 -1
View File
@@ -15,9 +15,10 @@ import (
logger2 "geekai/logger" logger2 "geekai/logger"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"gorm.io/gorm"
"strings" "strings"
"gorm.io/gorm"
"github.com/gin-gonic/gin" "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)) 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 { func (h *BaseHandler) IsLogin(c *gin.Context) bool {
return h.GetLoginUserId(c) > 0 return h.GetLoginUserId(c) > 0
} }
+41 -4
View File
@@ -8,23 +8,45 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// 今日头条函数实现
type CaptchaHandler struct { type CaptchaHandler struct {
App *core.AppServer
service *service.CaptchaService service *service.CaptchaService
} }
func NewCaptchaHandler(s *service.CaptchaService) *CaptchaHandler { func NewCaptchaHandler(app *core.AppServer, s *service.CaptchaService, sysConfig *types.SystemConfig) *CaptchaHandler {
return &CaptchaHandler{service: s} 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) { func (h *CaptchaHandler) Get(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
data, err := h.service.Get() data, err := h.service.Get()
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
@@ -36,6 +58,11 @@ func (h *CaptchaHandler) Get(c *gin.Context) {
// Check verify the captcha data // Check verify the captcha data
func (h *CaptchaHandler) Check(c *gin.Context) { func (h *CaptchaHandler) Check(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
var data struct { var data struct {
Key string `json:"key"` Key string `json:"key"`
Dots string `json:"dots"` Dots string `json:"dots"`
@@ -55,6 +82,11 @@ func (h *CaptchaHandler) Check(c *gin.Context) {
// SlideGet 获取滑动验证图片 // SlideGet 获取滑动验证图片
func (h *CaptchaHandler) SlideGet(c *gin.Context) { func (h *CaptchaHandler) SlideGet(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
data, err := h.service.SlideGet() data, err := h.service.SlideGet()
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
@@ -66,6 +98,11 @@ func (h *CaptchaHandler) SlideGet(c *gin.Context) {
// SlideCheck 滑动验证结果校验 // SlideCheck 滑动验证结果校验
func (h *CaptchaHandler) SlideCheck(c *gin.Context) { func (h *CaptchaHandler) SlideCheck(c *gin.Context) {
if !h.service.GetConfig().Enabled {
resp.ERROR(c, "验证码服务未启用")
return
}
var data struct { var data struct {
Key string `json:"key"` Key string `json:"key"`
X int `json:"x"` X int `json:"x"`
@@ -9,6 +9,7 @@ package handler
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
@@ -19,18 +20,31 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type ChatRoleHandler struct { type ChatAppHandler struct {
BaseHandler BaseHandler
} }
func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler { func NewChatAppHandler(app *core.AppServer, db *gorm.DB) *ChatAppHandler {
return &ChatRoleHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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 获取用户聊天应用列表 // List 获取用户聊天应用列表
func (h *ChatRoleHandler) List(c *gin.Context) { func (h *ChatAppHandler) List(c *gin.Context) {
tid := h.GetInt(c, "tid", 0) tid := h.GetInt(c, "tid", 0)
var roles []model.ChatRole var roles []model.ChatApp
session := h.DB.Where("enable", true) session := h.DB.Where("enable", true)
if tid > 0 { if tid > 0 {
session = session.Where("tid", tid) session = session.Where("tid", tid)
@@ -41,9 +55,9 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
return return
} }
var roleVos = make([]vo.ChatRole, 0) var roleVos = make([]vo.ChatApp, 0)
for _, r := range roles { for _, r := range roles {
var v vo.ChatRole var v vo.ChatApp
err := utils.CopyObject(r, &v) err := utils.CopyObject(r, &v)
if err == nil { if err == nil {
v.Id = r.Id v.Id = r.Id
@@ -54,20 +68,22 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
} }
// ListByUser 获取用户添加的角色列表 // ListByUser 获取用户添加的角色列表
func (h *ChatRoleHandler) ListByUser(c *gin.Context) { func (h *ChatAppHandler) ListByUser(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
var roles []model.ChatRole var roles []model.ChatApp
session := h.DB.Where("enable", true) session := h.DB.Where("enable", true)
// 如果用户没登录,则获取所有角色 // 如果用户没登录,则获取所有角色
if userId > 0 { if userId > 0 {
var user model.User var user model.User
h.DB.First(&user, userId) h.DB.First(&user, userId)
var roleKeys []string var roleKeys []string
err := utils.JsonDecode(user.ChatRoles, &roleKeys) if user.ChatRoles != "" {
if err != nil { err := utils.JsonDecode(user.ChatRoles, &roleKeys)
resp.ERROR(c, "角色解析失败!") if err != nil {
return resp.ERROR(c, "角色解析失败!")
return
}
} }
// 保证用户至少有一个角色可用 // 保证用户至少有一个角色可用
if len(roleKeys) > 0 { if len(roleKeys) > 0 {
@@ -84,9 +100,9 @@ func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
return return
} }
var roleVos = make([]vo.ChatRole, 0) var roleVos = make([]vo.ChatApp, 0)
for _, r := range roles { for _, r := range roles {
var v vo.ChatRole var v vo.ChatApp
err := utils.CopyObject(r, &v) err := utils.CopyObject(r, &v)
if err == nil { if err == nil {
v.Id = r.Id v.Id = r.Id
@@ -96,8 +112,8 @@ func (h *ChatRoleHandler) ListByUser(c *gin.Context) {
resp.SUCCESS(c, roleVos) resp.SUCCESS(c, roleVos)
} }
// UpdateRole 更新用户聊天角色 // UpdateApp 更新用户聊天应用
func (h *ChatRoleHandler) UpdateRole(c *gin.Context) { func (h *ChatAppHandler) UpdateApp(c *gin.Context) {
user, err := h.GetLoginUser(c) user, err := h.GetLoginUser(c)
if err != nil { if err != nil {
resp.NotAuth(c) resp.NotAuth(c)
+6
View File
@@ -19,6 +19,12 @@ func NewChatAppTypeHandler(app *core.AppServer, db *gorm.DB) *ChatAppTypeHandler
return &ChatAppTypeHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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类型列表 // List 获取App类型列表
func (h *ChatAppTypeHandler) List(c *gin.Context) { func (h *ChatAppTypeHandler) List(c *gin.Context) {
var items []model.AppType var items []model.AppType
+514 -197
View File
@@ -14,59 +14,185 @@ import (
"errors" "errors"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/sora"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"html/template" "io"
"net/http" "net/http"
"net/url" "net/url"
"regexp" "os"
"path"
"strings" "strings"
"time" "time"
"unicode/utf8" "unicode/utf8"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/sashabaranov/go-openai"
"gorm.io/gorm" "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 { type ChatHandler struct {
BaseHandler BaseHandler
redis *redis.Client redis *redis.Client
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
licenseService *service.LicenseService ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function userService *service.UserService
ChatContexts *types.LMap[string, []interface{}] // 聊天上下文 Map [chatId] => []Message moderationManager *moderation.ServiceManager
userService *service.UserService userLocks *types.UserLockManager
soraService *sora.SoraService
} }
func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manager *oss.UploaderManager, licenseService *service.LicenseService, userService *service.UserService) *ChatHandler { 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{ return &ChatHandler{
BaseHandler: BaseHandler{App: app, DB: db}, BaseHandler: BaseHandler{App: app, DB: db},
redis: redis, redis: redis,
uploadManager: manager, uploadManager: manager,
licenseService: licenseService, ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), userService: userService,
ChatContexts: types.NewLMap[string, []interface{}](), moderationManager: moderationManager,
userService: userService, userLocks: types.NewUserLockManager(),
soraService: soraService,
} }
} }
func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSession, role model.ChatRole, prompt string, ws *types.WsClient) error { // RegisterRoutes 注册路由
if !h.App.Debug { func (h *ChatHandler) RegisterRoutes() {
defer func() { group := h.App.Engine.Group("/api/chat/")
if r := recover(); r != nil { // 其他接口需要用户授权
logger.Error("Recover message from error: ", r) 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 var user model.User
res := h.DB.Model(&model.User{}).First(&user, session.UserId) res := h.DB.Model(&model.User{}).First(&user, input.UserId)
if res.Error != nil { if res.Error != nil {
return errors.New("未授权用户,您正在进行非法操作!") return errors.New("未授权用户,您正在进行非法操作!")
} }
@@ -77,12 +203,12 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
return errors.New("User 对象转换失败," + err.Error()) return errors.New("User 对象转换失败," + err.Error())
} }
if userVo.Status == false { if !userVo.Status {
return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!") return errors.New("您的账号已经被禁用,如果疑问,请联系管理员!")
} }
if userVo.Power < session.Model.Power { if userVo.Power < input.ChatModel.Power {
return fmt.Errorf("您当前剩余算力 %d 已不足以支付当前模型的单次对话需要消耗的算力 %d,[立即购买](/member)。", userVo.Power, session.Model.Power) return fmt.Errorf("您的算力不足,请购买算力。")
} }
if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() { if userVo.ExpiredTime > 0 && userVo.ExpiredTime <= time.Now().Unix() {
@@ -90,29 +216,29 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
} }
// 检查 prompt 长度是否超过了当前模型允许的最大上下文长度 // 检查 prompt 长度是否超过了当前模型允许的最大上下文长度
promptTokens, err := utils.CalcTokens(prompt, session.Model.Value) promptTokens, _ := utils.CalcTokens(input.Prompt, input.ChatModel.Value)
if promptTokens > session.Model.MaxContext { if promptTokens > input.ChatModel.MaxContext {
return errors.New("对话内容超出了当前模型允许的最大上下文长度!") return errors.New("对话内容超出了当前模型允许的最大上下文长度!")
} }
var req = types.ApiRequest{ var req = types.ApiRequest{
Model: session.Model.Value, Model: input.ChatModel.Value,
Stream: input.Stream,
Temperature: input.ChatModel.Temperature,
} }
// 兼容 GPT-O1 模型 // 兼容 OpenAI 模型
if strings.HasPrefix(session.Model.Value, "o1-") { if strings.HasPrefix(input.ChatModel.Value, "o1-") ||
utils.SendChunkMsg(ws, "AI 正在思考...\n") strings.HasPrefix(input.ChatModel.Value, "o3-") ||
req.Stream = false strings.HasPrefix(input.ChatModel.Value, "gpt") {
session.Start = time.Now().Unix() req.MaxCompletionTokens = input.ChatModel.MaxTokens
} else { } else {
req.MaxTokens = session.Model.MaxTokens req.MaxTokens = input.ChatModel.MaxTokens
req.Temperature = session.Model.Temperature
req.Stream = session.Stream
} }
if len(session.Tools) > 0 && !strings.HasPrefix(session.Model.Value, "o1-") { if len(input.Tools) > 0 && !strings.HasPrefix(input.ChatModel.Value, "o1-") {
var items []model.Function var items []model.Function
res = h.DB.Where("enabled", true).Where("id IN ?", session.Tools).Find(&items) res = h.DB.Where("enabled", true).Where("id IN ?", input.Tools).Find(&items)
if res.Error == nil { if res.Error == nil {
var tools = make([]types.Tool, 0) var tools = make([]types.Tool, 0)
for _, v := range items { for _, v := range items {
@@ -143,25 +269,34 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
} }
// 加载聊天上下文 // 加载聊天上下文
chatCtx := make([]interface{}, 0) chatCtx := make([]any, 0)
messages := make([]interface{}, 0) messages := make([]any, 0)
if h.App.SysConfig.EnableContext { if h.App.SysConfig.Base.EnableContext {
if h.ChatContexts.Has(session.ChatId) { _ = utils.JsonDecode(input.ChatRole.Context, &messages)
messages = h.ChatContexts.Get(session.ChatId) if h.App.SysConfig.Base.ContextDeep > 0 {
} else { var historyMessages []model.ChatMessage
_ = utils.JsonDecode(role.Context, &messages) dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
if h.App.SysConfig.ContextDeep > 0 { if input.LastMsgId > 0 { // 重新生成和编辑逻辑
var historyMessages []model.ChatMessage var lastMessage model.ChatMessage
res := h.DB.Where("chat_id = ? and use_context = 1", session.ChatId).Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages) err = dbSession.Where("id < ?", input.LastMsgId).Where("type", types.ReplyMsg).Order("id DESC").First(&lastMessage).Error
if res.Error == nil { if err != nil {
for i := len(historyMessages) - 1; i >= 0; i-- { input.LastMsgId = 0
msg := historyMessages[i] } else {
ms := types.Message{Role: "user", Content: msg.Content} input.LastMsgId = lastMessage.Id
if msg.Type == types.ReplyMsg { }
ms.Role = "assistant" dbSession = dbSession.Where("id < ?", input.LastMsgId)
} // 删除对应的聊天记录
chatCtx = append(chatCtx, ms) 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)
} }
} }
} }
@@ -176,90 +311,144 @@ func (h *ChatHandler) sendMessage(ctx context.Context, session *types.ChatSessio
v := messages[i] v := messages[i]
tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model) tks, _ = utils.CalcTokens(utils.JsonEncode(v), req.Model)
// 上下文 token 超出了模型的最大上下文长度 // 上下文 token 超出了模型的最大上下文长度
if tokens+tks >= session.Model.MaxContext { if tokens+tks >= input.ChatModel.MaxContext {
break break
} }
// 上下文的深度超出了模型的最大上下文深度 // 上下文的深度超出了模型的最大上下文深度
if len(chatCtx) >= h.App.SysConfig.ContextDeep { if len(chatCtx) >= h.App.SysConfig.Base.ContextDeep {
break break
} }
tokens += tks tokens += tks
chatCtx = append(chatCtx, v) chatCtx = append(chatCtx, v)
} }
logger.Debugf("聊天上下文:%+v", chatCtx)
} }
reqMgs := make([]interface{}, 0) 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-- { for i := len(chatCtx) - 1; i >= 0; i-- {
reqMgs = append(reqMgs, chatCtx[i]) reqMgs = append(reqMgs, chatCtx[i])
} }
fullPrompt := prompt fileContents := make([]string, 0) // 文件内容
text := prompt var finalPrompt = input.Prompt
// extract files in prompt imgList := make([]any, 0)
files := utils.ExtractFileURLs(prompt) for _, file := range input.Files {
logger.Debugf("detected FILES: %+v", files) logger.Debugf("detected file: %+v", file.URL)
// 如果不是逆向模型,则提取文件内容 // 处理图片
if len(files) > 0 && !(session.Model.Value == "gpt-4-all" || if isImageURL(file.URL) || isVideoURL(file.URL) {
strings.HasPrefix(session.Model.Value, "gpt-4-gizmo") || imgList = append(imgList, gin.H{
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", "type": "image_url",
"image_url": gin.H{ "image_url": gin.H{
"url": v, "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)
}
} }
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) 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
}
return h.sendOpenAiMessage(req, userVo, ctx, session, role, prompt, ws) 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 数量 // Tokens 统计 token 数量
@@ -275,17 +464,17 @@ func (h *ChatHandler) Tokens(c *gin.Context) {
} }
// 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文) // 如果没有传入 text 字段,则说明是获取当前 reply 总的 token 消耗(带上下文)
//if data.Text == "" && data.ChatId != "" { if data.Text == "" && data.ChatId != "" {
// var item model.ChatMessage var item model.ChatMessage
// userId, _ := c.Get(types.LoginUserID) userId, _ := c.Get(types.LoginUserID)
// res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).Last(&item) res := h.DB.Where("user_id = ?", userId).Where("chat_id = ?", data.ChatId).Last(&item)
// if res.Error != nil { if res.Error != nil {
// resp.ERROR(c, res.Error.Error()) resp.ERROR(c, res.Error.Error())
// return return
// } }
// resp.SUCCESS(c, item.Tokens) resp.SUCCESS(c, item.Tokens)
// return return
//} }
tokens, err := utils.CalcTokens(data.Text, data.Model) tokens, err := utils.CalcTokens(data.Text, data.Model)
if err != nil { if err != nil {
@@ -328,27 +517,27 @@ func (h *ChatHandler) StopGenerate(c *gin.Context) {
// 发送请求到 OpenAI 服务器 // 发送请求到 OpenAI 服务器
// useOwnApiKey: 是否使用了用户自己的 API KEY // useOwnApiKey: 是否使用了用户自己的 API KEY
func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, session *types.ChatSession, apiKey *model.ApiKey) (*http.Response, error) { 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 the chat model bind a KEY, use it directly
if session.Model.KeyId > 0 { if input.ChatModel.KeyId > 0 {
h.DB.Where("id", session.Model.KeyId).Find(apiKey) h.DB.Where("id", input.ChatModel.KeyId).Where("enabled", true).Find(apiKey)
} } else { // use the last unused key
// use the last unused key
if apiKey.Id == 0 {
h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey) h.DB.Where("type", "chat").Where("enabled", true).Order("last_used_at ASC").First(apiKey)
} }
if apiKey.Id == 0 { if apiKey.Id == 0 {
return nil, errors.New("no available key, please import key") 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("对话请求消息体:%+v", req) logger.Debugf("对话请求消息体:%+v", req)
var apiURL string
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) 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 请求对象 // 创建 HttpClient 请求对象
var client *http.Client var client *http.Client
requestBody, err := json.Marshal(req) requestBody, err := json.Marshal(req)
@@ -380,16 +569,16 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, sessi
} }
// 扣减用户算力 // 扣减用户算力
func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, promptTokens int, replyTokens int) { func (h *ChatHandler) subUserPower(userVo vo.User, input ChatInput, promptTokens int, replyTokens int) {
power := 1 power := 1
if session.Model.Power > 0 { if input.ChatModel.Power > 0 {
power = session.Model.Power power = input.ChatModel.Power
} }
err := h.userService.DecreasePower(int(userVo.Id), power, model.PowerLog{ err := h.userService.DecreasePower(userVo.Id, power, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: session.Model.Value, Model: input.ChatModel.Value,
Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", session.Model.Name, promptTokens, replyTokens), Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d,回复长度:%d", input.ChatModel.Name, promptTokens, replyTokens),
}) })
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
@@ -397,22 +586,43 @@ func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, p
} }
func (h *ChatHandler) saveChatHistory( func (h *ChatHandler) saveChatHistory(
c *gin.Context,
req types.ApiRequest, req types.ApiRequest,
usage Usage, usage Usage,
message types.Message, message types.Message,
session *types.ChatSession, input ChatInput,
role model.ChatRole,
userVo vo.User, userVo vo.User,
promptCreatedAt time.Time, promptCreatedAt time.Time,
replyCreatedAt time.Time) { replyCreatedAt time.Time) {
// 更新上下文消息 // 文本审核
if h.App.SysConfig.EnableContext { if h.App.SysConfig.Moderation.Enable {
chatCtx := req.Messages // 提问消息 moderationResult, err := h.moderationManager.GetService().Moderate(usage.Content)
chatCtx = append(chatCtx, message) // 回复消息 if err != nil {
h.ChatContexts.Put(session.ChatId, chatCtx) 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 // for prompt
var promptTokens, replyTokens, totalTokens int var promptTokens, replyTokens, totalTokens int
@@ -423,12 +633,15 @@ func (h *ChatHandler) saveChatHistory(
} }
historyUserMsg := model.ChatMessage{ historyUserMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: input.ChatId,
RoleId: role.Id, RoleId: input.RoleId,
Type: types.PromptMsg, Type: types.PromptMsg,
Icon: userVo.Avatar, Icon: userVo.Avatar,
Content: template.HTMLEscapeString(usage.Prompt), Content: utils.JsonEncode(vo.MsgContent{
Text: usage.Prompt,
Files: input.Files,
}),
Tokens: promptTokens, Tokens: promptTokens,
TotalTokens: promptTokens, TotalTokens: promptTokens,
UseContext: true, UseContext: true,
@@ -450,13 +663,26 @@ func (h *ChatHandler) saveChatHistory(
replyTokens, _ = utils.CalcTokens(message.Content, req.Model) replyTokens, _ = utils.CalcTokens(message.Content, req.Model)
totalTokens = replyTokens + getTotalTokens(req) 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{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: session.ChatId, ChatId: input.ChatId,
RoleId: role.Id, RoleId: input.RoleId,
Type: types.ReplyMsg, Type: types.ReplyMsg,
Icon: role.Icon, Icon: input.ChatRole.Icon,
Content: usage.Content, Content: utils.JsonEncode(vo.MsgContent{
Text: message.Content,
Files: files,
}),
Tokens: replyTokens, Tokens: replyTokens,
TotalTokens: totalTokens, TotalTokens: totalTokens,
UseContext: true, UseContext: true,
@@ -469,18 +695,35 @@ func (h *ChatHandler) saveChatHistory(
logger.Error("failed to save reply history message: ", err) 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 session.Model.Power > 0 { if input.ChatModel.Power > 0 {
h.subUserPower(userVo, session, promptTokens, replyTokens) h.subUserPower(userVo, input, promptTokens, replyTokens)
} }
// 保存当前会话 // 保存当前会话
var chatItem model.ChatItem var chatItem model.ChatItem
err = h.DB.Where("chat_id = ?", session.ChatId).First(&chatItem).Error err = h.DB.Where("chat_id = ?", input.ChatId).First(&chatItem).Error
if err != nil { if err != nil {
chatItem.ChatId = session.ChatId chatItem.ChatId = input.ChatId
chatItem.UserId = userVo.Id chatItem.UserId = userVo.Id
chatItem.RoleId = role.Id chatItem.RoleId = input.RoleId
chatItem.ModelId = session.Model.Id chatItem.ModelId = input.ModelId
if utf8.RuneCountInString(usage.Prompt) > 30 { if utf8.RuneCountInString(usage.Prompt) > 30 {
chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..." chatItem.Title = string([]rune(usage.Prompt)[:30]) + "..."
} else { } else {
@@ -494,28 +737,102 @@ func (h *ChatHandler) saveChatHistory(
} }
} }
// 将AI回复消息中生成的图片链接下载到本地 // TextToSpeech 文本生成语音
func (h *ChatHandler) extractImgUrl(text string) string { func (h *ChatHandler) TextToSpeech(c *gin.Context) {
pattern := `!\[([^\]]*)]\(([^)]+)\)` var data struct {
re := regexp.MustCompile(pattern) ModelId int `json:"model_id"`
matches := re.FindAllStringSubmatch(text, -1) Text string `json:"text"`
}
// 下载图片并替换链接地址 if err := c.ShouldBindJSON(&data); err != nil {
for _, match := range matches { resp.ERROR(c, types.InvalidArgs)
imageURL := match[2] return
logger.Debug(imageURL) }
// 对于相同地址的图片,已经被替换了,就不再重复下载了
if !strings.Contains(text, imageURL) { textHash := utils.Sha256(fmt.Sprintf("%d/%s", data.ModelId, data.Text))
continue audioFile := fmt.Sprintf("%s/audio", h.App.Config.StaticDir)
} if _, err := os.Stat(audioFile); err != nil {
resp.ERROR(c, err.Error())
newImgURL, err := h.uploadManager.GetUploadHandler().PutUrlFile(imageURL, false) return
if err != nil { }
logger.Error("error with download image: ", err)
continue if err := os.MkdirAll(audioFile, 0755); err != nil {
} resp.ERROR(c, err.Error())
return
text = strings.ReplaceAll(text, imageURL, newImgURL) }
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)
} }
return text
} }
+18 -15
View File
@@ -20,6 +20,7 @@ import (
// List 获取会话列表 // List 获取会话列表
func (h *ChatHandler) List(c *gin.Context) { func (h *ChatHandler) List(c *gin.Context) {
logger.Info(h.GetLoginUserId(c))
if !h.IsLogin(c) { if !h.IsLogin(c) {
resp.SUCCESS(c) resp.SUCCESS(c)
return return
@@ -28,7 +29,7 @@ func (h *ChatHandler) List(c *gin.Context) {
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
var items = make([]vo.ChatItem, 0) var items = make([]vo.ChatItem, 0)
var chats []model.ChatItem var chats []model.ChatItem
h.DB.Where("user_id", userId).Order("id DESC").Find(&chats) h.DB.Debug().Where("user_id", userId).Order("id DESC").Find(&chats)
if len(chats) == 0 { if len(chats) == 0 {
resp.SUCCESS(c, items) resp.SUCCESS(c, items)
return return
@@ -41,9 +42,9 @@ func (h *ChatHandler) List(c *gin.Context) {
modelValues = append(modelValues, chat.Model) modelValues = append(modelValues, chat.Model)
} }
var roles []model.ChatRole var roles []model.ChatApp
var models []model.ChatModel var models []model.ChatModel
roleMap := make(map[uint]model.ChatRole) roleMap := make(map[uint]model.ChatApp)
modelMap := make(map[string]model.ChatModel) modelMap := make(map[string]model.ChatModel)
h.DB.Where("id IN ?", roleIds).Find(&roles) h.DB.Where("id IN ?", roleIds).Find(&roles)
h.DB.Where("value IN ?", modelValues).Find(&models) h.DB.Where("value IN ?", modelValues).Find(&models)
@@ -104,8 +105,6 @@ func (h *ChatHandler) Clear(c *gin.Context) {
var chatIds = make([]string, 0) var chatIds = make([]string, 0)
for _, chat := range chats { for _, chat := range chats {
chatIds = append(chatIds, chat.ChatId) chatIds = append(chatIds, chat.ChatId)
// 清空会话上下文
h.ChatContexts.Delete(chat.ChatId)
} }
err = h.DB.Transaction(func(tx *gorm.DB) error { err = h.DB.Transaction(func(tx *gorm.DB) error {
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{}) res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
@@ -133,20 +132,28 @@ func (h *ChatHandler) Clear(c *gin.Context) {
func (h *ChatHandler) History(c *gin.Context) { func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID chatId := c.Query("chat_id") // 会话 ID
var items []model.ChatMessage 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) res := h.DB.Where("chat_id = ?", chatId).Find(&items)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "No history message") resp.ERROR(c, "No history message")
return return
} else { } else {
for _, item := range items { for _, item := range items {
var v vo.HistoryMessage var v vo.ChatMessage
err := utils.CopyObject(item, &v) 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.CreatedAt = item.CreatedAt.Unix()
v.UpdatedAt = item.UpdatedAt.Unix() v.UpdatedAt = item.UpdatedAt.Unix()
if err == nil { messages = append(messages, v)
messages = append(messages, v)
}
} }
} }
@@ -179,10 +186,6 @@ func (h *ChatHandler) Remove(c *gin.Context) {
return return
} }
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
// 清空会话上下文
h.ChatContexts.Delete(chatId)
resp.SUCCESS(c, types.OkMsg) resp.SUCCESS(c, types.OkMsg)
} }
@@ -202,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) res = h.DB.Where("id", chatItem.RoleId).First(&role)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "Role not found") resp.ERROR(c, "Role not found")
+10 -2
View File
@@ -26,18 +26,26 @@ func NewChatModelHandler(app *core.AppServer, db *gorm.DB) *ChatModelHandler {
return &ChatModelHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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 模型列表 // List 模型列表
func (h *ChatModelHandler) List(c *gin.Context) { func (h *ChatModelHandler) List(c *gin.Context) {
var items []model.ChatModel var items []model.ChatModel
var chatModels = make([]vo.ChatModel, 0) var chatModels = make([]vo.ChatModel, 0)
session := h.DB.Session(&gorm.Session{}).Where("type", "chat").Where("enabled", true) session := h.DB.Session(&gorm.Session{}).Where("enabled", true)
t := c.Query("type") t := c.Query("type")
if t != "" { if t != "" {
session = session.Where("type", t) session = session.Where("type", t)
} else {
session = session.Where("type", "chat")
} }
session = session.Where("open", true) session = session.Where("open", true)
if h.IsLogin(c) { if h.IsLogin(c) && t == "chat" {
user, _ := h.GetLoginUser(c) user, _ := h.GetLoginUser(c)
var models []int var models []int
err := utils.JsonDecode(user.ChatModels, &models) err := utils.JsonDecode(user.ChatModels, &models)
+49 -25
View File
@@ -21,6 +21,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/gin-gonic/gin"
req2 "github.com/imroc/req/v3" req2 "github.com/imroc/req/v3"
) )
@@ -55,18 +56,16 @@ func (h *ChatHandler) sendOpenAiMessage(
req types.ApiRequest, req types.ApiRequest,
userVo vo.User, userVo vo.User,
ctx context.Context, ctx context.Context,
session *types.ChatSession, input ChatInput,
role model.ChatRole, c *gin.Context) error {
prompt string,
ws *types.WsClient) error {
promptCreatedAt := time.Now() // 记录提问时间 promptCreatedAt := time.Now() // 记录提问时间
start := time.Now() start := time.Now()
var apiKey = model.ApiKey{} var apiKey = model.ApiKey{}
response, err := h.doRequest(ctx, req, session, &apiKey) response, err := h.doRequest(ctx, req, input, &apiKey)
logger.Info("HTTP请求完成,耗时:", time.Now().Sub(start)) logger.Info("HTTP请求完成,耗时:", time.Since(start))
if err != nil { if err != nil {
if strings.Contains(err.Error(), "context canceled") { if strings.Contains(err.Error(), "context canceled") {
return fmt.Errorf("用户取消了请求:%s", prompt) return fmt.Errorf("用户取消了请求:%s", input.Prompt)
} else if strings.Contains(err.Error(), "no available key") { } else if strings.Contains(err.Error(), "no available key") {
return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!") return errors.New("抱歉😔😔😔,系统已经没有可用的 API KEY,请联系管理员!")
} }
@@ -89,6 +88,8 @@ func (h *ChatHandler) sendOpenAiMessage(
var function model.Function var function model.Function
var toolCall = false var toolCall = false
var arguments = make([]string, 0) var arguments = make([]string, 0)
var reasoning = false
scanner := bufio.NewScanner(response.Body) scanner := bufio.NewScanner(response.Body)
for scanner.Scan() { for scanner.Scan() {
line := scanner.Text() line := scanner.Text()
@@ -103,12 +104,14 @@ func (h *ChatHandler) sendOpenAiMessage(
if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行 if len(responseBody.Choices) == 0 { // Fixed: 兼容 Azure API 第一个输出空行
continue continue
} }
if responseBody.Choices[0].Delta.Content == nil && responseBody.Choices[0].Delta.ToolCalls == nil { if responseBody.Choices[0].Delta.Content == nil &&
responseBody.Choices[0].Delta.ToolCalls == nil &&
responseBody.Choices[0].Delta.ReasoningContent == "" {
continue continue
} }
if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 { if responseBody.Choices[0].FinishReason == "stop" && len(contents) == 0 {
utils.SendChunkMsg(ws, "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。") pushMessage(c, "text", "抱歉😔😔😔,AI助手由于未知原因已经停止输出内容。")
break break
} }
@@ -136,7 +139,7 @@ func (h *ChatHandler) sendOpenAiMessage(
if res.Error == nil { if res.Error == nil {
toolCall = true toolCall = true
callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label) callMsg := fmt.Sprintf("正在调用工具 `%s` 作答 ...\n\n", function.Label)
utils.SendChunkMsg(ws, callMsg) pushMessage(c, "text", callMsg)
contents = append(contents, callMsg) contents = append(contents, callMsg)
} }
continue continue
@@ -150,23 +153,39 @@ func (h *ChatHandler) sendOpenAiMessage(
// output stopped // output stopped
if responseBody.Choices[0].FinishReason != "" { if responseBody.Choices[0].FinishReason != "" {
break // 输出完成或者输出中断了 break // 输出完成或者输出中断了
} else { } else { // 正常输出结果
content := responseBody.Choices[0].Delta.Content // 兼容思考过程
contents = append(contents, utils.InterfaceToString(content)) if responseBody.Choices[0].Delta.ReasoningContent != "" {
utils.SendChunkMsg(ws, responseBody.Choices[0].Delta.Content) 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 } // end for
if err := scanner.Err(); err != nil { if err := scanner.Err(); err != nil {
if strings.Contains(err.Error(), "context canceled") { if strings.Contains(err.Error(), "context canceled") {
logger.Info("用户取消了请求:", prompt) logger.Info("用户取消了请求:", input.Prompt)
} else { } else {
logger.Error("信息读取出错:", err) logger.Error("信息读取出错:", err)
} }
} }
if toolCall { // 调用函数完成任务 if toolCall { // 调用函数完成任务
params := make(map[string]interface{}) params := make(map[string]any)
_ = utils.JsonDecode(strings.Join(arguments, ""), &params) _ = utils.JsonDecode(strings.Join(arguments, ""), &params)
logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params) logger.Debugf("函数名称: %s, 函数参数:%s", function.Name, params)
params["user_id"] = userVo.Id params["user_id"] = userVo.Id
@@ -193,21 +212,29 @@ func (h *ChatHandler) sendOpenAiMessage(
} else { } else {
errMsg = utils.InterfaceToString(apiRes.Data) errMsg = utils.InterfaceToString(apiRes.Data)
contents = append(contents, errMsg) 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),
})
}
} }
utils.SendChunkMsg(ws, errMsg) pushMessage(c, "text", errMsg)
} }
// 消息发送成功 // 消息发送成功
if len(contents) > 0 { if len(contents) > 0 {
usage := Usage{ usage := Usage{
Prompt: prompt, Prompt: input.Prompt,
Content: strings.Join(contents, ""), Content: strings.Join(contents, ""),
PromptTokens: 0, PromptTokens: 0,
CompletionTokens: 0, CompletionTokens: 0,
TotalTokens: 0, TotalTokens: 0,
} }
message.Content = usage.Content message.Content = usage.Content
h.saveChatHistory(req, usage, message, session, role, userVo, promptCreatedAt, replyCreatedAt) h.saveChatHistory(c, req, usage, message, input, userVo, promptCreatedAt, replyCreatedAt)
} }
} else { // 非流式输出 } else { // 非流式输出
var respVo OpenAIResVo var respVo OpenAIResVo
@@ -220,13 +247,10 @@ func (h *ChatHandler) sendOpenAiMessage(
return fmt.Errorf("解析响应失败:%v", body) return fmt.Errorf("解析响应失败:%v", body)
} }
content := respVo.Choices[0].Message.Content content := respVo.Choices[0].Message.Content
if strings.HasPrefix(req.Model, "o1-") { pushMessage(c, "text", content)
content = fmt.Sprintf("AI思考结束,耗时:%d 秒。\n%s", time.Now().Unix()-session.Start, respVo.Choices[0].Message.Content) respVo.Usage.Prompt = input.Prompt
}
utils.SendChunkMsg(ws, content)
respVo.Usage.Prompt = prompt
respVo.Usage.Content = content respVo.Usage.Content = content
h.saveChatHistory(req, respVo.Usage, respVo.Choices[0].Message, session, role, userVo, promptCreatedAt, time.Now()) h.saveChatHistory(c, req, respVo.Usage, respVo.Choices[0].Message, input, userVo, promptCreatedAt, time.Now())
} }
return nil return nil
+13 -13
View File
@@ -9,7 +9,6 @@ package handler
import ( import (
"geekai/core" "geekai/core"
"geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
@@ -20,25 +19,32 @@ import (
type ConfigHandler struct { type ConfigHandler struct {
BaseHandler BaseHandler
licenseService *service.LicenseService
} }
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler { func NewConfigHandler(app *core.AppServer, db *gorm.DB) *ConfigHandler {
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService} 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 获取指定的系统配置 // Get 获取指定的系统配置
func (h *ConfigHandler) Get(c *gin.Context) { func (h *ConfigHandler) Get(c *gin.Context) {
key := c.Query("key") key := c.Query("key")
var config model.Config var config model.Config
res := h.DB.Where("marker", key).First(&config) res := h.DB.Where("name", key).First(&config)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, res.Error.Error()) resp.ERROR(c, res.Error.Error())
return return
} }
var value map[string]interface{} var value map[string]any
err := utils.JsonDecode(config.Config, &value) err := utils.JsonDecode(config.Value, &value)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -46,9 +52,3 @@ func (h *ConfigHandler) Get(c *gin.Context) {
resp.SUCCESS(c, value) resp.SUCCESS(c, value)
} }
// License 获取 License 配置
func (h *ConfigHandler) License(c *gin.Context) {
license := h.licenseService.GetLicense()
resp.SUCCESS(c, license.Configs)
}
+59 -14
View File
@@ -10,9 +10,11 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/dalle" "geekai/service/dalle"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
@@ -25,16 +27,18 @@ import (
type DallJobHandler struct { type DallJobHandler struct {
BaseHandler BaseHandler
dallService *dalle.Service dallService *dalle.Service
uploader *oss.UploaderManager uploader *oss.UploaderManager
userService *service.UserService 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{ return &DallJobHandler{
dallService: service, dallService: service,
uploader: manager, uploader: manager,
userService: userService, userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: app, App: app,
DB: db, DB: db,
@@ -42,6 +46,24 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service,
} }
} }
// 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)
}
}
// Image 创建一个绘画任务 // Image 创建一个绘画任务
func (h *DallJobHandler) Image(c *gin.Context) { func (h *DallJobHandler) Image(c *gin.Context) {
var data types.DallTask var data types.DallTask
@@ -50,6 +72,29 @@ func (h *DallJobHandler) Image(c *gin.Context) {
return 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 var chatModel model.ChatModel
if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil { if res := h.DB.Where("id = ?", data.ModelId).First(&chatModel); res.Error != nil {
resp.ERROR(c, "模型不存在") resp.ERROR(c, "模型不存在")
@@ -70,15 +115,15 @@ func (h *DallJobHandler) Image(c *gin.Context) {
idValue, _ := c.Get(types.LoginUserID) idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0) userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
task := types.DallTask{ task := types.DallTask{
ClientId: data.ClientId,
UserId: uint(userId), UserId: uint(userId),
ModelId: chatModel.Id, ModelId: chatModel.Id,
ModelName: chatModel.Value, ModelName: chatModel.Name,
ModelValue: chatModel.Value,
Image: data.Image,
Prompt: data.Prompt, Prompt: data.Prompt,
Quality: data.Quality, AspectRatio: data.AspectRatio,
Size: data.Size, Size: data.Size,
Style: data.Style, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
TranslateModelId: h.App.SysConfig.TranslateModelId,
Power: chatModel.Power, Power: chatModel.Power,
} }
job := model.DallJob{ job := model.DallJob{
@@ -97,9 +142,9 @@ func (h *DallJobHandler) Image(c *gin.Context) {
h.dallService.PushTask(task) h.dallService.PushTask(task)
// 扣减算力 // 扣减算力
err = h.userService.DecreasePower(int(user.Id), chatModel.Power, model.PowerLog{ err = h.userService.DecreasePower(user.Id, chatModel.Power, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: chatModel.Value, Model: chatModel.Name,
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)), Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(task.Prompt, 10)),
}) })
if err != nil { if err != nil {
+33 -32
View File
@@ -30,7 +30,6 @@ import (
type FunctionHandler struct { type FunctionHandler struct {
BaseHandler BaseHandler
config types.ApiConfig
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
dallService *dalle.Service dallService *dalle.Service
userService *service.UserService userService *service.UserService
@@ -48,13 +47,23 @@ func NewFunctionHandler(
App: server, App: server,
DB: db, DB: db,
}, },
config: config.ApiConfig,
uploadManager: manager, uploadManager: manager,
dallService: dallService, dallService: dallService,
userService: userService, 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 { type resVo struct {
Code types.BizCode `json:"code"` Code types.BizCode `json:"code"`
Message string `json:"message"` Message string `json:"message"`
@@ -106,16 +115,10 @@ func (h *FunctionHandler) WeiBo(c *gin.Context) {
return return
} }
if h.config.Token == "" { url := fmt.Sprintf("%s/api/weibo/fetch", types.GeekAPIURL)
resp.ERROR(c, "无效的 API Token")
return
}
url := fmt.Sprintf("%s/api/weibo/fetch", h.config.ApiURL)
var res resVo var res resVo
r, err := req.C().R(). r, err := req.C().R().
SetHeader("AppId", h.config.AppId). SetHeader("Authorization", "Bearer geekai-plus").
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
SetSuccessResult(&res).Get(url) SetSuccessResult(&res).Get(url)
if err != nil { if err != nil {
resp.ERROR(c, fmt.Sprintf("%v", err)) resp.ERROR(c, fmt.Sprintf("%v", err))
@@ -145,16 +148,10 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
return return
} }
if h.config.Token == "" { url := fmt.Sprintf("%s/api/zaobao/fetch", types.GeekAPIURL)
resp.ERROR(c, "无效的 API Token")
return
}
url := fmt.Sprintf("%s/api/zaobao/fetch", h.config.ApiURL)
var res resVo var res resVo
r, err := req.C().R(). r, err := req.C().R().
SetHeader("AppId", h.config.AppId). SetHeader("Authorization", "Bearer geekai-plus").
SetHeader("Authorization", fmt.Sprintf("Bearer %s", h.config.Token)).
SetSuccessResult(&res).Get(url) SetSuccessResult(&res).Get(url)
if err != nil { if err != nil {
resp.ERROR(c, fmt.Sprintf("%v", err)) resp.ERROR(c, fmt.Sprintf("%v", err))
@@ -192,16 +189,23 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
return 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) logger.Debugf("绘画参数:%+v", params)
var user model.User 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 { if res.Error != nil {
resp.ERROR(c, "当前用户不存在!") resp.ERROR(c, "当前用户不存在!")
return return
} }
if user.Power < h.App.SysConfig.DallPower { if user.Power < chatModel.Power {
resp.ERROR(c, "创建 DALL-E 绘图任务失败,算力不足") resp.ERROR(c, "创建绘图任务失败,算力不足")
return return
} }
@@ -210,24 +214,21 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
task := types.DallTask{ task := types.DallTask{
UserId: user.Id, UserId: user.Id,
Prompt: prompt, Prompt: prompt,
ModelId: 0, ModelId: chatModel.Id,
ModelName: "dall-e-3", ModelValue: chatModel.Value,
TranslateModelId: h.App.SysConfig.TranslateModelId, ModelName: chatModel.Name,
N: 1, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
Quality: "standard", Power: chatModel.Power,
Size: "1024x1024",
Style: "vivid",
Power: h.App.SysConfig.DallPower,
} }
job := model.DallJob{ job := model.DallJob{
UserId: user.Id, UserId: user.Id,
Prompt: prompt, Prompt: prompt,
Power: h.App.SysConfig.DallPower, Power: chatModel.Power,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
} }
err := h.DB.Create(&job).Error err := h.DB.Create(&job).Error
if err != nil { if err != nil {
resp.ERROR(c, "创建 DALL-E 绘图任务失败:"+err.Error()) resp.ERROR(c, "创建绘图任务失败:"+err.Error())
return return
} }
@@ -239,7 +240,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
} }
// 扣减算力 // 扣减算力
err = h.userService.DecreasePower(int(user.Id), job.Power, model.PowerLog{ err = h.userService.DecreasePower(user.Id, job.Power, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: task.ModelName, Model: task.ModelName,
Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)), Remark: fmt.Sprintf("绘画提示词:%s", utils.CutWords(job.Prompt, 10)),
+134 -14
View File
@@ -8,14 +8,18 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"strings"
) )
// InviteHandler 用户邀请 // InviteHandler 用户邀请
@@ -27,6 +31,23 @@ func NewInviteHandler(app *core.AppServer, db *gorm.DB) *InviteHandler {
return &InviteHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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 获取当前用户邀请码 // Code 获取当前用户邀请码
func (h *InviteHandler) Code(c *gin.Context) { func (h *InviteHandler) Code(c *gin.Context) {
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
@@ -65,21 +86,34 @@ func (h *InviteHandler) List(c *gin.Context) {
var total int64 var total int64
session.Model(&model.InviteLog{}).Count(&total) session.Model(&model.InviteLog{}).Count(&total)
var items []model.InviteLog var items []model.InviteLog
var list = make([]vo.InviteLog, 0)
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
res := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items) err := session.Order("id DESC").Offset(offset).Limit(pageSize).Find(&items).Error
if res.Error == nil { if err != nil {
for _, item := range items { resp.ERROR(c, err.Error())
var v vo.InviteLog return
err := utils.CopyObject(item, &v) }
if err == nil {
v.Id = item.Id userIds := make([]uint, 0)
v.CreatedAt = item.CreatedAt.Unix() for _, item := range items {
list = append(list, v) userIds = append(userIds, item.UserId)
} else { }
logger.Error(err) 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)) 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)) h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("hits", gorm.Expr("hits + ?", 1))
resp.SUCCESS(c) 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,
})
}
+14 -1
View File
@@ -10,11 +10,13 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -34,6 +36,17 @@ func NewMarkMapHandler(app *core.AppServer, db *gorm.DB, userService *service.Us
} }
} }
// 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 生成思维导图 // Generate 生成思维导图
func (h *MarkMapHandler) Generate(c *gin.Context) { func (h *MarkMapHandler) Generate(c *gin.Context) {
var data struct { var data struct {
@@ -95,7 +108,7 @@ func (h *MarkMapHandler) Generate(c *gin.Context) {
// 扣减算力 // 扣减算力
if chatModel.Power > 0 { if chatModel.Power > 0 {
err = h.userService.DecreasePower(int(userId), chatModel.Power, model.PowerLog{ err = h.userService.DecreasePower(userId, chatModel.Power, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: chatModel.Value, Model: chatModel.Value,
Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value), Remark: fmt.Sprintf("AI绘制思维导图,模型名称:%s, ", chatModel.Value),
+26 -1
View File
@@ -13,6 +13,7 @@ import (
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -25,6 +26,13 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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 数据列表 // List 数据列表
func (h *MenuHandler) List(c *gin.Context) { func (h *MenuHandler) List(c *gin.Context) {
index := h.GetBool(c, "index") index := h.GetBool(c, "index")
@@ -33,7 +41,7 @@ func (h *MenuHandler) List(c *gin.Context) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
session = session.Where("enabled", true) session = session.Where("enabled", true)
if index { 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) res := session.Order("sort_num ASC").Find(&items)
if res.Error == nil { if res.Error == nil {
@@ -47,3 +55,20 @@ func (h *MenuHandler) List(c *gin.Context) {
} }
resp.SUCCESS(c, list) 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)
}
+67 -26
View File
@@ -10,9 +10,11 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/mj" "geekai/service/mj"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
@@ -27,18 +29,20 @@ import (
type MidJourneyHandler struct { type MidJourneyHandler struct {
BaseHandler BaseHandler
mjService *mj.Service mjService *mj.Service
snowflake *service.Snowflake snowflake *service.Snowflake
uploader *oss.UploaderManager uploader *oss.UploaderManager
userService *service.UserService 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{ return &MidJourneyHandler{
snowflake: snowflake, snowflake: snowflake,
mjService: service, mjService: service,
uploader: manager, uploader: manager,
userService: userService, userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: app, App: app,
DB: db, DB: db,
@@ -46,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 { func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
user, err := h.GetLoginUser(c) user, err := h.GetLoginUser(c)
if err != nil { if err != nil {
@@ -53,7 +76,7 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
return false return false
} }
if user.Power < h.App.SysConfig.MjPower { if user.Power < h.App.SysConfig.Base.MjPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false return false
} }
@@ -66,7 +89,6 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
func (h *MidJourneyHandler) Image(c *gin.Context) { func (h *MidJourneyHandler) Image(c *gin.Context) {
var data struct { var data struct {
TaskType string `json:"task_type"` TaskType string `json:"task_type"`
ClientId string `json:"client_id"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
NegPrompt string `json:"neg_prompt"` NegPrompt string `json:"neg_prompt"`
Rate string `json:"rate"` Rate string `json:"rate"`
@@ -91,6 +113,29 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return 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 = "" var params = ""
if data.Rate != "" && !strings.Contains(params, "--ar") { if data.Rate != "" && !strings.Contains(params, "--ar") {
params += " --ar " + data.Rate params += " --ar " + data.Rate
@@ -153,7 +198,6 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return return
} }
task := types.MjTask{ task := types.MjTask{
ClientId: data.ClientId,
TaskId: taskId, TaskId: taskId,
Type: types.TaskType(data.TaskType), Type: types.TaskType(data.TaskType),
Prompt: data.Prompt, Prompt: data.Prompt,
@@ -161,17 +205,17 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
Params: params, Params: params,
UserId: userId, UserId: userId,
ImgArr: data.ImgArr, ImgArr: data.ImgArr,
Mode: h.App.SysConfig.MjMode, Mode: h.App.SysConfig.Base.MjMode,
TranslateModelId: h.App.SysConfig.TranslateModelId, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
} }
job := model.MidJourneyJob{ job := model.MidJourneyJob{
Type: data.TaskType, Type: data.TaskType,
UserId: userId, UserId: uint(userId),
TaskId: taskId, TaskId: taskId,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
Progress: 0, Progress: 0,
Prompt: fmt.Sprintf("%s %s", data.Prompt, params), Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
Power: h.App.SysConfig.MjPower, Power: h.App.SysConfig.Base.MjPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
opt := "绘图" opt := "绘图"
@@ -207,7 +251,6 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
type reqVo struct { type reqVo struct {
Index int `json:"index"` Index int `json:"index"`
ClientId string `json:"client_id"`
ChannelId string `json:"channel_id"` ChannelId string `json:"channel_id"`
MessageId string `json:"message_id"` MessageId string `json:"message_id"`
MessageHash string `json:"message_hash"` MessageHash string `json:"message_hash"`
@@ -229,22 +272,21 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
userId := utils.IntValue(utils.InterfaceToString(idValue), 0) userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, _ := h.snowflake.Next(true) taskId, _ := h.snowflake.Next(true)
task := types.MjTask{ task := types.MjTask{
ClientId: data.ClientId,
Type: types.TaskUpscale, Type: types.TaskUpscale,
UserId: userId, UserId: userId,
ChannelId: data.ChannelId, ChannelId: data.ChannelId,
Index: data.Index, Index: data.Index,
MessageId: data.MessageId, MessageId: data.MessageId,
MessageHash: data.MessageHash, MessageHash: data.MessageHash,
Mode: h.App.SysConfig.MjMode, Mode: h.App.SysConfig.Base.MjMode,
} }
job := model.MidJourneyJob{ job := model.MidJourneyJob{
Type: types.TaskUpscale.String(), Type: types.TaskUpscale.String(),
UserId: userId, UserId: uint(userId),
TaskId: taskId, TaskId: taskId,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
Progress: 0, Progress: 0,
Power: h.App.SysConfig.MjActionPower, Power: h.App.SysConfig.Base.MjActionPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
@@ -286,22 +328,21 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
taskId, _ := h.snowflake.Next(true) taskId, _ := h.snowflake.Next(true)
task := types.MjTask{ task := types.MjTask{
Type: types.TaskVariation, Type: types.TaskVariation,
ClientId: data.ClientId,
UserId: userId, UserId: userId,
Index: data.Index, Index: data.Index,
ChannelId: data.ChannelId, ChannelId: data.ChannelId,
MessageId: data.MessageId, MessageId: data.MessageId,
MessageHash: data.MessageHash, MessageHash: data.MessageHash,
Mode: h.App.SysConfig.MjMode, Mode: h.App.SysConfig.Base.MjMode,
} }
job := model.MidJourneyJob{ job := model.MidJourneyJob{
Type: types.TaskVariation.String(), Type: types.TaskVariation.String(),
ChannelId: data.ChannelId, ChannelId: data.ChannelId,
UserId: userId, UserId: uint(userId),
TaskId: taskId, TaskId: taskId,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
Progress: 0, Progress: 0,
Power: h.App.SysConfig.MjActionPower, Power: h.App.SysConfig.Base.MjActionPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 { if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
@@ -427,7 +468,7 @@ func (h *MidJourneyHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetInt(c, "user_id", 0) userId := h.GetInt(c, "user_id", 0)
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享 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 { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
+30 -5
View File
@@ -9,17 +9,19 @@ package handler
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service/oss" "geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"io" "io"
"net/http" "net/http"
"time" "time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
) )
type NetHandler struct { 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} 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) { func (h *NetHandler) Upload(c *gin.Context) {
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file") file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil { if err != nil {
@@ -46,7 +64,7 @@ func (h *NetHandler) Upload(c *gin.Context) {
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
res := h.DB.Create(&model.File{ res := h.DB.Create(&model.File{
UserId: int(userId), UserId: uint(userId),
Name: file.Name, Name: file.Name,
ObjKey: file.ObjKey, ObjKey: file.ObjKey,
URL: file.URL, URL: file.URL,
@@ -143,7 +161,15 @@ func (h *NetHandler) Download(c *gin.Context) {
return return
} }
// 使用http.Get下载文件 // 使用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 { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -156,6 +182,5 @@ func (h *NetHandler) Download(c *gin.Context) {
} }
c.Status(http.StatusOK) c.Status(http.StatusOK)
// 将下载的文件内容写入响应
_, _ = io.Copy(c.Writer, r.Body) _, _ = io.Copy(c.Writer, r.Body)
} }
+22 -18
View File
@@ -9,12 +9,12 @@ package handler
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
@@ -28,6 +28,18 @@ func NewOrderHandler(app *core.AppServer, db *gorm.DB) *OrderHandler {
return &OrderHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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 订单列表 // List 订单列表
func (h *OrderHandler) List(c *gin.Context) { func (h *OrderHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1) page := h.GetInt(c, "page", 1)
@@ -48,20 +60,21 @@ func (h *OrderHandler) List(c *gin.Context) {
order.Id = item.Id order.Id = item.Id
order.CreatedAt = item.CreatedAt.Unix() order.CreatedAt = item.CreatedAt.Unix()
order.UpdatedAt = item.UpdatedAt.Unix() order.UpdatedAt = item.UpdatedAt.Unix()
payMethod, ok := types.PayMethods[item.PayWay] payChannel, ok := types.PayChannel[item.Channel]
if !ok { if !ok {
payMethod = item.PayWay payChannel = item.PayWay
} }
payName, ok := types.PayNames[item.PayType] payWays, ok := types.PayWays[item.PayWay]
if !ok { if !ok {
payName = item.PayWay payWays = item.PayWay
} }
order.PayMethod = payMethod order.ChannelName = payChannel
order.PayName = payName order.PayName = payWays
list = append(list, order) list = append(list, order)
} else { } else {
logger.Error(err) logger.Error(err)
} }
} }
} }
resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list)) resp.SUCCESS(c, vo.NewPage(total, page, pageSize, list))
@@ -82,17 +95,8 @@ func (h *OrderHandler) Query(c *gin.Context) {
return return
} }
counter := 0 var item model.Order
for { h.DB.Where("order_no = ?", orderNo).First(&item)
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++
}
resp.SUCCESS(c, gin.H{"status": order.Status}) resp.SUCCESS(c, gin.H{"status": order.Status})
} }
+275 -241
View File
@@ -11,6 +11,7 @@ import (
"embed" "embed"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/payment" "geekai/service/payment"
@@ -33,52 +34,149 @@ type PayWay struct {
// PaymentHandler 支付服务回调 handler // PaymentHandler 支付服务回调 handler
type PaymentHandler struct { type PaymentHandler struct {
BaseHandler BaseHandler
alipayService *payment.AlipayService alipayService *payment.AlipayService
huPiPayService *payment.HuPiPayService epayService *payment.EPayService
geekPayService *payment.GeekPayService wxpayService *payment.WxPayService
wechatPayService *payment.WechatPayService snowflake *service.Snowflake
snowflake *service.Snowflake userService *service.UserService
userService *service.UserService fs embed.FS
fs embed.FS lock sync.Mutex
lock sync.Mutex config *types.PaymentConfig
signKey string // 用来签名的随机秘钥
} }
func NewPaymentHandler( func NewPaymentHandler(
server *core.AppServer, server *core.AppServer,
alipayService *payment.AlipayService, alipayService *payment.AlipayService,
huPiPayService *payment.HuPiPayService, geekPayService *payment.EPayService,
geekPayService *payment.GeekPayService, wxpayService *payment.WxPayService,
wechatPayService *payment.WechatPayService,
db *gorm.DB, db *gorm.DB,
userService *service.UserService, userService *service.UserService,
snowflake *service.Snowflake, snowflake *service.Snowflake,
fs embed.FS) *PaymentHandler { fs embed.FS,
sysConfig *types.SystemConfig) *PaymentHandler {
return &PaymentHandler{ return &PaymentHandler{
alipayService: alipayService, alipayService: alipayService,
huPiPayService: huPiPayService, epayService: geekPayService,
geekPayService: geekPayService, wxpayService: wxpayService,
wechatPayService: wechatPayService, snowflake: snowflake,
snowflake: snowflake, userService: userService,
userService: userService, fs: fs,
fs: fs, lock: sync.Mutex{},
lock: sync.Mutex{},
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: server, App: server,
DB: db, DB: db,
}, },
signKey: utils.RandString(32), config: &sysConfig.Payment,
} }
} }
func (h *PaymentHandler) Pay(c *gin.Context) { // 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)
}
}
func (h *PaymentHandler) StartSyncOrders() {
go func() {
for {
err := h.SyncOrders()
if err != nil {
logger.Error(err)
}
time.Sleep(time.Second * 5)
}
}()
}
// 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 {
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 { var data struct {
PayWay string `json:"pay_way"` PayWay string `json:"pay_way,omitempty"` // 支付方式:支付宝,微信
PayType string `json:"pay_type"` Pid int `json:"pid,omitempty"`
ProductId int `json:"product_id"` Device string `json:"device,omitempty"`
UserId int `json:"user_id"` Domain string `json:"domain,omitempty"` // 支付回调域名
Device string `json:"device"` Channel string `json:"channel,omitempty"`
Host string `json:"host"`
} }
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -86,7 +184,7 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
} }
var product model.Product var product model.Product
err := h.DB.Where("id", data.ProductId).First(&product).Error err := h.DB.Where("id", data.Pid).First(&product).Error
if err != nil { if err != nil {
resp.ERROR(c, "Product not found") resp.ERROR(c, "Product not found")
return return
@@ -97,136 +195,118 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
resp.ERROR(c, "error with generate trade no: "+err.Error()) resp.ERROR(c, "error with generate trade no: "+err.Error())
return return
} }
userId := h.GetLoginUserId(c)
var user model.User var user model.User
err = h.DB.Where("id", data.UserId).First(&user).Error err = h.DB.Where("id", userId).First(&user).Error
if err != nil { if err != nil {
resp.NotAuth(c) resp.NotAuth(c)
return return
} }
amount := product.Discount amount := product.Price
var payURL, returnURL, notifyURL string var payURL, notifyURL string
switch data.PayWay { switch data.PayWay {
case "alipay": case "wxpay":
if h.App.Config.AlipayConfig.NotifyURL != "" { // 用于本地调试支付 logger.Debugf("微信支付,%+v", data)
notifyURL = h.App.Config.AlipayConfig.NotifyURL data.Channel = payment.PayChannelWX
} else { // 优先使用微信官方支付
notifyURL = fmt.Sprintf("%s/api/payment/notify/alipay", data.Host) if h.config.WxPay.Enabled {
} data.Channel = "wxpay"
if h.App.Config.AlipayConfig.ReturnURL != "" { // 用于本地调试支付 if h.config.WxPay.Domain != "" {
returnURL = h.App.Config.AlipayConfig.ReturnURL data.Domain = h.config.WxPay.Domain
} else { }
returnURL = fmt.Sprintf("%s/payReturn", data.Host) notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain)
} payURL, err = h.wxpayService.Pay(payment.PayRequest{
money := fmt.Sprintf("%.2f", amount)
if data.Device == "wechat" {
payURL, err = h.alipayService.PayMobile(payment.AlipayParams{
OutTradeNo: orderNo, OutTradeNo: orderNo,
Subject: product.Name, TotalFee: fmt.Sprintf("%d", int(amount*100)),
TotalFee: money,
ReturnURL: returnURL,
NotifyURL: notifyURL,
})
} else {
payURL, err = h.alipayService.PayPC(payment.AlipayParams{
OutTradeNo: orderNo,
Subject: product.Name,
TotalFee: money,
ReturnURL: returnURL,
NotifyURL: notifyURL,
})
}
if err != nil {
resp.ERROR(c, "error with generate pay url: "+err.Error())
return
}
break
case "wechat":
if h.App.Config.WechatPayConfig.NotifyURL != "" {
notifyURL = h.App.Config.WechatPayConfig.NotifyURL
} else {
notifyURL = fmt.Sprintf("%s/api/payment/notify/wechat", data.Host)
}
if data.Device == "wechat" {
payURL, err = h.wechatPayService.PayUrlH5(payment.WechatPayParams{
OutTradeNo: orderNo,
TotalFee: int(amount * 100),
Subject: product.Name, Subject: product.Name,
NotifyURL: notifyURL, NotifyURL: notifyURL,
ClientIP: c.ClientIP(), ClientIP: c.ClientIP(),
Device: data.Device,
PayWay: payment.PayWayWX,
}) })
} else { if err != nil {
payURL, err = h.wechatPayService.PayUrlNative(payment.WechatPayParams{ 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, OutTradeNo: orderNo,
TotalFee: int(amount * 100),
Subject: product.Name, 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, NotifyURL: notifyURL,
}) })
}
if err != nil {
resp.ERROR(c, err.Error())
return
}
break
case "hupi":
if h.App.Config.HuPiPayConfig.NotifyURL != "" {
notifyURL = h.App.Config.HuPiPayConfig.NotifyURL
} else {
notifyURL = fmt.Sprintf("%s/api/payment/notify/hupi", data.Host)
}
if h.App.Config.HuPiPayConfig.ReturnURL != "" {
returnURL = h.App.Config.HuPiPayConfig.ReturnURL
} else {
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
}
r, err := h.huPiPayService.Pay(payment.HuPiPayParams{
Version: "1.1",
TradeOrderId: orderNo,
TotalFee: fmt.Sprintf("%f", amount),
Title: product.Name,
NotifyURL: notifyURL,
ReturnURL: returnURL,
WapName: "GeekAI助手",
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
payURL = r.URL
break
case "geek":
if h.App.Config.GeekPayConfig.NotifyURL != "" {
notifyURL = h.App.Config.GeekPayConfig.NotifyURL
} else {
notifyURL = fmt.Sprintf("%s/api/payment/notify/geek", data.Host)
}
if h.App.Config.GeekPayConfig.ReturnURL != "" {
data.Host = utils.GetBaseURL(h.App.Config.GeekPayConfig.ReturnURL)
}
if data.Device == "wechat" { // 微信客户端打开,调回手机端用户中心页面
returnURL = fmt.Sprintf("%s/mobile/profile", data.Host)
} else {
returnURL = fmt.Sprintf("%s/payReturn", data.Host)
}
params := payment.GeekPayParams{
OutTradeNo: orderNo,
Method: "web",
Name: product.Name,
Money: fmt.Sprintf("%f", amount),
ClientIP: c.ClientIP(),
Device: data.Device,
Type: data.PayType,
ReturnURL: returnURL,
NotifyURL: notifyURL,
}
res, err := h.geekPayService.Pay(params) if err != nil {
if err != nil { resp.ERROR(c, "error with generate pay url: "+err.Error())
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: 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 return
} }
payURL = res.PayURL
default: default:
resp.ERROR(c, "不支持的支付渠道") resp.ERROR(c, "不支持的支付渠道")
return return
@@ -234,43 +314,40 @@ func (h *PaymentHandler) Pay(c *gin.Context) {
// 创建订单 // 创建订单
remark := types.OrderRemark{ remark := types.OrderRemark{
Days: product.Days, Power: product.Power,
Power: product.Power, Name: product.Name,
Name: product.Name, Price: product.Price,
Price: product.Price,
Discount: product.Discount,
} }
order := model.Order{ order := model.Order{
UserId: user.Id, UserId: user.Id,
Username: user.Username, Username: user.Username,
ProductId: product.Id, OrderNo: orderNo,
OrderNo: orderNo, Subject: product.Name,
Subject: product.Name, Amount: amount,
Amount: amount, Status: types.OrderNotPaid,
Status: types.OrderNotPaid, PayWay: data.PayWay,
PayWay: data.PayWay, Channel: data.Channel,
PayType: data.PayType, Remark: utils.JsonEncode(remark),
Remark: utils.JsonEncode(remark),
} }
err = h.DB.Create(&order).Error err = h.DB.Create(&order).Error
if err != nil { if err != nil {
resp.ERROR(c, "error with create order: "+err.Error()) resp.ERROR(c, "error with create order: "+err.Error())
return return
} }
resp.SUCCESS(c, payURL) resp.SUCCESS(c, gin.H{"pay_url": payURL, "order_no": orderNo})
} }
// 异步通知回调公共逻辑 // 支付成功处理
func (h *PaymentHandler) notify(orderNo string, tradeNo string) error { func (h *PaymentHandler) paySuccess(info payment.OrderInfo) error {
h.lock.Lock()
defer h.lock.Unlock()
var order model.Order var order model.Order
err := h.DB.Where("order_no = ?", orderNo).First(&order).Error err := h.DB.Where("order_no", info.OutTradeNo).First(&order).Error
if err != nil { if err != nil {
return fmt.Errorf("error with fetch order: %v", err) return fmt.Errorf("error with fetch order: %v", err)
} }
h.lock.Lock()
defer h.lock.Unlock()
// 已支付订单,直接返回 // 已支付订单,直接返回
if order.Status == types.OrderPaidSuccess { if order.Status == types.OrderPaidSuccess {
return nil return nil
@@ -289,20 +366,22 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
} }
// 增加用户算力 // 增加用户算力
err = h.userService.IncreasePower(int(order.UserId), remark.Power, model.PowerLog{ err = h.userService.IncreasePower(order.UserId, remark.Power, model.PowerLog{
Type: types.PowerRecharge, Type: types.PowerRecharge,
Model: order.PayWay, Model: order.Subject,
Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo), Remark: fmt.Sprintf("充值算力,金额:%f,订单号:%s", order.Amount, order.OrderNo),
CreatedAt: time.Now(),
}) })
if err != nil { if err != nil {
return err return err
} }
// 更新订单状态 // 更新订单状态
order.PayTime = time.Now().Unix() order.PayTime = utils.Str2stamp(info.PayTime)
order.Status = types.OrderPaidSuccess order.Status = types.OrderPaidSuccess
order.TradeNo = tradeNo order.TradeNo = info.TradeId
err = h.DB.Updates(&order).Error order.Checked = true
err = h.DB.Debug().Updates(&order).Error
if err != nil { if err != nil {
return fmt.Errorf("error with update order info: %v", err) return fmt.Errorf("error with update order info: %v", err)
} }
@@ -317,54 +396,6 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
return nil return nil
} }
// GetPayWays 获取支付方式
func (h *PaymentHandler) GetPayWays(c *gin.Context) {
payWays := make([]gin.H, 0)
if h.App.Config.AlipayConfig.Enabled {
payWays = append(payWays, gin.H{"pay_way": "alipay", "pay_type": "alipay"})
}
if h.App.Config.HuPiPayConfig.Enabled {
payWays = append(payWays, gin.H{"pay_way": "hupi", "pay_type": "wxpay"})
}
if h.App.Config.GeekPayConfig.Enabled {
for _, v := range h.App.Config.GeekPayConfig.Methods {
payWays = append(payWays, gin.H{"pay_way": "geek", "pay_type": v})
}
}
if h.App.Config.WechatPayConfig.Enabled {
payWays = append(payWays, gin.H{"pay_way": "wechat", "pay_type": "wxpay"})
}
resp.SUCCESS(c, payWays)
}
// 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("收到虎皮椒订单支付回调,%+v", c.Request.Form)
if err = h.huPiPayService.Check(orderNo); err != nil {
logger.Error("订单校验失败:", err)
c.String(http.StatusOK, "fail")
return
}
err = h.notify(orderNo, tradeNo)
if err != nil {
logger.Error(err)
c.String(http.StatusOK, "fail")
return
}
c.String(http.StatusOK, "success")
}
// AlipayNotify 支付宝支付回调 // AlipayNotify 支付宝支付回调
func (h *PaymentHandler) AlipayNotify(c *gin.Context) { func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
err := c.Request.ParseForm() err := c.Request.ParseForm()
@@ -373,16 +404,15 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
return return
} }
result := h.alipayService.TradeVerify(c.Request) orderInfo, err := h.alipayService.Query(c.Request.Form.Get("out_trade_no"))
logger.Infof("收到支付宝商号订单支付回调:%+v", result) logger.Infof("收到支付宝商号订单支付回调:%+v", orderInfo)
if !result.Success() { if !orderInfo.Success() {
logger.Error("订单校验失败:", result.Message) logger.Errorf("订单校验失败:%v", err)
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return
} }
tradeNo := c.Request.Form.Get("trade_no") err = h.paySuccess(orderInfo)
err = h.notify(result.OutTradeNo, tradeNo)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
@@ -392,28 +422,35 @@ func (h *PaymentHandler) AlipayNotify(c *gin.Context) {
c.String(http.StatusOK, "success") c.String(http.StatusOK, "success")
} }
// GeekPayNotify 支付异步回调 // EPayNotify 易支付支付异步回调
func (h *PaymentHandler) GeekPayNotify(c *gin.Context) { func (h *PaymentHandler) EPayNotify(c *gin.Context) {
var params = make(map[string]string) var params = make(map[string]string)
for k := range c.Request.URL.Query() { for k := range c.Request.URL.Query() {
params[k] = c.Query(k) params[k] = c.Query(k)
} }
logger.Infof("收到GeekPay订单支付回调:%+v", params) logger.Infof("收到易支付订单支付回调:%+v", params)
// 检查支付状态 // 检查支付状态, 如果未支付,则返回成功
if params["trade_status"] != "TRADE_SUCCESS" { if params["trade_status"] != "TRADE_SUCCESS" {
c.String(http.StatusOK, "success") c.String(http.StatusOK, "success")
return return
} }
sign := h.geekPayService.Sign(params) sign := h.epayService.Sign(params)
if sign != c.Query("sign") { if sign != c.Query("sign") {
logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign")) logger.Errorf("签名验证失败, %s, %s", sign, c.Query("sign"))
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return
} }
// 查询订单状态
order, err := h.epayService.Query(params["out_trade_no"])
if err != nil {
logger.Error(err)
c.String(http.StatusOK, "fail")
return
}
err := h.notify(params["out_trade_no"], params["trade_no"]) err = h.paySuccess(order)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
@@ -423,26 +460,23 @@ func (h *PaymentHandler) GeekPayNotify(c *gin.Context) {
c.String(http.StatusOK, "success") c.String(http.StatusOK, "success")
} }
// WechatPayNotify 微信商户支付异步回调 // WxpayNotify 微信商户支付异步回调
func (h *PaymentHandler) WechatPayNotify(c *gin.Context) { func (h *PaymentHandler) WxpayNotify(c *gin.Context) {
err := c.Request.ParseForm() err := c.Request.ParseForm()
if err != nil { if err != nil {
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
return return
} }
result := h.wechatPayService.TradeVerify(c.Request) orderInfo, err := h.wxpayService.TradeVerify(c.Request)
logger.Infof("收到微信商号订单支付回调:%+v", result) logger.Infof("收到微信商号订单支付回调:%+v", orderInfo)
if !result.Success() { if err != nil {
logger.Error("订单校验失败:", err) logger.Errorf("订单校验失败:%v", err)
c.JSON(http.StatusBadRequest, gin.H{ c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL"})
"code": "FAIL",
"message": err.Error(),
})
return return
} }
err = h.notify(result.OutTradeNo, result.TradeId) err = h.paySuccess(orderInfo)
if err != nil { if err != nil {
logger.Error(err) logger.Error(err)
c.String(http.StatusOK, "fail") c.String(http.StatusOK, "fail")
+56
View File
@@ -9,11 +9,13 @@ package handler
import ( import (
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
@@ -27,6 +29,18 @@ func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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) { func (h *PowerLogHandler) List(c *gin.Context) {
var data struct { var data struct {
Model string `json:"model"` 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)) 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/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -25,6 +26,12 @@ func NewProductHandler(app *core.AppServer, db *gorm.DB) *ProductHandler {
return &ProductHandler{BaseHandler: BaseHandler{App: app, DB: db}} 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 模型列表 // List 模型列表
func (h *ProductHandler) List(c *gin.Context) { func (h *ProductHandler) List(c *gin.Context) {
var items []model.Product var items []model.Product
+22 -31
View File
@@ -10,12 +10,14 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
@@ -39,6 +41,20 @@ func NewPromptHandler(app *core.AppServer, db *gorm.DB, userService *service.Use
} }
} }
// 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 生成歌词 // Lyric 生成歌词
func (h *PromptHandler) Lyric(c *gin.Context) { func (h *PromptHandler) Lyric(c *gin.Context) {
var data struct { var data struct {
@@ -48,21 +64,12 @@ func (h *PromptHandler) Lyric(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId) content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.LyricPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
if h.App.SysConfig.PromptPower > 0 {
userId := h.GetLoginUserId(c)
h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
Type: types.PowerConsume,
Model: h.getPromptModel(),
Remark: "生成歌词",
})
}
resp.SUCCESS(c, content) resp.SUCCESS(c, content)
} }
@@ -75,19 +82,12 @@ func (h *PromptHandler) Image(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.TranslateModelId) content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.ImagePromptOptimizeTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
if h.App.SysConfig.PromptPower > 0 {
userId := h.GetLoginUserId(c)
h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
Type: types.PowerConsume,
Model: h.getPromptModel(),
Remark: "生成绘画提示词",
})
}
resp.SUCCESS(c, strings.Trim(content, `"`)) resp.SUCCESS(c, strings.Trim(content, `"`))
} }
@@ -100,21 +100,12 @@ func (h *PromptHandler) Video(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.TranslateModelId) content, err := utils.OpenAIRequest(h.DB, fmt.Sprintf(service.VideoPromptTemplate, data.Prompt), h.App.SysConfig.Base.AssistantModelId)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
if h.App.SysConfig.PromptPower > 0 {
userId := h.GetLoginUserId(c)
h.userService.DecreasePower(int(userId), h.App.SysConfig.PromptPower, model.PowerLog{
Type: types.PowerConsume,
Model: h.getPromptModel(),
Remark: "生成视频脚本",
})
}
resp.SUCCESS(c, strings.Trim(content, `"`)) resp.SUCCESS(c, strings.Trim(content, `"`))
} }
@@ -146,9 +137,9 @@ func (h *PromptHandler) MetaPrompt(c *gin.Context) {
} }
func (h *PromptHandler) getPromptModel() string { func (h *PromptHandler) getPromptModel() string {
if h.App.SysConfig.TranslateModelId > 0 { if h.App.SysConfig.Base.AssistantModelId > 0 {
var chatModel model.ChatModel var chatModel model.ChatModel
h.DB.Where("id", h.App.SysConfig.TranslateModelId).First(&chatModel) h.DB.Where("id", h.App.SysConfig.Base.AssistantModelId).First(&chatModel)
return chatModel.Value return chatModel.Value
} }
return "gpt-4o" return "gpt-4o"
+27 -2
View File
@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/store/model" "geekai/store/model"
@@ -39,6 +40,18 @@ func NewRealtimeHandler(server *core.AppServer, db *gorm.DB, userService *servic
return &RealtimeHandler{BaseHandler: BaseHandler{App: server, DB: db}, userService: userService} 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) { func (h *RealtimeHandler) Connection(c *gin.Context) {
// 获取客户端请求中指定的子协议 // 获取客户端请求中指定的子协议
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol") clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
@@ -146,6 +159,19 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
return 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 var response utils.OpenAIResponse
client := req.C() client := req.C()
if len(apiKey.ProxyURL) > 5 { if len(apiKey.ProxyURL) > 5 {
@@ -185,8 +211,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix()) h.DB.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
// 扣减算力 // 扣减算力
userId := h.GetLoginUserId(c) err = h.userService.DecreasePower(userId, h.App.SysConfig.Base.AdvanceVoicePower, model.PowerLog{
err = h.userService.DecreasePower(int(userId), h.App.SysConfig.AdvanceVoicePower, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: "advanced-voice", Model: "advanced-voice",
Remark: "实时语音通话", Remark: "实时语音通话",
+16 -3
View File
@@ -10,14 +10,16 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"sync" "sync"
"time" "time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
) )
type RedeemHandler struct { 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} 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) { func (h *RedeemHandler) Verify(c *gin.Context) {
var data struct { var data struct {
Code string `json:"code"` Code string `json:"code"`
@@ -61,7 +74,7 @@ func (h *RedeemHandler) Verify(c *gin.Context) {
} }
tx := h.DB.Begin() 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, Type: types.PowerRedeem,
Model: "兑换码", Model: "兑换码",
Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]), Remark: fmt.Sprintf("兑换码核销,算力:%d,兑换码:%s...", item.Power, item.Code[:10]),
+64 -19
View File
@@ -10,8 +10,10 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/sd" "geekai/service/sd"
"geekai/store" "geekai/store"
@@ -28,12 +30,13 @@ import (
type SdJobHandler struct { type SdJobHandler struct {
BaseHandler BaseHandler
redis *redis.Client redis *redis.Client
sdService *sd.Service sdService *sd.Service
uploader *oss.UploaderManager uploader *oss.UploaderManager
snowflake *service.Snowflake snowflake *service.Snowflake
leveldb *store.LevelDB leveldb *store.LevelDB
userService *service.UserService userService *service.UserService
moderationManager *moderation.ServiceManager
} }
func NewSdJobHandler(app *core.AppServer, func NewSdJobHandler(app *core.AppServer,
@@ -42,13 +45,15 @@ func NewSdJobHandler(app *core.AppServer,
manager *oss.UploaderManager, manager *oss.UploaderManager,
snowflake *service.Snowflake, snowflake *service.Snowflake,
userService *service.UserService, userService *service.UserService,
levelDB *store.LevelDB) *SdJobHandler { levelDB *store.LevelDB,
moderationManager *moderation.ServiceManager) *SdJobHandler {
return &SdJobHandler{ return &SdJobHandler{
sdService: service, sdService: service,
uploader: manager, uploader: manager,
snowflake: snowflake, snowflake: snowflake,
leveldb: levelDB, leveldb: levelDB,
userService: userService, userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: app, App: app,
DB: db, DB: db,
@@ -56,6 +61,23 @@ func NewSdJobHandler(app *core.AppServer,
} }
} }
// RegisterRoutes 注册路由
func (h *SdJobHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/sd/")
// 公开接口,不需要授权
group.GET("imgWall", h.ImgWall)
// 需要用户授权的接口
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 { func (h *SdJobHandler) preCheck(c *gin.Context) bool {
user, err := h.GetLoginUser(c) user, err := h.GetLoginUser(c)
if err != nil { if err != nil {
@@ -63,7 +85,7 @@ func (h *SdJobHandler) preCheck(c *gin.Context) bool {
return false return false
} }
if user.Power < h.App.SysConfig.SdPower { if user.Power < h.App.SysConfig.Base.SdPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!") resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false return false
} }
@@ -84,6 +106,29 @@ func (h *SdJobHandler) Image(c *gin.Context) {
return 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 { if data.Width <= 0 {
data.Width = 512 data.Width = 512
} }
@@ -102,6 +147,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
if data.Sampler == "" { if data.Sampler == "" {
data.Sampler = "Euler a" data.Sampler = "Euler a"
} }
idValue, _ := c.Get(types.LoginUserID) idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0) userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, err := h.snowflake.Next(true) taskId, err := h.snowflake.Next(true)
@@ -111,8 +157,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
} }
task := types.SdTask{ task := types.SdTask{
ClientId: data.ClientId, Type: types.TaskImage,
Type: types.TaskImage,
Params: types.SdTaskParams{ Params: types.SdTaskParams{
TaskId: taskId, TaskId: taskId,
Prompt: data.Prompt, Prompt: data.Prompt,
@@ -131,18 +176,18 @@ func (h *SdJobHandler) Image(c *gin.Context) {
HdSteps: data.HdSteps, HdSteps: data.HdSteps,
}, },
UserId: userId, UserId: userId,
TranslateModelId: h.App.SysConfig.TranslateModelId, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
} }
job := model.SdJob{ job := model.SdJob{
UserId: userId, UserId: uint(userId),
Type: types.TaskImage.String(), Type: types.TaskImage.String(),
TaskId: taskId, TaskId: taskId,
Params: utils.JsonEncode(task.Params), Params: utils.JsonEncode(task.Params),
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
Prompt: data.Prompt, Prompt: data.Prompt,
Progress: 0, Progress: 0,
Power: h.App.SysConfig.SdPower, Power: h.App.SysConfig.Base.SdPower,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
res := h.DB.Create(&job) res := h.DB.Create(&job)
@@ -273,7 +318,7 @@ func (h *SdJobHandler) Publish(c *gin.Context) {
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享 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 { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
+25 -22
View File
@@ -24,24 +24,31 @@ const CodeStorePrefix = "/verify/codes/"
type SmsHandler struct { type SmsHandler struct {
BaseHandler BaseHandler
redis *redis.Client redis *redis.Client
sms *sms.ServiceManager sms *sms.SmsManager
smtp *service.SmtpService smtp *service.SmtpService
captcha *service.CaptchaService captchaService *service.CaptchaService
} }
func NewSmsHandler( func NewSmsHandler(
app *core.AppServer, app *core.AppServer,
client *redis.Client, client *redis.Client,
sms *sms.ServiceManager, sms *sms.SmsManager,
smtp *service.SmtpService, smtp *service.SmtpService,
captcha *service.CaptchaService) *SmsHandler { captcha *service.CaptchaService) *SmsHandler {
return &SmsHandler{ return &SmsHandler{
redis: client, redis: client,
sms: sms, sms: sms,
captcha: captcha, captchaService: captcha,
smtp: smtp, smtp: smtp,
BaseHandler: BaseHandler{App: app}} BaseHandler: BaseHandler{App: app}}
}
// RegisterRoutes 注册路由
func (h *SmsHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/sms/")
// 无需授权的接口
group.POST("code", h.SendCode)
} }
// SendCode 发送验证码 // SendCode 发送验证码
@@ -56,12 +63,12 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
if h.App.SysConfig.EnabledVerify { if h.captchaService.GetConfig().Enabled {
var check bool var check bool
if data.X != 0 { if data.X != 0 {
check = h.captcha.SlideCheck(data) check = h.captchaService.SlideCheck(data)
} else { } else {
check = h.captcha.Check(data) check = h.captchaService.Check(data)
} }
if !check { if !check {
resp.ERROR(c, "请先完人机验证") resp.ERROR(c, "请先完人机验证")
@@ -72,14 +79,14 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
code := utils.RandomNumber(6) code := utils.RandomNumber(6)
var err error var err error
if strings.Contains(data.Receiver, "@") { // email 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, "系统已禁用邮箱注册!") resp.ERROR(c, "系统已禁用邮箱注册!")
return return
} }
// 检查邮箱后缀是否在白名单 // 检查邮箱后缀是否在白名单
if len(h.App.SysConfig.EmailWhiteList) > 0 { if len(h.App.SysConfig.Base.EmailWhiteList) > 0 {
inWhiteList := false inWhiteList := false
for _, suffix := range h.App.SysConfig.EmailWhiteList { for _, suffix := range h.App.SysConfig.Base.EmailWhiteList {
if strings.HasSuffix(data.Receiver, suffix) { if strings.HasSuffix(data.Receiver, suffix) {
inWhiteList = true inWhiteList = true
break break
@@ -92,7 +99,7 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
} }
err = h.smtp.SendVerifyCode(data.Receiver, code) err = h.smtp.SendVerifyCode(data.Receiver, code)
} else { } else {
if !utils.Contains(h.App.SysConfig.RegisterWays, "mobile") { if !utils.Contains(h.App.SysConfig.Base.RegisterWays, "mobile") {
resp.ERROR(c, "系统已禁用手机号注册!") resp.ERROR(c, "系统已禁用手机号注册!")
return return
} }
@@ -111,9 +118,5 @@ func (h *SmsHandler) SendCode(c *gin.Context) {
return return
} }
if h.App.Debug { resp.SUCCESS(c)
resp.SUCCESS(c, code)
} else {
resp.SUCCESS(c)
}
} }
+59 -13
View File
@@ -10,42 +10,65 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/suno" "geekai/service/suno"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type SunoHandler struct { type SunoHandler struct {
BaseHandler BaseHandler
sunoService *suno.Service sunoService *suno.Service
uploader *oss.UploaderManager uploader *oss.UploaderManager
userService *service.UserService 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{ return &SunoHandler{
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: app, App: app,
DB: db, DB: db,
}, },
sunoService: service, sunoService: service,
uploader: uploader, uploader: uploader,
userService: userService, userService: userService,
moderationManager: moderationManager,
}
}
// RegisterRoutes 注册路由
func (h *SunoHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/suno/")
// 公开接口,不需要授权
group.GET("play", h.Play)
// 需要用户授权的接口
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) { func (h *SunoHandler) Create(c *gin.Context) {
var data struct { var data struct {
ClientId string `json:"client_id"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Instrumental bool `json:"instrumental"` Instrumental bool `json:"instrumental"`
Lyrics string `json:"lyrics"` Lyrics string `json:"lyrics"`
@@ -64,13 +87,36 @@ func (h *SunoHandler) Create(c *gin.Context) {
return 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) user, err := h.GetLoginUser(c)
if err != nil { if err != nil {
resp.NotAuth(c) resp.NotAuth(c)
return return
} }
if user.Power < h.App.SysConfig.SunoPower { if user.Power < h.App.SysConfig.Base.SunoPower {
resp.ERROR(c, "您的算力不足,请充值后再试!") resp.ERROR(c, "您的算力不足,请充值后再试!")
return return
} }
@@ -90,7 +136,6 @@ func (h *SunoHandler) Create(c *gin.Context) {
} }
} }
task := types.SunoTask{ task := types.SunoTask{
ClientId: data.ClientId,
UserId: int(h.GetLoginUserId(c)), UserId: int(h.GetLoginUserId(c)),
Type: data.Type, Type: data.Type,
Title: data.Title, Title: data.Title,
@@ -98,6 +143,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
RefSongId: data.RefSongId, RefSongId: data.RefSongId,
ExtendSecs: data.ExtendSecs, ExtendSecs: data.ExtendSecs,
Prompt: data.Prompt, Prompt: data.Prompt,
Lyrics: data.Lyrics,
Tags: data.Tags, Tags: data.Tags,
Model: data.Model, Model: data.Model,
Instrumental: data.Instrumental, Instrumental: data.Instrumental,
@@ -107,7 +153,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
// 插入数据库 // 插入数据库
job := model.SunoJob{ job := model.SunoJob{
UserId: task.UserId, UserId: uint(task.UserId),
Prompt: data.Prompt, Prompt: data.Prompt,
Instrumental: data.Instrumental, Instrumental: data.Instrumental,
ModelName: data.Model, ModelName: data.Model,
@@ -118,7 +164,7 @@ func (h *SunoHandler) Create(c *gin.Context) {
RefSongId: data.RefSongId, RefSongId: data.RefSongId,
RefTaskId: data.RefTaskId, RefTaskId: data.RefTaskId,
ExtendSecs: data.ExtendSecs, ExtendSecs: data.ExtendSecs,
Power: h.App.SysConfig.SunoPower, Power: h.App.SysConfig.Base.SunoPower,
SongId: utils.RandString(32), SongId: utils.RandString(32),
} }
if data.Lyrics != "" { if data.Lyrics != "" {
+19 -4
View File
@@ -1,21 +1,36 @@
package handler package handler
import ( import (
"geekai/core"
"geekai/core/middleware"
"geekai/service" "geekai/service"
"geekai/service/payment" "geekai/service/payment"
"net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"net/http"
) )
type TestHandler struct { type TestHandler struct {
App *core.AppServer
db *gorm.DB db *gorm.DB
snowflake *service.Snowflake snowflake *service.Snowflake
js *payment.GeekPayService js *payment.EPayService
} }
func NewTestHandler(db *gorm.DB, snowflake *service.Snowflake, js *payment.GeekPayService) *TestHandler { func NewTestHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, js *payment.EPayService) *TestHandler {
return &TestHandler{db: db, snowflake: snowflake, js: js} 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) { func (h *TestHandler) SseTest(c *gin.Context) {
+252 -242
View File
@@ -8,15 +8,17 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"context"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"github.com/imroc/req/v3"
"strings" "strings"
"time" "time"
@@ -32,9 +34,11 @@ type UserHandler struct {
BaseHandler BaseHandler
searcher *xdb.Searcher searcher *xdb.Searcher
redis *redis.Client redis *redis.Client
licenseService *service.LicenseService levelDB *store.LevelDB
captcha *service.CaptchaService captchaService *service.CaptchaService
userService *service.UserService userService *service.UserService
wxLoginService *service.WxLoginService
ipSearcher *xdb.Searcher
} }
func NewUserHandler( func NewUserHandler(
@@ -42,16 +46,46 @@ func NewUserHandler(
db *gorm.DB, db *gorm.DB,
searcher *xdb.Searcher, searcher *xdb.Searcher,
client *redis.Client, client *redis.Client,
levelDB *store.LevelDB,
captcha *service.CaptchaService, captcha *service.CaptchaService,
userService *service.UserService, userService *service.UserService,
licenseService *service.LicenseService) *UserHandler { wxLoginService *service.WxLoginService,
ipSearcher *xdb.Searcher) *UserHandler {
return &UserHandler{ return &UserHandler{
BaseHandler: BaseHandler{DB: db, App: app}, BaseHandler: BaseHandler{DB: db, App: app},
searcher: searcher, searcher: searcher,
redis: client, redis: client,
captcha: captcha, levelDB: levelDB,
licenseService: licenseService, captchaService: captcha,
userService: userService, 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)
} }
} }
@@ -75,12 +109,13 @@ func (h *UserHandler) Register(c *gin.Context) {
return return
} }
if h.App.SysConfig.EnabledVerify && data.RegWay == "username" { // 人机验证
if h.captchaService.GetConfig().Enabled {
var check bool var check bool
if data.X != 0 { if data.X != 0 {
check = h.captcha.SlideCheck(data) check = h.captchaService.SlideCheck(data)
} else { } else {
check = h.captcha.Check(data) check = h.captchaService.Check(data)
} }
if !check { if !check {
resp.ERROR(c, "请先完人机验证") resp.ERROR(c, "请先完人机验证")
@@ -94,14 +129,6 @@ func (h *UserHandler) Register(c *gin.Context) {
return 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 var key string
if data.RegWay == "email" { if data.RegWay == "email" {
@@ -120,28 +147,8 @@ 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
}
}
salt := utils.RandString(8)
user := model.User{
Username: data.Username,
Password: utils.GenPassword(data.Password, salt),
Avatar: "/images/avatar/user.png",
Salt: salt,
Status: true,
ChatRoles: utils.JsonEncode([]string{"gpt"}), // 默认只订阅通用助手角色
Power: h.App.SysConfig.InitPower,
}
// check if the username is existing // check if the username is existing
user := model.User{Username: data.Username, Password: data.Password}
var item model.User var item model.User
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if data.Mobile != "" { if data.Mobile != "" {
@@ -161,73 +168,19 @@ func (h *UserHandler) Register(c *gin.Context) {
return return
} }
// 被邀请人也获得赠送算力 user, err := h.createNewUser(user, data.InviteCode)
if data.InviteCode != "" { if err != nil {
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 {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
// 记录邀请关系 token, err := h.doLogin(&user, c.ClientIP())
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: "Invite",
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))
if err != nil { if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error()) resp.ERROR(c, err.Error())
return return
} }
// 保存到 redis
key = fmt.Sprintf("users/%d", user.Id) resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
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})
} }
// Login 用户登录 // Login 用户登录
@@ -243,15 +196,12 @@ func (h *UserHandler) Login(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
verifyKey := fmt.Sprintf("users/verify/%s", data.Username) if h.captchaService.GetConfig().Enabled {
needVerify, err := h.redis.Get(c, verifyKey).Bool()
if h.App.SysConfig.EnabledVerify && needVerify {
var check bool var check bool
if data.X != 0 { if data.X != 0 {
check = h.captcha.SlideCheck(data) check = h.captchaService.SlideCheck(data)
} else { } else {
check = h.captcha.Check(data) check = h.captchaService.Check(data)
} }
if !check { if !check {
resp.ERROR(c, "请先完人机验证") resp.ERROR(c, "请先完人机验证")
@@ -262,54 +212,28 @@ func (h *UserHandler) Login(c *gin.Context) {
var user model.User var user model.User
res := h.DB.Where("username = ?", data.Username).First(&user) res := h.DB.Where("username = ?", data.Username).First(&user)
if res.Error != nil { if res.Error != nil {
h.redis.Set(c, verifyKey, true, 0)
resp.ERROR(c, "用户名不存在") resp.ERROR(c, "用户名不存在")
return return
} }
password := utils.GenPassword(data.Password, user.Salt) password := utils.GenPassword(data.Password, user.Salt)
if password != user.Password { if password != user.Password {
h.redis.Set(c, verifyKey, true, 0)
resp.ERROR(c, "用户名或密码错误") resp.ERROR(c, "用户名或密码错误")
return return
} }
if user.Status == false { if !user.Status {
resp.ERROR(c, "该用户已被禁止登录,请联系管理员") resp.ERROR(c, "该用户已被禁止登录,请联系管理员")
return return
} }
// 更新最后登录时间和IP token, err := h.doLogin(&user, c.ClientIP())
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))
if err != nil { if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error()) resp.ERROR(c, err.Error())
return return
} }
// 保存到 redis
sessionKey := fmt.Sprintf("users/%d", user.Id) resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
if _, err = h.redis.Set(c, sessionKey, tokenString, 0).Result(); err != nil {
resp.ERROR(c, "error with save token: "+err.Error())
return
}
// 移除登录行为验证码
h.redis.Del(c, verifyKey)
resp.SUCCESS(c, gin.H{"token": tokenString, "user_id": user.Id, "username": user.Username})
} }
// Logout 注 销 // Logout 注 销
@@ -321,134 +245,168 @@ func (h *UserHandler) Logout(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// CLogin 第三方登录请求二维码 // GetWxLoginQRCode 获取微信登录二维码URL
func (h *UserHandler) CLogin(c *gin.Context) { func (h *UserHandler) GetWxLoginQRCode(c *gin.Context) {
returnURL := h.GetTrim(c, "return_url") if !h.wxLoginService.GetConfig().Enabled {
var res types.BizVo resp.ERROR(c, "微信登录功能未启用")
apiURL := fmt.Sprintf("%s/api/clogin/request", h.App.Config.ApiConfig.ApiURL) return
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)). if h.wxLoginService.GetConfig().ApiKey == "" {
SetSuccessResult(&res). resp.ERROR(c, "微信登录服务令牌未配置")
Post(apiURL) return
}
state := utils.RandString(32)
qrCodeURL, err := h.wxLoginService.GetLoginQrCodeUrl(state)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
if r.IsErrorState() {
resp.ERROR(c, "error with login http status: "+r.Status)
return
}
if res.Code != types.Success { resp.SUCCESS(c, gin.H{
resp.ERROR(c, "error with http response: "+res.Message) "url": qrCodeURL,
return "state": state,
} })
resp.SUCCESS(c, res.Data)
} }
// CLoginCallback 第三方登录回调 // 查询微信登录状态
func (h *UserHandler) CLoginCallback(c *gin.Context) { func (h *UserHandler) GetWxLoginState(c *gin.Context) {
loginType := c.Query("login_type") state := c.Query("state")
code := c.Query("code") if state == "" {
userId := h.GetInt(c, "user_id", 0) resp.ERROR(c, "参数错误")
action := c.Query("action") return
}
var res types.BizVo status, err := h.wxLoginService.GetLoginStatus(state)
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)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
if r.IsErrorState() {
resp.ERROR(c, "error with login http status: "+r.Status) if status.Status != service.LoginStatusSuccess {
resp.SUCCESS(c, status)
return 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 var user model.User
if action == "bind" && userId > 0 { h.DB.Where("openid = ?", status.OpenID).First(&user)
err = h.DB.Where("openid", data["openid"]).First(&user).Error if user.Id == 0 {
if err == nil { // 创建新用户
resp.ERROR(c, "该微信已经绑定其他账号,请先解绑") user, err = h.createNewUser(model.User{OpenId: status.OpenID}, "")
return
}
err = h.DB.Where("id", userId).First(&user).Error
if err != nil { if err != nil {
resp.ERROR(c, "绑定用户不存在") resp.ERROR(c, err.Error())
return return
} }
}
err = h.DB.Model(&user).UpdateColumn("openid", data["openid"]).Error token, err := h.doLogin(&user, c.ClientIP())
if err != nil { if err != nil {
resp.ERROR(c, "更新用户信息失败,"+err.Error()) resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{"token": ""})
return return
} }
session := gin.H{} status.Status = service.LoginStatusExpired
tx := h.DB.Where("openid", data["openid"]).First(&user) h.wxLoginService.SetLoginStatus(state, *status)
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
}
salt := utils.RandString(8) status.Status = service.LoginStatusSuccess
password := fmt.Sprintf("%d", utils.RandomNumber(8)) status.Token = token
user = model.User{ resp.SUCCESS(c, status)
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"}), // 默认只订阅通用助手角色
Power: h.App.SysConfig.InitPower,
OpenId: fmt.Sprintf("%s", data["openid"]),
Nickname: fmt.Sprintf("%s", data["nickname"]),
}
tx = h.DB.Create(&user) // createNewUser 创建新用户
if tx.Error != nil { func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
resp.ERROR(c, "保存数据失败") if user.OpenId != "" {
logger.Error(tx.Error) user.Platform = "wechat"
return 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
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": user.Id, "user_id": user.Id,
@@ -456,17 +414,42 @@ func (h *UserHandler) CLoginCallback(c *gin.Context) {
}) })
tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey)) tokenString, err := token.SignedString([]byte(h.App.Config.Session.SecretKey))
if err != nil { if err != nil {
resp.ERROR(c, "Failed to generate token, "+err.Error()) return "", fmt.Errorf("failed to generate token: %v", err)
return
} }
// 保存到 redis // 保存到 redis
key := fmt.Sprintf("users/%d", user.Id) sessionKey := fmt.Sprintf("users/%d", user.Id)
if _, err := h.redis.Set(c, key, tokenString, 0).Result(); err != nil { if _, err = h.redis.Set(context.Background(), sessionKey, tokenString, 0).Result(); err != nil {
resp.ERROR(c, "error with save token: "+err.Error()) 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 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 获取/验证会话 // Session 获取/验证会话
@@ -712,3 +695,30 @@ func (h *UserHandler) BindEmail(c *gin.Context) {
_ = h.redis.Del(c, key) // 删除短信验证码 _ = h.redis.Del(c, key) // 删除短信验证码
resp.SUCCESS(c) 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)
}
+189 -22
View File
@@ -10,42 +10,61 @@ package handler
import ( import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/video" "geekai/service/video"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm" "gorm.io/gorm"
"time"
) )
type VideoHandler struct { type VideoHandler struct {
BaseHandler BaseHandler
videoService *video.Service videoService *video.Service
uploader *oss.UploaderManager uploader *oss.UploaderManager
userService *service.UserService 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{ return &VideoHandler{
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
App: app, App: app,
DB: db, DB: db,
}, },
videoService: service, videoService: service,
uploader: uploader, uploader: uploader,
userService: userService, userService: userService,
moderationManager: moderationManager,
}
}
// RegisterRoutes 注册路由
func (h *VideoHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/video/")
// 需要用户授权的接口
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)
} }
} }
func (h *VideoHandler) LumaCreate(c *gin.Context) { func (h *VideoHandler) LumaCreate(c *gin.Context) {
var data struct { var data struct {
ClientId string `json:"client_id"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
FirstFrameImg string `json:"first_frame_img,omitempty"` FirstFrameImg string `json:"first_frame_img,omitempty"`
EndFrameImg string `json:"end_frame_img,omitempty"` EndFrameImg string `json:"end_frame_img,omitempty"`
@@ -56,6 +75,34 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return 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) user, err := h.GetLoginUser(c)
if err != nil { if err != nil {
@@ -63,37 +110,31 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
return return
} }
if user.Power < h.App.SysConfig.LumaPower { if user.Power < h.App.SysConfig.Base.LumaPower {
resp.ERROR(c, "您的算力不足,请充值后再试!") resp.ERROR(c, "您的算力不足,请充值后再试!")
return return
} }
if data.Prompt == "" {
resp.ERROR(c, "prompt is needed")
return
}
userId := int(h.GetLoginUserId(c)) userId := int(h.GetLoginUserId(c))
params := types.VideoParams{ params := types.LumaVideoParams{
PromptOptimize: data.ExpandPrompt, PromptOptimize: data.ExpandPrompt,
Loop: data.Loop, Loop: data.Loop,
StartImgURL: data.FirstFrameImg, StartImgURL: data.FirstFrameImg,
EndImgURL: data.EndFrameImg, EndImgURL: data.EndFrameImg,
} }
task := types.VideoTask{ task := types.VideoTask{
ClientId: data.ClientId,
UserId: userId, UserId: userId,
Type: types.VideoLuma, Type: types.VideoLuma,
Prompt: data.Prompt, Prompt: data.Prompt,
Params: params, Params: params,
TranslateModelId: h.App.SysConfig.TranslateModelId, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
} }
// 插入数据库 // 插入数据库
job := model.VideoJob{ job := model.VideoJob{
UserId: userId, UserId: uint(userId),
Type: types.VideoLuma, Type: types.VideoLuma,
Prompt: data.Prompt, Prompt: data.Prompt,
Power: h.App.SysConfig.LumaPower, Power: h.App.SysConfig.Base.LumaPower,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
} }
tx := h.DB.Create(&job) tx := h.DB.Create(&job)
@@ -119,20 +160,117 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
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)
}
func (h *VideoHandler) List(c *gin.Context) { func (h *VideoHandler) List(c *gin.Context) {
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
t := c.Query("type") t := c.Query("type")
page := h.GetInt(c, "page", 1) page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20) pageSize := h.GetInt(c, "page_size", 20)
all := h.GetBool(c, "all") all := h.GetBool(c, "all")
session := h.DB.Session(&gorm.Session{}).Where("user_id", userId) session := h.DB.Session(&gorm.Session{})
if t != "" { if t != "" {
session = session.Where("type", t) session = session.Where("type", t)
} }
if all { if all {
session = session.Where("publish", 0).Where("progress", 100) session = session.Where("publish", 0).Where("progress", 100)
} else { } else {
session = session.Where("user_id", h.GetLoginUserId(c)) session = session.Where("user_id", userId)
} }
// 统计总数 // 统计总数
var total int64 var total int64
@@ -161,6 +299,33 @@ func (h *VideoHandler) List(c *gin.Context) {
if item.VideoURL == "" { if item.VideoURL == "" {
item.VideoURL = v.WaterURL 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) items = append(items, item)
} }
@@ -192,6 +357,8 @@ func (h *VideoHandler) Remove(c *gin.Context) {
// 删除文件 // 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL) _ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL) _ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
resp.SUCCESS(c)
} }
func (h *VideoHandler) Publish(c *gin.Context) { func (h *VideoHandler) Publish(c *gin.Context) {
-150
View File
@@ -1,150 +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 (
"context"
"geekai/core"
"geekai/core/types"
"geekai/service"
"geekai/store/model"
"geekai/utils"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"gorm.io/gorm"
"net/http"
"strings"
)
// Websocket 连接处理 handler
type WebsocketHandler struct {
BaseHandler
wsService *service.WebsocketService
chatHandler *ChatHandler
}
func NewWebsocketHandler(app *core.AppServer, s *service.WebsocketService, db *gorm.DB, chatHandler *ChatHandler) *WebsocketHandler {
return &WebsocketHandler{
BaseHandler: BaseHandler{App: app, DB: db},
chatHandler: chatHandler,
wsService: s,
}
}
func (h *WebsocketHandler) Client(c *gin.Context) {
clientProtocols := c.GetHeader("Sec-WebSocket-Protocol")
ws, err := (&websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
Subprotocols: strings.Split(clientProtocols, ","),
}).Upgrade(c.Writer, c.Request, nil)
if err != nil {
logger.Error(err)
c.Abort()
return
}
clientId := c.Query("client_id")
client := types.NewWsClient(ws, clientId)
userId := h.GetLoginUserId(c)
if userId == 0 {
_ = client.Send([]byte("Invalid user_id"))
c.Abort()
return
}
var user model.User
if err := h.DB.Where("id", userId).First(&user).Error; err != nil {
_ = client.Send([]byte("Invalid user_id"))
c.Abort()
return
}
h.wsService.Clients.Put(clientId, client)
logger.Infof("New websocket connected, IP: %s", c.RemoteIP())
go func() {
for {
_, msg, err := client.Receive()
if err != nil {
logger.Debugf("close connection: %s", client.Conn.RemoteAddr())
client.Close()
h.wsService.Clients.Delete(clientId)
break
}
var message types.InputMessage
err = utils.JsonDecode(string(msg), &message)
if err != nil {
continue
}
logger.Debugf("Receive a message:%+v", message)
if message.Type == types.MsgTypePing {
utils.SendChannelMsg(client, types.ChPing, "pong")
continue
}
// 当前只处理聊天消息,其他消息全部丢弃
var chatMessage types.ChatMessage
err = utils.JsonDecode(utils.JsonEncode(message.Body), &chatMessage)
if err != nil || message.Channel != types.ChChat {
logger.Warnf("invalid message body:%+v", message.Body)
continue
}
var chatRole model.ChatRole
err = h.DB.First(&chatRole, chatMessage.RoleId).Error
if err != nil || !chatRole.Enable {
utils.SendAndFlush(client, "当前聊天角色不存在或者未启用,请更换角色之后再发起对话!!!")
continue
}
// if the role bind a model_id, use role's bind model_id
if chatRole.ModelId > 0 {
chatMessage.RoleId = chatRole.ModelId
}
// get model info
var chatModel model.ChatModel
err = h.DB.Where("id", chatMessage.ModelId).First(&chatModel).Error
if err != nil || chatModel.Enabled == false {
utils.SendAndFlush(client, "当前AI模型暂未启用,请更换模型后再发起对话!!!")
continue
}
session := &types.ChatSession{
ClientIP: c.ClientIP(),
UserId: userId,
}
// use old chat data override the chat model and role ID
var chat model.ChatItem
h.DB.Where("chat_id", chatMessage.ChatId).First(&chat)
if chat.Id > 0 {
chatModel.Id = chat.ModelId
chatMessage.RoleId = int(chat.RoleId)
}
session.ChatId = chatMessage.ChatId
session.Tools = chatMessage.Tools
session.Stream = chatMessage.Stream
// 复制模型数据
err = utils.CopyObject(chatModel, &session.Model)
if err != nil {
logger.Error(err, chatModel)
}
ctx, cancel := context.WithCancel(context.Background())
h.chatHandler.ReqCancelFunc.Put(clientId, cancel)
err = h.chatHandler.sendMessage(ctx, session, chatRole, chatMessage.Content, client)
if err != nil {
logger.Error(err)
utils.SendAndFlush(client, err.Error())
} else {
utils.SendMsg(client, types.ReplyMessage{Channel: types.ChChat, Type: types.MsgTypeEnd})
logger.Infof("回答完毕: %v", message.Body)
}
}
}()
}
+4 -3
View File
@@ -8,11 +8,12 @@ package logger
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"os"
"strings"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2" "gopkg.in/natefinch/lumberjack.v2"
"os"
"strings"
) )
var logger *zap.Logger var logger *zap.Logger
@@ -23,7 +24,7 @@ func GetLogger() *zap.SugaredLogger {
return sugarLogger return sugarLogger
} }
logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("LOG_LEVEL"))) logLevel := zap.NewAtomicLevelAt(getLogLevel(os.Getenv("GEEKAI_LOG_LEVEL")))
encoder := getEncoder() encoder := getEncoder()
writerSyncer := getLogWriter() writerSyncer := getLogWriter()
fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel) fileCore := zapcore.NewCore(encoder, writerSyncer, logLevel)
+129 -262
View File
@@ -17,11 +17,14 @@ import (
logger2 "geekai/logger" logger2 "geekai/logger"
"geekai/service" "geekai/service"
"geekai/service/dalle" "geekai/service/dalle"
"geekai/service/jimeng"
"geekai/service/mj" "geekai/service/mj"
"geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/payment" "geekai/service/payment"
"geekai/service/sd" "geekai/service/sd"
"geekai/service/sms" "geekai/service/sms"
"geekai/service/sora"
"geekai/service/suno" "geekai/service/suno"
"geekai/service/video" "geekai/service/video"
"geekai/store" "geekai/store"
@@ -29,7 +32,7 @@ import (
"log" "log"
"os" "os"
"os/signal" "os/signal"
"strconv" "runtime/debug"
"syscall" "syscall"
"time" "time"
@@ -70,15 +73,16 @@ func main() {
if configFile == "" { if configFile == "" {
configFile = "config.toml" configFile = "config.toml"
} }
debug, _ := strconv.ParseBool(os.Getenv("APP_DEBUG"))
logger.Info("Loading config file: ", configFile) logger.Info("Loading config file: ", configFile)
if !debug { defer func() {
defer func() { if err := recover(); err != nil {
if err := recover(); err != nil { logger.Error("Panic Error:", err)
logger.Error("Panic Error:", err) // 打印堆栈信息
if os.Getenv("GEEKAI_DEBUG") == "true" {
debug.PrintStack()
} }
}() }
} }()
app := fx.New( app := fx.New(
// 初始化配置应用配置 // 初始化配置应用配置
@@ -88,16 +92,16 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
config.Path = configFile config.Path = configFile
if debug {
_ = core.SaveConfig(config)
}
return config return config
}), }),
// 创建应用服务 // 创建应用服务
fx.Provide(core.NewServer), fx.Provide(core.NewServer),
// 初始化 // 初始化
fx.Invoke(func(s *core.AppServer, client *redis.Client) { 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)
}), }),
// 初始化数据库 // 初始化数据库
@@ -125,7 +129,7 @@ func main() {
}), }),
// 创建控制器 // 创建控制器
fx.Provide(handler.NewChatRoleHandler), fx.Provide(handler.NewChatAppHandler),
fx.Provide(handler.NewUserHandler), fx.Provide(handler.NewUserHandler),
fx.Provide(handler.NewChatHandler), fx.Provide(handler.NewChatHandler),
fx.Provide(handler.NewNetHandler), fx.Provide(handler.NewNetHandler),
@@ -140,7 +144,14 @@ func main() {
fx.Provide(handler.NewProductHandler), fx.Provide(handler.NewProductHandler),
fx.Provide(handler.NewConfigHandler), fx.Provide(handler.NewConfigHandler),
fx.Provide(handler.NewPowerLogHandler), 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.NewConfigHandler),
fx.Provide(admin.NewAdminHandler), fx.Provide(admin.NewAdminHandler),
fx.Provide(admin.NewApiKeyHandler), fx.Provide(admin.NewApiKeyHandler),
@@ -151,29 +162,17 @@ func main() {
fx.Provide(admin.NewChatModelHandler), fx.Provide(admin.NewChatModelHandler),
fx.Provide(admin.NewProductHandler), fx.Provide(admin.NewProductHandler),
fx.Provide(admin.NewOrderHandler), fx.Provide(admin.NewOrderHandler),
fx.Provide(admin.NewChatHandler),
fx.Provide(admin.NewPowerLogHandler), fx.Provide(admin.NewPowerLogHandler),
fx.Provide(admin.NewAdminJimengHandler),
// 创建服务
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(service.NewSmtpService), fx.Provide(service.NewSmtpService),
// License 服务 // Dalle 服务
fx.Provide(service.NewLicenseService), fx.Provide(dalle.NewService),
fx.Invoke(func(licenseService *service.LicenseService) { fx.Invoke(func(s *dalle.Service) {
// licenseService.SyncLicense() s.Run()
s.DownloadImages()
s.CheckTaskStatus()
}), }),
// MidJourney service pool // MidJourney service pool
@@ -182,350 +181,226 @@ func main() {
fx.Invoke(func(s *mj.Service) { fx.Invoke(func(s *mj.Service) {
s.Run() s.Run()
s.SyncTaskProgress() s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadImages() s.DownloadImages()
}), }),
// Sora service
fx.Provide(sora.NewSoraService),
// Stable Diffusion 机器人 // Stable Diffusion 机器人
fx.Provide(sd.NewService), fx.Provide(sd.NewService),
fx.Invoke(func(s *sd.Service, config *types.AppConfig) { fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
s.Run() s.Run()
s.CheckTaskStatus() s.CheckTaskStatus()
s.CheckTaskNotify()
}), }),
fx.Provide(suno.NewService), fx.Provide(suno.NewService),
fx.Invoke(func(s *suno.Service) { fx.Invoke(func(s *suno.Service) {
s.Run() s.Run()
s.SyncTaskProgress() s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadFiles() s.DownloadFiles()
}), }),
fx.Provide(video.NewService), fx.Provide(video.NewService),
fx.Invoke(func(s *video.Service) { fx.Invoke(func(s *video.Service) {
s.Run() s.Run()
s.SyncTaskProgress() s.SyncTaskProgress()
s.CheckTaskNotify()
s.DownloadFiles() s.DownloadFiles()
}), }),
fx.Provide(service.NewUserService),
fx.Provide(payment.NewAlipayService), // 即梦AI 服务
fx.Provide(payment.NewHuPiPay), fx.Provide(jimeng.NewClient),
fx.Provide(payment.NewJPayService), fx.Provide(jimeng.NewService),
fx.Provide(payment.NewWechatService), fx.Invoke(func(service *jimeng.Service) {
service.Start()
}),
fx.Provide(service.NewSnowflake), fx.Provide(service.NewSnowflake),
fx.Provide(service.NewXXLJobExecutor),
fx.Invoke(func(exec *service.XXLJobExecutor, config *types.AppConfig) { // 创建短信服务
if config.XXLConfig.Enabled { fx.Provide(sms.NewAliYunSmsService),
go func() { fx.Provide(sms.NewBaoSmsService),
log.Fatal(exec.Run()) 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) { fx.Invoke(func(s *core.AppServer, h *handler.ChatAppHandler) {
group := s.Engine.Group("/api/app/") h.RegisterRoutes()
group.GET("list", h.List)
group.GET("list/user", h.ListByUser)
group.POST("update", h.UpdateRole)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) { fx.Invoke(func(s *core.AppServer, h *handler.UserHandler) {
group := s.Engine.Group("/api/user/") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ChatHandler) {
group := s.Engine.Group("/api/chat/") h.RegisterRoutes()
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.NetHandler) { fx.Invoke(func(s *core.AppServer, h *handler.NetHandler) {
s.Engine.POST("/api/upload", h.Upload) h.RegisterRoutes()
s.Engine.POST("/api/upload/list", h.List)
s.Engine.GET("/api/upload/remove", h.Remove)
s.Engine.GET("/api/download", h.Download)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) { fx.Invoke(func(s *core.AppServer, h *handler.SmsHandler) {
group := s.Engine.Group("/api/sms/") h.RegisterRoutes()
group.POST("code", h.SendCode)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) { fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
group := s.Engine.Group("/api/captcha/") h.RegisterRoutes()
group.GET("get", h.Get)
group.POST("check", h.Check)
group.GET("slide/get", h.SlideGet)
group.POST("slide/check", h.SlideCheck)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) { fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
group := s.Engine.Group("/api/redeem/") h.RegisterRoutes()
group.POST("verify", h.Verify)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
group := s.Engine.Group("/api/mj/") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) { fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
group := s.Engine.Group("/api/sd") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
group := s.Engine.Group("/api/config/") h.RegisterRoutes()
group.GET("get", h.Get)
group.GET("license", h.License)
}), }),
// 管理后台控制器 // 管理后台路由注册
fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ConfigHandler) {
group := s.Engine.Group("/api/admin/config") h.RegisterRoutes()
group.POST("update", h.Update)
group.GET("get", h.Get)
group.POST("active", h.Active)
group.GET("fixData", h.FixData)
group.GET("license", h.GetLicense)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ManagerHandler) {
group := s.Engine.Group("/api/admin/") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ApiKeyHandler) {
group := s.Engine.Group("/api/admin/apikey/") h.RegisterRoutes()
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("set", h.Set)
group.GET("remove", h.Remove)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) { fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
group := s.Engine.Group("/api/admin/user/") h.RegisterRoutes()
group.GET("list", h.List)
group.POST("save", h.Save)
group.GET("remove", h.Remove)
group.GET("loginLog", h.LoginLog)
group.POST("resetPass", h.ResetPass)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ChatAppHandler) {
group := s.Engine.Group("/api/admin/role/") h.RegisterRoutes()
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.RedeemHandler) { fx.Invoke(func(s *core.AppServer, h *admin.RedeemHandler) {
group := s.Engine.Group("/api/admin/redeem/") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) { fx.Invoke(func(s *core.AppServer, h *admin.DashboardHandler) {
group := s.Engine.Group("/api/admin/dashboard/") h.RegisterRoutes()
group.GET("stats", h.Stats)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ChatModelHandler) {
group := s.Engine.Group("/api/model/") h.RegisterRoutes()
group.GET("list", h.List)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ChatModelHandler) {
group := s.Engine.Group("/api/admin/model/") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) { fx.Invoke(func(s *core.AppServer, h *handler.PaymentHandler) {
group := s.Engine.Group("/api/payment/") h.RegisterRoutes()
group.POST("doPay", h.Pay) h.StartSyncOrders()
group.GET("payWays", h.GetPayWays)
group.POST("notify/alipay", h.AlipayNotify)
group.GET("notify/geek", h.GeekPayNotify)
group.POST("notify/wechat", h.WechatPayNotify)
group.POST("notify/hupi", h.HuPiPayNotify)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ProductHandler) {
group := s.Engine.Group("/api/admin/product/") h.RegisterRoutes()
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) { fx.Invoke(func(s *core.AppServer, h *admin.OrderHandler) {
group := s.Engine.Group("/api/admin/order/") h.RegisterRoutes()
group.POST("list", h.List)
group.GET("remove", h.Remove)
group.GET("clear", h.Clear)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) { fx.Invoke(func(s *core.AppServer, h *handler.OrderHandler) {
group := s.Engine.Group("/api/order/") h.RegisterRoutes()
group.GET("list", h.List)
group.GET("query", h.Query)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ProductHandler) {
group := s.Engine.Group("/api/product/") h.RegisterRoutes()
group.GET("list", h.List)
}), }),
fx.Provide(handler.NewInviteHandler), fx.Provide(handler.NewInviteHandler),
fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) { fx.Invoke(func(s *core.AppServer, h *handler.InviteHandler) {
group := s.Engine.Group("/api/invite/") h.RegisterRoutes()
group.GET("code", h.Code)
group.GET("list", h.List)
group.GET("hits", h.Hits)
}), }),
fx.Provide(admin.NewFunctionHandler), fx.Provide(admin.NewFunctionHandler),
fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) { fx.Invoke(func(s *core.AppServer, h *admin.FunctionHandler) {
group := s.Engine.Group("/api/admin/function/") h.RegisterRoutes()
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)
}), }),
fx.Provide(admin.NewUploadHandler), fx.Provide(admin.NewUploadHandler),
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) { fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
s.Engine.POST("/api/admin/upload", h.Upload) h.RegisterRoutes()
}), }),
fx.Provide(handler.NewFunctionHandler), fx.Provide(handler.NewFunctionHandler),
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) { fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
group := s.Engine.Group("/api/function/") h.RegisterRoutes()
group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3)
group.GET("list", h.List)
}), }),
fx.Provide(admin.NewChatHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ChatHandler) {
group := s.Engine.Group("/api/admin/chat/") h.RegisterRoutes()
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)
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) { fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
group := s.Engine.Group("/api/powerLog/") h.RegisterRoutes()
group.POST("list", h.List)
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) { fx.Invoke(func(s *core.AppServer, h *admin.PowerLogHandler) {
group := s.Engine.Group("/api/admin/powerLog/") h.RegisterRoutes()
group.POST("list", h.List)
}), }),
fx.Provide(admin.NewMenuHandler), fx.Provide(admin.NewMenuHandler),
fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) { fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) {
group := s.Engine.Group("/api/admin/menu/") h.RegisterRoutes()
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
group.GET("remove", h.Remove)
}), }),
fx.Provide(handler.NewMenuHandler), fx.Provide(handler.NewMenuHandler),
fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) { fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) {
group := s.Engine.Group("/api/menu/") h.RegisterRoutes()
group.GET("list", h.List)
}), }),
fx.Provide(handler.NewMarkMapHandler), fx.Provide(handler.NewMarkMapHandler),
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) { fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
s.Engine.POST("/api/markMap/gen", h.Generate) h.RegisterRoutes()
}), }),
fx.Provide(handler.NewDallJobHandler), fx.Provide(handler.NewDallJobHandler),
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) { fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) {
group := s.Engine.Group("/api/dall") h.RegisterRoutes()
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)
group.GET("models", h.GetModels)
}), }),
fx.Provide(handler.NewSunoHandler), fx.Provide(handler.NewSunoHandler),
fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) { fx.Invoke(func(s *core.AppServer, h *handler.SunoHandler) {
group := s.Engine.Group("/api/suno") h.RegisterRoutes()
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)
}), }),
fx.Provide(handler.NewVideoHandler), fx.Provide(handler.NewVideoHandler),
fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) { fx.Invoke(func(s *core.AppServer, h *handler.VideoHandler) {
group := s.Engine.Group("/api/video") h.RegisterRoutes()
group.POST("luma/create", h.LumaCreate) }),
group.GET("list", h.List)
group.GET("remove", h.Remove) // 即梦AI 路由
group.GET("publish", h.Publish) 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.Provide(admin.NewChatAppTypeHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ChatAppTypeHandler) {
group := s.Engine.Group("/api/admin/app/type") h.RegisterRoutes()
group.POST("save", h.Save)
group.GET("list", h.List)
group.GET("remove", h.Remove)
group.POST("enable", h.Enable)
group.POST("sort", h.Sort)
}), }),
fx.Provide(handler.NewChatAppTypeHandler), fx.Provide(handler.NewChatAppTypeHandler),
fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ChatAppTypeHandler) {
group := s.Engine.Group("/api/app/type") h.RegisterRoutes()
group.GET("list", h.List)
}), }),
fx.Provide(handler.NewTestHandler), fx.Provide(handler.NewTestHandler),
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) { fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) {
group := s.Engine.Group("/api/test") h.RegisterRoutes()
group.Any("sse", h.PostTest, h.SseTest)
}),
fx.Provide(service.NewWebsocketService),
fx.Provide(handler.NewWebsocketHandler),
fx.Invoke(func(s *core.AppServer, h *handler.WebsocketHandler) {
s.Engine.Any("/api/ws", h.Client)
}), }),
fx.Provide(handler.NewPromptHandler), fx.Provide(handler.NewPromptHandler),
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) { fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
group := s.Engine.Group("/api/prompt") h.RegisterRoutes()
group.POST("/lyric", h.Lyric)
group.POST("/image", h.Image)
group.POST("/video", h.Video)
group.POST("/meta", h.MetaPrompt)
}), }),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) { fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() { go func() {
@@ -550,23 +425,15 @@ func main() {
}), }),
fx.Provide(admin.NewImageHandler), fx.Provide(admin.NewImageHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
group := s.Engine.Group("/api/admin/image") h.RegisterRoutes()
group.POST("/list/mj", h.MjList)
group.POST("/list/sd", h.SdList)
group.POST("/list/dall", h.DallList)
group.GET("/remove", h.Remove)
}), }),
fx.Provide(admin.NewMediaHandler), fx.Provide(admin.NewMediaHandler),
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) { fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
group := s.Engine.Group("/api/admin/media") h.RegisterRoutes()
group.POST("/list/suno", h.SunoList)
group.POST("/list/luma", h.LumaList)
group.GET("/remove", h.Remove)
}), }),
fx.Provide(handler.NewRealtimeHandler), fx.Provide(handler.NewRealtimeHandler),
fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) { fx.Invoke(func(s *core.AppServer, h *handler.RealtimeHandler) {
s.Engine.Any("/api/realtime", h.Connection) h.RegisterRoutes()
s.Engine.POST("/api/realtime/voice", h.VoiceChat)
}), }),
) )
// 启动应用程序 // 启动应用程序
-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: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

+24 -28
View File
@@ -8,35 +8,38 @@ package service
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"errors"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"github.com/imroc/req/v3"
"time" "time"
"github.com/imroc/req/v3"
) )
type CaptchaService struct { type CaptchaService struct {
config types.ApiConfig config types.CaptchaConfig
client *req.Client client *req.Client
} }
func NewCaptchaService(config types.ApiConfig) *CaptchaService { func NewCaptchaService(sysConfig *types.SystemConfig) *CaptchaService {
return &CaptchaService{ return &CaptchaService{
config: config, config: sysConfig.Captcha,
client: req.C().SetTimeout(10 * time.Second), client: req.C().SetTimeout(10 * time.Second),
} }
} }
func (s *CaptchaService) Get() (interface{}, error) { func (s *CaptchaService) UpdateConfig(config types.CaptchaConfig) {
if s.config.Token == "" { s.config = config
return nil, errors.New("无效的 API Token") }
}
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 var res types.BizVo
r, err := s.client.R(). r, err := s.client.R().
SetHeader("AppId", s.config.AppId). SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetSuccessResult(&res).Get(url) SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() { if err != nil || r.IsErrorState() {
return nil, fmt.Errorf("请求 API 失败:%v", err) return nil, fmt.Errorf("请求 API 失败:%v", err)
@@ -49,12 +52,11 @@ func (s *CaptchaService) Get() (interface{}, error) {
return res.Data, nil return res.Data, nil
} }
func (s *CaptchaService) Check(data interface{}) bool { func (s *CaptchaService) Check(data any) bool {
url := fmt.Sprintf("%s/api/captcha/check", s.config.ApiURL) url := fmt.Sprintf("%s/api/captcha/check", types.GeekAPIURL)
var res types.BizVo var res types.BizVo
r, err := s.client.R(). r, err := s.client.R().
SetHeader("AppId", s.config.AppId). SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetBodyJsonMarshal(data). SetBodyJsonMarshal(data).
SetSuccessResult(&res).Post(url) SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() { if err != nil || r.IsErrorState() {
@@ -68,16 +70,11 @@ func (s *CaptchaService) Check(data interface{}) bool {
return true return true
} }
func (s *CaptchaService) SlideGet() (interface{}, error) { func (s *CaptchaService) SlideGet() (any, error) {
if s.config.Token == "" { url := fmt.Sprintf("%s/api/captcha/slide/get", types.GeekAPIURL)
return nil, errors.New("无效的 API Token")
}
url := fmt.Sprintf("%s/api/captcha/slide/get", s.config.ApiURL)
var res types.BizVo var res types.BizVo
r, err := s.client.R(). r, err := s.client.R().
SetHeader("AppId", s.config.AppId). SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetSuccessResult(&res).Get(url) SetSuccessResult(&res).Get(url)
if err != nil || r.IsErrorState() { if err != nil || r.IsErrorState() {
return nil, fmt.Errorf("请求 API 失败:%v", err) return nil, fmt.Errorf("请求 API 失败:%v", err)
@@ -90,12 +87,11 @@ func (s *CaptchaService) SlideGet() (interface{}, error) {
return res.Data, nil return res.Data, nil
} }
func (s *CaptchaService) SlideCheck(data interface{}) bool { func (s *CaptchaService) SlideCheck(data any) bool {
url := fmt.Sprintf("%s/api/captcha/slide/check", s.config.ApiURL) url := fmt.Sprintf("%s/api/captcha/slide/check", types.GeekAPIURL)
var res types.BizVo var res types.BizVo
r, err := s.client.R(). r, err := s.client.R().
SetHeader("AppId", s.config.AppId). SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.ApiKey)).
SetHeader("Authorization", fmt.Sprintf("Bearer %s", s.config.Token)).
SetBodyJsonMarshal(data). SetBodyJsonMarshal(data).
SetSuccessResult(&res).Post(url) SetSuccessResult(&res).Post(url)
if err != nil || r.IsErrorState() { if err != nil || r.IsErrorState() {
@@ -16,7 +16,6 @@ import (
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"io"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
@@ -34,33 +33,29 @@ type Service struct {
db *gorm.DB db *gorm.DB
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
taskQueue *store.RedisQueue taskQueue *store.RedisQueue
notifyQueue *store.RedisQueue
userService *service.UserService userService *service.UserService
wsService *service.WebsocketService
clientIds map[uint]string
} }
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService, wsService *service.WebsocketService) *Service { func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{ return &Service{
httpClient: req.C().SetTimeout(time.Minute * 3), httpClient: req.C().SetTimeout(time.Minute * 3),
db: db, db: db,
taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli), taskQueue: store.NewRedisQueue("DallE_Task_Queue", redisCli),
notifyQueue: store.NewRedisQueue("DallE_Notify_Queue", redisCli),
wsService: wsService,
uploadManager: manager, uploadManager: manager,
userService: userService, userService: userService,
clientIds: map[uint]string{},
} }
} }
// PushTask push a new mj task in to task queue // PushTask push a new mj task in to task queue
func (s *Service) PushTask(task types.DallTask) { func (s *Service) PushTask(task types.DallTask) {
logger.Infof("add a new DALL-E task to the task list: %+v", task) 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() { func (s *Service) Run() {
// 将数据库中未提交的人物加载到队列 // 将数据库中未提交的任务加载到队列
var jobs []model.DallJob var jobs []model.DallJob
s.db.Where("progress", 0).Find(&jobs) s.db.Where("progress", 0).Find(&jobs)
for _, v := range jobs { for _, v := range jobs {
@@ -84,27 +79,27 @@ func (s *Service) Run() {
continue continue
} }
logger.Infof("handle a new DALL-E task: %+v", task) logger.Infof("handle a new DALL-E task: %+v", task)
s.clientIds[task.Id] = task.ClientId go func() {
_, err = s.Image(task, false) _, err = s.Image(task, false)
if err != nil { if err != nil {
logger.Errorf("error with image task: %v", err) logger.Errorf("error with image task: %v", err)
s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{ s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"progress": service.FailTaskProgress, "progress": service.FailTaskProgress,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed}) }
} }()
} }
}() }()
} }
type imgReq struct { type imgReq struct {
Model string `json:"model"` Model string `json:"model"`
Prompt string `json:"prompt"` Image []string `json:"image,omitempty"`
N int `json:"n,omitempty"` Prompt string `json:"prompt"`
Size string `json:"size,omitempty"` AspectRatio string `json:"aspect_ratio,omitempty"`
Quality string `json:"quality,omitempty"` Size string `json:"size,omitempty"`
Style string `json:"style,omitempty"` ResponseFormat string `json:"response_format,omitempty"`
} }
type imgRes struct { type imgRes struct {
@@ -118,27 +113,22 @@ type imgRes struct {
type ErrRes struct { type ErrRes struct {
Error struct { Error struct {
Code interface{} `json:"code"` Code any `json:"code"`
Message string `json:"message"` Message string `json:"message"`
Param interface{} `json:"param"` Param any `json:"param"`
Type string `json:"type"` Type string `json:"type"`
} `json:"error"` } `json:"error"`
} }
func (s *Service) Image(task types.DallTask, sync bool) (string, error) { func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
logger.Debugf("绘画参数:%+v", task) logger.Debugf("绘画参数:%+v", task)
prompt := task.Prompt
// translate prompt
if utils.HasChinese(prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, prompt), task.TranslateModelId)
if err == nil {
prompt = content
logger.Debugf("重写后提示词:%s", prompt)
}
}
var chatModel model.ChatModel var chatModel model.ChatModel
s.db.Where("id = ?", task.ModelId).First(&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 // get image generation API KEY
var apiKey model.ApiKey var apiKey model.ApiKey
@@ -160,13 +150,18 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
} }
apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL) apiURL := fmt.Sprintf("%s/v1/images/generations", apiKey.ApiURL)
reqBody := imgReq{ reqBody := imgReq{
Model: chatModel.Value, Model: chatModel.Value,
Prompt: prompt, Prompt: task.Prompt,
N: 1, AspectRatio: task.AspectRatio,
Size: task.Size, Size: task.Size,
Style: task.Style, ResponseFormat: "url",
Quality: task.Quality,
} }
// 图片编辑
if len(task.Image) > 0 {
reqBody.Image = task.Image
}
logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody) logger.Infof("Channel:%s, API KEY:%s, BODY: %+v", apiURL, apiKey.Value, reqBody)
r, err := s.httpClient.R().SetHeader("Body-Type", "application/json"). r, err := s.httpClient.R().SetHeader("Body-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value). SetHeader("Authorization", "Bearer "+apiKey.Value).
@@ -184,15 +179,12 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
return "", fmt.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)
} }
all, _ := io.ReadAll(r.Body)
logger.Debugf("response: %+v", string(all))
// update the api key last use time // update the api key last use time
s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix()) s.db.Model(&apiKey).UpdateColumn("last_used_at", time.Now().Unix())
var imgURL string var imgURL string
var data = map[string]interface{}{ var data = map[string]any{
"progress": 100, "progress": 100,
"prompt": prompt, "prompt": task.Prompt,
} }
// 如果返回的是base64,则需要上传到oss // 如果返回的是base64,则需要上传到oss
if res.Data[0].B64Json != "" { if res.Data[0].B64Json != "" {
@@ -212,39 +204,14 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
return "", fmt.Errorf("err with update database: %v", err) return "", fmt.Errorf("err with update database: %v", err)
} }
s.notifyQueue.RPush(service.NotifyMessage{ClientId: task.ClientId, UserId: int(task.UserId), JobId: int(task.Id), Message: service.TaskStatusFailed})
var content string var content string
if sync { if sync {
imgURL, err := s.downloadImage(task.Id, int(task.UserId), res.Data[0].Url) content = fmt.Sprintf("```\n%s\n```\n下面是我为你创作的图片:\n\n![](%s)\n", task.Prompt, imgURL)
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)
} }
return content, nil 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
}
logger.Debugf("notify message: %+v", message)
client := s.wsService.Clients.Get(message.ClientId)
if client == nil {
continue
}
utils.SendChannelMsg(client, types.ChDall, message.Message)
}
}()
}
func (s *Service) CheckTaskStatus() { func (s *Service) CheckTaskStatus() {
go func() { go func() {
logger.Info("Running DALL-E task status checking ...") logger.Info("Running DALL-E task status checking ...")
@@ -254,7 +221,7 @@ func (s *Service) CheckTaskStatus() {
s.db.Where("progress < ?", 100).Find(&jobs) s.db.Where("progress < ?", 100).Find(&jobs)
for _, job := range 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.Progress = service.FailTaskProgress
job.ErrMsg = "任务超时" job.ErrMsg = "任务超时"
s.db.Updates(&job) s.db.Updates(&job)
@@ -269,7 +236,7 @@ func (s *Service) CheckTaskStatus() {
if err != nil { if err != nil {
continue continue
} }
err = s.userService.IncreasePower(int(job.UserId), job.Power, model.PowerLog{ err = s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund, Type: types.PowerRefund,
Model: task.ModelName, Model: task.ModelName,
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg), Remark: fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg),
@@ -301,7 +268,7 @@ func (s *Service) DownloadImages() {
} }
logger.Infof("try to download image: %s", v.OrgURL) 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 { if err != nil {
logger.Error("error with download image: %s, error: %v", imgURL, err) logger.Error("error with download image: %s, error: %v", imgURL, err)
continue continue
@@ -316,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 // sava image
imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, false) imgURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(orgURL, ".png", false)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -328,6 +295,5 @@ func (s *Service) downloadImage(jobId uint, userId int, orgURL string) (string,
if res.Error != nil { if res.Error != nil {
return "", err return "", err
} }
s.notifyQueue.RPush(service.NotifyMessage{ClientId: s.clientIds[jobId], UserId: userId, JobId: int(jobId), Message: service.TaskStatusFinished})
return imgURL, nil return imgURL, nil
} }
+281
View File
@@ -0,0 +1,281 @@
package jimeng
import (
"context"
"encoding/json"
"errors"
"fmt"
"geekai/core/types"
"net/http"
"net/url"
"strings"
"time"
"github.com/volcengine/volc-sdk-golang/base"
"github.com/volcengine/volc-sdk-golang/service/visual"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/volcengine/volcengine-go-sdk/volcengine"
)
// Client 即梦API客户端
type Client struct {
visual *visual.Visual
config types.JimengConfig
}
// NewClient 创建即梦API客户端
func NewClient(sysConfig *types.SystemConfig) *Client {
client := &Client{}
client.UpdateConfig(sysConfig.Jimeng)
return client
}
func (c *Client) UpdateConfig(config types.JimengConfig) error {
// 使用官方SDK的visual实例
visualInstance := visual.NewInstance()
visualInstance.Client.SetAccessKey(config.AccessKey)
visualInstance.Client.SetSecretKey(config.SecretKey)
// 添加即梦AI专有的API配置
jimengApis := map[string]*base.ApiInfo{
"CVSync2AsyncSubmitTask": {
Method: http.MethodPost,
Path: "/",
Query: url.Values{
"Action": []string{"CVSync2AsyncSubmitTask"},
"Version": []string{"2022-08-31"},
},
},
"CVSync2AsyncGetResult": {
Method: http.MethodPost,
Path: "/",
Query: url.Values{
"Action": []string{"CVSync2AsyncGetResult"},
"Version": []string{"2022-08-31"},
},
},
"CVSubmitTask": {
Method: http.MethodPost,
Path: "/",
Query: url.Values{
"Action": []string{"CVSubmitTask"},
"Version": []string{"2022-08-31"},
},
},
"CVGetResult": {
Method: http.MethodPost,
Path: "/",
Query: url.Values{
"Action": []string{"CVGetResult"},
"Version": []string{"2022-08-31"},
},
},
"CVProcess": {
Method: http.MethodPost,
Path: "/",
Query: url.Values{
"Action": []string{"CVProcess"},
"Version": []string{"2022-08-31"},
},
},
}
// 将即梦API添加到现有的ApiInfoList中
for name, info := range jimengApis {
visualInstance.Client.ApiInfoList[name] = info
}
c.config = config
c.visual = visualInstance
return c.testConnection()
}
// GetErrorMessage 根据错误代码获取对应的错误信息
func GetErrorMessage(code int) string {
if message, exists := errorCodeMessages[code]; exists {
return message
}
return fmt.Sprintf("未知错误代码: %d", code)
}
// HandleResponseError 处理响应错误,根据错误代码返回详细的错误信息
func HandleResponseError(code int, message string) error {
if code == ECSuccess {
return nil
}
return errors.New(GetErrorMessage(code))
}
// testConnection 测试即梦AI连接
func (c *Client) testConnection() error {
// 使用一个简单的查询任务来测试连接
testReq := &QueryTaskRequest{
ReqKey: "test_connection",
TaskId: "test_task_id_12345",
}
_, err := c.QueryTask(testReq, ASyncActionGetResult)
// 即使任务不存在,只要不是认证错误就说明连接正常
if err != nil {
// 检查是否是认证错误
if strings.Contains(err.Error(), "InvalidAccessKey") {
return fmt.Errorf("认证失败,请检查AccessKey和SecretKey是否正确")
}
// 其他错误(如任务不存在)说明连接正常
return nil
}
return nil
}
// SubmitTask 提交异步任务
func (c *Client) SubmitTask(req map[string]any) (*SubmitTaskResponse, error) {
// 直接将请求转为map[string]interface{}
reqBodyBytes, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 直接使用序列化后的字节
jsonBody := reqBodyBytes
action := ASyncActionSubmit
if v, ok := req["action"]; ok {
action = v.(string)
delete(req, "action")
}
// 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
if err != nil {
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
}
logger.Infof("Jimeng SubmitTask Response: %s", string(respBody))
// 解析响应
var result SubmitTaskResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("unmarshal response failed: %w", err)
}
// 检查响应错误代码
if err := HandleResponseError(result.Code, result.Message); err != nil {
return nil, err
}
return &result, nil
}
// 识别数字人主体
func (c *Client) AvatarRecognition(imgUrl string, reqKey string) error {
params := map[string]any{
"image_url": imgUrl,
"req_key": reqKey,
}
reqBodyBytes, err := json.Marshal(params)
if err != nil {
return fmt.Errorf("marshal request failed: %w", err)
}
// 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json(SyncActionSubmit, nil, string(reqBodyBytes))
if err != nil {
return fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
}
// 解析响应
var result SubmitTaskResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("unmarshal response failed: %w", err)
}
// 检查响应错误代码
if err := HandleResponseError(result.Code, result.Message); err != nil {
return err
}
// 等待任务完成
for {
resp, err := c.QueryTask(&QueryTaskRequest{
ReqKey: reqKey,
TaskId: result.Data.TaskId,
}, SyncActionGetResult)
if err != nil {
return fmt.Errorf("query task failed: %w", err)
}
if resp.Data.Status != types.JMTaskStatusDone {
time.Sleep(time.Second * 3)
continue
}
var respData map[string]int
if err := json.Unmarshal([]byte(resp.Data.RespData), &respData); err != nil {
return fmt.Errorf("unmarshal response failed: %w", err)
}
logger.Debugf("Jimeng AvatarRecognition Response: %+v", resp)
if respData["status"] == 1 {
return nil
} else {
return errors.New("不包含人、类人、拟人等主体")
}
}
}
// QueryTask 查询任务结果
func (c *Client) QueryTask(req *QueryTaskRequest, action string) (*QueryTaskResponse, error) {
// 序列化请求
jsonBody, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request failed: %w", err)
}
// 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
if err != nil {
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
}
logger.Infof("Jimeng QueryTask Response: %s", string(respBody))
// 解析响应
var result QueryTaskResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("unmarshal response failed: %w", err)
}
// 检查响应错误代码
if err := HandleResponseError(result.Code, result.Message); err != nil {
return nil, err
}
return &result, nil
}
// SubmitSyncImageTask 提交同步生图任务
func (c *Client) SubmitSyncImageTask(req types.JimengTaskRequest) (*model.ImagesResponse, error) {
// 配置火山引擎访问密钥,目前只支持API Key验证
client := arkruntime.NewClientWithApiKey(c.config.ApiKey)
// 构造生图请求
sequentialImageGeneration := model.SequentialImageGeneration("disabled")
generateReq := model.GenerateImagesRequest{
Model: req.ReqKey, // 模型名称
Prompt: req.Prompt, // 提示词
Size: volcengine.String(req.Size), // 图片尺寸
SequentialImageGeneration: &sequentialImageGeneration, // 禁用序列生成
ResponseFormat: volcengine.String(model.GenerateImagesResponseFormatURL), // 响应格式为 URL
Watermark: volcengine.Bool(false), // 不添加水印
OptimizePrompt: volcengine.Bool(true), // 优化提示词
}
if len(req.ImageUrls) > 0 {
generateReq.Image = req.ImageUrls
}
// 调用生图 API
resp, err := client.GenerateImages(context.Background(), generateReq)
if err != nil {
return nil, err
}
return &resp, nil
}

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