Compare commits

..

1174 Commits

Author SHA1 Message Date
RockYang 9ccff4efbc feat(release): migrate GeekAI v4.3.0 to open source
- Sync backend and frontend from GeekAI Plus v4.3.0

- Remove commercial License flows and update open-source deployment defaults

- Preserve Docker Compose deployment and bump image tags to v4.3.0

BREAKING CHANGE: commercial License configuration and related endpoints are removed
2026-08-11 14:51:20 +08:00
RockYang 37e024acea update gitignore 2026-08-03 11:37:55 +08:00
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
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
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
GeekMaster 2b5165324c update docker image version 2025-04-17 10:22:41 +08:00
GeekMaster 8f67826072 update docker image version 2025-04-17 10:08:38 +08:00
GeekMaster ac9a31f049 merge code for v4.1.8 2025-04-17 10:07:04 +08:00
RockYang ed2cdbbc31 更新 README 2025-04-07 17:52:06 +08:00
RockYang 8bd2feaf7e Merge branch 'main' of gitee.com:blackfox/geekai 2025-03-25 10:48:46 +08:00
RockYang 4eccfe6d2c remove sensitive message for openai API KEY 2025-03-25 10:48:18 +08:00
RockYang 5ba2e000a1 update database file 2025-03-24 20:29:03 +08:00
RockYang fe9adadb3a update database file 2025-03-24 20:29:03 +08:00
RockYang 805c2a045e merge release v4.1.7 2025-03-24 17:13:16 +08:00
RockYang fd7b196134 merge release v4.1.7 2025-03-24 17:13:16 +08:00
RockYang 1b1c327c35 Merge tag 'v4.1.7' of gitee.com:blackfox/geekai-plus 2025-03-24 11:27:11 +08:00
RockYang a228f603d2 Merge tag 'v4.1.7' of gitee.com:blackfox/geekai-plus 2025-03-24 11:27:11 +08:00
RockYang 7d0a05ee11 update v4.1.5 sql file 2025-03-06 14:03:44 +08:00
RockYang 5f0a62b63e update v4.1.5 sql file 2025-03-06 14:03:44 +08:00
RockYang 825a1b1027 文档对话不显示文档列表的 bug 2025-03-06 14:00:13 +08:00
RockYang 38dde1a373 文档对话不显示文档列表的 bug 2025-03-06 14:00:13 +08:00
RockYang 950e7d1b00 Merge branch 'dev' 2025-03-05 18:42:44 +08:00
RockYang 89ded74345 Merge branch 'dev' 2025-03-05 18:42:44 +08:00
RockYang d8f9f48278 merge v4.1.6 2025-03-05 18:42:30 +08:00
RockYang 818bb38617 merge v4.1.6 2025-03-05 18:42:30 +08:00
GeekMaser bdd76addf3 Merge pull request #238 from gotoworld/main
v4.1.5中,存在Merge冲突脚本 #237
2025-02-25 09:27:15 +08:00
GeekMaser 0202d75ff4 Merge pull request #238 from gotoworld/main
v4.1.5中,存在Merge冲突脚本 #237
2025-02-25 09:27:15 +08:00
Welcome Aboard 2936f21f12 Merge pull request #1 from gotoworld/gotoworld-patch-1
Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql
2025-02-24 19:59:00 +08:00
Welcome Aboard a0831ec6cd Merge pull request #1 from gotoworld/gotoworld-patch-1
Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql
2025-02-24 19:59:00 +08:00
Welcome Aboard 705ca4d20a Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql 2025-02-24 19:57:40 +08:00
Welcome Aboard 2926aab7a4 Bugs: fixed merge conflict in geekai_plus-v4.1.5.sql 2025-02-24 19:57:40 +08:00
RockYang 6a066f1b8e 默认允许跨域请求API 2025-02-11 14:34:31 +08:00
RockYang 775fb1f853 默认允许跨域请求API 2025-02-11 14:34:31 +08:00
RockYang b7d137247a 优化 docker 容器依赖关系 2025-02-11 10:10:43 +08:00
RockYang 22278c40bf 优化 docker 容器依赖关系 2025-02-11 10:10:43 +08:00
RockYang 4373642ebd 优化打包脚本,新增 arm64 架构打包脚本 2025-02-11 10:01:05 +08:00
RockYang e29b32bd2d 优化打包脚本,新增 arm64 架构打包脚本 2025-02-11 10:01:05 +08:00
RockYang 5bf90920f5 resolve conflicts 2025-02-11 09:53:41 +08:00
RockYang fa6bb5e46a resolve conflicts 2025-02-11 09:53:41 +08:00
RockYang edcbb3e226 merge v4.1.5 2025-02-11 09:45:26 +08:00
RockYang cebf8497a4 merge v4.1.5 2025-02-11 09:45:26 +08:00
RockYang 5213bdf08b fixed bug for calculate chat message tokens 2025-01-19 13:08:38 +08:00
RockYang cded4bdcc7 fixed bug for calculate chat message tokens 2025-01-19 13:08:38 +08:00
RockYang cf817fd8ea merge code for v4.1.4 2025-01-18 23:20:41 +08:00
RockYang 596b700d63 merge code for v4.1.4 2025-01-18 23:20:41 +08:00
RockYang a2481ff1cf update readme 2025-01-07 11:58:16 +08:00
RockYang 59dfa95bf8 update readme 2025-01-07 11:58:16 +08:00
RockYang bc7d06d3e5 修复登录失效的 Bug 2024-12-21 21:44:52 +08:00
RockYang f6b5a94b29 修复登录失效的 Bug 2024-12-21 21:44:52 +08:00
RockYang 8e81dfa12a update database name 2024-12-16 11:26:54 +08:00
RockYang 188fb23f08 update database name 2024-12-16 11:26:54 +08:00
RockYang 0ff76f0f21 update database file 2024-12-16 11:11:42 +08:00
RockYang 5347a12035 update database file 2024-12-16 11:11:42 +08:00
RockYang 787caa84c8 merge config.toml file 2024-12-16 10:09:23 +08:00
RockYang 691d453f41 merge config.toml file 2024-12-16 10:09:23 +08:00
RockYang c2503e663a merge v4.1.3 2024-12-16 10:07:52 +08:00
RockYang 5b7f2603ae merge v4.1.3 2024-12-16 10:07:52 +08:00
RockYang 405a88862b Merge branch 'dev' 2024-11-27 15:04:26 +08:00
RockYang 3fd7f810f0 Merge branch 'dev' 2024-11-27 15:04:26 +08:00
RockYang 296eabe09a merge v4.1.2 2024-11-27 15:00:02 +08:00
RockYang e04a48623d merge v4.1.2 2024-11-27 15:00:02 +08:00
RockYang 2e1bad387c redeem export function is ready 2024-11-27 11:52:18 +08:00
RockYang 54b45ec2ff update docker-compose.yaml 2024-11-14 16:37:43 +08:00
RockYang 5c18a50330 update docker-compose.yaml 2024-11-14 16:37:43 +08:00
RockYang c434f85045 update database 2024-11-14 16:01:27 +08:00
RockYang d78d3ffe02 update database 2024-11-14 16:01:27 +08:00
RockYang 4d10279870 merge v4.1.1 and fixed conflicts 2024-11-13 18:40:04 +08:00
RockYang 44240f65d5 merge v4.1.1 and fixed conflicts 2024-11-13 18:40:04 +08:00
RockYang 97e81a7dcc fixed bug for chat context not work for chating with image 2024-11-12 18:23:27 +08:00
RockYang 9e8f1ed6bf meta prompt function is ready 2024-11-12 17:13:38 +08:00
RockYang 4dbfdab50d auto restore user's power for failure tasks 2024-11-11 18:12:35 +08:00
RockYang c39814ce2b save task origin info for AI generating jobs 2024-11-11 17:22:08 +08:00
RockYang 95a071014c enable to set the translate model 2024-11-08 18:06:39 +08:00
RockYang 6e03f4b363 fixed bug for audio and video downloading 2024-11-05 11:38:32 +08:00
RockYang 9de9489673 remove sensitive words 2024-11-04 10:05:57 +08:00
RockYang bb8644dea0 remove sensitive words 2024-11-04 10:05:57 +08:00
RockYang ff9142ddd4 micro fix 2024-10-30 18:11:36 +08:00
RockYang 9814fec930 update the default api url to https://api.geekai.pro 2024-10-29 14:09:55 +08:00
RockYang 35f469fb82 update the default api url to https://api.geekai.pro 2024-10-29 14:09:55 +08:00
RockYang 53ba731159 update database sql file 2024-10-29 14:07:49 +08:00
RockYang 705ad58a8f update database sql file 2024-10-29 14:07:49 +08:00
RockYang c96f86fbbf 优化实时语音对话组件,处理异常 2024-10-23 18:04:09 +08:00
RockYang 4b3b64e9e2 modify text link color for register page 2024-10-21 18:26:19 +08:00
RockYang 2f2e146951 Merge branch 'main' of gitee.com:blackfox/geekai-plus 2024-10-21 18:21:21 +08:00
RockYang a09c529414 更换登录页面背景图片 2024-10-21 18:21:04 +08:00
RockYang a8d1d58e95 给 realtime 语音对话增加音效 2024-10-18 06:26:05 +08:00
RockYang 6695be815e 优化充值产品定价逻辑,确保手机端和PC端显示的价格一致 2024-10-17 18:15:25 +08:00
RockYang fbd3478772 the relay server for openai websocket is ready 2024-10-17 16:46:41 +08:00
RockYang 2102e1afbb add websocket relayer for openai realtime api 2024-10-16 18:16:09 +08:00
RockYang 155c56f502 integrated openai realtime console 2024-10-15 19:25:18 +08:00
RockYang 37a9b0e485 add PCM16 audio stream to wave is reday 2024-10-14 18:39:50 +08:00
RockYang 52a3f13f1e add voice chat test case 2024-10-12 19:07:29 +08:00
RockYang a678a11c33 suno and luma task management funtion in admin console is ready 2024-10-10 17:07:40 +08:00
RockYang d34b785238 image task list page for admin console is ready 2024-10-09 18:17:44 +08:00
RockYang 1086e1d631 fixed bug in FileSelect component for deleting files 2024-10-08 18:00:46 +08:00
RockYang b2f57aa483 merge v4.1.0 and fixed conflicts 2024-10-08 17:54:08 +08:00
RockYang 3094b9c8fd merge v4.1.0 and fixed conflicts 2024-10-08 17:54:08 +08:00
RockYang 4c2dba1004 merge v4.1.0 and fixed conflicts 2024-10-08 17:51:14 +08:00
RockYang 0f0a4c0a7e merge v4.1.0 and fixed conflicts 2024-10-08 17:51:14 +08:00
RockYang c374126f69 fixed bug for websocket message handler rebind 2024-10-08 16:41:19 +08:00
RockYang 8498cd71dc fixed webscoket event re-bind bug 2024-10-05 21:18:59 +08:00
RockYang bf30517393 fixed alipay mobile payment 2024-10-05 11:45:44 +08:00
RockYang 495f86e7fc fixed alipay mobile payment 2024-10-05 10:23:00 +08:00
RockYang 471017657f auto jump to mobile page when use mobile device access the page 2024-10-04 11:25:01 +08:00
RockYang 4861dd75be fixed bug for: websocket is not auto connected when user not login 2024-10-02 07:26:34 +08:00
RockYang 342ceea371 update database 2024-09-30 17:12:23 +08:00
RockYang 880c34dfee add message handler ONLY when websocket connect successfully 2024-09-30 16:33:26 +08:00
RockYang a1e487100d support wechat and alipay payment for mobile page 2024-09-30 16:20:40 +08:00
RockYang 77948b1e16 optimize the vue component communication, replace event listening with share data 2024-09-30 14:20:59 +08:00
RockYang e28a12a1ee websocket api refactor is ready 2024-09-29 19:28:47 +08:00
RockYang 00a8bc6784 mj websocket refactor is ready 2024-09-29 07:51:08 +08:00
RockYang 8fffa60569 sd websocket refactor is finished 2024-09-27 18:28:54 +08:00
RockYang 2debe7e927 refactor websocket message protocol, keep the only connection for all clients 2024-09-27 17:50:54 +08:00
RockYang 478bc32ddd add ws handler 2024-09-25 18:43:12 +08:00
RockYang dfd2be1265 update database sql file for v4.1.4 2024-09-23 15:54:22 +08:00
RockYang 33ff2e29e5 logout the user when it has been disabled 2024-09-20 16:49:03 +08:00
RockYang 4df1c7a136 add release v4.1.4 2024-09-20 15:50:04 +08:00
RockYang c522248d39 add email white list check in register handler 2024-09-20 14:10:40 +08:00
RockYang 246d96ee63 add email white list 2024-09-20 10:40:37 +08:00
RockYang 909ae4aa00 payment for mobile page is ready 2024-09-19 19:03:03 +08:00
RockYang 26e3ababcf wechat payment for mobile page is ready 2024-09-19 17:59:27 +08:00
RockYang 4d9f89f630 wechat payment for pc is ready 2024-09-19 14:42:25 +08:00
RockYang b989199edb fixed bug geek-plus#6, register page first tab not auto active 2024-09-19 09:03:14 +08:00
RockYang 0d81fe6c8e geek payment notify api is ready 2024-09-18 22:24:05 +08:00
RockYang be45f41e34 urgent bug fix: remove suno and luma task will recharge user power 2024-09-18 20:33:29 +08:00
RockYang e9ac58b1ef Geek Pay notify is ready 2024-09-18 18:07:49 +08:00
RockYang 59d9ae96ac add geek payment 2024-09-18 07:03:46 +08:00
RockYang 3cff6f7189 recommend user to use Google Chrome 2024-09-15 10:25:50 +08:00
RockYang 14aee28289 merge app type function branch 2024-09-14 18:17:55 +08:00
RockYang 8c1b4d4516 refactor AI chat message struct, allow users to set whether the AI responds in stream, compatible with the GPT-o1 model 2024-09-14 17:06:13 +08:00
胡双明 068df8fa15 feat: 应用分类功能 2024-09-14 11:05:49 +08:00
RockYang e371310d02 refactor chat message body struct 2024-09-14 07:11:45 +08:00
RockYang 96b8121210 return at least one chat role for getUserRoles API 2024-09-14 05:55:56 +08:00
RockYang 1960a85ead add tid field for chat app role 2024-09-13 18:32:13 +08:00
RockYang 69cb479e01 fixed bug, filelist page support pagination, do not load captcha component for user login first time 2024-09-13 17:03:05 +08:00
胡双明 db90131bd6 Merge branch 'main' into husm_2024-09-02 2024-09-13 10:25:15 +08:00
RockYang c18d272413 fixed bug for reset password 2024-09-12 17:25:19 +08:00
RockYang cd0952e170 fixed bug for register page code verification 2024-09-12 15:42:09 +08:00
胡双明 e6a6d0485d Merge branch 'main' into husm_2024-09-02 2024-09-11 09:57:01 +08:00
RockYang a642ae0332 optimize download function for suno 2024-09-11 08:39:28 +08:00
胡双明 67f508c7da feat: 210 AI对话页面文件列表增加分页功能 2024-09-10 18:14:34 +08:00
RockYang 2225f09797 file list api support pagination 2024-09-10 15:24:36 +08:00
胡双明 fe7fa46a0c Merge branch 'main' into husm_2024-09-02 2024-09-10 14:29:08 +08:00
RockYang 6d8901fa21 update v4.1.3 database sql file 2024-09-10 11:15:26 +08:00
RockYang 7750cb0021 update v4.1.3 database sql file 2024-09-10 11:11:17 +08:00
RockYang 327d68a347 support multiple delete users, update database sql file 2024-09-10 10:56:04 +08:00
RockYang 7c272b0efd update config file 2024-09-09 18:58:03 +08:00
RockYang 5eb4ed157e merge luma page code for v4.1.3 2024-09-09 18:07:10 +08:00
RockYang d4e00b960c Merge remote-tracking branch 'inet/husm_2024-09-02' into dev-4.1.3 2024-09-09 10:54:43 +08:00
RockYang 79adc871ef export the newest database sql file 2024-09-05 15:29:00 +08:00
RockYang af0eff88b8 export the newest database sql file 2024-09-05 15:29:00 +08:00
RockYang c755befa28 优化聊天页面代码,刷新页面之后自动加载当前对话的 role_id 和 model_id 2024-09-05 14:50:37 +08:00
胡双明 3f6890da27 feat: 生成视频页面2 2024-09-05 11:56:02 +08:00
RockYang 8144fada25 merged v4.0.9 and fixed conflicts 2024-09-05 11:02:32 +08:00
RockYang 08319391ea merged v4.0.9 and fixed conflicts 2024-09-05 11:02:32 +08:00
RockYang ae7f1c03e2 add auto execute task to downloading video files 2024-09-05 10:54:58 +08:00
胡双明 151017ed47 feat: 生成视频页面 2024-09-05 08:50:49 +08:00
RockYang 941d15b635 remove unused css file 2024-09-04 22:42:56 +08:00
RockYang d8058400dd adjust chat page styles 2024-09-04 18:07:39 +08:00
RockYang 5a1ed953de user can select function tools by themself 2024-09-04 14:53:21 +08:00
RockYang 58d127b602 add page and total field for pagination vo 2024-09-03 18:32:15 +08:00
RockYang 13bf73e6cf add sync lock for sub or add user's power 2024-09-03 12:09:36 +08:00
RockYang aa46fdb113 luma create and list api is ready 2024-09-02 18:08:50 +08:00
胡双明 4a09da3a25 feat(Luma): 加上传前后帧调换功能 2024-09-02 17:33:03 +08:00
RockYang d70fcbb9f3 optimize ChatPlus page, fixed bug for websocket reconnection 2024-09-02 16:35:15 +08:00
RockYang a16ef6476d add luma api service 2024-08-30 18:12:14 +08:00
RockYang dae75343d0 update change log file 2024-08-30 16:47:52 +08:00
RockYang d7564127d2 suno add new function for merging full songs and upload custom music 2024-08-30 16:46:48 +08:00
RockYang c6bf54fd9d add logo for favirate icon 2024-08-29 13:36:35 +08:00
RockYang 8d47072e1c luma page, upload image and remove image function is ready 2024-08-26 17:59:05 +08:00
RockYang d601226187 add download for video 2024-08-26 07:24:04 +08:00
RockYang 4801832d9d luma page video list component is ready 2024-08-23 18:25:58 +08:00
RockYang 6b0f42d0b8 luma page is ready 2024-08-20 14:31:40 +08:00
RockYang 08379c21ac refactor reset password functions 2024-08-19 12:05:00 +08:00
RockYang 2499bd9ad3 fixed styles 2024-08-19 06:42:53 +08:00
RockYang ad6da0d320 add bind mobile, bind email, bind wechat function is ready 2024-08-14 15:56:50 +08:00
RockYang a2583c0591 add mobile and email filed for user 2024-08-13 18:40:50 +08:00
RockYang 0ea4a29152 add verification code for login and register page 2024-08-13 14:55:47 +08:00
RockYang 7fe50788a5 add Captcha components 2024-08-13 10:01:07 +08:00
RockYang c4a68076d6 fixed conflicts 2024-08-13 06:48:22 +08:00
RockYang 9e365895af add wechat login for login dialog 2024-08-12 18:00:34 +08:00
RockYang 9ece25e17d add drag icon for dragable rows 2024-08-12 14:00:50 +08:00
RockYang 2140eff51b release v4.1.2 2024-08-09 18:50:02 +08:00
RockYang dfba3c30a5 redeem code function is ready 2024-08-09 18:19:51 +08:00
RockYang 4b717109d2 add redeem code function 2024-08-08 18:28:50 +08:00
RockYang e54e908fbc add cache for getting user info and system configs 2024-08-08 14:37:33 +08:00
RockYang 7a11a9ef15 add clear unpaid order functions 2024-08-07 18:00:28 +08:00
RockYang c9d0700fd9 refactor stable diffusion service, use api key instead of configs 2024-08-07 17:30:59 +08:00
RockYang f9b809801d refactor midjourney service, use api key in database 2024-08-06 18:30:57 +08:00
RockYang cc551ba266 show sql error message 2024-08-05 16:14:44 +08:00
RockYang 754ba02263 fixed build script 2024-08-01 18:45:53 +08:00
RockYang 23787ff462 fixed build script 2024-08-01 18:45:53 +08:00
RockYang 7ddf57ae06 merge v4.0.8 2024-08-01 18:09:00 +08:00
RockYang 9f603c5e77 merge v4.0.8 2024-08-01 18:09:00 +08:00
RockYang 3d720bdd81 update change log 2024-08-01 08:54:04 +08:00
RockYang cc5180a6f7 update readme 2024-08-01 08:52:46 +08:00
RockYang a8eccbe43b update readme 2024-08-01 08:52:46 +08:00
RockYang 2ac77ac39f restore use power when removed not finish jobs 2024-07-31 16:08:46 +08:00
RockYang 4eaa518cf3 remove platform field for api key and chat model 2024-07-30 17:24:21 +08:00
RockYang 5622f94fc8 update datebasesl 2024-07-30 14:55:49 +08:00
RockYang 95457b7dcd add back-to-top component for all list page 2024-07-29 11:00:53 +08:00
RockYang 6a9de72c78 remove chat debug log 2024-07-28 18:55:17 +08:00
RockYang 088a614160 add function to generate lyrics 2024-07-28 10:04:53 +08:00
RockYang 013ee98f53 song detail page is ready 2024-07-26 19:12:44 +08:00
RockYang dc46ed0e05 adjust chat records layout styles 2024-07-25 11:01:27 +08:00
RockYang 2a0c657ca3 add put url file for oss interface 2024-07-23 18:36:26 +08:00
RockYang a0aee80c63 remove other platform supports, ONLY use chatGPT API 2024-07-22 18:36:58 +08:00
RockYang 4910be3403 optimize foot copyright snaps 2024-07-22 17:54:09 +08:00
RockYang 1541d74c84 add close button for music player 2024-07-22 07:12:21 +08:00
RockYang 34c9151dc1 enable use random pure color background for index page 2024-07-19 18:43:01 +08:00
RockYang 6c69770ed6 the music player is ready 2024-07-18 18:34:11 +08:00
RockYang 5b7c38c67f add suno page 2024-07-17 18:58:09 +08:00
RockYang b7bec8ecb7 support upload file from clipboard 2024-07-17 10:23:02 +08:00
RockYang 82fe9c3596 allow user to use chat role directly, no need to add to workspace 2024-07-16 18:28:08 +08:00
RockYang 9f44c34d34 update docs url 2024-07-16 18:15:34 +08:00
RockYang afd84516b0 update docs url 2024-07-16 18:15:34 +08:00
RockYang 024f00b73c show error message for Midjourney task list page 2024-07-16 17:16:58 +08:00
RockYang 24a21bf2ee fixed bug for function call for openai 2024-07-16 06:25:40 +08:00
RockYang f22c6bf658 优化版权显示逻辑,允许激活用户更改自定义版权 2024-07-15 18:44:14 +08:00
RockYang 0df700ec18 update docker-compose file 2024-07-12 18:13:43 +08:00
RockYang 46141f87b8 update database file, add tika host config 2024-07-12 18:10:32 +08:00
RockYang eecce10018 tidy apis 2024-07-12 14:39:14 +08:00
RockYang b793b81768 update geekai image version 2024-07-05 11:09:13 +08:00
RockYang c8506e3d3b update geekai image version 2024-07-05 11:09:13 +08:00
RockYang bddd611cc1 wechat login is ready 2024-07-04 15:34:32 +08:00
RockYang b399fc557a feat: support wechat login function 2024-07-02 18:27:06 +08:00
RockYang 233f6e00f0 fixed conflicts 2024-06-30 06:09:12 +08:00
RockYang b20ea734fd fixed conflicts 2024-06-30 06:09:12 +08:00
RockYang b7dba68549 optimize ngin configuration for chat-plus.conf 2024-06-30 05:44:39 +08:00
RockYang 7ab0acd6a4 optimize ngin configuration for chat-plus.conf 2024-06-30 05:44:39 +08:00
RockYang 1d60ccc516 docs: update change log file 2024-06-28 16:21:43 +08:00
RockYang 2a8d2213b1 feat: optimize chat page data list style, support list style and chat style 2024-06-28 15:53:49 +08:00
RockYang a27ce36a32 feat: chat with file function is ready 2024-06-27 18:01:49 +08:00
RockYang 3fdcc895ed enable set custom index background image 2024-06-27 10:49:31 +08:00
RockYang 88e510d07a add test code for reading pdf files 2024-06-26 18:50:48 +08:00
RockYang 2526feb0d9 feat: new UI for chat file manager is ready 2024-06-25 18:59:27 +08:00
RockYang 4171b2bdfb fixed bug: mobile chat list could not update chat title 2024-06-25 09:53:08 +08:00
RockYang 0611f8de6c update version 2024-06-23 17:54:42 +08:00
RockYang df9e495dc5 add wechat payment configs sample 2024-06-22 16:04:04 +08:00
RockYang 8a48476105 update docker-compose file 2024-06-22 12:37:41 +08:00
RockYang 9d4a1705a5 add database files 2024-06-22 12:17:35 +08:00
RockYang 871e5733c7 finish mobile wechat payment 2024-06-22 12:10:43 +08:00
RockYang bdea12c51a update docker image mirror url 2024-06-19 08:35:05 +08:00
RockYang e8a663e3c7 update docker image mirror url 2024-06-19 08:35:05 +08:00
RockYang a27d9ea259 Merge branch 'main' of gitee.com:blackfox/geekai 2024-06-19 08:22:00 +08:00
RockYang 422d439627 Merge branch 'main' of gitee.com:blackfox/geekai 2024-06-19 08:22:00 +08:00
RockYang 7cd824c284 update docker image version to 4.0.6 2024-06-15 15:35:32 +08:00
RockYang edeff3c169 update docker image version to 4.0.6 2024-06-15 15:35:32 +08:00
RockYang e27d95e2b5 update docker image version to 4.0.6 2024-06-15 15:33:38 +08:00
RockYang 13076421a1 update docker image version to 4.0.6 2024-06-15 15:33:38 +08:00
RockYang 3943e7f05f update change log files 2024-06-14 18:23:54 +08:00
GeekMaster 6839827db0 Merge branch 'main' of https://github.com/yangjian102621/geekai into main 2024-06-12 15:44:26 +08:00
GeekMaster cd0d9dad98 Merge branch 'main' of https://github.com/yangjian102621/geekai into main 2024-06-12 15:44:26 +08:00
RockYang 857da34b9c wechat payment is ready for PC 2024-06-12 14:20:37 +08:00
RockYang ccad7e7bb5 change payment component, upgrade golang to 1.22.4 2024-06-11 11:48:41 +08:00
RockYang b8de15d66e fix bug: free model not record the chat history 2024-06-06 15:01:32 +08:00
RockYang c02661ea29 fixe page styles 2024-06-05 18:08:23 +08:00
RockYang 3c70c8ae59 fixed bug markmap generation 2024-06-04 16:21:08 +08:00
RockYang d6a04f96fe Merge pull request #208 from mari1995/main
图片墙,选择框单选问题
2024-06-04 08:33:51 +08:00
RockYang b985b62068 Merge pull request #208 from mari1995/main
图片墙,选择框单选问题
2024-06-04 08:33:51 +08:00
RockYang 297b760293 dalle3 and gptt-4o api compatible with azure 2024-06-03 18:34:37 +08:00
RockYang 5f820b9dc1 merge v4.0.6 2024-06-03 14:22:08 +08:00
RockYang 088abfe7ab merge v4.0.6 2024-06-03 14:22:08 +08:00
RockYang 17340ae0ac fixed bug for function call error None is not of type 'array' 2024-05-30 09:59:44 +08:00
RockYang fe687af8ca fixed bug for mobile chat share 2024-05-30 08:37:14 +08:00
RockYang 7e26029268 add v4.0.8 database sql file 2024-05-29 17:41:37 +08:00
RockYang 03f7f2a53e feat: add dalle3 page for h5 2024-05-29 17:25:01 +08:00
RockYang 296260bf6a feat: add system config for enable rand background image for index page 2024-05-29 16:24:56 +08:00
RockYang dd07acd21a feat: add system config for enable rand background image for index page 2024-05-29 16:23:42 +08:00
RockYang 89f6402fbf fix markdown formula parse plugin 2024-05-29 13:49:45 +08:00
SSMario 6788edbe9d Update Image.vue 2024-05-27 21:23:11 +08:00
SSMario 853059c814 Update Image.vue 2024-05-27 21:23:11 +08:00
SSMario 3895305882 Update Image.vue 2024-05-27 21:00:59 +08:00
SSMario 5a0876f8dc Update Image.vue 2024-05-27 21:00:59 +08:00
RockYang 2e32238652 micro fixs 2024-05-27 17:39:17 +08:00
SSMario c2acbaaa94 Update ImagesWall.vue 2024-05-27 17:04:15 +08:00
SSMario ad1a99aa44 Update ImagesWall.vue 2024-05-27 17:04:15 +08:00
RockYang 24e4be019a fixed bug for dalle prompt translate 2024-05-27 11:42:14 +08:00
RockYang 13c917ad7e put model and app selector on the top of chat page 2024-05-24 12:33:22 +08:00
RockYang 0a8cf6870f fixed bug for payment api authorization 2024-05-24 11:31:38 +08:00
RockYang 061291cebb chore: use config value for order pay timeout 2024-05-22 18:15:06 +08:00
RockYang b8e0d7760b feat: add sign check for PC QR code payment 2024-05-22 17:47:53 +08:00
RockYang 962de0183c extract code for saving chat history 2024-05-22 15:32:44 +08:00
RockYang 627396dbf7 check if the api url in whitelist for mj plus client 2024-05-22 11:47:04 +08:00
RockYang 6c300bb018 fixed bug for mobile chat page change chat model not work 2024-05-21 17:54:03 +08:00
RockYang 9273df4af2 auto resize the input element rows, when use inputed more than one line 2024-05-21 17:36:47 +08:00
RockYang 4a99be2f15 remove license code 2024-05-21 16:20:29 +08:00
RockYang 669d784f54 remove license code 2024-05-21 16:20:29 +08:00
RockYang 5253d657b6 add logs for updating database failed 2024-05-21 11:55:38 +08:00
RockYang 27c816cf3b merge conflicts for v4.0.5 2024-05-21 11:30:40 +08:00
RockYang c2548c5007 merge conflicts for v4.0.5 2024-05-21 11:30:40 +08:00
RockYang 0d81776212 update docker image name 2024-05-21 11:21:27 +08:00
RockYang bdec823148 update docker image name 2024-05-21 11:21:27 +08:00
RockYang a6de958daa update docker image url 2024-05-21 11:03:11 +08:00
RockYang cccab31c0f rename project name to geekai 2024-05-20 15:14:02 +08:00
RockYang 50c25d4574 rename project name to geekai 2024-05-20 15:14:02 +08:00
RockYang 68100f7f24 rename project name to geekai 2024-05-20 15:11:14 +08:00
RockYang 38777ea285 update database file 2024-05-19 19:37:16 +08:00
RockYang 00c8f08179 remove code for set left component fixed height 2024-05-18 08:07:09 +08:00
RockYang 45d6579fb6 refactor login dialog for front page 2024-05-18 00:27:32 +08:00
RockYang dad9254128 finished refactor chat page UI 2024-05-17 19:25:38 +08:00
RockYang 4ddf3bf2bf fixed bugs for send message captcha component 2024-05-17 08:41:09 +08:00
RockYang 9934143f00 fixed bugs for send message captcha component 2024-05-17 08:41:09 +08:00
RockYang f617bde81b fixed bugs for send message captcha component 2024-05-16 22:33:00 +08:00
RockYang b2771b7b3f feat: add top navbar for front page 2024-05-16 20:10:00 +08:00
RockYang 44b3237dd6 feat: support add external link menu 2024-05-16 10:53:00 +08:00
RockYang b987a2d365 handler chat error in the chat entry func 2024-05-15 15:30:34 +08:00
RockYang 1cf299f090 add charge link for insufficient of power 2024-05-15 07:10:31 +08:00
RockYang c87d78c666 fix bug for white-list api key check 2024-05-14 22:30:42 +08:00
RockYang 3d37a3d367 update readme 2024-05-14 18:23:12 +08:00
RockYang 79c8d90049 update readme 2024-05-14 18:23:12 +08:00
RockYang 73d8236697 update change log 2024-05-14 18:20:52 +08:00
RockYang 1afc3e5e1c update change log 2024-05-14 18:20:52 +08:00
RockYang 87e8bbfa41 update version 2024-05-14 18:03:58 +08:00
RockYang 3e3d3e162e refactor: use waterflow component in mj, sd and dall image drawing page 2024-05-13 19:04:00 +08:00
RockYang cc80b87fee fixed sd page waterfall component 2024-05-11 18:27:35 +08:00
RockYang b614ecccc2 enable to update AI Drawing configuarations in admin console page 2024-05-11 17:27:14 +08:00
RockYang 8251c6589b fix: markmap do not cost power in front page 2024-05-11 07:08:14 +08:00
RockYang 4edf2ea02a optimize chat handler error handle code 2024-05-10 18:26:19 +08:00
RockYang 12dd4659cd fix bug: remove chat role failed 2024-05-10 17:38:55 +08:00
RockYang 5d4f40c004 fixed bug for dalle3 task not decrease power 2024-05-10 11:18:37 +08:00
RockYang 3c34e8e0e7 fixed bug for dalle3 task not decrease power 2024-05-10 11:17:52 +08:00
RockYang 7441a3a717 fixed bug for dalle3 task not decrease power 2024-05-10 11:17:52 +08:00
RockYang 3eef9a836b add toolbar for markmap component 2024-05-10 06:38:34 +08:00
RockYang 922202734a fixed bug for markmap 2024-05-09 21:55:40 +08:00
RockYang fffd7e375f fixed bug for markmap 2024-05-09 21:55:40 +08:00
RockYang d87f09c957 use proxy for downloading discord images 2024-05-09 18:48:53 +08:00
RockYang b29884f8df add stable diffusion default negtive prompt system config 2024-05-07 16:49:54 +08:00
RockYang b270960a04 remove license code 2024-05-07 16:41:35 +08:00
RockYang c0ee2c2d8e remove license code 2024-05-07 16:41:35 +08:00
RockYang 5c4899df6e upgrade to v4.0.4 2024-05-07 16:32:05 +08:00
RockYang be1580e949 upgrade to v4.0.4 2024-05-07 16:32:05 +08:00
RockYang 6424eb871c add stable diffusion default negtive prompt system config 2024-05-07 16:21:31 +08:00
RockYang 8a8c43c7a5 handle the exception for web front page 2024-05-06 17:39:58 +08:00
RockYang 9a503ddc72 fixed conflicts 2024-05-06 14:44:09 +08:00
RockYang b130466c8f chore: change module name to geekai, add copyright in source code 2024-05-06 14:41:27 +08:00
RockYang 2f0215ac87 Update README.md 2024-05-06 10:45:50 +08:00
RockYang 3ab5459778 Update README.md 2024-05-06 10:45:50 +08:00
RockYang dd5cc206e5 update LICENSE.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-05-05 03:05:23 +00:00
RockYang 4eb1bf2de2 update LICENSE.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-05-05 03:05:23 +00:00
RockYang 6c47551b03 fix bug for waterflow component 2024-05-05 10:52:29 +08:00
RockYang 657ecccee3 update changelog 2024-05-04 21:32:43 +08:00
RockYang b4ba70c0f3 update changelog 2024-05-04 21:32:43 +08:00
RockYang c2bf5e845a fix bug for license synchronize 2024-05-04 21:30:29 +08:00
RockYang 8900e72e45 opt: optimize mobile images page styles 2024-05-03 08:14:33 +08:00
RockYang 0cb192135d fix: fix bug for dalle power refund 2024-05-01 08:48:32 +08:00
RockYang c1ca687630 chore: update docker image name 2024-05-01 07:59:20 +08:00
RockYang 29cc24508b opt: styles and view micro optimization 2024-05-01 07:40:56 +08:00
RockYang 3db55cbd48 opt: close unused websocket connections 2024-04-30 22:54:39 +08:00
RockYang c0a7f41747 chore: replace docker image url with AliYun 2024-04-30 19:08:33 +08:00
RockYang 1ba08bbfa6 feat: change theme for mobile site is ready 2024-04-30 18:57:15 +08:00
RockYang 51a8f42d89 feat: mobile page refactor is finished 2024-04-29 19:22:00 +08:00
RockYang 3b081ff0f4 opt: add chat config for mobile chat session 2024-04-29 09:39:23 +08:00
RockYang 7f31a301e3 feat: add index page for mobile 2024-04-28 19:09:26 +08:00
RockYang 039d949523 theme change is ready 2024-04-27 13:13:28 +08:00
RockYang 5b16dc6ee7 feat: admin console dark theme 2024-04-26 18:10:17 +08:00
RockYang 34648c704f opt: optimize index and login page UI 2024-04-26 16:07:02 +08:00
RockYang 1966e69460 synchronize license every 10 secs 2024-04-26 14:35:01 +08:00
RockYang 74b1b07b31 change index page background 2024-04-25 18:54:33 +08:00
RockYang 2e023cb8dc update readme 2024-04-25 06:27:30 +08:00
RockYang 118c0e1ff1 update readme 2024-04-25 06:27:30 +08:00
RockYang e933f32d9c remove unused files 2024-04-24 21:22:56 +08:00
RockYang 81d075c028 remove unused files 2024-04-24 21:22:56 +08:00
RockYang 514dd6c76a show license info in admin active page, optimize markdown generate prompt 2024-04-24 19:00:28 +08:00
RockYang 0b2501c1d8 output the error to chat page directly, replace the common error message 'AI开小差' 2024-04-24 10:10:03 +08:00
RockYang ddc323a8c3 output the error to chat page directly, replace the common error message 'AI开小差' 2024-04-24 10:10:03 +08:00
RockYang 2933c057a2 fixed bug for markmap 2024-04-23 20:47:06 +08:00
RockYang c1d892069e release v4.0.4 2024-04-23 18:47:23 +08:00
RockYang 20ed6d1d3d release v4.0.4 2024-04-23 18:47:23 +08:00
RockYang 90f5275201 optimize styles and release v4.0.4 2024-04-23 18:46:32 +08:00
RockYang fe64b203da feat: hide more navigator items 2024-04-22 16:27:53 +08:00
RockYang 9eb1353455 feat: support markmap svg export and download as png image 2024-04-22 14:20:51 +08:00
RockYang 8a1c55c731 allow users to select a chatApp to chat in chat app list page 2024-04-22 11:18:55 +08:00
RockYang 9059bebd07 optimize index page UI 2024-04-22 10:43:33 +08:00
RockYang d18f79fe74 optimize code for remove timeout and failed image drawing job 2024-04-21 21:44:28 +08:00
RockYang 3a8f0be28a image wall page add dalle 2024-04-21 20:42:42 +08:00
RockYang 60cf380f96 dalle image page is ready 2024-04-21 20:23:47 +08:00
RockYang ab8240613e DO NOT refresh finished jobs when job is running 2024-04-20 21:30:55 +08:00
RockYang d6e9dc6839 fixed markdown generating styles 2024-04-19 18:22:45 +08:00
RockYang 6891bdde53 support send email use TLS 2024-04-19 12:04:59 +08:00
RockYang bda335212d Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2024-04-19 10:56:05 +08:00
RockYang ecadfeab19 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2024-04-19 10:56:05 +08:00
RockYang 06f4cdc649 fixed bug for QWen response blank quotes 2024-04-19 10:55:29 +08:00
RockYang a38d547200 fixed bug for QWen response blank quotes 2024-04-19 10:55:29 +08:00
RockYang a15f431a7f fixed bug for QWen response blank quotes 2024-04-19 10:30:02 +08:00
RockYang 3bb5b66e8b fixed bug for chat handler doRequest method 2024-04-16 23:49:56 +08:00
RockYang 9bf7fa4081 compatible freeGPT35 API 2024-04-15 21:03:19 +08:00
RockYang d9ff3242c6 compatible freeGPT35 API 2024-04-15 21:03:19 +08:00
RockYang cd562ab8ed fixed bug for websocket close tip message 2024-04-15 18:15:15 +08:00
RockYang 3a8a69ac2e feat: markmap function is ready 2024-04-15 17:23:59 +08:00
RockYang 4ca9dfd9c0 fixed upscale and variation action url 2024-04-15 15:14:49 +08:00
RockYang 0d5b4936bd fixed upscale and variation action url 2024-04-15 15:14:49 +08:00
RockYang b5f6eaf159 opt: close the old connection for mj and sd clients 2024-04-15 09:34:20 +08:00
RockYang adfee8bf58 update version 2024-04-15 09:05:54 +08:00
RockYang 25c80921e0 update version 2024-04-15 09:05:54 +08:00
RockYang fbfa2a71a9 release v4.0.3 2024-04-15 08:26:07 +08:00
RockYang 5c51ed48c3 release v4.0.3 2024-04-15 08:26:07 +08:00
RockYang 22febfc42a markmap enable to select ai model 2024-04-15 06:16:53 +08:00
RockYang de482dd9f4 fixed chat page styles 2024-04-12 21:57:41 +08:00
RockYang 74af67a41f feat: markmap page view is ready 2024-04-12 18:49:24 +08:00
RockYang 170b41441b feat: allow chat model bind a fixed api key 2024-04-12 17:09:22 +08:00
RockYang 4991e50e16 chore: update build.sh 2024-04-11 21:26:33 +08:00
RockYang 1f236ef929 feat: use custom mode for mj upscale and variation operarions 2024-04-11 17:32:34 +08:00
RockYang 7f86c5984d feat: add index page 2024-04-10 18:23:55 +08:00
RockYang 6fcee49b19 feat: support for freeGPT35 API 2024-04-10 14:49:07 +08:00
RockYang ac527851a0 feat: support gpt-4-turbo-2014-04-09 vision function 2024-04-10 11:47:10 +08:00
RockYang 64bbeaedcb fixed bug for gpt-4-turbo-2024-0409 model function calls 2024-04-10 10:23:45 +08:00
RockYang 3e984a1bcb fix bug: remove timeout task ONLY for unfinished(progress < 100) 2024-04-10 06:25:54 +08:00
RockYang a7237fe62f Merge branch 'main' of gitee.com:blackfox/chatgpt-plus 2024-04-07 18:33:55 +08:00
RockYang 79165214d7 Merge branch 'main' of gitee.com:blackfox/chatgpt-plus 2024-04-07 18:33:55 +08:00
RockYang c3c454b7d7 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2024-04-07 18:32:20 +08:00
RockYang 3f7855dd6e Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2024-04-07 18:32:20 +08:00
RockYang d4d708d44b update database files 2024-04-07 18:26:45 +08:00
RockYang f0610281e5 update database files 2024-04-07 18:26:45 +08:00
RockYang 7f0b6a3a46 set the enable status for adding new api key with default value true 2024-04-07 10:17:10 +08:00
RockYang e87e7e2164 set the enable status for adding new api key with default value true 2024-04-07 10:17:10 +08:00
RockYang c2a7c089d2 update change log 2024-04-07 08:40:43 +08:00
RockYang 1a3ad390fd update change log 2024-04-07 08:40:43 +08:00
RockYang df5bd4df60 Merge branch 'dev' 2024-04-07 08:03:14 +08:00
RockYang c383367e9f Merge branch 'dev' 2024-04-07 08:03:14 +08:00
RockYang 79b6010104 optimize ngin configuration for chat-plus.conf 2024-04-07 08:02:47 +08:00
RockYang 7a9f45c96b optimize ngin configuration for chat-plus.conf 2024-04-07 08:02:47 +08:00
RockYang 97b0a98793 fix 404 error with remove api keys 2024-04-07 08:01:25 +08:00
RockYang 7277cb289f fix 404 error with remove api keys 2024-04-07 08:01:25 +08:00
RockYang 86b2d5eff2 fix bug for remove api 404 errorc 2024-04-06 20:36:52 +08:00
RockYang 7873847616 feat: show bind model on chat role list page 2024-04-05 21:21:28 +08:00
RockYang 5e46f149d9 feat: support uploading role icon 2024-04-05 17:41:23 +08:00
RockYang 574fc52332 feat: allow bind a chat model for chat role 2024-04-05 12:51:18 +08:00
RockYang 8c924ca98f update change log 2024-04-05 06:58:13 +08:00
RockYang a9adc0c1a2 feat: stable diffusion image drawing on mobile is ready 2024-04-03 18:13:48 +08:00
RockYang ed286c8894 feat: midjourney role and style consistency is ready 2024-04-02 19:01:28 +08:00
RockYang d900a3d08e update README.md.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-04-02 10:00:45 +00:00
RockYang a55da15202 update README.md.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-04-02 10:00:45 +00:00
RockYang cdf5b66729 Update README.md 2024-04-02 17:59:53 +08:00
RockYang 9728118f94 Update README.md 2024-04-02 17:59:53 +08:00
RockYang 9cc2ea412b feat: image preview for stable-diffusion task is ready 2024-04-02 17:24:38 +08:00
RockYang 3f1ad4b7dc feat: support midjourney --cref and --sref for role consistency 2024-04-02 14:59:53 +08:00
RockYang 56c225bf20 feat: update menu icons, add version in site titles 2024-04-01 18:20:00 +08:00
RockYang 8b68bfb28c show power for chat and imaging page 2024-03-31 20:49:12 +08:00
RockYang a5299b52d4 opt: change the relative path with absolute path for midjourney image uploading 2024-03-31 17:45:22 +08:00
RockYang 9e9bc52737 Merge branch 'main' into dev 2024-03-30 11:57:31 +08:00
RockYang 76106e4504 remove dead code 2024-03-30 11:57:23 +08:00
RockYang a6c00c42fa fixed conflicts 2024-03-29 18:10:59 +08:00
RockYang 4e23005b5b fixed conflicts 2024-03-29 18:10:59 +08:00
RockYang f27e2de220 update readme file 2024-03-29 18:06:55 +08:00
RockYang eefcabcff3 update databases 2024-03-29 17:43:38 +08:00
RockYang 63e4669e3f feat: support custom menu 2024-03-29 15:41:58 +08:00
RockYang 7d5428f775 add tip for mj image buttons 2024-03-28 21:41:49 +08:00
RockYang ab3c4562fa fix: fix overflow hidden for admin page 2024-03-28 18:51:09 +08:00
RockYang 13f89bf335 fix: fix overflow hidden for mobile page 2024-03-28 18:13:33 +08:00
RockYang e6a18445c3 fix: use slide captcha for iphone 2024-03-28 15:00:53 +08:00
RockYang c8ab209426 feat: add mj_action_power system config item 2024-03-28 09:53:41 +08:00
RockYang 360fea4085 feat: change midjourney origin implements, replace midjourney bot with midjourney-proxy 2024-03-27 18:57:15 +08:00
RockYang 9794d67eaa feat: auto translate and rewrite prompt for midjourney and stable-diffusion 2024-03-27 13:45:52 +08:00
RockYang b60a639312 feat: stable-diffusion refactored, replace websocket api with sdapi 2024-03-26 18:23:08 +08:00
RockYang 870706c4ff fix: can not change user's power in admin console 2024-03-25 11:40:03 +08:00
RockYang 491471fa10 fix: fix bug for update user's power in admin page did not work 2024-03-23 16:21:37 +08:00
RockYang 6dbf61d4e4 No need to login with Stable-Diffusion page and Invite page 2024-03-23 15:45:37 +08:00
RockYang 81545d192b fix: fix code highlight error when add formule detecting 2024-03-22 19:09:04 +08:00
RockYang 37cdc26160 chore: correct prompt messages 2024-03-22 18:27:57 +08:00
RockYang fb6a35ebe4 feat: add chart for admin dashbord 2024-03-22 16:57:30 +08:00
RockYang f7a565bb80 feat: integrate xxl-job-admin to implements automatic task scheduling 2024-03-22 13:47:16 +08:00
RockYang bf5e72b7e0 feat: save prompt in power log for dalle-3 2024-03-21 15:55:39 +08:00
RockYang ec6186596d feat: add manager list page in console page 2024-03-21 15:24:28 +08:00
RockYang dece19cec6 feat: add powerlog page for admin console 2024-03-21 13:46:39 +08:00
RockYang 6aa1d27711 opt: optimize the formula show styles 2024-03-21 11:04:12 +08:00
RockYang 264e77f383 always parse authorization token for all request 2024-03-20 21:11:52 +08:00
RockYang 0cbc284e42 fixed conflicts 2024-03-20 20:40:22 +08:00
RockYang 9e42a334fa update README.md.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2024-03-20 20:39:44 +08:00
RockYang 49438d2ee1 feat: Async loading midjourney job for mobile MidJourney page 2024-03-20 18:39:14 +08:00
RockYang 735aa24020 feat: h5 payment for payjs is ready 2024-03-20 17:46:39 +08:00
RockYang 14f63a203d feat: payment for mobile pages is ready 2024-03-20 16:14:02 +08:00
RockYang e11c0a3633 feat: the power log page is ready 2024-03-20 14:14:30 +08:00
RockYang ca83f139f7 feat: no need login refactor with member and chatApps page 2024-03-19 18:59:02 +08:00
RockYang 58e44b7d12 feat: load preview page do not require user to login 2024-03-19 18:25:01 +08:00
RockYang 8f47474edd feat: optimize login dialog 2024-03-19 10:47:13 +08:00
RockYang c5200aada8 feat: refactoring adjustments for reward page is ready 2024-03-18 18:28:34 +08:00
RockYang 4e39b93673 feat: refactoring adjustments for member pages 2024-03-18 16:59:07 +08:00
RockYang f215643f2e feat: The 'chat_models' field of user table, holds the model IDS in place of the model values 2024-03-18 15:37:46 +08:00
RockYang aa72be6ff3 remove new-ui files 2024-03-18 12:01:34 +08:00
RockYang b567e56b60 chore: adjust page styles 2024-03-18 06:46:08 +08:00
RockYang 17431c1707 重构主体工作完成 2024-03-15 18:35:10 +08:00
RockYang 7c9ebef5e9 restore new ui files 2024-03-15 11:13:02 +08:00
廖彦棋 72b0e11543 fix(ui): 交互修复调整 2024-03-15 11:07:41 +08:00
RockYang 9e2af9dbca feat: refactor user list page for new UI 2024-03-15 09:29:19 +08:00
廖彦棋 b7cc987132 fix(ui): 用户管理有效期传参调整,权限标识补充 2024-03-15 09:25:06 +08:00
廖彦棋 8476c73b0c Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 17:49:18 +08:00
廖彦棋 e76b49afd2 refactor(ui): 无权限页面调整 2024-03-14 17:49:15 +08:00
吴汉强 c712b95e0d feat(ui): 后台首页去掉权限判断 2024-03-14 17:24:40 +08:00
廖彦棋 1ee1a8d6d9 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 17:19:41 +08:00
廖彦棋 f01c764589 feat(ui): 无权限判断 2024-03-14 17:19:39 +08:00
吴汉强 6df66d150b Merge remote-tracking branch 'origin/ui' into ui 2024-03-14 17:12:53 +08:00
吴汉强 6132f94eff feat(ui): 新增 sql 2024-03-14 17:12:48 +08:00
廖彦棋 ac5e67ea73 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 17:06:15 +08:00
吴汉强 143d2b44d0 feat(ui): 403,没权限 2024-03-14 16:41:38 +08:00
吴汉强 ef130fe377 feat(ui): 网站配置需授权,去掉 2024-03-14 16:28:49 +08:00
廖彦棋 d7d1e2100c fix(ui): 调整 2024-03-14 16:14:49 +08:00
吴汉强 af5afd7700 feat(ui): 后端加权限验证 2024-03-14 15:39:12 +08:00
廖彦棋 a64df5cf0c feat(ui): 角色管理 2024-03-14 15:25:17 +08:00
廖彦棋 3866319373 feat(ui): 新增系统分类菜单 2024-03-14 15:17:53 +08:00
廖彦棋 ec5ad809f2 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-14 10:56:56 +08:00
廖彦棋 268acb174d fix(ui): 修复 2024-03-14 10:56:54 +08:00
吴汉强 3733877be1 Merge remote-tracking branch 'origin/ui' into ui 2024-03-14 10:28:39 +08:00
吴汉强 6d5579a8d6 feat(ui): 登录接口返回权限 2024-03-14 10:28:32 +08:00
廖彦棋 306cd2f945 feat(ui): 管理后台新增权限及部分组合式函数优化 2024-03-14 10:27:09 +08:00
廖彦棋 5d4dd1e66f Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-13 17:30:26 +08:00
廖彦棋 5f371bdc4a feat(ui): web移动端初始化 2024-03-13 17:30:24 +08:00
吴汉强 aa58ba392e Merge remote-tracking branch 'origin/ui' into ui 2024-03-13 17:24:38 +08:00
吴汉强 0485610818 feat(ui): 增加角色管理,管理员方法新增角色关联 2024-03-13 17:24:30 +08:00
chenzifan 7cb8becc09 Merge remote-tracking branch 'origin/ui' into ui 2024-03-13 14:40:45 +08:00
chenzifan 8fa535a01b refactor: remove api change to post request 2024-03-13 14:40:38 +08:00
吴汉强 5f202e03a2 Merge remote-tracking branch 'origin/ui' into ui 2024-03-13 11:41:07 +08:00
吴汉强 ef2dfe330b feat(ui): 增加系统权限管理 2024-03-13 11:41:01 +08:00
廖彦棋 fcd86fbebd fix(ui): ui调整 2024-03-13 09:54:20 +08:00
RockYang 960d294aa2 docs: update sql file 2024-03-13 08:49:40 +08:00
RockYang 27f8e63f3d Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-13 08:48:10 +08:00
chenzifan e8d7ad58be fix: 删除系统管理员失效的问题 2024-03-13 08:47:17 +08:00
chenzifan f4d537e0f5 Merge remote-tracking branch 'origin/ui' into ui
# Conflicts:
#	api/handler/admin/admin_user_handler.go
2024-03-13 08:46:16 +08:00
chenzifan d6c93dd537 fix: 删除系统管理员失效的问题 2024-03-13 08:45:09 +08:00
廖彦棋 aa5fff47de Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-13 08:44:50 +08:00
廖彦棋 c920d8423d feat(ui): 调整 2024-03-13 08:44:48 +08:00
chenzf@pvc123.com b8fcfe2e7c feat: 超级管理员不支持修改和删除 2024-03-12 21:16:05 +08:00
RockYang 072c96e1ec Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-12 18:07:24 +08:00
RockYang 38c8ece642 fix conflicts 2024-03-12 18:07:19 +08:00
chenzifan 9bf1576fb4 feat: 增加系统管理员 2024-03-12 18:06:49 +08:00
RockYang d648e177c4 fix conflicts 2024-03-12 18:03:24 +08:00
RockYang e8f62af7f6 refactor: use power replace calls for front pages 2024-03-12 17:47:06 +08:00
RockYang e746aafa2f refactor: 重构项目,为所有的 AI 工具都引入算力,采用算力统一结算各个工具的调用次数和权限 2024-03-12 15:40:44 +08:00
廖彦棋 71f7a1b166 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-12 08:37:29 +08:00
廖彦棋 678f570a90 refactor(ui): 调整 2024-03-12 08:37:27 +08:00
huangqj 4bc1f195db fix(ui):环境变量 2024-03-11 16:10:49 +08:00
廖彦棋 b2ff49ee94 feat(ui): 新增系统管理员 2024-03-11 15:59:15 +08:00
廖彦棋 d9558f13ad fix(ui): 删除冗余 2024-03-11 14:23:11 +08:00
廖彦棋 8dc8f2567e fix(ui): 删除冗余 2024-03-11 14:22:39 +08:00
RockYang 316636f83c feat: replace Tools param with Function param for OpenAI chat API 2024-03-11 14:09:19 +08:00
廖彦棋 e3c2fbf82a Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-11 13:52:24 +08:00
廖彦棋 1b6175e598 fix(ui): type 2024-03-11 13:52:22 +08:00
chenzifan 26dc479596 feat: 优化后台UI 2024-03-11 13:51:26 +08:00
chenzifan dca25dbb78 Merge remote-tracking branch 'origin/ui' into ui 2024-03-11 13:46:46 +08:00
廖彦棋 c55275872e refactor(ui): 调整优化 2024-03-11 13:46:08 +08:00
廖彦棋 d4d1fb26f6 feat(ui): 细节优化 2024-03-11 12:02:20 +08:00
廖彦棋 a48cb7bc34 feat(ui): 上传功能补充 2024-03-11 11:41:50 +08:00
廖彦棋 8af6e9290f feat(ui): 登录新增验证码及记住密码功能 2024-03-11 10:49:13 +08:00
廖彦棋 14b277d813 feat(ul): 顶部信息 2024-03-11 09:00:00 +08:00
huangqj 20915038a3 feat(ui):看板图表 样式调整 2024-03-11 08:35:21 +08:00
chenzifan 15723457cb Merge remote-tracking branch 'origin/ui' into ui 2024-03-11 08:01:54 +08:00
RockYang be8a0ec184 opt: remove global keyup event bind 2024-03-10 09:45:59 +08:00
RockYang ec79fc933c opt: remove global keyup event bind 2024-03-10 09:45:59 +08:00
RockYang b02e3aad95 docs: update config.toml 2024-03-10 09:45:59 +08:00
RockYang 3ed5a420a7 docs: update config.toml 2024-03-10 09:45:59 +08:00
RockYang 08eca511ad docs: add database sql file for v3.2.7 2024-03-10 09:45:59 +08:00
RockYang 711e701933 docs: add database sql file for v3.2.7 2024-03-10 09:45:59 +08:00
RockYang c34e911596 feat: allow to view chat message in manager console 2024-03-10 09:45:59 +08:00
RockYang e6bbd83c6b feat: allow to view chat message in manager console 2024-03-10 09:45:59 +08:00
RockYang 8a452c3072 feat: download image which ai generated in dialog and replace the image url 2024-03-10 09:45:59 +08:00
RockYang b99b310306 feat: download image which ai generated in dialog and replace the image url 2024-03-10 09:45:59 +08:00
RockYang 13bfb14107 feat: image-wall page for mobile is ready 2024-03-10 09:45:59 +08:00
RockYang e3c46016e0 feat: image-wall page for mobile is ready 2024-03-10 09:45:59 +08:00
RockYang 4188b0969e feat: allow user config third-party platform openai and mj api key 2024-03-10 09:45:59 +08:00
RockYang eb739fcccb feat: allow user config third-party platform openai and mj api key 2024-03-10 09:45:59 +08:00
RockYang 0c27795a10 feat: added delete file function 2024-03-10 09:45:59 +08:00
RockYang 9a94505f43 feat: added delete file function 2024-03-10 09:45:59 +08:00
RockYang d05693c5c1 fix: verifycation component touch event coordinates misplace in iphone browser 2024-03-10 09:45:59 +08:00
RockYang 7c5c3d8a3c fix: verifycation component touch event coordinates misplace in iphone browser 2024-03-10 09:45:59 +08:00
RockYang c0b2063b38 fix: fix bug for regenerate button did not work 2024-03-10 09:45:59 +08:00
RockYang 7027f9a55b fix: fix bug for regenerate button did not work 2024-03-10 09:45:59 +08:00
RockYang 4d183747b1 fix: Upscale and Variation task overrite each other 2024-03-10 09:45:59 +08:00
RockYang cf3d8af260 fix: Upscale and Variation task overrite each other 2024-03-10 09:45:59 +08:00
RockYang 08fe1b2f75 feat: midjourney mobile page all function is ready 2024-03-10 09:45:59 +08:00
RockYang 4ea7352674 feat: midjourney mobile page all function is ready 2024-03-10 09:45:59 +08:00
RockYang db3e8a267e feat: mobile mj list page is ready 2024-03-10 09:45:59 +08:00
RockYang 6e6885849b feat: mobile mj list page is ready 2024-03-10 09:45:59 +08:00
RockYang 8fc62682c4 feat: add mj image list component for mobile page. fixed bug for html tag escape 2024-03-10 09:45:59 +08:00
RockYang 827f2b9739 feat: add mj image list component for mobile page. fixed bug for html tag escape 2024-03-10 09:45:59 +08:00
RockYang 75031914a3 feat: add functions mj page for mobile 2024-03-10 09:45:59 +08:00
RockYang 54d70623ab feat: add functions mj page for mobile 2024-03-10 09:45:59 +08:00
RockYang a4c9fdd95a opt: add default extension for mj image 2024-03-10 09:45:59 +08:00
RockYang ed819e8c79 opt: add default extension for mj image 2024-03-10 09:45:59 +08:00
RockYang 6a9bfeb5aa feat: mj for mobile page payout is ready 2024-03-10 09:45:59 +08:00
RockYang 94680c4411 feat: mj for mobile page payout is ready 2024-03-10 09:45:59 +08:00
RockYang e654766f60 add docs and github link 2024-03-10 09:45:59 +08:00
RockYang 8bcbb68438 add docs and github link 2024-03-10 09:45:59 +08:00
RockYang 0ef6955f96 opt: enable use cdn url for mj-plus 2024-03-10 09:45:59 +08:00
RockYang b9f31d42f4 opt: enable use cdn url for mj-plus 2024-03-10 09:45:59 +08:00
RockYang b4501557c9 feat: LaTeX parse is ready 2024-03-10 09:45:59 +08:00
RockYang dc2b768c6b feat: LaTeX parse is ready 2024-03-10 09:45:59 +08:00
RockYang a2ed99e6cb feat: add model field for chat_item and and chat_history data table 2024-03-10 09:45:59 +08:00
RockYang 60df732db7 feat: add model field for chat_item and and chat_history data table 2024-03-10 09:45:59 +08:00
RockYang 6bd6bb3885 feat: add err_msg field for mj and sd jobs 2024-03-10 09:45:59 +08:00
RockYang 6d85d128cc feat: add err_msg field for mj and sd jobs 2024-03-10 09:45:59 +08:00
RockYang 399cf65fc9 feat: blend and swap face function for midjourney-plus is ready 2024-03-10 09:45:59 +08:00
RockYang 96785ca037 feat: blend and swap face function for midjourney-plus is ready 2024-03-10 09:45:59 +08:00
RockYang 24906a6df1 feat: add blend and swapface task implements for midjourney 2024-03-10 09:45:59 +08:00
RockYang 7cfc747653 feat: add blend and swapface task implements for midjourney 2024-03-10 09:45:59 +08:00
RockYang d772bbebe6 opt: refactor chat session page for mobile device 2024-03-10 09:45:59 +08:00
RockYang 05f911628a opt: refactor chat session page for mobile device 2024-03-10 09:45:59 +08:00
RockYang 14988853a3 opt: optimize chat list page for mobile 2024-03-10 09:45:59 +08:00
RockYang 18b718a328 opt: optimize chat list page for mobile 2024-03-10 09:45:59 +08:00
RockYang 7b3f16ac9f feat: use vant replace element-plus as mobile UI framework 2024-03-10 09:45:59 +08:00
RockYang 6d9b9bd3f7 feat: use vant replace element-plus as mobile UI framework 2024-03-10 09:45:59 +08:00
RockYang 82b2755c18 feat: add websocket heartbeat message for mj page 2024-03-10 09:45:59 +08:00
RockYang 04e990aeb3 feat: add websocket heartbeat message for mj page 2024-03-10 09:45:59 +08:00
廖彦棋 cbbcd4c571 fix(ui): 细节调整 2024-03-08 17:46:48 +08:00
huangqj 5ad9adeb4a feat(ui):细节调整 2024-03-08 10:24:38 +08:00
廖彦棋 13c9594be0 feat(ui): prettier 2024-03-08 09:59:40 +08:00
廖彦棋 e4f5f78bfa refactor(ui): 登录页重构 2024-03-08 09:45:09 +08:00
廖彦棋 bfa0208692 refactor(ui): 优化 2024-03-08 09:12:39 +08:00
huangqj 64994cdeda feat(ui):路由 2024-03-08 08:35:41 +08:00
廖彦棋 9796a841d9 Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-07 18:03:08 +08:00
廖彦棋 404b493d23 feat(ui): 过期跳转登录 2024-03-07 18:03:06 +08:00
huangqj c04f9eb993 feat(ui):apiKey 语言模型 角色管理 产品 2024-03-07 17:58:25 +08:00
廖彦棋 825da42208 feat(ui): 新增系统设置 2024-03-07 17:24:50 +08:00
廖彦棋 721a8f1ecb feat(ui): 对话管理 2024-03-07 15:32:32 +08:00
huangqj 586a174d6c feat(ui):用户 2024-03-07 15:05:01 +08:00
huangqj bec6966542 feat(ui):simpleTable 2024-03-07 14:59:45 +08:00
huangqj b1f4e1d4d8 feat(ui):searchtable 2024-03-07 14:59:16 +08:00
huangqj d7d64e8259 feat(ui):用户 2024-03-07 14:03:55 +08:00
廖彦棋 08b498fccd feat(ui): 函数管理 2024-03-07 11:40:57 +08:00
廖彦棋 ecc5a52232 feat(ui): 新增弹窗及时间格式化 2024-03-07 09:23:45 +08:00
chenzifan 7327b90255 Merge branch 'main' of 172.28.1.6:yangjian/chatgpt-plus into ui 2024-03-07 08:37:54 +08:00
chenzifan 6ce7a9c16f feat: 增加系统用户管理 2024-03-07 08:37:48 +08:00
廖彦棋 eb2658a91c fix(ui): ts类型 2024-03-06 18:20:07 +08:00
RockYang 9f59a18756 feat: update changelogs 2024-03-06 17:58:17 +08:00
廖彦棋 ef06f0da98 feat(ui): 新增登录 2024-03-06 17:54:38 +08:00
RockYang 4dec0e2c69 feat: Mj and sd jobs data loading in pages 2024-03-06 17:31:54 +08:00
RockYang 5f36f1af11 add prompt translating function for mobile midjourney page 2024-03-06 16:22:03 +08:00
huangqj 5a1a596098 feat(ui):simpleTable 2024-03-06 15:33:37 +08:00
廖彦棋 7a25c6e062 feat(ui): 新增请求方法及表格 2024-03-06 13:55:38 +08:00
廖彦棋 a73ed71b9d feat(ui): 管理后台基础配置 2024-03-06 10:23:55 +08:00
廖彦棋 2abbb95ea2 chore(ui): 目录结构调整 2024-03-06 09:32:47 +08:00
廖彦棋 1f80ba9463 feat(ui): 初始化 2024-03-06 09:27:11 +08:00
chenzifan 4327fdac12 refactor: 初始化UI重构 2024-03-06 08:57:46 +08:00
RockYang a6f05a5874 Merge branch 'dev' of gitee.com:blackfox/chatgpt-plus-pro into dev 2024-03-04 08:34:12 +08:00
RockYang 3fb74a1598 feat: add removing order button in admin order list page 2024-03-03 19:27:22 +08:00
RockYang 2ac44cdeb6 fix: fix major bugs for unauthorized access to data 2024-03-03 10:40:32 +08:00
RockYang 4e4dc4cb73 Update README.md 2024-03-01 23:08:05 +08:00
RockYang 855a953010 Update README.md 2024-03-01 23:08:05 +08:00
RockYang 3264fda06e fix: fixed bug image preview im mobile chat session page 2024-02-29 15:41:45 +08:00
RockYang 2c7d472069 feat: add draw same image for midjourney page 2024-02-29 11:44:09 +08:00
RockYang 8182e6797f opt: add logs for mj-plus api error 2024-02-28 15:50:42 +08:00
RockYang 99cb48ce88 opt: replace proxy url for discord image url 2024-02-27 17:45:57 +08:00
RockYang e391bfca80 feat: add change password in with mobile page 2024-02-27 15:36:20 +08:00
RockYang 0181ad3715 feat: add image preview for mobile chat page 2024-02-26 18:11:37 +08:00
RockYang b95dff0751 feat: replace http polling with webscoket notify in sd image page 2024-02-26 15:45:54 +08:00
RockYang 668d4c9c64 chore: replace 'token' with power 2024-02-23 18:11:57 +08:00
RockYang 66c0d1b2f7 docs: update mj-plus api domain 2024-02-23 15:41:02 +08:00
RockYang bef0996375 opt: remove global keyup event bind 2024-02-23 11:43:27 +08:00
RockYang 391a1e4cf0 docs: update config.toml 2024-02-22 17:53:39 +08:00
RockYang 38facf517a docs: add database sql file for v3.2.7 2024-02-22 17:28:22 +08:00
RockYang 5c9b1e8764 feat: allow to view chat message in manager console 2024-02-22 17:16:44 +08:00
RockYang dbcf14b566 feat: download image which ai generated in dialog and replace the image url 2024-02-20 18:38:03 +08:00
RockYang cc59f0c761 feat: image-wall page for mobile is ready 2024-02-20 17:38:18 +08:00
RockYang f263fc067a feat: allow user config third-party platform openai and mj api key 2024-02-20 11:23:55 +08:00
RockYang 6635b88baa feat: added delete file function 2024-02-19 16:43:03 +08:00
RockYang 87ea2a611f fix: verifycation component touch event coordinates misplace in iphone browser 2024-02-19 14:04:50 +08:00
RockYang ea8a2135cd fix: fix bug for regenerate button did not work 2024-02-19 11:22:42 +08:00
RockYang 76d86395ef fix: Upscale and Variation task overrite each other 2024-02-16 18:08:29 +08:00
RockYang e268870636 feat: midjourney mobile page all function is ready 2024-02-16 15:55:04 +08:00
RockYang 49e9f41ef2 feat: mobile mj list page is ready 2024-02-15 18:11:22 +08:00
RockYang 68cda968a1 feat: add mj image list component for mobile page. fixed bug for html tag escape 2024-02-15 11:39:04 +08:00
RockYang f6826fcefc feat: add functions mj page for mobile 2024-01-31 07:24:35 +08:00
RockYang 56a91db7e7 opt: add default extension for mj image 2024-01-30 21:46:17 +08:00
RockYang 8f4d94f046 feat: mj for mobile page payout is ready 2024-01-30 18:34:01 +08:00
RockYang 9446bf5f98 add docs and github link 2024-01-30 16:18:27 +08:00
RockYang 23e353b18b opt: enable use cdn url for mj-plus 2024-01-28 21:56:25 +08:00
RockYang 46daaa4ba1 feat: LaTeX parse is ready 2024-01-26 18:04:53 +08:00
RockYang f66d59531d feat: add model field for chat_item and and chat_history data table 2024-01-26 16:54:00 +08:00
RockYang b76cd1d5ae feat: add err_msg field for mj and sd jobs 2024-01-26 14:50:36 +08:00
RockYang 55ba0d08a5 feat: blend and swap face function for midjourney-plus is ready 2024-01-26 11:57:08 +08:00
RockYang cc78780a8e feat: add blend and swapface task implements for midjourney 2024-01-25 18:50:24 +08:00
RockYang 4a00809061 opt: refactor chat session page for mobile device 2024-01-25 14:07:10 +08:00
RockYang cd760bbd84 opt: optimize chat list page for mobile 2024-01-24 18:23:24 +08:00
RockYang 84bdc6be7f feat: use vant replace element-plus as mobile UI framework 2024-01-24 17:34:30 +08:00
RockYang 8dd6bf8933 feat: add websocket heartbeat message for mj page 2024-01-24 09:33:04 +08:00
RockYang 08f170d217 add v3.2.6 database sql file 2024-01-23 18:00:49 +08:00
RockYang 20da6a1c20 doc: update config sample file 2024-01-23 17:56:22 +08:00
RockYang 8586931630 fix: auto fill apiURL when platform changed for ApiKey add page 2024-01-23 17:30:54 +08:00
RockYang 667fc79a6f feat: merge sms branch,add DuanXinBao sms service implemetation 2024-01-23 16:16:47 +08:00
RockYang ac7430dded opt: add heartbeat message for websocket connects 2024-01-22 18:42:51 +08:00
whale_fall 9c73e16560 feat: 添加支持多个短信服务商支持 添加短信宝服务商支持,同时添加配置示例 2024-01-22 16:38:44 +08:00
RockYang c605e222a7 fix: fix bug for wechat transfer message parse failed 2024-01-22 16:10:08 +08:00
RockYang 578f26a738 feat: HuPiPay order check function is ready 2024-01-22 15:17:26 +08:00
RockYang 24218e7570 opt: verify the order in notify callback 2024-01-22 13:58:25 +08:00
RockYang 16f946550e chore: print error detail when call http api failed with mj 2024-01-21 22:30:24 +08:00
RockYang 9960a31485 opt: add image upload support for md-editor-3 2024-01-19 18:43:13 +08:00
RockYang 437632f859 fixed conflicts 2024-01-19 18:21:49 +08:00
RockYang 98f0f442eb feat: system notice function is ready 2024-01-19 18:19:51 +08:00
RockYang 3b999dc5e2 feat: system notice function is ready 2024-01-19 18:18:10 +08:00
RockYang 20943ad3d3 fix: fixed bug for img_call increased when upscale task run failed 2024-01-19 17:10:52 +08:00
RockYang 3b1544b5e4 feat: add system config item for wechat qrcode 2024-01-19 16:58:13 +08:00
RockYang b317d597ac chore: optimize variable name 2024-01-19 11:26:22 +08:00
RockYang 3bb19811e7 Merge branch 'main' into dev 2024-01-19 10:09:18 +08:00
RockYang 065a01de48 Merge branch 'main' of gitee.com:blackfox/chatgpt-plus 2024-01-19 10:09:01 +08:00
RockYang 09fb2f6557 !4 添加支持阿里旗下的大模型 通义千问对话
Merge pull request !4 from 鲸落/qwen
2024-01-19 02:06:17 +00:00
whale_fall 9ca97bf4bc feat: 添加支持阿里的通义千问对话 2024-01-19 09:52:16 +08:00
RockYang 0c59503edb feat: add image publish function, ONLY published image show in image wall page 2024-01-19 06:52:23 +08:00
RockYang 6fa08861f8 update change log 2024-01-18 18:11:25 +08:00
RockYang 72b95151a1 feat: add system config disable user registeration 2024-01-18 17:24:02 +08:00
RockYang 378e6ec9af opt: compatible wechat old message format for parsing wechat transfer message 2024-01-18 16:58:20 +08:00
RockYang 27b68c1174 opt: optimize order query alg, reduce polling times 2024-01-18 09:39:36 +08:00
RockYang 1ae8eabe99 update config 2024-01-16 15:24:06 +08:00
RockYang 72726c1cf9 docs: update change log 2024-01-16 15:05:54 +08:00
RockYang 6203d9a150 docs: update config files 2024-01-16 14:38:18 +08:00
RockYang 861f11af66 opt: optimize markdown image parser, identify image and blockquote tags 2024-01-16 10:13:00 +08:00
RockYang 39fdfae541 feat: attachments manage function is ready 2024-01-15 18:48:01 +08:00
RockYang 9f57fb1421 feat: gpt-4-gizmo-g-* model is supported 2024-01-15 15:03:05 +08:00
RockYang 059f57db2d feat: gpt-4-all model is ready 2024-01-15 14:07:24 +08:00
RockYang 13c8be8d06 opt: optimize vip recharge logic 2024-01-15 11:01:57 +08:00
RockYang e0dbfb0488 Merge branch 'dev' 2024-01-15 10:29:01 +08:00
RockYang 722ed128a1 opt: optimize vip recharge logic 2024-01-15 10:28:46 +08:00
RockYang cc628821e6 feat: add asynchronously pull midjourney task progress in case the synchronization callback is fails 2024-01-12 18:24:28 +08:00
RockYang 857af61af1 feat: midjourney plus service is ready 2024-01-11 18:16:48 +08:00
RockYang f6fc484cb3 feat: update video tutorial 2024-01-10 08:50:13 +08:00
RockYang ba07946f1e feat: update video tutorial 2024-01-10 08:48:05 +08:00
RockYang 600f668c63 remove api key for hupipay 2024-01-09 17:16:27 +08:00
RockYang 8a8990b12b fix: fixed bug for gorm insert record failed and Error is not nil 2024-01-08 18:10:32 +08:00
RockYang fe85c1bbb9 feat: change mobile field to username 2024-01-08 17:34:09 +08:00
RockYang a89ff72308 add changelog 2024-01-08 12:01:58 +08:00
RockYang a99e58184b chore: do not close pop window when click model 2024-01-08 11:01:19 +08:00
RockYang d981bc8fd0 fix: function call 兼容中转 API 2024-01-07 22:32:59 +08:00
RockYang ff22480d89 opt: add support to disable code verify 2024-01-07 17:31:26 +08:00
RockYang 8ec85e2829 feat: payjs payment channel is ready 2024-01-07 14:36:02 +08:00
RockYang 706aacd8e6 fix: add user failed in admin user list page 2024-01-07 10:49:36 +08:00
RockYang 803bc2d9fc release v3.2.4 2024-01-06 21:09:19 +08:00
RockYang 7cc67cf8f0 chore: remove useless system config items 2024-01-06 17:38:55 +08:00
RockYang 9909c3a0ed chore: error recover is enable ONLY in debug mode 2024-01-06 17:16:02 +08:00
RockYang 60a3751839 feat: payjs service is ready 2024-01-06 15:53:30 +08:00
RockYang ce87e0c40b chore: rename bind username api 2024-01-05 18:21:47 +08:00
RockYang 71cd548c00 feat: email registration function is ready 2024-01-05 18:17:11 +08:00
RockYang 1ddd05302b feat: update api key last_use_time after dalle3 call 2024-01-04 18:15:00 +08:00
RockYang ea3301c75c feat: support dall-e3 api mirrors, add name field for ApiKey 2024-01-04 16:29:57 +08:00
RockYang 4b1c4f7ccc feat: refactor LLM api request code, get API URL from ApiKey object 2024-01-04 14:51:33 +08:00
RockYang 21f2622a4b feat: api key manage page funciton is ready 2024-01-04 10:48:04 +08:00
RockYang 79f6a43019 fix: fixed bug for concurrency risk for getting token for chat histroy with issue #92 2024-01-04 09:03:19 +08:00
RockYang 3d75093b2c fix: add unique key for MidJourney task_id 2024-01-03 18:06:10 +08:00
RockYang 023dc89c3e feat: show notice in chat page 2024-01-03 15:19:24 +08:00
RockYang b5641be30d feat: fixed bug for wechat bot to parse transactions. enable user to exchange reward with img_calls 2024-01-03 11:15:54 +08:00
RockYang ff4515bbf1 fix: fixed chat export page styles 2024-01-02 11:32:36 +08:00
RockYang 5dc0fe05af fix: fixed for img_call repeated reductions 2024-01-01 18:54:48 +08:00
RockYang 25bba912f6 feat: add switch for enable|disable chat role 2023-12-29 17:51:56 +08:00
RockYang cb6e323596 feat: add nickname field for user 2023-12-29 17:39:37 +08:00
RockYang cf4b04e047 feat: add authorization for local function call 2023-12-29 17:21:29 +08:00
RockYang 243b5be31c update changelog 2023-12-29 11:53:37 +08:00
RockYang c3d753cf38 feat: add router for function manager 2023-12-29 11:22:26 +08:00
RockYang 70c46d098b fix: restore user img_calls quota when image task run failed 2023-12-29 10:41:29 +08:00
RockYang 816e4a096e merge pull request #72 2023-12-29 10:09:37 +08:00
RockYang 1d1e819817 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-12-29 09:39:52 +08:00
RockYang 342daf1be8 Merge pull request #72 from Unclesimonlau/main
重新设计了移动端web页面,新增了移动端CSS,增加移动端SD绘图页面
2023-12-29 09:39:33 +08:00
RockYang f6bb9b582a merge pull request #71 2023-12-29 09:31:25 +08:00
RockYang aad62bc976 Merge pull request #71 from JingHong0202/main
fix: Azure Api request failure after changing the API-version parameter
2023-12-29 09:27:23 +08:00
RockYang 38a49a7f37 chore: remove dead code 2023-12-29 09:02:55 +08:00
RockYang f603bf6be7 feat: function manager refactor is ready 2023-12-28 18:14:38 +08:00
JingHong 6cc05e9e27 Merge branch 'yangjian102621:main' into main 2023-12-27 23:33:46 +08:00
UncleSimonlau c00e67542f Merge branch 'main' of https://github.com/Unclesimonlau/chatgpt-plus 2023-12-26 14:19:25 +08:00
RockYang 22660362a3 fix: fixed bug #70, XunFei 1.5 url version map error 2023-12-26 14:19:00 +08:00
RockYang b1ab9975b7 fix: fixed bug #70, XunFei 1.5 url version map error 2023-12-25 08:54:17 +08:00
RockYang 7ca89d8b54 feat: function CRUD operation is ready 2023-12-24 22:12:12 +08:00
jinghong0202 998b07ab8c fix: Azure Api 更换api-version参数后请求失败的问题 2023-12-24 08:36:34 +01:00
RockYang 4936896ff7 feat: function add for admin page is ready 2023-12-23 22:30:27 +08:00
RockYang 18b7484c5b feat: support CDN reverse proxy for MidJourney and OpenAI API 2023-12-22 17:25:31 +08:00
RockYang 6754c8e85e feat: add function list page in admin console 2023-12-21 18:06:09 +08:00
RockYang 57243ff010 opt: optimize image compress alg, add cache control for image 2023-12-21 15:00:46 +08:00
RockYang 7c4dfe96ee feat: add funcitons manger page 2023-12-21 08:58:24 +08:00
RockYang bf19120c27 feat: auto translate image creating prompt 2023-12-19 18:54:19 +08:00
RockYang 3d37087916 fix: fixed bug for HuPiPay qrcode generation. set field 'openid' of result struct to Any data type 2023-12-19 11:31:57 +08:00
RockYang 6949f1328c chore: update copyright information 2023-12-18 18:19:41 +08:00
RockYang b63090c20f feat: add remove action to remove task and images for MJ and SD task list page 2023-12-18 17:44:52 +08:00
RockYang b15ebda948 opt: merge RAG branch 2023-12-18 16:41:40 +08:00
RockYang 252eef2e5e opt: make sure the Upscale and Variation task is assign to the same mj service with Image task 2023-12-18 16:34:33 +08:00
RockYang 3bea8f9706 fix: fixed bug for mj service pool config pointer 2023-12-15 22:52:57 +08:00
RockYang dfe808bee7 feat: add img_calls field for recharge products 2023-12-15 16:56:56 +08:00
RockYang 58f3dd5336 chore: update default config.toml 2023-12-15 11:23:13 +08:00
RockYang b00f16648f opt: limit the image display size in reply component 2023-12-15 10:48:13 +08:00
RockYang fc66c31e89 docs: update changelog 2023-12-15 09:04:02 +08:00
RockYang a26c849532 docs: add sql file 2023-12-14 17:05:51 +08:00
RockYang d2991e60b6 refactor: refactor stable diffusion service, add service pool support 2023-12-14 16:48:54 +08:00
RockYang 10ba1430f9 chore: add sub dir support for OSS 2023-12-13 17:02:49 +08:00
RockYang 6d71f24f75 refactor: add midjourney pool implementation, add translate prompt for mj drawing 2023-12-13 16:38:27 +08:00
RockYang 8f4d20e411 fix: fixed bug for aliyun OSS img url 2023-12-13 09:49:55 +08:00
RockYang cf758d773e refactor mj service, add mj service pool support 2023-12-12 18:33:24 +08:00
RockYang c012f0c4c5 opt: remove default value for stable-diffusion page 2023-12-12 09:59:20 +08:00
RockYang 9e85900057 docs: update readme file 2023-12-12 09:52:28 +08:00
RockYang 68e3b787f9 docs: update build config.toml 2023-12-12 07:25:36 +08:00
RockYang 6706704ad6 docs: update changelog file 2023-12-11 17:17:17 +08:00
RockYang 1067fa015e add translate api for midjourney 2023-12-11 17:01:02 +08:00
RockYang 411335ebcb feat: add prompt translate handler 2023-12-11 06:56:00 +08:00
RockYang f19c15491e feat: add system config item for dall e3 generate image num 2023-12-10 17:13:25 +08:00
RockYang 0da81a4d10 chore: change default params for stable diffusion 2023-12-10 14:45:22 +08:00
RockYang 049839a5c7 feat: add HuPiPay payment support 2023-12-08 19:43:13 +08:00
RockYang a901927844 feat: add image generation API URL in chat configurations 2023-12-07 16:31:32 +08:00
RockYang 07ab0bc5a1 docs: add arm64 build script 2023-12-07 15:44:20 +08:00
RockYang a6025e6fab fix: fixed bug for prompt code format, prevent xss attacks 2023-12-07 14:02:13 +08:00
RockYang e841a61bf0 opt: save chat ID when the chat websocket disconnect 2023-12-07 11:07:08 +08:00
RockYang 2f1c2110c6 feat: adjust task list component styles 2023-12-06 19:05:51 +08:00
RockYang 8e573eea4c Merge branch 'pr_3' into dev 2023-12-06 18:54:45 +08:00
RockYang 149765764e feat: refactor midjourney image creating page 2023-12-06 18:54:30 +08:00
RockYang 45d6444c7e docs: update readme file 2023-12-06 14:44:06 +08:00
liyuwanglan aa65f49190 修改 2023-12-05 11:08:03 +08:00
RockYang 1bda41ff67 docs: update comments 2023-11-30 17:35:56 +08:00
RockYang 5a799139cd fix: fixed bug for upload image failed 2023-11-29 17:46:46 +08:00
RockYang eb36d0742a opt: create new chat session when change role or model, fix bug for mobile no validate 2023-11-29 17:36:27 +08:00
RockYang d8a9123852 docs: update change logs 2023-11-28 15:33:30 +08:00
RockYang 19031c2197 docs: 添加一键部署脚本 2023-11-28 15:25:51 +08:00
RockYang 7a2ffdf39c opt: 缩略图生成算法 2023-11-28 14:50:19 +08:00
RockYang ce8fa79206 feat: 为大图片生成缩略图,加快前端图片加载速度 2023-11-28 12:04:02 +08:00
RockYang b05c63549d fix: fix bug for oss image domain 2023-11-28 07:27:18 +08:00
RockYang 9f98660423 docs: update build script 2023-11-27 18:24:52 +08:00
RockYang 7b0fa862d2 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-27 12:03:23 +08:00
RockYang bfe2d4d573 feat: merge mysql and redis docker service to docker-compose.yaml file 2023-11-27 10:56:18 +08:00
RockYang 26950c5673 Merge pull request #55 from openjst/main
修改code显示颜色样式
2023-11-27 10:27:13 +08:00
RockYang 86788362a5 docs: make a full docker-compose.yaml 2023-11-27 07:21:37 +08:00
openjst deedc5fb2e 修改code显示颜色样式 2023-11-26 23:26:08 +08:00
RockYang f4fbe67db9 feat: implements image function replace Mj with DALL-E-3 2023-11-26 20:37:48 +08:00
RockYang 599ce0eade feat: add type field for api key 2023-11-24 18:05:59 +08:00
RockYang 11f3ab8dc7 feat: add support for registing use force use invite code 2023-11-24 12:02:28 +08:00
RockYang 75b4a6dd46 opt: optimize image preview for MidJourney image list page, only preview current image not for all images 2023-11-23 17:55:12 +08:00
RockYang 86bbea337d opt: 增加中间件自动对HTTP请求的参数去掉首尾空格 2023-11-23 17:50:55 +08:00
RockYang e63a30064b opt: optimize styles for invitation page 2023-11-23 17:40:15 +08:00
RockYang 222b1ddbd9 feat: add invitation and promotion functions 2023-11-23 16:30:15 +08:00
RockYang a88dd88a07 docs: add database sql file for v3.1.9 2023-11-23 09:58:01 +08:00
RockYang a47c01fd41 docs: update change log 2023-11-23 09:46:49 +08:00
RockYang 35e385f7a7 docs: update change log and readme file 2023-11-23 09:14:42 +08:00
RockYang 191e3b7d2c feat: 支持讯飞大模型 v3.0 2023-11-23 07:11:13 +08:00
RockYang 13fbbc190a feat: reset password function is ready 2023-11-22 18:00:45 +08:00
RockYang 61c6a6e7f3 feat: add copy code btn in chat page, fixed bug for code wrap in model of ChatGLM and XunFei 2023-11-22 17:00:43 +08:00
RockYang 911f218f0b feat: add feekback orcode in chat page 2023-11-22 12:05:58 +08:00
RockYang af41350eb9 fix: fixed bug for enable user vip in admin console not work 2023-11-22 11:30:58 +08:00
RockYang 8f63e348b3 option: replace leveldb with redis in storing message code 2023-11-22 10:57:24 +08:00
RockYang 8901cf8e81 Merge pull request #52 from KunMingStar/fix_migrate
fix: remove dead code
2023-11-22 08:23:01 +08:00
tongkunming e11736e1d2 fix: remove dead code 2023-11-21 22:18:53 +08:00
RockYang c062fba69c chore: remove dead code 2023-11-21 19:04:53 +08:00
RockYang 4628095fe7 feat: add wechat id card for closing register 2023-11-20 16:38:04 +08:00
RockYang 787ed9bc0f docs: update docs installation docs url 2023-11-17 18:42:13 +08:00
RockYang daaff8974e docs: update readme and config sample files 2023-11-16 17:20:38 +08:00
RockYang 4fcbd511c0 fix: fix bug with missing chat context 2023-11-15 18:20:34 +08:00
RockYang 64d6bff6d9 fix: unbind event for login page when the component is unmount 2023-11-14 18:19:40 +08:00
RockYang 998cbace92 feat: add link on logo 2023-11-14 13:50:44 +08:00
RockYang 2204038951 fix: fix member page flex styles 2023-11-14 11:21:16 +08:00
RockYang 998f67ac5d fix: deducating the user's img call quota after stable diffusion callback 2023-11-14 08:59:39 +08:00
RockYang be981a2c9b feat: 更改 MJ 和 SD 菜单图标 2023-11-13 11:27:42 +08:00
RockYang da3cf0d4a3 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-13 10:11:45 +08:00
RockYang 7124b9a36c feat: 更改 MJ 和 SD 菜单图标 2023-11-13 10:11:21 +08:00
RockYang 712a6b6c39 chore: change xxl-job name 2023-11-12 15:39:12 +08:00
RockYang ed0a1c57d6 opt: 优化 ItemList 组件样式,调整支付页面布局 2023-11-11 22:11:04 +08:00
RockYang 7858228505 docs: update change log file 2023-11-11 12:58:57 +08:00
RockYang 2525a22d78 fix: fix bug for issue 49, stable diffusion service not decrease img_calls 2023-11-11 11:00:26 +08:00
RockYang 7b4ef8fc31 opt: 优化 ItemList 组件样式 2023-11-11 10:53:33 +08:00
RockYang ce14cd02e4 feat: add order list compponent 2023-11-10 18:06:32 +08:00
RockYang 05f501af52 Merge branch 'alipay' 2023-11-10 16:51:00 +08:00
RockYang e94fa4844f docs: export database, update readme doc, remove useless configs for alipay 2023-11-10 16:49:07 +08:00
RockYang e0965aae5e feat: add switch to disable xxl-job service, update readme 2023-11-10 15:22:35 +08:00
RockYang 414c1de963 feat: 增加订单倒计时组件,自动清理过期未支付订单 2023-11-10 14:39:27 +08:00
RockYang b589102be8 feat: add system configration switch option for order pay service, support sandbox env for alipay 2023-11-09 18:28:56 +08:00
RockYang df367e0d47 feat: check if the user's chat quota is gt than current chat model required before starting a conversation 2023-11-09 16:56:44 +08:00
RockYang 680219ebcb Merge pull request #47 from KunMingStar/fix/sendMessage
fix: 添加messages为空校验
2023-11-09 08:21:24 +08:00
RockYang ef87487f60 fix: fix bug for token expired with QiNiu oss upload file 2023-11-08 21:14:09 +08:00
RockYang 8bafe72434 feat: adjust layout for user profile page 2023-11-08 18:33:26 +08:00
RockYang 76dcc69e44 feat: order payment function is ready 2023-11-08 17:48:07 +08:00
tongkunming 682ef22194 fix: 添加messages为空校验 2023-11-08 16:51:36 +08:00
RockYang ee8dd41605 feat: update stable diffusion api version 2023-11-08 14:54:10 +08:00
RockYang d0b8d666e4 feat: finish payment page layout 2023-11-07 18:10:28 +08:00
RockYang 4a81826d19 feat: adjust table styles for markdown 2023-11-07 12:02:16 +08:00
RockYang 848a10c3a8 feat: integrated Alipay payment module 2023-11-06 17:55:46 +08:00
RockYang a4baa29678 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-11-06 15:04:11 +08:00
RockYang a9b5280691 opt:update mj api version, wrapper chat api error message as a constant 2023-11-06 15:03:56 +08:00
RockYang 264706210b chore: remove invalid filepath for windows 2023-11-02 19:16:02 +08:00
RockYang 7677ae254f feat: different AI model consuming different amounts of use_calls 2023-10-26 14:38:06 +08:00
RockYang 7ccb4c5f06 docs: add database full backup 2023-10-26 14:00:06 +08:00
RockYang 01205af018 feat: 支持文心4.0模型,不同的用户可以订阅不同的AI模型 2023-10-26 13:41:49 +08:00
RockYang 5b5150e6d4 feat: add system configration item to close rewarding function 2023-10-19 11:19:29 +08:00
RockYang 9d8e3f5049 docs: add alipay reward qrcode 2023-10-17 17:58:03 +08:00
RockYang 99ede83bdc chore: set app list page as the index page 2023-10-16 14:20:44 +08:00
RockYang 61488e840d update changelog file 2023-10-16 13:40:44 +08:00
RockYang a80d01209c opt: adjust styles for ItemList component, cut string for chat role's hello message 2023-10-16 10:46:10 +08:00
RockYang d2a8d655c8 feat: finish adding chat role to user function 2023-10-16 06:56:42 +08:00
RockYang 80e2b34abc opt: adjust ItemList component styles 2023-10-15 15:47:42 +08:00
RockYang fb3d43d2d5 opt: optimize imgae loading for image wall page, loading thumb image replace source image 2023-10-13 23:07:41 +08:00
RockYang ebdec4fa44 feat: create chat app list page, build the layout of page 2023-10-13 18:05:40 +08:00
RockYang ead8dbbaa5 feat: auto login when register successfully 2023-10-13 16:27:40 +08:00
RockYang 95615a5501 docs: update images for readme 2023-10-13 15:55:30 +08:00
RockYang 75487b1f58 Merge branch 'main' into image-wall 2023-10-13 15:39:34 +08:00
RockYang c7daaa00cd feat: image wall stable diffusion image list component is ready 2023-10-13 15:16:40 +08:00
RockYang 606f24e981 feat: optimize the midjourney image list styles 2023-10-13 11:14:39 +08:00
RockYang c83d9287a7 docs: update readme, database files 2023-10-13 09:59:05 +08:00
RockYang fa8092b2cd docs: update readme file 2023-10-13 06:49:21 +08:00
RockYang b2b07a2be6 docs: update change log 2023-10-13 06:39:44 +08:00
RockYang a21628e350 style: fix style for update chat title input element 2023-10-13 06:29:25 +08:00
RockYang 9fcd686fda feat: image wall page is ready 2023-10-12 18:09:50 +08:00
RockYang 1759fd4cf9 opt: close websocket connection when finish a chat call for XunFei API 2023-10-12 10:02:12 +08:00
RockYang 12e7837602 feat: XunFei ai mode api implements is ready 2023-10-11 18:17:03 +08:00
RockYang ff349f2edf feat: adjust package struct, put chat code the seperate 'chatimpl' package, fix bug: baidu api chat context number must be even number 2023-10-11 15:46:40 +08:00
RockYang 088d8ca207 feat: add system configration for enable/disable funciton in chat session 2023-10-11 14:35:47 +08:00
RockYang c37902d3a4 finish baidu ai model api implementation 2023-10-11 14:21:16 +08:00
RockYang da38684cdd add baidu ai model api configrations 2023-10-10 18:19:56 +08:00
RockYang cb2cb72333 docs: update readme 2023-10-09 12:13:21 +08:00
RockYang d53c8f9830 docs: add 3.1.4 release version change logs 2023-10-09 12:05:51 +08:00
RockYang c8136d4231 doc: update database sql file 2023-10-08 20:44:55 +08:00
RockYang e4b8380530 docs: udpate readme and config.toml template 2023-10-08 18:09:46 +08:00
RockYang fbd599194c feat: add system config item for reward image, add app config item to use custom text2img param json file 2023-10-08 17:48:50 +08:00
RockYang 0270ec26fb feat: add configuration handler for AliYun SMS signature and template ID 2023-10-08 12:01:09 +08:00
RockYang 79e342be57 optimze the style for stable diffusion image dialog 2023-10-08 11:39:55 +08:00
RockYang c89f23b018 feat: stable diffusion page function is ready 2023-10-06 22:25:37 +08:00
RockYang f971ec5d34 fix: fixed bug for generating the upload file path 2023-10-05 18:09:42 +08:00
RockYang c5776ce41f feat: stable diffusion page is ready 2023-09-28 18:09:45 +08:00
RockYang 75c5ebbffa feat: migrate the chatgpt-plus-ext project code to this project 2023-09-27 18:14:07 +08:00
RockYang d51a724ade feat: add implements for stable diffusion service 2023-09-26 18:16:51 +08:00
RockYang c1143d7a6d fix: fixed bug for register error with parse args 2023-09-26 09:30:18 +08:00
RockYang f3697431a4 fix: fixed bug for add user in admin console page that calls and img_calls parameter not work 2023-09-22 18:15:32 +08:00
RockYang e3d6e5f420 feat: the upload handler for AliYun OSS is ready 2023-09-22 09:56:45 +08:00
RockYang af6f7a7146 docs: adjust ui styles for mj image page 2023-09-20 14:33:15 +08:00
RockYang c18d95e89a docs: update docker-compose.yaml, change image version to v3.1.3 2023-09-20 11:42:23 +08:00
RockYang fa42d03a65 some page ui optimization, add release v3.1.3 2023-09-20 11:39:11 +08:00
RockYang 22f7de2c09 fix socket connect for mj task notify 2023-09-20 06:59:30 +08:00
RockYang b4b9df81cb opt: add sessionId for mj task 2023-09-19 18:15:08 +08:00
RockYang 2a71c2b0e7 feat: mj advance drawing page function is ready, use better task scheduling argorithm 2023-09-17 18:03:45 +08:00
RockYang c37b3d3df5 feat: add task type params for add new mj task 2023-09-16 14:01:53 +08:00
RockYang ae135b89d6 feat: adjust UI for task list page 2023-09-15 18:16:59 +08:00
RockYang 4112426848 feat: midjourney page task and image list component is ready 2023-09-15 17:40:39 +08:00
RockYang 638b399b31 feat: midjourney page parameter ui is ready 2023-09-14 18:28:24 +08:00
RockYang 8a5713ef7c feat: finish mj model layout 2023-09-14 06:55:03 +08:00
RockYang 382d7bfaa1 feat: add ImageMj page view 2023-09-13 18:04:31 +08:00
RockYang 3daf2f7738 feat: optimize mj notidy api, use job queue to send ai drawing request 2023-09-13 15:50:00 +08:00
RockYang 944d35971d feat: chat chrawing function is refactored 2023-09-13 06:57:25 +08:00
RockYang 9ce8af0b82 feat: refactor MidJourney service for conpatible drawing in chat and draw in app 2023-09-12 18:01:24 +08:00
RockYang d825b9ec26 fix: fixed bug for jwt token expire caculation 2023-09-12 10:49:55 +08:00
RockYang 9afe4aea4b feat: new WebUI for the main page, add MJ and SD drawing function pages 2023-09-11 16:22:11 +08:00
RockYang 3cc8c3284a opt: optimize the styles of chat page; caculate all tokens of context as chat history's token 2023-09-11 13:34:20 +08:00
RockYang e2c18c4e1e chore: update gpt avatar 2023-09-10 12:02:37 +08:00
RockYang 81fe768f6a opt: refactor the web page's router and layout 2023-09-08 22:14:58 +08:00
RockYang 8923779ab0 opt: return error when download mj image failed 2023-09-08 21:02:21 +08:00
RockYang 583218a045 opt: add lock for mj task callback 2023-09-08 18:12:18 +08:00
RockYang 153b2bfa53 opt: add lock for mj task callback 2023-09-08 17:23:32 +08:00
RockYang 0a9b325360 opt: Only create actived upload service 2023-09-08 06:46:24 +08:00
RockYang af9e6f2c46 update desktop package deps 2023-09-07 18:05:13 +08:00
RockYang 0e31726fd3 fix: remove username and nickname field for updating user info 2023-09-07 15:41:01 +08:00
RockYang 590d9c2e61 Update README.md 2023-09-07 15:23:38 +08:00
RockYang c12fe9f25e adjust home page styles 2023-09-07 15:12:43 +08:00
RockYang 9bd83f3acf use absolute path for all routes 2023-09-07 10:27:18 +08:00
RockYang aada15fc41 feat: add linux package configs for desktop 2023-09-07 09:45:11 +08:00
RockYang 6a2d383be6 feat: extract css to a single file, add chat_id path variable support 2023-09-07 09:29:16 +08:00
RockYang 545c8fa482 feat: use eletron to build desktop version for chatgpt-plus 2023-09-06 18:19:27 +08:00
RockYang e3d18643b8 opt: clear space in the reward transfer number 2023-09-06 17:09:55 +08:00
RockYang 2f318bbe9f feat: add QiNiu OSS storage implements 2023-09-06 14:37:13 +08:00
RockYang fac7eb61b3 remove username search for console user list page 2023-09-05 17:52:46 +08:00
RockYang 4450d03a54 update sql file, remove truncate table sql 2023-09-05 17:05:33 +08:00
RockYang e4e7868b30 fix: 采用弹窗的方式显示验证码,解决验证码在低分辨率下被掩盖的Bug 2023-09-05 16:47:40 +08:00
RockYang 3d7fa680cb docs: update change log and create a new release version 2023-09-05 14:28:30 +08:00
RockYang c38035e25e fix: replace session handler with jwt authorization 2023-09-05 11:47:03 +08:00
RockYang 8d4fdaf902 refactor: refactor mobile pages for the chat model updating 2023-09-04 18:15:56 +08:00
RockYang 2820adad53 feat: allow user to set custom api keys for different platforms 2023-09-04 17:34:29 +08:00
RockYang f7a427d2c0 refactor: refactor chat model, replace mode value with mode id. refactored system config module, add seperate configration for every chat model 2023-09-04 16:32:20 +08:00
RockYang 59ed8c9660 refactor: refactor chat_handler, add support for Azure and ChatGLM 2023-09-04 06:43:15 +08:00
RockYang 71562ab0e5 docs: add log dir to docker 2023-08-24 08:55:11 +08:00
RockYang 278ddf037f docs: update docker-compose file 2023-08-23 06:56:04 +08:00
RockYang ac3151af92 chore: update bin file name 2023-08-22 22:18:07 +08:00
RockYang 1189ad7862 feat: change order by for login log and reward records 2023-08-22 22:04:46 +08:00
RockYang 88c13e7b9a opt: optimize element ui dialog styles, make it to the center of screen 2023-08-22 08:57:14 +08:00
RockYang 596baac62e opt: upgrade element-plus to 2.3.0, optimize the styles for for element dialog 2023-08-22 07:14:13 +08:00
RockYang 5a8503ec14 chore: remove debug codes 2023-08-21 10:55:49 +08:00
RockYang e44f95724d doc: update readme docs 2023-08-21 06:57:04 +08:00
RockYang dec0d0dea7 feat: delete the old avatar image file when update the user profile 2023-08-21 06:27:30 +08:00
RockYang 46f96e94ec feat: add oss service factory implements, add support for setting custom upload handler, localstorage and minio oss 2023-08-20 22:29:08 +08:00
RockYang 89b30bcf58 opt: optimize code for remove chat item 2023-08-20 19:06:18 +08:00
RockYang 473d8f5f85 feat: add minio service implementation, download midjourney image to local storage 2023-08-20 16:17:42 +08:00
RockYang 08d8d65599 opt:Optimize the algorithm to check whether Midjourney images can continue to variation 2023-08-17 14:20:16 +08:00
RockYang 2964c1d82e docs: update demo url 2023-08-17 07:18:44 +08:00
RockYang 75ab8552d9 chore: show img_calls num for user profile page 2023-08-17 07:09:53 +08:00
RockYang b78d5b3d03 docs: update docs 2023-08-17 06:55:11 +08:00
RockYang 064081c771 feat: add authorization for MidJourney function calls 2023-08-16 23:16:44 +08:00
RockYang 3525d35d15 chore: update database sql file 2023-08-15 22:53:32 +08:00
RockYang 9d9ee7c585 opt: unset timeout for websocket connection 2023-08-15 18:29:53 +08:00
RockYang e65c32c4c7 feat: midjourney image variation function is ready 2023-08-15 17:31:02 +08:00
RockYang 113769791f feat: midjourney image upscale function is ready 2023-08-15 15:28:40 +08:00
RockYang 5d2a1d21d5 feat: midjourney drawing image function is ready 2023-08-14 17:59:21 +08:00
RockYang 6e40f92aaf feat: add midjourney message receive handler 2023-08-14 07:09:52 +08:00
RockYang 302cb8a5be feat: add midjouney api implements, optimize function calls 2023-08-11 18:46:56 +08:00
RockYang 0b27890484 feat: remove wechat bot, replace with api callback for receive wechat payment transactions 2023-08-10 17:24:23 +08:00
RockYang 5db247e632 feat: add samples and function introduce in welcome page 2023-08-08 14:19:09 +08:00
RockYang c1155a4338 docs: add dashboard image 2023-08-07 20:07:05 +08:00
RockYang da806b9492 feat: enabled user to set default GPT model for chat 2023-08-04 14:25:54 +08:00
RockYang 3d6d46b30d feat: chat export function is ready 2023-08-04 12:08:07 +08:00
RockYang 57c69738ba feat: add chat export button 2023-08-03 18:24:30 +08:00
RockYang 0c8157dbc0 feat: add rewards statistic in dashboard page 2023-08-03 10:19:37 +08:00
RockYang d7b278f2f7 opt: add enabled_msg_service config var to system config database 2023-08-03 10:04:45 +08:00
RockYang 18d74f1057 opt: optimize build script, auto push to aliyun docker hub 2023-08-02 18:09:19 +08:00
RockYang e04856f794 opt: optimize the layout for regiseter page. add function to disable registration 2023-08-02 17:44:13 +08:00
RockYang d7bbfb0fc3 feat: the dashboard page is ready for admin console 2023-08-02 16:37:47 +08:00
RockYang a0857817de refactor: refactor admin console page layout 2023-08-02 15:00:18 +08:00
RockYang 8d39234fd0 fix: fixed bug for chat request's token reach the max token limit for OpenAI 2023-08-01 17:58:03 +08:00
RockYang 0fce1c0fd8 feat: new function for add user in manger console user list page 2023-08-01 16:02:49 +08:00
RockYang c23c285f82 doc: add video tutorial for deploying system 2023-08-01 10:15:43 +08:00
RockYang 81b560d791 docs: remove docs for deploy manually 2023-07-31 20:04:22 +08:00
RockYang 55459a4f42 opt: optimize the build script and docker-compose yaml file 2023-07-31 17:31:02 +08:00
RockYang 9eafd3e6ca feat: add env var to set log level 2023-07-31 08:34:11 +08:00
RockYang e7ac26ff5a feat: update system config cache for AppServer when updating system config in admin console 2023-07-31 08:13:20 +08:00
RockYang fad78641d6 add system config cache for AppServer object 2023-07-31 06:56:28 +08:00
RockYang a32cb0142b feat: allow user to login with username and mobile no 2023-07-27 15:37:50 +08:00
RockYang 4a3c133152 fix: deauthorize some apis 2023-07-27 10:53:14 +08:00
RockYang ae79c34508 fix: fix bug for mobile verify code function not available in register page 2023-07-27 10:29:59 +08:00
RockYang 7eb69c1ad7 style: adjust reward image styles 2023-07-26 17:57:19 +08:00
RockYang 8aaff0af89 chore: optimize build script, compress reward img 2023-07-26 17:48:16 +08:00
RockYang 4b77280c65 feat: add reset password function to user list page 2023-07-26 15:22:11 +08:00
RockYang a0df84e4f2 docs: add change log file 2023-07-25 17:37:20 +08:00
RockYang 8876e6a098 docs: 更新文档和数据库文件 2023-07-25 17:25:23 +08:00
RockYang 9bee2a90f9 feat: 完成人机交互验证 API 接入,增加短信防刷验证 2023-07-25 17:00:24 +08:00
RockYang cd6a811bbd feat: 头条,微博热搜等函数 API 实现 2023-07-25 15:02:43 +08:00
RockYang 84f9a83f55 opt: 取消强制手机号验证,更新配置 2023-07-24 18:18:09 +08:00
RockYang 2efc669ab2 feat: 完成众筹后台管理功能 2023-07-24 15:59:29 +08:00
RockYang 5444ed77ad chore: add error log for reading chat response buffer 2023-07-24 12:05:38 +08:00
RockYang dd71fe80a5 opt: record user_id for reward verify 2023-07-22 08:42:30 +08:00
RockYang b73092eb64 feat: reward verify is ready 2023-07-21 22:29:14 +08:00
RockYang aa28f87b0c feat: 增加打赏核销功能 2023-07-21 18:26:51 +08:00
RockYang 1e1bcd4a30 feat: 集成微信收款服务 2023-07-20 17:46:32 +08:00
RockYang d24b3c46bf put logs in log file 2023-07-20 16:36:12 +08:00
RockYang 37222f07d9 fix: 修复阿里云短信发送失败的 bug 2023-07-16 16:05:11 +08:00
RockYang 49646a79e5 fix: 用户使用自己绑定的 API 调用,则不计算 calls 和 token 消耗 2023-07-16 11:55:45 +08:00
RockYang 9940e1210e Merge branch 'main' into prod 2023-07-16 09:57:27 +08:00
RockYang 10892812c3 chore: 更新项目打包脚本 2023-07-16 09:55:00 +08:00
RockYang d8ff5987dd feat: plugin function is ready 2023-07-15 21:52:30 +08:00
RockYang d014d418e9 feat: 支持上下文深度配置,计算每轮对话消耗的总 token 数量 2023-07-15 18:37:25 +08:00
RockYang cc1b56501d refactor: 更改 OpenAI 请求 Body 数据结构,兼容函数调用请求 2023-07-15 18:00:40 +08:00
RockYang a5ad9648bf chore: small fixs 2023-07-14 18:00:01 +08:00
RockYang 3630099234 feat: 完成每日早报函数开发 2023-07-10 18:59:53 +08:00
RockYang 941a24c75b fix: fixed conflicts 2023-07-10 10:11:17 +08:00
RockYang 50bdc01484 docs: update readme file 2023-07-10 09:44:24 +08:00
RockYang d9e340ddc4 refactor: 调整项目目录结构,移除其他语言 API 目录 2023-07-10 09:42:11 +08:00
RockYang beee1e91d6 test: add plugin test code 2023-07-10 07:05:56 +08:00
RockYang 35935d2bac fixed conflicts 2023-07-06 10:49:38 +08:00
RockYang 5d0b0ad33e opt: add tip message when no available key 2023-07-06 10:47:36 +08:00
RockYang d1a6ac531f opt: add tip message when no available key 2023-07-06 10:34:01 +08:00
RockYang ae255a3bd9 fix: 取消 ElMessage 的 appendTo 属性,防止被 Dialog 组件覆盖 2023-07-04 17:59:55 +08:00
RockYang fa733afa51 opt: 优化验证码发送逻辑,加入防刷验证 2023-07-04 17:15:02 +08:00
RockYang b60c723457 Merge branch 'main' into feat-sms 2023-07-04 10:55:20 +08:00
RockYang 2bb7ff3c13 Merge branch 'main' of github.com:yangjian102621/chatgpt-plus 2023-07-04 10:54:35 +08:00
RockYang 68b4cf00e2 docs: update README, 增加免责申明 2023-07-04 10:52:52 +08:00
RockYang 440169ff60 feat: 短信验证码功能已完成,手机端同步实现。 2023-07-03 15:18:15 +08:00
RockYang 1fc361242e opt: 将短信发送按钮封装成组件 2023-07-03 06:55:15 +08:00
RockYang 72cc6f3d75 feat: 注册短信验证码验证功能已经开启 2023-07-02 20:51:13 +08:00
RockYang a2b1924e00 Merge branch 'main' into prod 2023-07-02 00:04:59 +08:00
RockYang 7e926167f0 fix: 修复 nodejs apple M1 跨平台打包,运行报错 exec format error 2023-07-02 00:04:12 +08:00
RockYang 32484bd32e feat: 增加用户 token 消耗统计功能 2023-07-01 23:29:24 +08:00
RockYang 04ade1008b Merge branch 'main' into prod 2023-07-01 10:24:08 +08:00
RockYang 4603de0e70 Merge pull request #18 from ly307787186/main
Update Setting.vue
2023-07-01 10:22:01 +08:00
RockYang 12c1ff65e9 opt: remove chat role info from user login api's response 2023-06-30 18:52:43 +08:00
ly307787186 7b8cbf7f86 Update Setting.vue
关掉aip-key 必需的验证
2023-06-30 13:20:30 +08:00
RockYang c198e83f70 fix: fixed bug for Emoji can not insert to MySQL 2023-06-30 09:15:57 +08:00
RockYang e60863a290 chore: change ubuntu docker image with aliyun 2023-06-29 15:56:11 +08:00
RockYang 946563d3b2 chore: add test code for fix role icon url of db 2023-06-29 08:58:19 +08:00
RockYang 3f2ef1d54e fix: 修正前端 user_init_call 字段错误和用户注册初始化头像路径问题 2023-06-28 20:01:44 +08:00
RockYang 0d2b60b905 fix: 修复 PC 端聊天界面滚动条问题 2023-06-28 18:16:28 +08:00
RockYang 161b11e428 opt: 优化启动参数接收处理 2023-06-28 05:51:55 +08:00
RockYang f7748d51df opt: 通过环境变量来传参,修正 docker compose 配置参数 2023-06-27 18:29:46 +08:00
RockYang 5d13b4b705 docs: 更新文档,新增移动端预览图 2023-06-27 14:28:00 +08:00
RockYang 92e52a2284 fix: 修复 markdown 换行符不解析的 Bug,修复新发布的模型 token 统计失败错误 2023-06-27 14:18:20 +08:00
RockYang 3efd5fb77a feat: vue-mobile => 完成用户信息修改功能,前后端都添加文件上传功能。 2023-06-27 12:11:55 +08:00
RockYang c3d62bb8d8 feat: vue-mobile => 完成移动端聊天配置功能 2023-06-26 18:18:45 +08:00
RockYang c099e843d5 feat: vue-mobile => 完成会话聊天页面功能,增加主题切换功能 2023-06-26 16:39:00 +08:00
RockYang 5f3a5871f1 feat: vue-mobile => 优化聊天记录拍版样式 2023-06-25 18:21:38 +08:00
RockYang f01fdd0070 feat: vue-mobile => 完善移动端聊天列表页功能 2023-06-25 17:01:04 +08:00
RockYang d20cc367b8 fixed: go-api => 增加全局错误处理 handler,修复业务处理异常导致服务退出的 Bug 2023-06-25 11:34:55 +08:00
RockYang eadb9a733f docs: 增加容器部署文档 2023-06-25 11:06:18 +08:00
RockYang 32ac454b5b opt: 优化前端登录判断逻辑 2023-06-25 09:46:23 +08:00
RockYang 2cadd6af44 feat: chat list page for mobile is ready 2023-06-25 06:53:22 +08:00
RockYang 3c8b5cb313 feat: 完成移动端前段框架搭建 2023-06-24 11:45:26 +08:00
RockYang fcf2387794 style: 调整聊天侧边栏样式 2023-06-23 18:31:50 +08:00
RockYang f9783b4806 opt: 优化 docker-compse 构建脚本,修复后端路由 Bug 2023-06-23 18:04:16 +08:00
RockYang cbdc532f83 chore: 添加 docker 镜像构建脚本 2023-06-23 07:08:16 +08:00
RockYang 124554bae5 opt: 抽离 session 验证函数,修正前端路由覆盖 bug 2023-06-23 06:31:25 +08:00
RockYang 2725536d7d chore: 使用阿里云镜像仓库 2023-06-22 22:23:48 +08:00
RockYang beeef7a4f7 docs: 增加 docker-compose 部署支持 2023-06-22 22:14:18 +08:00
RockYang 7f2ebb6aeb docs: 完善新版本文档 2023-06-22 15:47:51 +08:00
RockYang c8e30383ba refactor: embed xdb file for ip2region 2023-06-22 11:08:44 +08:00
RockYang a7d5a6ccb9 style: 调整后台管理框架样式 2023-06-21 18:44:18 +08:00
RockYang be3380aaf3 refactor: 更新 iconfont 图标,增加打赏二维码弹窗 2023-06-21 15:34:38 +08:00
RockYang 40a4ab5410 refactor: refactor the frame layout of admin module 2023-06-21 14:22:28 +08:00
RockYang 54a2181960 refactor: user login log list for admin is ready 2023-06-21 06:53:41 +08:00
RockYang e490e15bc4 feat: API Key manage is ready 2023-06-20 18:05:33 +08:00
RockYang 469844d97b opt: optimize role sorting 2023-06-20 16:07:26 +08:00
RockYang 892ea29ba8 refactor: chat role manage for admin is ready 2023-06-20 11:46:13 +08:00
RockYang acde1c6742 refactor: user remove is ready for console 2023-06-20 07:08:37 +08:00
RockYang 07da11d852 refactor: 管理后台用户编辑功能 is ready 2023-06-19 21:53:07 +08:00
RockYang 502b8c2270 refactor: 管理后台用户列表页面重构 2023-06-19 18:23:09 +08:00
RockYang af3f7ac810 refactor: 完成管理后台的系统设置页面重构 2023-06-19 15:58:52 +08:00
RockYang 935c6caf96 feat: admin login page is ready 2023-06-19 11:09:23 +08:00
RockYang 90bce1d437 refactor: refactor controller handler module and admin module 2023-06-19 07:06:59 +08:00
RockYang 831dd3e2e0 style: optimize code styles 2023-06-18 15:17:59 +08:00
RockYang b8e208eb32 style: optimize styles for change password page 2023-06-17 00:02:49 +08:00
RockYang 542c908e30 fix: fixed bu for 'slice bounds out of range' 2023-06-16 23:30:04 +08:00
RockYang 08529242bf style: add logo at login and register page 2023-06-16 18:14:43 +08:00
RockYang 1ae9449d92 chore: tracke the logo file 2023-06-16 17:44:38 +08:00
RockYang 7fd0b1fa08 feat: import iconfont styles, change password function is ready 2023-06-16 17:28:21 +08:00
RockYang 111572e3f2 fix: add lock map data structure, fixed bug for 'concurrent map writes' 2023-06-16 15:32:11 +08:00
RockYang c9875d24b4 feat: add footbar for login and register page, add send button for chat page 2023-06-16 13:57:05 +08:00
RockYang 94a75603c3 chore: compress bg images 2023-06-16 11:14:45 +08:00
RockYang d55925bccd chore: delete bin file 2023-06-16 10:01:34 +08:00
RockYang 0b5adcbfab fix: fixed bug for ssl websocket url 2023-06-16 10:00:05 +08:00
RockYang 8d84562f32 chore: update config sample file 2023-06-15 16:03:42 +08:00
RockYang 136d159833 opt: add email and mobile validation for register 2023-06-15 15:09:00 +08:00
RockYang e3026062d8 opt: automatic get the host for api and websocket from the 'location' var 2023-06-15 14:39:05 +08:00
RockYang 8a69c69b7f opt: update the main chat compnent after updating the user profile 2023-06-15 11:29:16 +08:00
RockYang 197714a57a refactor: add system config key 'user_init_calls' to init the new register user's api calls 2023-06-15 10:06:21 +08:00
RockYang 567cdc6f8d refactor: V3 版本重构已基本完成 2023-06-15 09:41:30 +08:00
RockYang be6e8c7713 Merge pull request #6 from EyreFree/master
feat: docker supported
2023-06-05 08:32:34 +08:00
EyreFree f667b35403 docker 2023-06-03 20:48:32 +08:00
RockYang 942e482ca4 chore: 打招呼的时候输出本项目地址 2023-05-09 16:11:50 +08:00
RockYang 10b381965e Update 1.bug.yml 2023-05-09 15:43:48 +08:00
RockYang 79c3a99a38 Update 1.bug.yml 2023-05-09 15:39:47 +08:00
RockYang 463d3c8e97 Update 1.bug.yml 2023-05-09 15:35:07 +08:00
RockYang da007729c6 Update 1.bug.yml 2023-05-09 15:32:57 +08:00
RockYang b7b42b5fd4 chore: 重命名 issue 模版 2023-05-09 15:29:57 +08:00
RockYang 735f9bd053 chore: 添加 issue 和 PR 模板 2023-05-09 15:26:38 +08:00
RockYang 6096b65374 chore: 替换文字描述 用户 => 2023-05-09 14:30:09 +08:00
RockYang 727d342eb3 docs: 优化文档 2023-05-06 18:33:27 +08:00
RockYang 58815ba8bc docs: 优化文档 2023-05-06 18:13:44 +08:00
RockYang 49d0502775 docs: 优化文档排版 2023-05-06 16:36:04 +08:00
RockYang 0b6c6e6d2c docs: 完善文档 2023-05-06 16:27:44 +08:00
RockYang 45ede6047a opt: 优化配置文档加载 2023-05-06 13:58:56 +08:00
RockYang 8f469d4ebb Merge branch 'master' of gitee.com:blackfox/wechat-gpt 2023-05-06 12:07:17 +08:00
RockYang f3264d056d add LICENSE.
Signed-off-by: RockYang <yangjian102621@gmail.com>
2023-05-06 04:03:33 +00:00
335 changed files with 37747 additions and 16954 deletions
+226
View File
@@ -0,0 +1,226 @@
# ui-ux-pro-max
Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices.
## Prerequisites
Check if Python is installed:
```bash
python3 --version || python --version
```
If Python is not installed, install it based on user's OS:
**macOS:**
```bash
brew install python3
```
**Ubuntu/Debian:**
```bash
sudo apt update && sudo apt install python3
```
**Windows:**
```powershell
winget install Python.Python.3.12
```
---
## How to Use This Workflow
When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow:
### Step 1: Analyze User Requirements
Extract key information from user request:
- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc.
- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc.
- **Industry**: healthcare, fintech, gaming, education, etc.
- **Stack**: React, Vue, Next.js, or default to `html-tailwind`
### Step 2: Search Relevant Domains
Use `search.py` multiple times to gather comprehensive information. Search until you have enough context.
```bash
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>]
```
**Recommended search order:**
1. **Product** - Get style recommendations for product type
2. **Style** - Get detailed style guide (colors, effects, frameworks)
3. **Typography** - Get font pairings with Google Fonts imports
4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border)
5. **Landing** - Get page structure (if landing page)
6. **Chart** - Get chart recommendations (if dashboard/analytics)
7. **UX** - Get best practices and anti-patterns
8. **Stack** - Get stack-specific guidelines (default: html-tailwind)
### Step 3: Stack Guidelines (Default: html-tailwind)
If user doesn't specify a stack, **default to `html-tailwind`**.
```bash
python3 .shared/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind
```
Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter`
---
## Search Reference
### Available Domains
| Domain | Use For | Example Keywords |
|--------|---------|------------------|
| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service |
| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism |
| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern |
| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service |
| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof |
| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie |
| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading |
| `prompt` | AI prompts, CSS keywords | (style name) |
### Available Stacks
| Stack | Focus |
|-------|-------|
| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) |
| `react` | State, hooks, performance, patterns |
| `nextjs` | SSR, routing, images, API routes |
| `vue` | Composition API, Pinia, Vue Router |
| `svelte` | Runes, stores, SvelteKit |
| `swiftui` | Views, State, Navigation, Animation |
| `react-native` | Components, Navigation, Lists |
| `flutter` | Widgets, State, Layout, Theming |
---
## Example Workflow
**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp"
**AI should:**
```bash
# 1. Search product type
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product
# 2. Search style (based on industry: beauty, elegant)
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style
# 3. Search typography
python3 .shared/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography
# 4. Search color palette
python3 .shared/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color
# 5. Search landing page structure
python3 .shared/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing
# 6. Search UX guidelines
python3 .shared/ui-ux-pro-max/scripts/search.py "animation" --domain ux
python3 .shared/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux
# 7. Search stack guidelines (default: html-tailwind)
python3 .shared/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind
```
**Then:** Synthesize all search results and implement the design.
---
## Tips for Better Results
1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app"
2. **Search multiple times** - Different keywords reveal different insights
3. **Combine domains** - Style + Typography + Color = Complete design system
4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues
5. **Use stack flag** - Get implementation-specific best practices
6. **Iterate** - If first search doesn't match, try different keywords
7. **Split Into Multiple Files** - For better maintainability:
- Separate components into individual files (e.g., `Header.tsx`, `Footer.tsx`)
- Extract reusable styles into dedicated files
- Keep each file focused and under 200-300 lines
---
## Common Rules for Professional UI
These are frequently overlooked issues that make UI look unprofessional:
### Icons & Visual Elements
| Rule | Do | Don't |
|------|----|----- |
| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons |
| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout |
| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths |
| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly |
### Interaction & Cursor
| Rule | Do | Don't |
|------|----|----- |
| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements |
| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive |
| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) |
### Light/Dark Mode Contrast
| Rule | Do | Don't |
|------|----|----- |
| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) |
| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text |
| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter |
| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) |
### Layout & Spacing
| Rule | Do | Don't |
|------|----|----- |
| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` |
| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements |
| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths |
---
## Pre-Delivery Checklist
Before delivering UI code, verify these items:
### Visual Quality
- [ ] No emojis used as icons (use SVG instead)
- [ ] All icons from consistent icon set (Heroicons/Lucide)
- [ ] Brand logos are correct (verified from Simple Icons)
- [ ] Hover states don't cause layout shift
### Interaction
- [ ] All clickable elements have `cursor-pointer`
- [ ] Hover states provide clear visual feedback
- [ ] Transitions are smooth (150-300ms)
- [ ] Focus states visible for keyboard navigation
### Light/Dark Mode
- [ ] Light mode text has sufficient contrast (4.5:1 minimum)
- [ ] Glass/transparent elements visible in light mode
- [ ] Borders visible in both modes
- [ ] Test both modes before delivery
### Layout
- [ ] Floating elements have proper spacing from edges
- [ ] No content hidden behind fixed navbars
- [ ] Responsive at 320px, 768px, 1024px, 1440px
- [ ] No horizontal scroll on mobile
### Accessibility
- [ ] All images have alt text
- [ ] Form inputs have labels
- [ ] Color is not the only indicator
- [ ] `prefers-reduced-motion` respected
+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
+4
View File
@@ -14,3 +14,7 @@ logs
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
miniprogram
.shared
.claude
docs/mydocs
+29
View File
@@ -0,0 +1,29 @@
# Repository Guidelines
## 项目结构与模块组织
- `api/`Go + Gin 后端,包含 `core/` 业务、`handler/` 控制器、`service/` 接口调用以及 `store/` 数据访问,`config.toml` 存放默认配置,`Makefile` 用于多架构交叉构建。
- `web/`Vue3 + Vite 前端,源码集中在 `src/``public/` 为静态资源,`dist/` 存放构建结果并可被 `api/static/``desktop/` 引用。
- `desktop/`Electron 客户端入口为 `index.js`,配合 `electron-builder` 可打包 AppImage/DMG/NSIS。
- `miniprogram/``docs/``database/``config/` 分别承载小程序壳、部署文档、SQL 脚本及全局 YAML 配置;`build/` 包含 Dockerfile、安装脚本。
## 构建、测试与开发命令
- `docker-compose up -d`:根目录拉起全部容器,需提前准备好 MySQL、Redis 与模型密钥。
- `cd api && go run main.go`:本地热调试;`make amd64` / `make arm64` 生成无 CGO 二进制至 `api/bin/` 便于镜像打包。
- `cd web && pnpm install && pnpm dev --host`Vite 开发模式;`pnpm build` 产出静态文件;`pnpm lint` 运行 ESLint 自动修复。
- `cd desktop && npm install && npm run start`:调试 Electron`npm run package` 通过 electron-builder 生成多平台安装包。
## 编码风格与命名规范
- Go 代码必须经过 `gofmt`/`goimports`,保持 tab 缩进与驼峰命名;HTTP 路由遵循 `/api/v1/resources` 模式,与 handler 函数命名 (`ResourceHandler`) 对应。
- Vue 组件文件使用 PascalCase(如 `ChatPanel.vue`),Pinia store 与工具采用 kebab-case 文件名(如 `chat-session.ts`);统一通过 ESLint、Tailwind 与 `postcss.config.js` 约束样式。
## 测试指南
- `cd api && go test ./... -race` 是最低要求,新增 service/handler 需补 `_test.go` 并用 mock 隔离第三方 API;涉及时序逻辑可新增 `Test*Integration` 验证。
- 前端暂未启用单测框架,至少运行 `pnpm lint` 并在 PR 中附关键页面截图或录屏证明交互可用;桌面端如修改构建脚本,需在 macOS/Linux/Windows 中至少验证一个安装包。
## 提交与 Pull Request 规范
- 参考历史记录(如“支持腾讯云短信服务”),提交信息使用中文动词开头、聚焦单一变更,并可加子系统前缀:`web: 优化聊天动画`
- PR 描述需包含变更背景、实现概述、验证方式(命令、截图或日志)与关联 issue/任务号;涉及配置或部署脚本,还要说明回滚流程并 @ 相关 reviewer。
## 安全与配置提示
- 禁止提交真实密钥,请复制 `config.sample.toml``config/config.yaml` 生成私有文件,并用 `git update-index --skip-worktree` 忽略。
- 对象存储、短信、支付等凭证统一放入 Vault 或 CI Secret,代码中仅引用占位常量;`docs/` 中同步记录新增敏感字段与启用步骤。
+57
View File
@@ -1,5 +1,62 @@
# 更新日志 # 更新日志
## v4.3.0
- 功能新增:**PPT 生成功能**,复刻 NotebookLLM 的演示文稿生成功能,支持编辑和导出 🔥🔥🔥
- 功能新增:AI 绘图与视频创作提示词支持 @ 引用图片 🔥🔥🔥
- 功能新增:接入豆包视频生成,优化 Sora 图生视频体验 🔥🔥🔥
- 功能重构:移除 Stable Diffusion(SD)生图模块及相关前后端代码;数据库表 `geekai_sd_jobs` 保留不删除;管理后台算力配置移除 SD、可灵、Luma 等相关算力项
- 功能重构:图片生成模块从 dalle 重命名为 image,支持查看任务详情 🔥🔥🔥
- 功能优化:任务详情弹窗优化并补充图片任务创建时间
- 功能优化:图片生成页尺寸支持自定义输入与预设选择;优化比例与尺寸参数展示
- 功能优化:优化上传组件,支持拖拽上传;更新 iconfont;图片接口改造以兼容 kapon 聚合服务
- 功能优化:MJ 支持局部重绘与算力配置优化;任务列表返回创建时间
- 功能重构:重构 Gem(智能体)模块,调整智能体标识与工作区存储
- 功能优化:优化 ChatPlus 欢迎页与会话输入体验,与 ImageMj 交互及 UI 一致性
- 功能优化:支持批量导入用户信息,优化管理后台 UI 样式
- 功能优化:重构管理后台 UI,采用现代科技风格
- 功能优化:优化左侧菜单 UI,增加选中菜单对比度
- Bug修复:完善数据迁移中各表自增主键及用户表主键迁移;迁移时清理历史 Dalle 与 SD 相关数据
## v4.2.9
- 功能优化:支持缩略图功能配置,支持本地存储,七牛云,腾讯云 OSS 和阿里云 OSS 四种存储介质。
- 功能优化:重构视频生成模块,支持 Sora2, Veo3.1, Luma, 可灵,通义万相,MiniMax 等视频生成模型。
- 功能新增:腾讯云短信服务 🔥🔥🔥
- 功能新增:支持腾讯云 OSS 存储,支持腾讯云 COS 文件上传和下载 🔥🔥🔥
- 功能新增:支持微信支付原生的 JSAPI 支付,支持微信公众号授权登录。🔥🔥🔥
- 功能优化:对话页面输入框支持粘贴剪切板内容上传截图 🔥🔥🔥
- 功能优化:给 Sora2 生成视频下载增加重试机制,防止因为网络不稳定导致下载失败 🎉🎉🎉
- Bug 修复:删除即梦 4.0 生图不支持的分片率参数,支持 1K, 2K, 4K 分辨率
- 功能优化:即梦 AI 新增 3.0 电商营销产品背景替换功能 🔥🔥🔥
- Bug 修复:调整 PC 端会员页面的列表样式,重构页面布局,移除多余的 css 样式,采用 tailwindcss 样式
- Bug 修复:修复管理后台重置管理密码不生效的问题
- Bug 修复:修复 Chat 页面输入框输入内容会自动滚动到最底部的问题
- Bug 修复:修复即梦视频生成任务 duration(视频时长) 参数不生效的问题 🎉🎉🎉
- Bug 修复:修复管理后台系统配置邮箱白名单更改不生效问题
- Bug 修复:修复聊天页面角色列表显示 system prompt 提示词的问题
## v4.2.8
- Bug 修复:修复管理后台邮件配置报“参数错误”问题
- 功能优化:手机端也不显示后端禁用的菜单功能页面
- 功能优化:**聊天页面支持 Sora2 文生视频/图生视频功能** 🔥🔥🔥
- 功能优化:**重构路由代码,优化路由加载逻辑,支持关闭手机站功能** 🔥🔥🔥
- Bug 修复:修复用户算力扣减日志记录逻辑错误问题
- 功能优化:优化 404 页面样式,兼容 PC 端和移动端
- Bug 修复:ipv6 地址登录报错:Data too long for column 'last_login_ip'
- Bug 修复:移动端登录页面输入密码的时候会覆盖在确认按钮上层
- 功能优化:**移动端的 DALL-E 绘图页面支持上传参考图** 🔥🔥🔥
- 功能优化:优化 WebFooter 组件,如果配置了备案号,则显示备案号,否则不显示
## v4.2.7
- Bug 修复:修复超级管理员无法修改密码的 Bug
- Bug 修复:微信登录配置更新后,没有同步更新到系统配置
- 功能优化: 给 AI 对话 API 加上线程锁,确保同一个用户同时只有一个对话请求
- 功能新增:支持即梦 AI 4.0 图片编辑,即梦 AI 数字人,动作迁移功能。🔥🔥🔥
- 功能新增:新增 AI 对话编辑功能,并优化了重新生成逻辑
## v4.2.6 ## v4.2.6
- 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉 - 功能重构:优化系统配置管理功能,把 OSS,支付,短信,邮件等配置全部迁移到管理后台,无需通过修改配置文档的方式修改 🎉🎉🎉
+1 -1
View File
@@ -56,7 +56,7 @@ Key entities: User, ChatItem, ChatMessage, ChatRole, ChatModel, Order, Product,
### API Structure ### API Structure
- User APIs: `/api/user/*` (auth, profile, settings) - User APIs: `/api/user/*` (auth, profile, settings)
- Chat APIs: `/api/chat/*` (conversations, messages) - Chat APIs: `/api/chat/*` (conversations, messages)
- AI Service APIs: `/api/mj/*`, `/api/sd/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*` - AI Service APIs: `/api/mj/*`, `/api/dall/*`, `/api/suno/*`, `/api/video/*`
- Admin APIs: `/api/admin/*` (management functions) - Admin APIs: `/api/admin/*` (management functions)
### Configuration ### Configuration
+8
View File
@@ -0,0 +1,8 @@
# 项目固定指令(Cursor 必须遵守)
1. 代码必须使用清晰命名,禁止单字母变量
2. 代码必须加注释
3. 统一使用 4 空格缩进
4. 前端样式代码永远优先适应 Tailwind CSS 的样式, 别自定义样式。除非 Tailwind CSS 不支持或者做不到。
5. 不要生成多余代码,保持简洁
6. 每次生成代码前遵守以上规则
+9 -9
View File
@@ -1,15 +1,15 @@
# 🚀 GeekAI-PLUS:一站式 AI 创意生产力平台 # 🚀 GeekAI:一站式 AI 创意生产力平台
**重新定义 AI 创作体验,让每个人都能成为内容创作大师** **让创意触手可及,让创作变得简单**
基于 GeekAI 项目开发的高级版,增加了很多高级功能,比如思维导图,Dalle 绘画等。**高级版源码不会一次性开放,只提供镜像给大家免费使用**,源码会逐步逐步按照版同步迁移到[社区版(GeekAI](https://github.com/yangjian102621/geekai)。所以如果大家想要二次开发,请移步去社区版 一个功能完整、开箱即用的 AI 多模态内容创作平台,集成了对话、绘画、音乐、视频、思维导图等全链路 AI 创作能力。无论是个人创作者还是企业团队,都能快速搭建属于自己的 AI 创作工作台
## ✨ 核心特色 ## ✨ 核心特色
### 🎨 **全能 AI 创作矩阵** ### 🎨 **全能 AI 创作矩阵**
- **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话 - **智能对话**:集成 ChatGPT、Claude 等多款顶级 AI 模型,支持角色扮演和专业对话
- **图像生成**:整合 MidJourney、DALL-E、Stable Diffusion 三大主流 AI 绘画引擎 - **图像生成**:整合 MidJourney、DALL-E、Nano-Banana,即梦,可灵等主流 AI 绘画引擎
- **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐 - **音频创作**:Suno AI 音乐生成,从旋律到歌词一键创作专属音乐
- **视频制作**Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限 - **视频制作**Luma 和 KeLing,即梦,Veo3 视频 AI,文本到视频,创意无限
- **思维导图**:AI 辅助思维整理,复杂想法可视化呈现 - **思维导图**:AI 辅助思维整理,复杂想法可视化呈现
@@ -34,8 +34,8 @@
- **响应式设计**:完美适配桌面、平板、手机等全终端设备 - **响应式设计**:完美适配桌面、平板、手机等全终端设备
- **暗黑模式**:支持明暗主题切换,护眼舒适 - **暗黑模式**:支持明暗主题切换,护眼舒适
- **实时交互**WebSocket 实时通信,创作过程流畅无卡顿 - **实时交互**流式响应,创作过程流畅无卡顿
- **文件管理**:支持多种云存储,作品安全可靠 - **文件管理**:支持七牛云,阿里云 OSS,腾讯云 OSS,Minio 等多种云存储,作品安全可靠
## 🎪 **应用场景** ## 🎪 **应用场景**
@@ -44,7 +44,7 @@
- **教育培训**:课件制作、知识图谱、互动内容 - **教育培训**:课件制作、知识图谱、互动内容
- **个人娱乐**:AI 聊天、创意绘画、音乐创作 - **个人娱乐**:AI 聊天、创意绘画、音乐创作
## 🔥 **为什么选择 GeekAI-PLUS** ## 🔥 **为什么选择 GeekAI**
1. **技术领先**:集成当前最先进的 AI 技术,始终保持创新前沿 1. **技术领先**:集成当前最先进的 AI 技术,始终保持创新前沿
2. **开箱即用**:完整的商业化解决方案,无需从零开发 2. **开箱即用**:完整的商业化解决方案,无需从零开发
@@ -54,11 +54,11 @@
## 演示站点 ## 演示站点
[Geek-AI 创作系统](https://www.geekai.me) [Geek-AI 创作系统](https://chat.geekai.me)
## 文档地址 ## 文档地址
[Geek-AI 文档](https://www.geekai.me/docs/) [Geek-AI 文档](https://docs.geekai.me)
## 部署 ## 部署
+1
View File
@@ -16,6 +16,7 @@ tmp
bin bin
data data
config.toml config.toml
config.toml.*.bak
static/upload static/upload
static/audio static/audio
storage.json storage.json
+1 -100
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:password@tcp(127.0.0.1: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"
@@ -14,102 +14,3 @@ TikaHost = "http://tika:9998"
Port = 6379 Port = 6379
Password = "" Password = ""
DB = 0 DB = 0
[ApiConfig] # 微博热搜,今日头条等函数服务 API 配置,此为第三方插件服务,如需使用请联系作者开通
ApiURL = "https://sapi.geekai.me"
AppId = ""
Token = ""
[SMS] # Sms 配置,用于发送短信
Active = "Ali" # 当前启用的短信服务,默认使用阿里云
[SMS.Bao]
Username = ""
Password = ""
Domain = "api.smsbao.com"
Sign = "【极客学长】"
CodeTemplate = "您的验证码是{code}。5分钟有效,若非本人操作,请忽略本短信。"
[SMS.Ali]
AccessKey = ""
AccessSecret = ""
Product = "Dysmsapi"
Domain = "dysmsapi.aliyuncs.com"
Sign = ""
CodeTempId = ""
[OSS] # OSS 配置,用于存储 MJ 绘画图片
Active = "local" # 默认使用本地文件存储引擎
[OSS.Local]
BasePath = "./static/upload" # 本地文件上传根路径
BaseURL = "http://localhost:5678/static/upload" # 本地上传文件前缀 URL,线上需要把 localhost 替换成自己的实际域名或者IP
[OSS.Minio]
Endpoint = "" # 如 172.22.11.200:9000
AccessKey = "" # 自己去 Minio 控制台去创建一个 Access Key
AccessSecret = ""
Bucket = "chatgpt-plus" # 替换为你自己创建的 Bucket,注意要给 Bucket 设置公开的读权限,否则会出现图片无法显示。
UseSSL = false
Domain = "" # 地址必须是能够通过公网访问的,否则会出现图片无法显示。
[OSS.QiNiu] # 七牛云 OSS 配置
Zone = "z2" # 区域,z0:华东,z1: 华北,na0:北美,as0:新加坡
AccessKey = ""
AccessSecret = ""
Bucket = ""
Domain = "" # OSS Bucket 所绑定的域名,如 https://img.r9it.com
[OSS.AliYun]
Endpoint = "oss-cn-hangzhou.aliyuncs.com"
AccessKey = ""
AccessSecret = ""
Bucket = "chatgpt-plus"
SubDir = ""
Domain = ""
[XXLConfig] # xxl-job 配置,需要你部署 XXL-JOB 定时任务工具,用来定期清理未支付订单和清理过期 VIP,如果你没有启用支付服务,则该服务也无需启动
Enabled = false # 是否启用 XXL JOB 服务
ServerAddr = "http://172.22.11.47:8080/xxl-job-admin" # xxl-job-admin 管理地址
ExecutorIp = "172.22.11.47" # 执行器 IP 地址
ExecutorPort = "9999" # 执行器服务端口
AccessToken = "xxl-job-api-token" # 执行器 API 通信 token
RegistryKey = "chatgpt-plus" # 任务注册 key
[SmtpConfig] # 注意,阿里云服务器禁用了25号端口,请使用 465 端口,并开启 TLS 连接
UseTls = false
Host = "smtp.163.com"
Port = 25
AppName = "极客学长"
From = "test@163.com" # 发件邮箱人地址
Password = "" #邮箱 stmp 服务授权码
# 支付宝商户支付
[AlipayConfig]
Enabled = false # 启用支付宝支付通道
SandBox = false # 是否启用沙盒模式
UserId = "2088721020750581" # 商户ID
AppId = "9021000131658023" # App Id
PrivateKey = "certs/alipay/privateKey.txt" # 应用私钥
PublicKey = "certs/alipay/appPublicCert.crt" # 应用公钥证书
AlipayPublicKey = "certs/alipay/alipayPublicCert.crt" # 支付宝公钥证书
RootCert = "certs/alipay/alipayRootCert.crt" # 支付宝根证书
# 虎皮椒支付
[HuPiPayConfig]
Enabled = false
AppId = ""
AppSecret = ""
ApiURL = "https://api.xunhupay.com"
# 微信商户支付
[WechatPayConfig]
Enabled = false
AppId = "" # 商户应用ID
MchId = "" # 商户号
SerialNo = "" # API 证书序列号
PrivateKey = "certs/alipay/privateKey.txt" # API 证书私钥文件路径,跟支付宝一样,把私钥文件拷贝到对应的路径,证书路径要映射到容器内
ApiV3Key = "" # APIV3 私钥,这个是你自己在微信支付平台设置的
# 易支付
[GeekPayConfig]
Enabled = true
AppId = "" # 商户ID
PrivateKey = "" # 商户私钥
ApiURL = "https://pay.geekai.cn"
Methods = ["alipay", "wxpay", "qqpay", "jdpay", "douyin", "paypal"] # 支持的支付方式
+1 -1
View File
@@ -44,11 +44,11 @@ func NewServer(appConfig *types.AppConfig, redis *redis.Client, sysConfig *types
} }
func (s *AppServer) Init(client *redis.Client) { func (s *AppServer) Init(client *redis.Client) {
s.Engine.Use(middleware.ThumbMiddleware())
s.Engine.Use(middleware.ParameterHandlerMiddleware()) s.Engine.Use(middleware.ParameterHandlerMiddleware())
s.Engine.Use(errorHandler) s.Engine.Use(errorHandler)
// 添加静态资源访问 // 添加静态资源访问
s.Engine.Static("/static", s.Config.StaticDir) s.Engine.Static("/static", s.Config.StaticDir)
s.Engine.Use(middleware.StaticMiddleware())
} }
func (s *AppServer) Run(db *gorm.DB) error { func (s *AppServer) Run(db *gorm.DB) error {
+12 -12
View File
@@ -10,7 +10,7 @@ package core
import ( import (
"bytes" "bytes"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"os" "os"
@@ -19,7 +19,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
func NewDefaultConfig() *types.AppConfig { func NewDefaultConfig() *types.AppConfig {
return &types.AppConfig{ return &types.AppConfig{
@@ -85,15 +85,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
logger.Error("load system config error: ", err) logger.Error("load system config error: ", err)
} }
// 加载许可证配置
var license types.License
sysConfig.Id = 0
db.Where("name", types.ConfigKeyLicense).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &license)
if err != nil {
logger.Error("load license config error: ", err)
}
// 加载验证码配置 // 加载验证码配置
var captchaConfig types.CaptchaConfig var captchaConfig types.CaptchaConfig
sysConfig.Id = 0 sysConfig.Id = 0
@@ -166,9 +157,17 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
logger.Error("load jimeng config error: ", err) logger.Error("load jimeng config error: ", err)
} }
// 加载微信公众号配置
var wxGzhConfig types.WxGzhConfig
sysConfig.Id = 0
db.Where("name", types.ConfigKeyWxGzh).First(&sysConfig)
err = utils.JsonDecode(sysConfig.Value, &wxGzhConfig)
if err != nil {
logger.Error("load wx gzh config error: ", err)
}
return &types.SystemConfig{ return &types.SystemConfig{
Base: baseConfig, Base: baseConfig,
License: license,
SMS: smsConfig, SMS: smsConfig,
OSS: ossConfig, OSS: ossConfig,
SMTP: smtpConfig, SMTP: smtpConfig,
@@ -177,5 +176,6 @@ func LoadSystemConfig(db *gorm.DB) *types.SystemConfig {
WxLogin: wxLoginConfig, WxLogin: wxLoginConfig,
Moderation: moderationConfig, Moderation: moderationConfig,
Jimeng: jimengConfig, Jimeng: jimengConfig,
WxGzh: wxGzhConfig,
} }
} }
+3
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"geekai/log"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
"time" "time"
@@ -13,6 +14,8 @@ import (
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
) )
var logger = log.GetLogger()
// 前端用户授权验证 // 前端用户授权验证
func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc { func UserAuthMiddleware(secretKey string, redis *redis.Client) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
@@ -14,8 +14,8 @@ import (
"golang.org/x/image/webp" "golang.org/x/image/webp"
) )
// 静态资源中间件 // 缩略图中间件
func StaticMiddleware() gin.HandlerFunc { func ThumbMiddleware() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
url := c.Request.URL.String() url := c.Request.URL.String()
+27 -36
View File
@@ -37,22 +37,6 @@ 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)
} }
@@ -73,8 +57,11 @@ type BaseConfig struct {
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力 DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值 InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力,未配置分项时回退用
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 MjUpscalePower int `json:"mj_upscale_power,omitempty"` // MJ 放大/变换消耗算力
MjBlendPower int `json:"mj_blend_power,omitempty"` // MJ 融图消耗算力
MjSwapFacePower int `json:"mj_swap_face_power,omitempty"` // MJ 换脸消耗算力
MjModalPower int `json:"mj_modal_power,omitempty"` // MJ 局部重绘消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力 LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力 KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力
@@ -85,10 +72,10 @@ type BaseConfig struct {
EnableContext bool `json:"enable_context,omitempty"` EnableContext bool `json:"enable_context,omitempty"`
ContextDeep int `json:"context_deep,omitempty"` ContextDeep int `json:"context_deep,omitempty"`
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
IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单 IndexNavs []int `json:"index_navs"` // 首页显示的导航菜单
IndexPage string `json:"index_page"` // 首页显示的页面
Copyright string `json:"copyright"` // 版权信息 Copyright string `json:"copyright"` // 版权信息
ICP string `json:"icp"` // ICP 备案号 ICP string `json:"icp"` // ICP 备案号
GaBeian string `json:"ga_beian"` // 公安备案号 GaBeian string `json:"ga_beian"` // 公安备案号
@@ -96,6 +83,8 @@ type BaseConfig struct {
EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表 EmailWhiteList []string `json:"email_white_list"` // 邮箱白名单列表
AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id AssistantModelId int `json:"assistant_model_id"` // 用来做提示词,翻译的AI模型 id
MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB MaxFileSize int `json:"max_file_size"` // 最大文件大小,单位:MB
EnableMobileSite bool `json:"enable_mobile_site"` // 是否开启手机站点
} }
type SystemConfig struct { type SystemConfig struct {
@@ -107,25 +96,27 @@ type SystemConfig struct {
Captcha CaptchaConfig Captcha CaptchaConfig
WxLogin WxLoginConfig WxLogin WxLoginConfig
Jimeng JimengConfig Jimeng JimengConfig
License License
Moderation ModerationConfig Moderation ModerationConfig
WxGzh WxGzhConfig
} }
// 配置键名常量 // 配置键名常量
const ( const (
ConfigKeySystem = "system" ConfigKeySystem = "system" // 系统配置
ConfigKeyNotice = "notice" ConfigKeyNotice = "notice" // 公告配置
ConfigKeyAgreement = "agreement" ConfigKeyAgreement = "agreement" // 用户协议配置
ConfigKeyPrivacy = "privacy" ConfigKeyPrivacy = "privacy" // 隐私政策配置
ConfigKeyMarkMap = "mark_map" ConfigKeyMarkMap = "mark_map" // 水印配置
ConfigKeyCaptcha = "captcha" ConfigKeyCaptcha = "captcha" // 验证码配置
ConfigKeyWxLogin = "wx_login" ConfigKeyWxLogin = "wx_login" // 微信扫码登录配置
ConfigKeyLicense = "license" ConfigKeyWxGzh = "wx_gzh" // 微信公众号配置
ConfigKeySms = "sms" ConfigKeySms = "sms" // 短信配置
ConfigKeySmtp = "smtp" ConfigKeySmtp = "smtp" // SMTP 配置
ConfigKeyOss = "oss" ConfigKeyOss = "oss" // OSS 配置
ConfigKeyPayment = "payment" ConfigKeyPayment = "payment" // 支付配置
ConfigKeyModeration = "moderation" ConfigKeyModeration = "moderation" // 文本审查配置
ConfigKeyAI3D = "ai3d" ConfigKeyAI3D = "ai3d" // AI3D 配置
ConfigKeyJimeng = "jimeng" ConfigKeyJimeng = "jimeng" // 即梦AI配置
ConfigKeyVideo = "video" // 视频生成配置
ConfigKeyPPT = "ppt" // PPT 生成配置
) )
+16 -7
View File
@@ -20,14 +20,23 @@ func init() {
// CaptchaConfig 行为验证码配置 // CaptchaConfig 行为验证码配置
type CaptchaConfig struct { type CaptchaConfig struct {
ApiKey string `json:"api_key"` ApiKey string `json:"api_key,omitempty"`
Type string `json:"type"` // 验证码类型, 可选值: "dot" 或 "slide" Type string `json:"type,omitempty"` // 验证码类型, 可选值: "dot" 或 "slide"
Enabled bool `json:"enabled"` Enabled bool `json:"enabled,omitempty"`
} }
// WxLoginConfig 微信登录配置 // WxLoginConfig 微信扫码登录配置
type WxLoginConfig struct { type WxLoginConfig struct {
ApiKey string `json:"api_key"` ApiKey string `json:"api_key,omitempty"`
NotifyURL string `json:"notify_url"` // 登录成功回调 URL NotifyURL string `json:"notify_url,omitempty"` // 登录成功回调 URL
Enabled bool `json:"enabled"` // 是否启用微信登录 Enabled bool `json:"enabled,omitempty"` // 是否启用微信登录
}
// 微信公众号配置
type WxGzhConfig struct {
AppId string `json:"app_id,omitempty"`
Secret string `json:"secret,omitempty"`
Token string `json:"token"`
EncodingAESKey string `json:"encoding_aes_key"`
Enabled bool `json:"enabled"`
} }
+59 -11
View File
@@ -2,17 +2,65 @@ package types
// JimengConfig 即梦AI配置 // JimengConfig 即梦AI配置
type JimengConfig struct { type JimengConfig struct {
AccessKey string `json:"access_key"` // 即梦AI的AccessKey和SecretKey
SecretKey string `json:"secret_key"` AccessKey string `json:"access_key,omitempty"`
Power JimengPower `json:"power"` SecretKey string `json:"secret_key,omitempty"`
// 火山引擎大模型专用的验证方式
ApiKey string `json:"api_key,omitempty"`
// 算力配置
Powers map[string]int `json:"powers,omitempty"`
} }
// JimengPower 即梦AI算力配置 // JMTaskStatus 任务状态
type JimengPower struct { type JMTaskStatus string
TextToImage int `json:"text_to_image"`
ImageToImage int `json:"image_to_image"` const (
ImageEdit int `json:"image_edit"` JMTaskStatusSubmited = JMTaskStatus("submited") // 任务已提交
ImageEffects int `json:"image_effects"` JMTaskStatusInQueue = JMTaskStatus("in_queue") // 任务排队中
TextToVideo int `json:"text_to_video"` JMTaskStatusGenerating = JMTaskStatus("generating") // 任务处理中
ImageToVideo int `json:"image_to_video"` 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
} }
+17 -18
View File
@@ -9,13 +9,13 @@ package types
// 文本审查 // 文本审查
type ModerationConfig struct { type ModerationConfig struct {
Enable bool `json:"enable"` // 是否启用文本审查 Enable bool `json:"enable,omitempty"` // 是否启用文本审查
Active string `json:"active"` Active string `json:"active,omitempty"`
EnableGuide bool `json:"enable_guide"` // 是否启用模型引导提示词 EnableGuide bool `json:"enable_guide,omitempty"` // 是否启用模型引导提示词
GuidePrompt string `json:"guide_prompt"` // 模型引导提示词 GuidePrompt string `json:"guide_prompt,omitempty"` // 模型引导提示词
Gitee ModerationGiteeConfig `json:"gitee"` Gitee ModerationGiteeConfig `json:"gitee,omitempty"`
Baidu ModerationBaiduConfig `json:"baidu"` Baidu ModerationBaiduConfig `json:"baidu,omitempty"`
Tencent ModerationTencentConfig `json:"tencent"` Tencent ModerationTencentConfig `json:"tencent,omitempty"`
} }
const ( const (
@@ -26,26 +26,26 @@ const (
// GiteeAI 文本审查配置 // GiteeAI 文本审查配置
type ModerationGiteeConfig struct { type ModerationGiteeConfig struct {
ApiKey string `json:"api_key"` ApiKey string `json:"api_key,omitempty"`
Model string `json:"model"` // 文本审核模型 Model string `json:"model,omitempty"` // 文本审核模型
} }
// 百度文本审查配置 // 百度文本审查配置
type ModerationBaiduConfig struct { type ModerationBaiduConfig struct {
AccessKey string `json:"access_key"` AccessKey string `json:"access_key,omitempty"`
SecretKey string `json:"secret_key"` SecretKey string `json:"secret_key,omitempty"`
} }
// 腾讯云文本审查配置 // 腾讯云文本审查配置
type ModerationTencentConfig struct { type ModerationTencentConfig struct {
AccessKey string `json:"access_key"` AccessKey string `json:"access_key,omitempty"`
SecretKey string `json:"secret_key"` SecretKey string `json:"secret_key,omitempty"`
} }
type ModerationResult struct { type ModerationResult struct {
Flagged bool `json:"flagged"` Flagged bool `json:"flagged,omitempty"`
Categories map[string]bool `json:"categories"` Categories map[string]bool `json:"categories,omitempty"`
CategoryScores map[string]float64 `json:"category_scores"` CategoryScores map[string]float64 `json:"category_scores,omitempty"`
} }
var ModerationCategories = map[string]string{ var ModerationCategories = map[string]string{
@@ -65,8 +65,7 @@ var ModerationCategories = map[string]string{
const ( const (
ModerationSourceChat = "chat" ModerationSourceChat = "chat"
ModerationSourceMJ = "mj" ModerationSourceMJ = "mj"
ModerationSourceDalle = "dalle" ModerationSourceImage = "image"
ModerationSourceSD = "sd"
ModerationSourceSuno = "suno" ModerationSourceSuno = "suno"
ModerationSourceVideo = "video" ModerationSourceVideo = "video"
ModerationSourceJiMeng = "jimeng" ModerationSourceJiMeng = "jimeng"
+37 -23
View File
@@ -8,39 +8,53 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type OSSConfig struct { type OSSConfig struct {
Active string `json:"active"` Active string `json:"active,omitempty"`
Local LocalStorageConfig `json:"local"` Local LocalStorageConfig `json:"local,omitempty"`
Minio MiniOssConfig `json:"minio"` Minio MiniOssConfig `json:"minio,omitempty"`
QiNiu QiNiuOssConfig `json:"qiniu"` QiNiu QiNiuOssConfig `json:"qiniu,omitempty"`
AliYun AliYunOssConfig `json:"aliyun"` AliYun AliYunOssConfig `json:"aliyun,omitempty"`
Tencent TencentOssConfig `json:"tencent,omitempty"`
} }
type MiniOssConfig struct { type MiniOssConfig struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"access_key"` AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret"` AccessSecret string `json:"access_secret,omitempty"`
Bucket string `json:"bucket"` Bucket string `json:"bucket,omitempty"`
UseSSL bool `json:"use_ssl"` UseSSL bool `json:"use_ssl,omitempty"`
Domain string `json:"domain"` Domain string `json:"domain,omitempty"`
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符
} }
type QiNiuOssConfig struct { type QiNiuOssConfig struct {
Zone string `json:"zone"` Zone string `json:"zone,omitempty"`
AccessKey string `json:"access_key"` AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret"` AccessSecret string `json:"access_secret,omitempty"`
Bucket string `json:"bucket"` Bucket string `json:"bucket,omitempty"`
Domain string `json:"domain"` Domain string `json:"domain,omitempty"`
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/4/w/{width}/h/{height}/q/75
} }
type AliYunOssConfig struct { type AliYunOssConfig struct {
Endpoint string `json:"endpoint"` Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"access_key"` AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret"` AccessSecret string `json:"access_secret,omitempty"`
Bucket string `json:"bucket"` Bucket string `json:"bucket,omitempty"`
Domain string `json:"domain"` Domain string `json:"domain,omitempty"`
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?x-oss-process=image/resize,m_lfit,w_{width},h_{height}
} }
type LocalStorageConfig struct { type LocalStorageConfig struct {
BasePath string `json:"base_path"` BasePath string `json:"base_path,omitempty"`
BaseURL string `json:"base_url"` BaseURL string `json:"base_url,omitempty"`
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/4/w/{width}/h/{height}/q/75
}
type TencentOssConfig struct {
Region string `json:"region,omitempty"`
SecretId string `json:"secret_id,omitempty"`
SecretKey string `json:"secret_key,omitempty"`
Bucket string `json:"bucket,omitempty"`
Domain string `json:"domain,omitempty"`
ThumbTemplate string `json:"thumb_template,omitempty"` // 缩略图模板,使用{width}和{height}作为变量占位符,默认:?imageView2/1/w/{width}/h/{height}/format/jpg
} }
+21 -21
View File
@@ -1,19 +1,19 @@
package types package types
type PaymentConfig struct { type PaymentConfig struct {
Alipay AlipayConfig `json:"alipay"` // 支付宝支付渠道配置 Alipay AlipayConfig `json:"alipay,omitempty"` // 支付宝支付渠道配置
Epay EpayConfig `json:"epay"` // 易支付配置 Epay EpayConfig `json:"epay,omitempty"` // 易支付配置
WxPay WxPayConfig `json:"wxpay"` // 微信支付渠道配置 WxPay WxPayConfig `json:"wxpay,omitempty"` // 微信支付渠道配置
} }
// AlipayConfig 支付宝支付配置 // AlipayConfig 支付宝支付配置
type AlipayConfig struct { type AlipayConfig struct {
Enabled bool `json:"enabled"` // 是否启用该支付通道 Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
SandBox bool `json:"sandbox"` // 是否沙盒环境 SandBox bool `json:"sandbox,omitempty"` // 是否沙盒环境
AppId string `json:"app_id"` // 应用 ID AppId string `json:"app_id,omitempty"` // 应用 ID
PrivateKey string `json:"private_key"` // 应用私钥 PrivateKey string `json:"private_key,omitempty"` // 应用私钥
AlipayPublicKey string `json:"alipay_public_key"` // 支付宝公钥 AlipayPublicKey string `json:"alipay_public_key,omitempty"` // 支付宝公钥
Domain string `json:"domain"` // 支付回调域名 Domain string `json:"domain,omitempty"` // 支付回调域名
} }
func (c *AlipayConfig) Equal(other *AlipayConfig) bool { func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
@@ -25,13 +25,13 @@ func (c *AlipayConfig) Equal(other *AlipayConfig) bool {
// WxPayConfig 微信支付配置 // WxPayConfig 微信支付配置
type WxPayConfig struct { type WxPayConfig struct {
Enabled bool `json:"enabled"` // 是否启用该支付通道 Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
AppId string `json:"app_id"` // 公众号的APPID,如:wxd678efh567hg6787 AppId string `json:"app_id,omitempty"` // 公众号的APPID,如:wxd678efh567hg6787
MchId string `json:"mch_id"` // 直连商户的商户号,由微信支付生成并下发 MchId string `json:"mch_id,omitempty"` // 直连商户的商户号,由微信支付生成并下发
SerialNo string `json:"serial_no"` // 商户证书的证书序列号 SerialNo string `json:"serial_no,omitempty"` // 商户证书的证书序列号
PrivateKey string `json:"private_key"` // 商户证书私钥 PrivateKey string `json:"private_key,omitempty"` // 商户证书私钥
ApiV3Key string `json:"api_v3_key"` // API V3 秘钥 ApiV3Key string `json:"api_v3_key,omitempty"` // API V3 秘钥
Domain string `json:"domain"` // 支付回调域名 Domain string `json:"domain,omitempty"` // 支付回调域名
} }
func (c *WxPayConfig) Equal(other *WxPayConfig) bool { func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
@@ -45,11 +45,11 @@ func (c *WxPayConfig) Equal(other *WxPayConfig) bool {
// EpayConfig 易支付配置 // EpayConfig 易支付配置
type EpayConfig struct { type EpayConfig struct {
Enabled bool `json:"enabled"` // 是否启用该支付通道 Enabled bool `json:"enabled,omitempty"` // 是否启用该支付通道
AppId string `json:"app_id"` // 商户 ID AppId string `json:"app_id,omitempty"` // 商户 ID
PrivateKey string `json:"private_key"` // 私钥 PrivateKey string `json:"private_key,omitempty"` // 私钥
ApiURL string `json:"api_url"` // z支付 API 网关 ApiURL string `json:"api_url,omitempty"` // z支付 API 网关
Domain string `json:"domain"` // 支付回调域名 Domain string `json:"domain,omitempty"` // 支付回调域名
} }
func (c *EpayConfig) Equal(other *EpayConfig) bool { func (c *EpayConfig) Equal(other *EpayConfig) bool {
+47
View File
@@ -0,0 +1,47 @@
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
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// PPTImageProvider PPT 图片生成提供方
type PPTImageProvider string
const (
PPTImageProviderNanoBanana PPTImageProvider = "nano_banana"
PPTImageProviderSeedream PPTImageProvider = "seedream"
)
// PPTConfig PPT 生成配置(存储在 config 表,name = 'ppt'
type PPTConfig struct {
// 分镜 LLM 配置
OutlineLLMApiURL string `json:"outline_llm_api_url"` // 分镜 LLM API 地址
OutlineLLMApiKey string `json:"outline_llm_api_key"` // 分镜 LLM API Key
OutlineLLMModel string `json:"outline_llm_model"` // 分镜 LLM 模型名称,如 gpt-4o-mini
// 图片生成通用配置
ActiveImageProvider PPTImageProvider `json:"active_image_provider"` // 当前启用的图片模型提供方
MaxSlidesPerTask int `json:"max_slides_per_task"` // 单个任务最多生成的 PPT 页数
PowerCostPerSlide int `json:"power_cost_per_slide"` // 每张 PPT 图片消耗的算力
MaxConcurrentRequests int `json:"max_concurrent_requests"` // 图片生成最大并发数
QPSLimit int `json:"qps_limit"` // 外部图片 API 的 QPS 限制
// Nano Banana 配置
NanoBananaApiURL string `json:"nano_banana_api_url"` // Nano Banana API 地址,如 https://xxx/v1/images/generations
NanoBananaApiKey string `json:"nano_banana_api_key"` // Nano Banana API Key
NanoBananaModel string `json:"nano_banana_model"` // 模型名称,如 nano-banana、nano-banana-hd
NanoBananaResponseFormat string `json:"nano_banana_response_format"` // 响应格式:url 或 b64_json
NanoBananaAspectRatio string `json:"nano_banana_aspect_ratio"` // 宽高比,如 1:1、4:3、16:9;空则默认 16:9
// Doubao Seedream 配置
SeedreamBaseURL string `json:"seedream_base_url"` // Seedream base url,例如:https://ark.cn-beijing.volces.com/api/v3
SeedreamApiKey string `json:"seedream_api_key"` // Seedream API KeyARK_API_KEY
SeedreamModel string `json:"seedream_model"` // Seedream 模型 ID,例如:doubao-seedream-5-0-260128
SeedreamSize string `json:"seedream_size"` // 图片尺寸 WxH(如 1920x1080)或 2K/4K 等;空则默认 1920x108016:9
SeedreamOutputFormat string `json:"seedream_output_format"` // 输出格式,例如:png
SeedreamResponseType string `json:"seedream_response_format"` // 响应格式,例如:url
SeedreamWatermark bool `json:"seedream_watermark"` // 是否开启水印
}
+22 -11
View File
@@ -8,23 +8,34 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type SMSConfig struct { type SMSConfig struct {
Active string `json:"active"` Active string `json:"active,omitempty"`
Ali SmsConfigAli `json:"aliyun"` Ali SmsConfigAli `json:"aliyun,omitempty"`
Bao SmsConfigBao `json:"bao"` Bao SmsConfigBao `json:"bao,omitempty"`
Tencent SmsConfigTencent `json:"tencent,omitempty"`
} }
// SmsConfigAli 阿里云短信平台配置 // SmsConfigAli 阿里云短信平台配置
type SmsConfigAli struct { type SmsConfigAli struct {
AccessKey string `json:"access_key"` AccessKey string `json:"access_key,omitempty"`
AccessSecret string `json:"access_secret"` AccessSecret string `json:"access_secret,omitempty"`
Sign string `json:"sign"` // 短信签名 Sign string `json:"sign,omitempty"` // 短信签名
CodeTempId string `json:"code_temp_id"` // 验证码短信模板 ID CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板 ID
} }
// SmsConfigBao 短信宝平台配置 // SmsConfigBao 短信宝平台配置
type SmsConfigBao struct { type SmsConfigBao struct {
Username string `json:"username"` //短信宝平台注册的用户名 Username string `json:"username,omitempty"` //短信宝平台注册的用户名
Password string `json:"password"` //短信宝平台注册的密码 Password string `json:"password,omitempty"` //短信宝平台注册的密码
Sign string `json:"sign"` // 短信签名 Sign string `json:"sign,omitempty"` // 短信签名
CodeTemplate string `json:"code_template"` // 验证码短信模板 匹配 CodeTemplate string `json:"code_template,omitempty"` // 验证码短信模板 匹配
}
// SmsConfigTencent 腾讯云短信平台配置
type SmsConfigTencent struct {
SecretId string `json:"secret_id,omitempty"` // 腾讯云 SecretId
SecretKey string `json:"secret_key,omitempty"` // 腾讯云 SecretKey
SmsSdkAppId string `json:"sms_sdk_app_id,omitempty"` // 短信应用ID
Sign string `json:"sign,omitempty"` // 短信签名
CodeTempId string `json:"code_temp_id,omitempty"` // 验证码短信模板ID
Region string `json:"region,omitempty"` // 地区,默认 ap-guangzhou
} }
+6 -6
View File
@@ -8,12 +8,12 @@ package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
type SmtpConfig struct { type SmtpConfig struct {
UseTls bool `json:"use_tls"` // 是否使用 TLS 发送 UseTls bool `json:"use_tls,omitempty"` // 是否使用 TLS 发送
Host string `json:"host"` // 邮件服务器地址 Host string `json:"host,omitempty"` // 邮件服务器地址
Port int `json:"port"` // 邮件服务器端口 Port int `json:"port,omitempty"` // 邮件服务器端口
AppName string `json:"app_name"` // 应用名称 AppName string `json:"app_name,omitempty"` // 应用名称
From string `json:"from"` // 发件人邮箱地址 From string `json:"from,omitempty"` // 发件人邮箱地址
Password string `json:"password"` // 发件人邮箱密码 Password string `json:"password,omitempty"` // 发件人邮箱密码
} }
func (s *SmtpConfig) Equal(other *SmtpConfig) bool { func (s *SmtpConfig) Equal(other *SmtpConfig) bool {
+22 -86
View File
@@ -20,6 +20,7 @@ const (
TaskSwapFace = TaskType("swapFace") TaskSwapFace = TaskType("swapFace")
TaskUpscale = TaskType("upscale") TaskUpscale = TaskType("upscale")
TaskVariation = TaskType("variation") TaskVariation = TaskType("variation")
TaskModal = TaskType("modal") // 局部重绘
) )
// MjTask MidJourney 任务 // MjTask MidJourney 任务
@@ -38,48 +39,21 @@ type MjTask struct {
ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道 ChannelId string `json:"channel_id"` // 渠道ID,用来区分是哪个渠道创建的任务,一个任务的 create 和 action 操作必须要再同一个渠道
Mode string `json:"mode"` // 绘画模式,relax, fast, turbo Mode string `json:"mode"` // 绘画模式,relax, fast, turbo
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
MaskBase64 string `json:"mask_base64,omitempty"` // 局部重绘蒙版 base64(仅 TaskModal 使用)
} }
type SdTask struct { // ImageTask Image generation task
Id int `json:"id"` // job 数据库ID type ImageTask struct {
Type TaskType `json:"type"`
UserId int `json:"user_id"`
Params SdTaskParams `json:"params"`
RetryCount int `json:"retry_count"`
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type SdTaskParams struct {
TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词
NegPrompt string `json:"neg_prompt"` // 反向提示词
Steps int `json:"steps"` // 迭代步数,默认20
Sampler string `json:"sampler"` // 采样器
Scheduler string `json:"scheduler"` // 采样调度
FaceFix bool `json:"face_fix"` // 面部修复
CfgScale float32 `json:"cfg_scale"` //引导系数,默认 7
Seed int64 `json:"seed"` // 随机数种子
Height int `json:"height"`
Width int `json:"width"`
HdFix bool `json:"hd_fix"` // 启用高清修复
HdRedrawRate float32 `json:"hd_redraw_rate"` // 高清修复重绘幅度
HdScale int `json:"hd_scale"` // 放大倍数
HdScaleAlg string `json:"hd_scale_alg"` // 放大算法
HdSteps int `json:"hd_steps"` // 高清修复迭代步数
}
// DallTask DALL-E task
type DallTask struct {
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"`
Image []string `json:"image,omitempty"` Image []string `json:"image,omitempty"`
Id uint `json:"id"` Id uint `json:"id"`
TaskId string `json:"task_id"` // Kapon 异步任务 ID,与 API 返回的 task_id 一致
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
N int `json:"n"` AspectRatio string `json:"aspect_ratio"`
Quality string `json:"quality"`
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
} }
@@ -103,60 +77,22 @@ type SunoTask struct {
} }
const ( const (
VideoLuma = "luma" VideoLuma = "luma"
VideoRunway = "runway" VideoSora = "sora"
VideoCog = "cog" VideoVeo = "veo"
VideoKeLing = "keling" VideoKeLing = "keling"
VideoMiniMax = "minimax"
VideoWan = "wan"
VideoDoubao = "doubao"
) )
type VideoTask struct { type VideoTask struct {
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"` // provider(不带版本号:veo, sora, luma
TaskId string `json:"task_id"` TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词 Prompt string `json:"prompt"` // 提示词
Params interface{} `json:"params"` Params any `json:"params"`
TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID TranslateModelId int `json:"translate_model_id"` // 提示词翻译模型ID
}
type LumaVideoParams struct {
PromptOptimize bool `json:"prompt_optimize"` // 是否优化提示词
Loop bool `json:"loop"` // 是否循环参考图
StartImgURL string `json:"start_img_url"` // 第一帧参考图地址
EndImgURL string `json:"end_img_url"` // 最后一帧参考图地址
Model string `json:"model"` // 使用哪个模型生成视频
Radio string `json:"radio"` // 视频尺寸
Style string `json:"style"` // 风格
Duration int `json:"duration"` // 视频时长(秒)
}
type KeLingVideoParams struct {
TaskType string `json:"task_type"` // 任务类型: text2video/image2video
Model string `json:"model"` // 模型: default/anime
Prompt string `json:"prompt"` // 视频描述
NegPrompt string `json:"negative_prompt"` // 负面提示词
CfgScale float64 `json:"cfg_scale"` // 相关性系数(0-1)
Mode string `json:"mode"` // 生成模式: std/pro
AspectRatio string `json:"aspect_ratio"` // 画面比例: 16:9/9:16/1:1
Duration string `json:"duration"` // 视频时长: 5/10
CameraControl CameraControl `json:"camera_control"` // 摄像机控制
Image string `json:"image"` // 参考图片URL(image2video)
ImageTail string `json:"image_tail"` // 尾帧图片URL(image2video)
}
// CameraControl 摄像机控制
type CameraControl struct {
Type string `json:"type"` // 控制类型: simple/down_back/forward_up/right_turn_forward/left_turn_forward
Config CameraConfig `json:"config"` // 控制参数(仅simple类型时使用)
}
// CameraConfig 摄像机参数
type CameraConfig struct {
Horizontal int `json:"horizontal"` // 水平移动(-10到10)
Vertical int `json:"vertical"` // 垂直移动(-10到10)
Pan int `json:"pan"` // 左右旋转(-10到10)
Tilt int `json:"tilt"` // 上下旋转(-10到10)
Roll int `json:"roll"` // 横向翻转(-10到10)
Zoom int `json:"zoom"` // 镜头缩放(-10到10)
} }
+45
View File
@@ -0,0 +1,45 @@
package types
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import "sync"
// UserLockManager 提供基于用户ID的TryLock功能,确保同一用户并发请求串行化
type UserLockManager struct {
mu sync.Mutex
locks map[uint]bool
}
func NewUserLockManager() *UserLockManager {
return &UserLockManager{mu: sync.Mutex{}, locks: make(map[uint]bool)}
}
// TryLock 尝试为指定用户加锁。若已被占用返回 false
func (m *UserLockManager) TryLock(userId uint) bool {
if userId == 0 {
return true
}
m.mu.Lock()
defer m.mu.Unlock()
if m.locks[userId] {
return false
}
m.locks[userId] = true
return true
}
// Unlock 释放指定用户的锁
func (m *UserLockManager) Unlock(userId uint) {
if userId == 0 {
return
}
m.mu.Lock()
delete(m.locks, userId)
m.mu.Unlock()
}
+37
View File
@@ -0,0 +1,37 @@
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
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// VideoConfig 视频生成配置(存储在 config 表,name = 'video'
type VideoConfig struct {
ApiURL string `json:"api_url"` // API 地址
ApiKey string `json:"api_key"` // API 密钥
VideoPowers map[string]VideoModelPower `json:"video_powers"` // 模型算力配置
}
// VideoModelPower 单个模型的算力配置
// PowerConfig 说明:
// 新的价格配置方式:根据模型的 priceParams 生成笛卡尔乘积,每个组合对应一个价格
// 固定价格示例:{"fixed": 20}
// 多参数组合示例:{"5_720P": 10, "5_1080P": 20, "10_720P": 10, "10_1080P": 40}
// 复杂组合示例:{"std_5_sound": 10, "std_5_silent": 5, "pro_10_sound": 20, "pro_10_silent": 10}
type VideoModelPower struct {
Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora, luma
Model string `json:"model"` // 模型名称(带版本号:veo-2.0, sora-2.0
PowerConfig map[string]int `json:"power_config"` // 算力配置(基于 priceParams 的笛卡尔乘积)
ApiKeyType string `json:"api_key_type"` // ApiKey 表的 type 字段(可选,用于多 API Key 场景)
}
// 视频任务状态常量
const (
VideoStatusPending = "pending" // 等待处理
VideoStatusInProgress = "in_progress" // 处理中
VideoStatusDownloading = "downloading" // 视频下载中
VideoStatusSuccess = "success" // 成功
VideoStatusFailed = "failed" // 失败
)
+2 -3
View File
@@ -36,9 +36,8 @@ const (
ChPing = WsChannel("ping") ChPing = WsChannel("ping")
ChChat = WsChannel("chat") ChChat = WsChannel("chat")
ChMj = WsChannel("mj") ChMj = WsChannel("mj")
ChSd = WsChannel("sd") ChImage = WsChannel("image")
ChDall = WsChannel("dall")
ChSuno = WsChannel("suno") ChSuno = WsChannel("suno")
ChLuma = WsChannel("luma") ChLuma = WsChannel("luma")
ChKeLing = WsChannel("keling") ChKeLing = WsChannel("keling")
+28 -12
View File
@@ -1,8 +1,6 @@
module geekai module geekai
go 1.21 go 1.24.0
toolchain go1.22.4
require ( require (
github.com/BurntSushi/toml v1.1.0 github.com/BurntSushi/toml v1.1.0
@@ -18,6 +16,7 @@ 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/tencentyun/cos-go-sdk-v5 v0.7.47
github.com/volcengine/volc-sdk-golang v1.0.23 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
@@ -28,16 +27,24 @@ 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/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/jung-kurt/gofpdf/v2 v2.17.3
github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6
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/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
golang.org/x/image v0.15.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49
github.com/volcengine/volcengine-go-sdk v1.1.34
github.com/xuri/excelize/v2 v2.10.0
golang.org/x/image v0.25.0
) )
require ( require (
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/clbanning/mxj v1.8.4 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/go-pay/crypto v0.0.1 // indirect github.com/go-pay/crypto v0.0.1 // indirect
github.com/go-pay/errgroup v0.0.2 // indirect github.com/go-pay/errgroup v0.0.2 // indirect
@@ -45,11 +52,20 @@ require (
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.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.0.0 // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mozillazg/go-httpheader v0.2.1 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.4 // indirect
github.com/tiendc/go-deepcopy v1.7.1 // 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/xuri/efp v0.0.1 // indirect
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // 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 (
@@ -89,12 +105,12 @@ require (
go.uber.org/dig v1.16.1 // indirect go.uber.org/dig v1.16.1 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.25.0 // indirect golang.org/x/net v0.46.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.17.0
golang.org/x/text v0.15.0 // indirect golang.org/x/text v0.30.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0
golang.org/x/tools v0.21.0 // indirect golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -114,7 +130,7 @@ require (
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/fx v1.19.3 go.uber.org/fx v1.19.3
go.uber.org/multierr v1.7.0 // indirect go.uber.org/multierr v1.7.0 // indirect
golang.org/x/crypto v0.23.0 golang.org/x/crypto v0.43.0
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.37.0 // indirect
gorm.io/gorm v1.25.1 gorm.io/gorm v1.25.1
) )
+62 -17
View File
@@ -2,6 +2,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/QcloudApi/qcloud_sign_golang v0.0.0-20141224014652-e4130a326409/go.mod h1:1pk82RBxDY/JZnPQrtqHlUFfCctgdorsd9M06fMynOM=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405 h1:cKNFQmeCQFN0WNfjScKoVrGi7vXxTVbkCvCqSrOf+P4=
github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs= github.com/aliyun/alibaba-cloud-sdk-go v1.62.405/go.mod h1:Api2AkmMgGaSUAhmk76oaFObkoeCPc/bKAqcyplPODs=
github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k= github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible h1:Sg/2xHwDrioHpxTN6WMiwbXTpUEinBpHsN7mG21Rc2k=
@@ -11,6 +12,8 @@ github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= 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/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
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=
@@ -22,11 +25,14 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
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/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 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=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0= github.com/dlclark/regexp2 v1.8.1 h1:6Lcdwya6GjPUNsBct8Lg/yRPwMhABj269AAzdGSiR+0=
@@ -100,6 +106,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 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/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
@@ -110,13 +117,17 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/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.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.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-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA= github.com/google/go-tika v0.3.1 h1:l+jr10hDhZjcgxFRfcQChRLo1bPXQeLFluMyvDhXTTA=
github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c= github.com/google/go-tika v0.3.1/go.mod h1:DJh5N8qxXIl85QkqmXknd+PeeRkUOTbvwyYf7ieDz6c=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
@@ -144,6 +155,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jung-kurt/gofpdf/v2 v2.17.3 h1:otZXZby2gXJ7uU6pzprXHq/R57lsHLi0WtH79VabWxY=
github.com/jung-kurt/gofpdf/v2 v2.17.3/go.mod h1:Qx8ZNg4cNsO5i6uLDiBngnm+ii/FjtAqjRNO6drsoYU=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@@ -159,6 +172,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6 h1:pItMMIM7cHfp43QyuZFeYJyKHPk4yFs8JQUIdjlcWYo=
github.com/ktye/pptx v0.0.0-20250326170941-2a6bc4329df6/go.mod h1:X+eOu1OuD+D/IKBa+1HnJBcSaKBB75n5TmKkFVFDPU8=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
@@ -174,12 +189,16 @@ github.com/minio/minio-go/v7 v7.0.62 h1:qNYsFZHEzl+NfH8UxW4jpmlKav1qUAgfY30YNRne
github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= github.com/minio/minio-go/v7 v7.0.62/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mozillazg/go-httpheader v0.2.1 h1:geV7TrjbL8KXSyvghnFm+NyTux/hxwueTSrwhe88TQQ=
github.com/mozillazg/go-httpheader v0.2.1/go.mod h1:jJ8xECTlalr6ValeXYdOF8fFUISeBAdw6E61aqQma60=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@@ -215,6 +234,11 @@ github.com/quic-go/quic-go v0.45.0 h1:OHmkQGM37luZITyTSu6ff03HP/2IrwDX1ZFiNEhSFU
github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= github.com/quic-go/quic-go v0.45.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8= github.com/refraction-networking/utls v1.3.2 h1:o+AkWB57mkcoW36ET7uJ002CpBWHu0KPxi6vzxvPnv8=
github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E= github.com/refraction-networking/utls v1.3.2/go.mod h1:fmoaOww2bxzzEpIKOebIsnBvjQpqP7L2vcm/9KUfm/E=
github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.4 h1:WuESlvhX3gH2IHcd8UqyCuFY5yiq/GR/yqaSM/9/g00=
github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
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=
@@ -241,10 +265,21 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.563/go.mod h1:7sCQWVkxcsR38nffDW057DRGk8mUjK1Ing/EFOK8s8Y=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49 h1:BQwUw2V21zIRJxstnaxtG/22lBL3+FbUgWhaC6Qd9ws=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.1.49/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/kms v1.0.563/go.mod h1:uom4Nvi9W+Qkom0exYiJ9VWJjXwyxtPYTkKkaLMlfE0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49 h1:8mlcG8TmoeEIDQGLYvqc9fdBQrNwKEb56I2HVNy9jdw=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.1.49/go.mod h1:YAdTku4GgK5H2LKea4bPgWf3qFLBibIBzgU1LW5WgEc=
github.com/tencentyun/cos-go-sdk-v5 v0.7.47 h1:uoS4Sob16qEYoapkqJq1D1Vnsy9ira9BfNUMtoFYTI4=
github.com/tencentyun/cos-go-sdk-v5 v0.7.47/go.mod h1:DH9US8nB+AJXqwu/AMOrCFN1COv3dpytXuJWHgdg7kE=
github.com/tiendc/go-deepcopy v1.7.1 h1:LnubftI6nYaaMOcaz0LphzwraqN8jiWTwm416sitff4=
github.com/tiendc/go-deepcopy v1.7.1/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
@@ -259,6 +294,14 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
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/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8= github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= github.com/volcengine/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/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8=
github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.10.0 h1:8aKsP7JD39iKLc6dH5Tw3dgV3sPRh8uRVXu/fMstfW4=
github.com/xuri/excelize/v2 v2.10.0/go.mod h1:SC5TzhQkaOsTWpANfm+7bJCldzcnU/jrhqkTi/iBHBU=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE=
github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
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=
@@ -286,20 +329,20 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 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.25.0 h1:Y6uW6rH1y5y/LK1J8BPWZtr6yZ7hrsy6hFrXjgsc2fQ=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/image v0.25.0/go.mod h1:tCAmOEGthTtkalusGp1g3xa2gke8J6c2N565dTyl9Rs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 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.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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-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=
@@ -313,16 +356,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 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-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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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=
@@ -339,8 +382,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -357,8 +400,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
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=
@@ -369,8 +412,8 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn
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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
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= 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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
@@ -390,6 +433,8 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.23.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.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.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=
+23 -17
View File
@@ -14,7 +14,7 @@ import (
"geekai/core/middleware" "geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
logger2 "geekai/logger" "geekai/log"
"geekai/service" "geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
@@ -29,9 +29,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
const SuperManagerID = 1 const SuperUsername = "admin"
type ManagerHandler struct { type ManagerHandler struct {
handler.BaseHandler handler.BaseHandler
@@ -94,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
} }
@@ -125,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,
} }
@@ -227,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
@@ -263,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
} }
@@ -278,16 +291,9 @@ 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.Model(&model.AdminUser{}).Where("id", data.Id).UpdateColumn("password", password)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, res.Error.Error()) resp.ERROR(c, res.Error.Error())
return return
+7 -8
View File
@@ -8,7 +8,6 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware" "geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
@@ -59,15 +58,14 @@ func (h *ChatAppHandler) Save(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
// 管理后台创建/编辑的 Gem 均为系统内置
role.UserId = 0
if data.SystemPrompt != "" {
role.SystemPrompt = data.SystemPrompt
}
role.Id = data.Id role.Id = data.Id
if data.CreatedAt > 0 { if data.CreatedAt > 0 {
role.CreatedAt = time.Unix(data.CreatedAt, 0) role.CreatedAt = time.Unix(data.CreatedAt, 0)
} else {
err = h.DB.Where("marker", data.Key).First(&role).Error
if err == nil {
resp.ERROR(c, fmt.Sprintf("角色 %s 已存在", data.Key))
return
}
} }
err = h.DB.Save(&role).Error err = h.DB.Save(&role).Error
if err != nil { if err != nil {
@@ -83,7 +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.ChatApp var items []model.ChatApp
var roles = make([]vo.ChatApp, 0) var roles = make([]vo.ChatApp, 0)
res := h.DB.Order("sort_num ASC").Find(&items) // 仅展示系统内置智能体(user_id = 0),不展示用户自行创建的智能体
res := h.DB.Where("user_id = 0").Order("sort_num ASC").Find(&items)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "No data found") resp.ERROR(c, "No data found")
return return
+37 -58
View File
@@ -8,6 +8,7 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"errors"
"geekai/core" "geekai/core"
"geekai/core/middleware" "geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
@@ -26,7 +27,6 @@ import (
type ConfigHandler struct { type ConfigHandler struct {
handler.BaseHandler handler.BaseHandler
licenseService *service.LicenseService
sysConfig *types.SystemConfig sysConfig *types.SystemConfig
alipayService *payment.AlipayService alipayService *payment.AlipayService
wxpayService *payment.WxPayService wxpayService *payment.WxPayService
@@ -36,12 +36,12 @@ type ConfigHandler struct {
smtpService *service.SmtpService smtpService *service.SmtpService
captchaService *service.CaptchaService captchaService *service.CaptchaService
wxLoginService *service.WxLoginService wxLoginService *service.WxLoginService
wxGzhService *service.WxGzhService
} }
func NewConfigHandler( func NewConfigHandler(
app *core.AppServer, app *core.AppServer,
db *gorm.DB, db *gorm.DB,
licenseService *service.LicenseService,
sysConfig *types.SystemConfig, sysConfig *types.SystemConfig,
alipayService *payment.AlipayService, alipayService *payment.AlipayService,
wxpayService *payment.WxPayService, wxpayService *payment.WxPayService,
@@ -51,10 +51,10 @@ func NewConfigHandler(
smtpService *service.SmtpService, smtpService *service.SmtpService,
captchaService *service.CaptchaService, captchaService *service.CaptchaService,
wxLoginService *service.WxLoginService, wxLoginService *service.WxLoginService,
wxChatService *service.WxGzhService,
) *ConfigHandler { ) *ConfigHandler {
return &ConfigHandler{ return &ConfigHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db}, BaseHandler: handler.BaseHandler{App: app, DB: db},
licenseService: licenseService,
sysConfig: sysConfig, sysConfig: sysConfig,
alipayService: alipayService, alipayService: alipayService,
wxpayService: wxpayService, wxpayService: wxpayService,
@@ -64,6 +64,7 @@ func NewConfigHandler(
smtpService: smtpService, smtpService: smtpService,
captchaService: captchaService, captchaService: captchaService,
wxLoginService: wxLoginService, wxLoginService: wxLoginService,
wxGzhService: wxChatService,
} }
} }
@@ -87,8 +88,7 @@ func (h *ConfigHandler) RegisterRoutes() {
rg.POST("update/oss", h.UpdateOss) rg.POST("update/oss", h.UpdateOss)
rg.POST("update/smtp", h.UpdateStmp) rg.POST("update/smtp", h.UpdateStmp)
rg.GET("get", h.Get) rg.GET("get", h.Get)
rg.POST("license/active", h.Active) rg.POST("update/wx_gzh", h.UpdateWxGzh)
rg.GET("license/get", h.GetLicense)
} }
} }
@@ -101,19 +101,6 @@ func (h *ConfigHandler) UpdateBase(c *gin.Context) {
return return
} }
// 未授权的话不允许修改版权
license := h.licenseService.GetLicense()
if !license.IsActive && data.Copyright != h.sysConfig.Base.Copyright {
resp.ERROR(c, "未授权系统不允许修改版权信息")
return
}
// 未授权的话不允许修改 Logo
if !license.IsActive && data.Logo != h.sysConfig.Base.Logo {
resp.ERROR(c, "未授权系统不允许修改 Logo")
return
}
err := h.Update(types.ConfigKeySystem, data) err := h.Update(types.ConfigKeySystem, data)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
@@ -128,15 +115,18 @@ func (h *ConfigHandler) UpdateBase(c *gin.Context) {
// UpdatePower 更新系统配置 // UpdatePower 更新系统配置
func (h *ConfigHandler) UpdatePower(c *gin.Context) { func (h *ConfigHandler) UpdatePower(c *gin.Context) {
var data struct { var data struct {
InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值 InitPower int `json:"init_power,omitempty"` // 新用户注册赠送算力值
DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力 DailyPower int `json:"daily_power,omitempty"` // 每日签到赠送算力
InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值 InvitePower int `json:"invite_power,omitempty"` // 邀请新用户赠送算力值
MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力
MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 MjUpscalePower int `json:"mj_upscale_power,omitempty"` // MJ 放大/变换消耗算力
SunoPower int `json:"suno_power,omitempty"` // Suno 生成歌曲消耗算力 MjBlendPower int `json:"mj_blend_power,omitempty"` // MJ 融图消耗算力
LumaPower int `json:"luma_power,omitempty"` // Luma 生成视频消耗算力 MjSwapFacePower int `json:"mj_swap_face_power,omitempty"` // MJ 换脸消耗算力
KeLingPowers map[string]int `json:"keling_powers,omitempty"` // 可灵生成视频消耗算力 MjModalPower int `json:"mj_modal_power,omitempty"` // MJ 局部重绘消耗算力
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 { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -148,7 +138,10 @@ func (h *ConfigHandler) UpdatePower(c *gin.Context) {
h.sysConfig.Base.InvitePower = data.InvitePower h.sysConfig.Base.InvitePower = data.InvitePower
h.sysConfig.Base.MjPower = data.MjPower h.sysConfig.Base.MjPower = data.MjPower
h.sysConfig.Base.MjActionPower = data.MjActionPower h.sysConfig.Base.MjActionPower = data.MjActionPower
h.sysConfig.Base.SdPower = data.SdPower h.sysConfig.Base.MjUpscalePower = data.MjUpscalePower
h.sysConfig.Base.MjBlendPower = data.MjBlendPower
h.sysConfig.Base.MjSwapFacePower = data.MjSwapFacePower
h.sysConfig.Base.MjModalPower = data.MjModalPower
h.sysConfig.Base.SunoPower = data.SunoPower h.sysConfig.Base.SunoPower = data.SunoPower
h.sysConfig.Base.LumaPower = data.LumaPower h.sysConfig.Base.LumaPower = data.LumaPower
h.sysConfig.Base.KeLingPowers = data.KeLingPowers h.sysConfig.Base.KeLingPowers = data.KeLingPowers
@@ -368,10 +361,7 @@ func (h *ConfigHandler) UpdateWxLogin(c *gin.Context) {
return return
} }
if data.Enabled { h.wxLoginService.UpdateConfig(data)
h.wxLoginService.UpdateConfig(data)
}
h.sysConfig.WxLogin = data h.sysConfig.WxLogin = data
resp.SUCCESS(c, data) resp.SUCCESS(c, data)
} }
@@ -395,14 +385,19 @@ func (h *ConfigHandler) Update(name string, value any) error {
func (h *ConfigHandler) Get(c *gin.Context) { func (h *ConfigHandler) Get(c *gin.Context) {
name := c.Query("key") name := c.Query("key")
var config model.Config var config model.Config
res := h.DB.Where("name", name).First(&config) err := h.DB.Where("name", name).First(&config).Error
if res.Error != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
resp.ERROR(c, res.Error.Error()) resp.SUCCESS(c, nil)
return
}
if err != nil {
resp.ERROR(c, err.Error())
return return
} }
var value map[string]any var value map[string]any
err := utils.JsonDecode(config.Value, &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
@@ -411,35 +406,19 @@ func (h *ConfigHandler) Get(c *gin.Context) {
resp.SUCCESS(c, value) resp.SUCCESS(c, value)
} }
// Active 激活系统 func (h *ConfigHandler) UpdateWxGzh(c *gin.Context) {
func (h *ConfigHandler) Active(c *gin.Context) { var data types.WxGzhConfig
var data struct {
License string `json:"license"`
}
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
} }
err := h.Update(types.ConfigKeyWxGzh, data)
err := h.licenseService.ActiveLicense(data.License)
license := h.licenseService.GetLicense()
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
if err := h.Update(types.ConfigKeyLicense, license); err != nil {
resp.ERROR(c, err.Error())
return
}
// 更新系统配置
h.sysConfig.License = *license
resp.SUCCESS(c, license.MachineId)
} h.wxGzhService.UpdateConfig(data)
h.sysConfig.WxGzh = data
// GetLicense 获取 License 信息 resp.SUCCESS(c, data)
func (h *ConfigHandler) GetLicense(c *gin.Context) {
license := h.licenseService.GetLicense()
resp.SUCCESS(c, license)
} }
+6 -8
View File
@@ -123,22 +123,20 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders) h.DB.Model(&model.Order{}).Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", zeroTime).Count(&stats.TodayOrders)
// 图片生成任务统计 // 图片生成任务统计
var mjJobs, sdJobs, dallJobs, jimengImageJobs int64 var mjJobs, imageJobs, jimengImageJobs int64
h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs) h.DB.Model(&model.MidJourneyJob{}).Count(&mjJobs)
h.DB.Model(&model.SdJob{}).Count(&sdJobs) h.DB.Model(&model.ImageJob{}).Count(&imageJobs)
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) 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 stats.ImageJobs = mjJobs + imageJobs + jimengImageJobs
logger.Info("stats.ImageJobs", stats.ImageJobs) logger.Info("stats.ImageJobs", stats.ImageJobs)
// 今日图片生成任务统计 // 今日图片生成任务统计
var todayMjJobs, todaySdJobs, todayDallJobs, todayJimengImageJobs int64 var todayMjJobs, todayImageJobs, todayJimengImageJobs int64
h.DB.Model(&model.MidJourneyJob{}).Where("created_at > ?", zeroTime).Count(&todayMjJobs) 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.ImageJob{}).Where("created_at > ?", zeroTime).Count(&todayImageJobs)
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) 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 stats.TodayImageJobs = todayMjJobs + todayImageJobs + todayJimengImageJobs
// 视频生成任务统计 // 视频生成任务统计
var videoJobs, jimengVideoJobs int64 var videoJobs, jimengVideoJobs int64
+10 -70
View File
@@ -42,8 +42,7 @@ func (h *ImageHandler) RegisterRoutes() {
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis)) group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{ {
group.POST("list/mj", h.MjList) group.POST("list/mj", h.MjList)
group.POST("list/sd", h.SdList) group.POST("list/image", h.ImageList)
group.POST("list/dall", h.DallList)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
} }
} }
@@ -100,8 +99,8 @@ func (h *ImageHandler) MjList(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))
} }
// SdList Stable Diffusion 任务列表 // ImageList Image generation 任务列表
func (h *ImageHandler) SdList(c *gin.Context) { func (h *ImageHandler) ImageList(c *gin.Context) {
var data imageQuery var data imageQuery
if err := c.ShouldBindJSON(&data); err != nil { if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
@@ -123,59 +122,15 @@ func (h *ImageHandler) SdList(c *gin.Context) {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1]) session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
} }
var total int64 var total int64
session.Model(&model.SdJob{}).Count(&total) session.Model(&model.ImageJob{}).Count(&total)
var list []model.SdJob var list []model.ImageJob
var items = make([]vo.SdJob, 0) var items = make([]vo.ImageJob, 0)
offset := (data.Page - 1) * data.PageSize offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil { if err == nil {
// 填充数据 // 填充数据
for _, item := range list { for _, item := range list {
var job vo.SdJob var job vo.ImageJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// DallList DALL-E 任务列表
func (h *ImageHandler) DallList(c *gin.Context) {
var data imageQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.DallJob{}).Count(&total)
var list []model.DallJob
var items = make([]vo.DallJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.DallJob
err = utils.CopyObject(item, &job) err = utils.CopyObject(item, &job)
if err != nil { if err != nil {
continue continue
@@ -209,8 +164,8 @@ 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
case "sd": case "image":
var job model.SdJob var job model.ImageJob
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, "记录不存在")
return return
@@ -218,22 +173,7 @@ func (h *ImageHandler) Remove(c *gin.Context) {
// 删除任务 // 删除任务
tx.Delete(&job) tx.Delete(&job)
md = "stable-diffusion" md = "image-generation"
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
imgURL = job.ImgURL
case "dall":
var job model.DallJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = "dall-e-3"
power = job.Power power = job.Power
userId = int(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)
+13 -27
View File
@@ -131,7 +131,7 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
continue // 跳过不存在的 continue // 跳过不存在的
} }
tx := h.DB.Begin() tx := h.DB.Begin()
if job.Status != model.JMTaskStatusSuccess && job.Power > 0 { if job.Status != types.JMTaskStatusSuccess && job.Power > 0 {
remark := fmt.Sprintf("任务未成功,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg) remark := fmt.Sprintf("任务未成功,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ err = h.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund, Type: types.PowerRefund,
@@ -172,7 +172,7 @@ func (h *AdminJimengHandler) BatchRemove(c *gin.Context) {
// Stats 获取统计信息 // Stats 获取统计信息
func (h *AdminJimengHandler) Stats(c *gin.Context) { func (h *AdminJimengHandler) Stats(c *gin.Context) {
type StatResult struct { type StatResult struct {
Status model.JMTaskStatus `json:"status"` Status types.JMTaskStatus `json:"status"`
Count int64 `json:"count"` Count int64 `json:"count"`
} }
@@ -198,13 +198,13 @@ func (h *AdminJimengHandler) Stats(c *gin.Context) {
for _, stat := range stats { for _, stat := range stats {
result["totalTasks"] = result["totalTasks"].(int64) + stat.Count result["totalTasks"] = result["totalTasks"].(int64) + stat.Count
switch stat.Status { switch stat.Status {
case model.JMTaskStatusInQueue: case types.JMTaskStatusInQueue:
result["pendingTasks"] = stat.Count result["pendingTasks"] = stat.Count
case model.JMTaskStatusSuccess: case types.JMTaskStatusSuccess:
result["completedTasks"] = stat.Count result["completedTasks"] = stat.Count
case model.JMTaskStatusGenerating: case types.JMTaskStatusGenerating:
result["processingTasks"] = stat.Count result["processingTasks"] = stat.Count
case model.JMTaskStatusFailed: case types.JMTaskStatusFailed:
result["failedTasks"] = stat.Count result["failedTasks"] = stat.Count
} }
} }
@@ -231,29 +231,15 @@ func (h *AdminJimengHandler) UpdateConfig(c *gin.Context) {
} }
// 验证算力配置 // 验证算力配置
if req.Power.TextToImage <= 0 { if len(req.Powers) == 0 {
resp.ERROR(c, "文生图算力必须大于0") resp.ERROR(c, "请至少配置一个模型的积分")
return return
} }
if req.Power.ImageToImage <= 0 { for key, val := range req.Powers {
resp.ERROR(c, "图生图算力必须大于0") if val <= 0 {
return resp.ERROR(c, fmt.Sprintf("模型 %s 的积分必须大于0", key))
} return
if req.Power.ImageEdit <= 0 { }
resp.ERROR(c, "图片编辑算力必须大于0")
return
}
if req.Power.ImageEffects <= 0 {
resp.ERROR(c, "图片特效算力必须大于0")
return
}
if req.Power.TextToVideo <= 0 {
resp.ERROR(c, "文生视频算力必须大于0")
return
}
if req.Power.ImageToVideo <= 0 {
resp.ERROR(c, "图生视频算力必须大于0")
return
} }
// 保存配置 // 保存配置
-215
View File
@@ -1,215 +0,0 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type MediaHandler struct {
handler.BaseHandler
userService *service.UserService
uploader *oss.UploaderManager
}
func NewMediaHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *MediaHandler {
return &MediaHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
}
// RegisterRoutes 注册路由
func (h *MediaHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/media/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("suno", h.SunoList)
group.POST("videos", h.Videos)
group.GET("remove", h.Remove)
}
}
type mediaQuery struct {
Type string `json:"type"` // 任务类型 luma, keling
Prompt string `json:"prompt"`
Username string `json:"username"`
CreatedAt []string `json:"created_at"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// SunoList Suno 任务列表
func (h *MediaHandler) SunoList(c *gin.Context) {
var data mediaQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.SunoJob{}).Count(&total)
var list []model.SunoJob
var items = make([]vo.SunoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.SunoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// Videos 视频任务列表
func (h *MediaHandler) Videos(c *gin.Context) {
var data mediaQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{}).Where("type", data.Type)
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.VideoJob{}).Count(&total)
var list []model.VideoJob
var items = make([]vo.VideoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.VideoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
if job.VideoURL == "" {
job.VideoURL = job.WaterURL
}
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
func (h *MediaHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
tab := c.Query("tab")
tx := h.DB.Begin()
var md, remark, fileURL string
var power, userId, progress int
switch tab {
case "suno":
var job model.SunoJob
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
resp.ERROR(c, "记录不存在")
return
}
tx.Delete(&job)
md = "suno"
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("SUNO 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
fileURL = job.AudioURL
case "luma":
case "keling":
var job model.VideoJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = job.Type
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("LUMA 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
progress = job.Progress
fileURL = job.VideoURL
if fileURL == "" {
fileURL = job.WaterURL
}
default:
resp.ERROR(c, types.InvalidArgs)
return
}
if progress != 100 {
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund,
Model: md,
Remark: remark,
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove image
err := h.uploader.GetUploadHandler().Delete(fileURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
resp.SUCCESS(c)
}
+2 -6
View File
@@ -212,12 +212,8 @@ func (h *ModerationHandler) GetSourceList(c *gin.Context) {
"name": "Midjourney 绘图", "name": "Midjourney 绘图",
}, },
{ {
"id": types.ModerationSourceDalle, "id": types.ModerationSourceImage,
"name": "Dalle 绘图", "name": "AI图像生成",
},
{
"id": types.ModerationSourceSD,
"name": "StableDiffusion 绘图",
}, },
{ {
"id": types.ModerationSourceSuno, "id": types.ModerationSourceSuno,
+284
View File
@@ -0,0 +1,284 @@
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 (
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service/ppt"
"geekai/store/model"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// PPTHandler 管理后台 PPT 生成配置处理器
type PPTHandler struct {
handler.BaseHandler
pptService *ppt.PptService
}
// NewPPTHandler 创建管理后台 PPT 配置处理器
func NewPPTHandler(app *core.AppServer, db *gorm.DB, pptService *ppt.PptService) *PPTHandler {
return &PPTHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
pptService: pptService,
}
}
// RegisterRoutes 注册 PPT 配置相关路由
func (h *PPTHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/ppt/")
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
rg.GET("config", h.GetConfig)
rg.POST("config/update", h.UpdateConfig)
rg.GET("jobs", h.Jobs)
rg.GET("jobs/:task_id", h.JobDetail)
rg.GET("jobs/:task_id/export", h.ExportJob)
rg.GET("stats", h.Stats)
}
}
// GetConfig 获取 PPT 生成配置
func (h *PPTHandler) GetConfig(c *gin.Context) {
var cfg model.Config
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 返回一个默认空配置
resp.SUCCESS(c, types.PPTConfig{
OutlineLLMModel: "gpt-4o-mini",
MaxSlidesPerTask: 30,
PowerCostPerSlide: 0,
MaxConcurrentRequests: 3,
QPSLimit: 1,
NanoBananaModel: "nano-banana",
NanoBananaAspectRatio: "16:9",
SeedreamSize: "1920x1080",
})
return
}
resp.ERROR(c, "获取配置失败: "+err.Error())
return
}
var pptConfig types.PPTConfig
err = utils.JsonDecode(cfg.Value, &pptConfig)
if err != nil {
resp.ERROR(c, "解析配置失败: "+err.Error())
return
}
resp.SUCCESS(c, pptConfig)
}
// UpdateConfig 更新 PPT 生成配置
func (h *PPTHandler) UpdateConfig(c *gin.Context) {
var req types.PPTConfig
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, "参数错误")
return
}
// 基础校验
if req.MaxSlidesPerTask <= 0 {
resp.ERROR(c, "单个任务最多 PPT 页数必须大于 0")
return
}
if req.PowerCostPerSlide < 0 {
resp.ERROR(c, "每张 PPT 图片消耗算力不能小于 0")
return
}
if req.MaxConcurrentRequests <= 0 {
req.MaxConcurrentRequests = 3
}
if req.QPSLimit <= 0 {
req.QPSLimit = 1
}
// 根据当前生图提供方做必填校验
switch req.ActiveImageProvider {
case types.PPTImageProviderNanoBanana:
if req.NanoBananaApiURL == "" {
resp.ERROR(c, "Nano Banana API 地址不能为空")
return
}
if req.NanoBananaApiKey == "" {
resp.ERROR(c, "Nano Banana API Key 不能为空")
return
}
case types.PPTImageProviderSeedream:
if req.SeedreamBaseURL == "" {
resp.ERROR(c, "Seedream Base URL 不能为空")
return
}
if req.SeedreamApiKey == "" {
resp.ERROR(c, "Seedream API Key 不能为空")
return
}
if req.SeedreamModel == "" {
resp.ERROR(c, "Seedream 模型 ID 不能为空")
return
}
default:
// 允许为空,未来可以扩展更多 provider
}
value := utils.JsonEncode(&req)
var cfg model.Config
err := h.DB.Where("name", types.ConfigKeyPPT).First(&cfg).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
cfg.Name = types.ConfigKeyPPT
cfg.Value = value
if err = h.DB.Create(&cfg).Error; err != nil {
resp.ERROR(c, "创建配置失败: "+err.Error())
return
}
resp.SUCCESS(c, gin.H{"message": "配置创建成功"})
return
}
resp.ERROR(c, "获取配置失败: "+err.Error())
return
}
cfg.Value = value
if err = h.DB.Updates(&cfg).Error; err != nil {
resp.ERROR(c, "更新配置失败: "+err.Error())
return
}
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
}
// Jobs 管理后台查看 PPT 任务列表(内存任务)
func (h *PPTHandler) Jobs(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
filterUserId := h.GetInt(c, "user_id", 0)
status := h.GetTrim(c, "status")
filtered, total := h.pptService.ListAdminJobs(c.Request.Context(), page, pageSize, filterUserId, status)
jobs := make([]gin.H, 0, len(filtered))
for _, t := range filtered {
job := t.TaskSummaryMap()
job["user_id"] = t.UserID
job["error_message"] = t.ErrorMessage
jobs = append(jobs, job)
}
resp.SUCCESS(c, gin.H{
"jobs": jobs,
"total": total,
"page": page,
"page_size": pageSize,
})
}
func buildAdminPPTTaskDetail(task *ppt.Task) gin.H {
percentage := 0
if task.Total > 0 {
percentage = int(float64(task.Completed) / float64(task.Total) * 100)
}
return gin.H{
"task_id": task.TaskID,
"user_id": task.UserID,
"status": task.Status,
"progress": gin.H{
"total_slides": task.Total,
"completed_slides": task.Completed,
"percentage": percentage,
},
"slides": task.Slides,
"error_message": task.ErrorMessage,
"content": task.Content,
"prompt": task.Prompt,
"title": task.Title,
"thumb": task.Thumb,
}
}
// JobDetail 管理后台查看指定 PPT 任务详情
func (h *PPTHandler) JobDetail(c *gin.Context) {
taskID := c.Param("task_id")
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
task, ok := h.pptService.GetTask(taskID)
if !ok {
resp.ERROR(c, "任务不存在")
return
}
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
resp.SUCCESS(c, buildAdminPPTTaskDetail(task))
}
// ExportJob 管理后台导出 PPT 任务
func (h *PPTHandler) ExportJob(c *gin.Context) {
taskID := c.Param("task_id")
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
ef, ok := ppt.ParseExportFormat(c.Query("format"))
if !ok {
resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx")
return
}
task, exists := h.pptService.GetTask(taskID)
if !exists {
resp.ERROR(c, "任务不存在")
return
}
if task.Status != ppt.TaskStatusCompleted {
resp.ERROR(c, "仅已完成任务可导出")
return
}
h.pptService.EnsureTaskMeta(c.Request.Context(), task)
ossCfg := types.OSSConfig{}
if h.App.SysConfig != nil {
ossCfg = h.App.SysConfig.OSS
}
data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config)
if err != nil {
resp.ERROR(c, err.Error())
return
}
base := ppt.SanitizeExportBaseName(task.Title, task.TaskID)
filename := base + ppt.ExportFileExt(ef)
c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename))
c.Data(200, ppt.ExportMimeType(ef), data)
}
// Stats PPT 任务统计信息
func (h *PPTHandler) Stats(c *gin.Context) {
total, completed, processing, failed, pending := h.pptService.Stats()
resp.SUCCESS(c, gin.H{
"totalTasks": total,
"completedTasks": completed,
"processingTasks": processing,
"failedTasks": failed,
"pendingTasks": pending,
})
}
+137
View File
@@ -0,0 +1,137 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type SunoHandler struct {
handler.BaseHandler
userService *service.UserService
uploader *oss.UploaderManager
}
func NewSunoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *SunoHandler {
return &SunoHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}, userService: userService, uploader: manager}
}
// RegisterRoutes 注册路由
func (h *SunoHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/admin/suno/")
// 需要管理员授权的接口
group.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
group.POST("list", h.SunoList)
group.GET("remove", h.Remove)
}
}
type sunoQuery struct {
Title string `json:"title"`
Prompt string `json:"prompt"`
CreatedAt []string `json:"created_at"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// SunoList Suno 任务列表
func (h *SunoHandler) SunoList(c *gin.Context) {
var data sunoQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Title != "" {
session = session.Where("title LIKE ?", "%"+data.Title+"%")
}
if data.Prompt != "" {
// 同时查询 prompt 字段和 params JSON 字段中的 prompt
session = session.Where("prompt LIKE ? OR JSON_EXTRACT(params, '$.prompt') LIKE ?", "%"+data.Prompt+"%", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.SunoJob{}).Count(&total)
var list []model.SunoJob
var items = make([]vo.SunoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.SunoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
func (h *SunoHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
tx := h.DB.Begin()
var job model.SunoJob
if err := h.DB.Where("id", id).First(&job).Error; err != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md := "suno"
power := job.Power
userId := int(job.UserId)
remark := fmt.Sprintf("SUNO 任务失败,退回算力。任务ID%dErr: %s", job.Id, job.ErrMsg)
needRefund := job.Progress != 100
fileURL := job.AudioURL
if needRefund {
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund,
Model: md,
Remark: remark,
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove file
err := h.uploader.GetUploadHandler().Delete(fileURL)
if err != nil {
logger.Error("remove file failed: ", err)
}
resp.SUCCESS(c)
}
+233 -35
View File
@@ -8,32 +8,36 @@ package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"bytes"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware" "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"
"net/http"
"path/filepath"
"strconv"
"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/golang-jwt/jwt/v5"
"github.com/xuri/excelize/v2"
"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 注册路由 // RegisterRoutes 注册路由
@@ -49,9 +53,220 @@ func (h *UserHandler) RegisterRoutes() {
group.GET("loginLog", h.LoginLog) group.GET("loginLog", h.LoginLog)
group.GET("genLoginLink", h.GenLoginLink) group.GET("genLoginLink", h.GenLoginLink)
group.POST("resetPass", h.ResetPass) group.POST("resetPass", h.ResetPass)
group.GET("import/template", h.ImportTemplate)
group.POST("import", h.ImportUsers)
} }
} }
// ImportTemplate 下载用户导入模板
func (h *UserHandler) ImportTemplate(c *gin.Context) {
f := excelize.NewFile()
sheetName := "Sheet1"
// 表头
headers := []string{"用户名", "密码", "手机", "邮箱", "剩余算力", "启用状态"}
for i, title := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
_ = f.SetCellValue(sheetName, cell, title)
}
// 示例数据
sample := []interface{}{"user001", "Passw0rd!", "13800000000", "user001@example.com", 100, 1}
for i, v := range sample {
cell, _ := excelize.CoordinatesToCellName(i+1, 2)
_ = f.SetCellValue(sheetName, cell, v)
}
buf, err := f.WriteToBuffer()
if err != nil {
logger.Error("failed to generate user import template: ", err)
resp.ERROR(c, "生成模板失败")
return
}
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Header("Content-Disposition", "attachment; filename=\"user_import_template.xlsx\"")
c.Data(http.StatusOK, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes())
}
// ImportUsers 批量导入用户
func (h *UserHandler) ImportUsers(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
resp.ERROR(c, "文件上传失败: "+err.Error())
return
}
ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
if ext != ".xlsx" {
resp.ERROR(c, "只支持 .xlsx 格式的 Excel 文件")
return
}
src, err := fileHeader.Open()
if err != nil {
resp.ERROR(c, "无法读取上传文件: "+err.Error())
return
}
defer src.Close()
// 读取到内存,避免多次读取问题
var buf bytes.Buffer
if _, err = buf.ReadFrom(src); err != nil {
resp.ERROR(c, "读取文件内容失败: "+err.Error())
return
}
excel, err := excelize.OpenReader(bytes.NewReader(buf.Bytes()))
if err != nil {
resp.ERROR(c, "解析 Excel 失败: "+err.Error())
return
}
defer func() {
_ = excel.Close()
}()
rows, err := excel.GetRows("Sheet1")
if err != nil {
resp.ERROR(c, "读取工作表失败: "+err.Error())
return
}
if len(rows) < 2 {
resp.ERROR(c, "Excel 中没有可导入的数据")
return
}
type rowError struct {
Row int `json:"row"`
Error string `json:"error"`
}
var (
successCount int
failedCount int
errorsList []rowError
)
usernameSet := make(map[string]struct{})
// 从第二行开始读取
for index, row := range rows[1:] {
line := index + 2 // Excel 行号
if len(row) == 0 {
continue
}
get := func(i int) string {
if i < len(row) {
return strings.TrimSpace(row[i])
}
return ""
}
username := get(0)
password := get(1)
mobile := get(2)
email := get(3)
powerStr := get(4)
statusStr := get(5)
// 基础校验
if username == "" {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "用户名不能为空"})
continue
}
if _, ok := usernameSet[username]; ok {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "同一文件中用户名重复"})
continue
}
usernameSet[username] = struct{}{}
if len(password) < 8 || len(password) > 16 {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "密码必须为 8-16 位"})
continue
}
if mobile != "" && len(mobile) != 11 {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "手机号必须为 11 位"})
continue
}
// 解析算力
power := 0
if powerStr != "" {
p, err := strconv.Atoi(powerStr)
if err != nil {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "剩余算力必须为数字"})
continue
}
if p < 0 {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "剩余算力不能为负数"})
continue
}
power = p
}
// 解析启用状态
status := true
if statusStr != "" {
switch strings.TrimSpace(statusStr) {
case "0", "否", "false", "停用":
status = false
case "1", "是", "true", "启用":
status = true
default:
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "启用状态只支持 1/是 或 0/否"})
continue
}
}
// 检查用户名是否已存在
var exist model.User
if err = h.DB.Where("username = ?", username).First(&exist).Error; err == nil && exist.Id > 0 {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "用户名已存在"})
continue
}
salt := utils.RandString(8)
u := model.User{
Username: username,
Password: utils.GenPassword(password, salt),
Mobile: mobile,
Email: email,
Avatar: "/images/avatar/user.png",
Salt: salt,
Power: power,
Status: status,
ChatRoles: utils.JsonEncode([]string{}),
ChatConfig: "{}",
ChatModels: utils.JsonEncode([]int{}),
ExpiredTime: 0, // 长期有效
Vip: false,
}
u.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
if err = h.DB.Create(&u).Error; err != nil {
failedCount++
errorsList = append(errorsList, rowError{Row: line, Error: "写入数据库失败: " + err.Error()})
continue
}
successCount++
}
resp.SUCCESS(c, gin.H{
"success": successCount,
"failed": failedCount,
"errors": errorsList,
})
}
// List 用户列表 // List 用户列表
func (h *UserHandler) List(c *gin.Context) { func (h *UserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1) page := h.GetInt(c, "page", 1)
@@ -98,29 +313,21 @@ func (h *UserHandler) List(c *gin.Context) {
func (h *UserHandler) Save(c *gin.Context) { func (h *UserHandler) Save(c *gin.Context) {
var data struct { var data struct {
Id uint `json:"id"` Id uint `json:"id"`
Password string `json:"password"` Password string `json:"password"`
Username string `json:"username"` Username string `json:"username"`
Mobile string `json:"mobile"` Mobile string `json:"mobile"`
Email string `json:"email"` Email string `json:"email"`
ChatRoles []string `json:"chat_roles"` ChatModels []int `json:"chat_models"`
ChatModels []int `json:"chat_models"` ExpiredTime string `json:"expired_time"`
ExpiredTime string `json:"expired_time"` Status bool `json:"status"`
Status bool `json:"status"` Vip bool `json:"vip"`
Vip bool `json:"vip"` Power int `json:"power"`
Power int `json:"power"`
} }
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 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
@@ -137,11 +344,10 @@ func (h *UserHandler) Save(c *gin.Context) {
user.Status = data.Status user.Status = data.Status
user.Vip = data.Vip user.Vip = data.Vip
user.Power = data.Power user.Power = data.Power
user.ChatRoles = utils.JsonEncode(data.ChatRoles)
user.ChatModels = utils.JsonEncode(data.ChatModels) user.ChatModels = utils.JsonEncode(data.ChatModels)
user.ExpiredTime = utils.Str2stamp(data.ExpiredTime) user.ExpiredTime = utils.Str2stamp(data.ExpiredTime)
res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_roles_json", "chat_models_json", "expired_time").Updates(&user) res = h.DB.Select("username", "mobile", "email", "status", "vip", "power", "chat_models_json", "expired_time").Updates(&user)
if res.Error != nil { if res.Error != nil {
logger.Error("error with update database", res.Error) logger.Error("error with update database", res.Error)
@@ -193,16 +399,11 @@ func (h *UserHandler) Save(c *gin.Context) {
Salt: salt, Salt: salt,
Power: data.Power, Power: data.Power,
Status: true, Status: true,
ChatRoles: utils.JsonEncode(data.ChatRoles),
ChatConfig: "{}", 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
@@ -291,10 +492,7 @@ func (h *UserHandler) Remove(c *gin.Context) {
if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil { if err = tx.Where("user_id = ?", id).Delete(&model.MidJourneyJob{}).Error; err != nil {
break break
} }
if err = tx.Where("user_id = ?", id).Delete(&model.SdJob{}).Error; err != nil { if err = tx.Where("user_id = ?", id).Delete(&model.ImageJob{}).Error; err != nil {
break
}
if err = tx.Where("user_id = ?", id).Delete(&model.DallJob{}).Error; err != nil {
break break
} }
if err = tx.Where("user_id = ?", id).Delete(&model.SunoJob{}).Error; err != nil { if err = tx.Where("user_id = ?", id).Delete(&model.SunoJob{}).Error; err != nil {
+269
View File
@@ -0,0 +1,269 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/handler"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// VideoHandler 管理后台视频生成处理器
type VideoHandler struct {
handler.BaseHandler
userService *service.UserService
uploader *oss.UploaderManager
}
// NewVideoHandler 创建管理后台视频生成处理器
func NewVideoHandler(app *core.AppServer, db *gorm.DB, userService *service.UserService, manager *oss.UploaderManager) *VideoHandler {
return &VideoHandler{
BaseHandler: handler.BaseHandler{App: app, DB: db},
userService: userService,
uploader: manager,
}
}
// RegisterRoutes 注册视频生成管理后台路由
func (h *VideoHandler) RegisterRoutes() {
rg := h.App.Engine.Group("/api/admin/video/")
rg.Use(middleware.AdminAuthMiddleware(h.App.Config.AdminSession.SecretKey, h.App.Redis))
{
rg.GET("config", h.GetConfig)
rg.POST("config/update", h.UpdateConfig)
rg.POST("list", h.Videos)
rg.GET("remove", h.Remove)
}
}
// GetConfig 获取视频生成配置
func (h *VideoHandler) GetConfig(c *gin.Context) {
var config model.Config
err := h.DB.Where("name", types.ConfigKeyVideo).First(&config).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 返回空配置
resp.SUCCESS(c, types.VideoConfig{
ApiURL: "",
ApiKey: "",
VideoPowers: make(map[string]types.VideoModelPower),
})
return
}
resp.ERROR(c, "获取配置失败: "+err.Error())
return
}
var videoConfig types.VideoConfig
err = utils.JsonDecode(config.Value, &videoConfig)
if err != nil {
resp.ERROR(c, "解析配置失败: "+err.Error())
return
}
resp.SUCCESS(c, videoConfig)
}
// UpdateConfig 更新视频生成配置
func (h *VideoHandler) UpdateConfig(c *gin.Context) {
var req types.VideoConfig
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, "参数错误")
return
}
// 验证必填字段
if req.ApiURL == "" {
resp.ERROR(c, "API地址不能为空")
return
}
if req.ApiKey == "" {
resp.ERROR(c, "API密钥不能为空")
return
}
// 验证算力配置
if len(req.VideoPowers) == 0 {
resp.ERROR(c, "请至少配置一个模型的算力")
return
}
// 新的价格配置方式直接使用 power_config 中的 key(如 "fixed"、"5_720P" 等)
// 不再区分固定收费和按秒收费,所有价格配置都在 power_config 中
for key, modelPower := range req.VideoPowers {
// 验证 provider
if modelPower.Provider == "" {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 provider 不能为空", key))
return
}
// 验证 model
if modelPower.Model == "" {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 model 不能为空", key))
return
}
// 验证 power_config
if len(modelPower.PowerConfig) == 0 {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config 不能为空", key))
return
}
// 验证 power_config 中的值必须大于0
for configKey, configValue := range modelPower.PowerConfig {
if configValue <= 0 {
resp.ERROR(c, fmt.Sprintf("模型 %s 的 power_config.%s 必须大于0", key, configKey))
return
}
}
}
// 保存配置
tx := h.DB.Begin()
value := utils.JsonEncode(&req)
var exist model.Config
tx.Where("name", types.ConfigKeyVideo).First(&exist)
if exist.Id > 0 {
exist.Value = value
err := tx.Updates(&exist).Error
if err != nil {
resp.ERROR(c, "更新配置失败: "+err.Error())
tx.Rollback()
return
}
} else {
exist.Name = types.ConfigKeyVideo
exist.Value = value
err := tx.Create(&exist).Error
if err != nil {
resp.ERROR(c, "创建配置失败: "+err.Error())
tx.Rollback()
return
}
}
tx.Commit()
resp.SUCCESS(c, gin.H{"message": "配置更新成功"})
}
type videoQuery struct {
Type string `json:"type"` // 任务类型 luma, keling
Status string `json:"status"` // 任务状态 pending, in_progress, downloading, success, failed
Prompt string `json:"prompt"`
CreatedAt []string `json:"created_at"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// Videos 视频任务列表
func (h *VideoHandler) Videos(c *gin.Context) {
var data videoQuery
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Type != "" {
session = session.Where("type", data.Type)
}
if data.Status != "" {
session = session.Where("status", data.Status)
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
session = session.Where("created_at >= ? AND created_at <= ?", data.CreatedAt[0], data.CreatedAt[1])
}
var total int64
session.Model(&model.VideoJob{}).Count(&total)
var list []model.VideoJob
var items = make([]vo.VideoJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.VideoJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
func (h *VideoHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
tab := c.Query("tab")
tx := h.DB.Begin()
var md, remark, fileURL string
var power, userId int
var needRefund bool
switch tab {
case "luma", "keling":
var job model.VideoJob
if res := h.DB.Where("id", id).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
tx.Delete(&job)
md = job.Type
power = job.Power
userId = int(job.UserId)
remark = fmt.Sprintf("视频任务失败,退回算力。任务ID%d,Err: %s", job.Id, job.ErrMsg)
needRefund = job.Status != types.VideoStatusSuccess
fileURL = job.VideoURL
default:
resp.ERROR(c, types.InvalidArgs)
return
}
if needRefund {
err := h.userService.IncreasePower(uint(userId), power, model.PowerLog{
Type: types.PowerRefund,
Model: md,
Remark: remark,
})
if err != nil {
tx.Rollback()
resp.ERROR(c, err.Error())
return
}
}
tx.Commit()
// remove file
err := h.uploader.GetUploadHandler().Delete(fileURL)
if err != nil {
logger.Error("remove file failed: ", err)
}
resp.SUCCESS(c)
}
+2 -2
View File
@@ -12,7 +12,7 @@ import (
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"strings" "strings"
@@ -22,7 +22,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
type BaseHandler struct { type BaseHandler struct {
App *core.AppServer App *core.AppServer
+166 -31
View File
@@ -37,7 +37,11 @@ func (h *ChatAppHandler) RegisterRoutes() {
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{ {
group.GET("list/user", h.ListByUser) group.GET("list/user", h.ListByUser)
group.POST("create", h.Create)
group.POST("copy", h.Copy)
group.POST("update", h.UpdateApp) group.POST("update", h.UpdateApp)
group.POST("workspace", h.UpdateWorkArea)
group.POST("remove", h.Remove)
} }
} }
@@ -45,7 +49,7 @@ func (h *ChatAppHandler) RegisterRoutes() {
func (h *ChatAppHandler) 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.ChatApp var roles []model.ChatApp
session := h.DB.Where("enable", true) session := h.DB.Where("enable = ? AND user_id = 0", true)
if tid > 0 { if tid > 0 {
session = session.Where("tid", tid) session = session.Where("tid", tid)
} }
@@ -61,6 +65,9 @@ func (h *ChatAppHandler) List(c *gin.Context) {
err := utils.CopyObject(r, &v) err := utils.CopyObject(r, &v)
if err == nil { if err == nil {
v.Id = r.Id v.Id = r.Id
if r.UserId == 0 {
v.SystemPrompt = ""
}
roleVos = append(roleVos, v) roleVos = append(roleVos, v)
} }
} }
@@ -72,23 +79,11 @@ 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.ChatApp 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 session = session.Where("(user_id = 0 OR user_id = ?)", userId)
h.DB.First(&user, userId) } else {
var roleKeys []string session = session.Where("user_id = 0")
if user.ChatRoles != "" {
err := utils.JsonDecode(user.ChatRoles, &roleKeys)
if err != nil {
resp.ERROR(c, "角色解析失败!")
return
}
}
// 保证用户至少有一个角色可用
if len(roleKeys) > 0 {
session = session.Where("marker IN ?", roleKeys)
}
} }
if id > 0 { if id > 0 {
@@ -106,33 +101,173 @@ func (h *ChatAppHandler) ListByUser(c *gin.Context) {
err := utils.CopyObject(r, &v) err := utils.CopyObject(r, &v)
if err == nil { if err == nil {
v.Id = r.Id v.Id = r.Id
if r.UserId == 0 {
v.SystemPrompt = ""
}
roleVos = append(roleVos, v) roleVos = append(roleVos, v)
} }
} }
resp.SUCCESS(c, roleVos) resp.SUCCESS(c, roleVos)
} }
// UpdateApp 更新用户聊天应用 // Create 用户创建智能体
func (h *ChatAppHandler) UpdateApp(c *gin.Context) { func (h *ChatAppHandler) Create(c *gin.Context) {
user, err := h.GetLoginUser(c) userId := h.GetLoginUserId(c)
if err != nil { if userId == 0 {
resp.NotAuth(c) resp.NotAuth(c)
return return
} }
var data vo.ChatApp
var data struct { if err := c.ShouldBindJSON(&data); err != nil {
Keys []string `json:"keys"`
}
if err = c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
role := model.ChatApp{
err = h.DB.Model(&model.User{}).Where("id = ?", user.Id).UpdateColumn("chat_roles_json", utils.JsonEncode(data.Keys)).Error Name: data.Name,
if err != nil { Tid: data.Tid,
UserId: userId,
SystemPrompt: data.SystemPrompt,
HelloMsg: data.HelloMsg,
Icon: data.Icon,
Enable: true,
SortNum: int(data.SortNum),
ModelId: data.ModelId,
}
if role.Icon == "" {
role.Icon = "/images/avatar/gpt.png"
}
if err := h.DB.Create(&role).Error; err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
data.Id = role.Id
resp.SUCCESS(c) data.UserId = role.UserId
resp.SUCCESS(c, data)
}
// Copy 用户复制智能体(复制为当前用户名下)
func (h *ChatAppHandler) Copy(c *gin.Context) {
userId := h.GetLoginUserId(c)
if userId == 0 {
resp.NotAuth(c)
return
}
var body struct {
SourceId uint `json:"source_id"`
}
if err := c.ShouldBindJSON(&body); err != nil || body.SourceId == 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
var src model.ChatApp
if err := h.DB.First(&src, body.SourceId).Error; err != nil {
resp.ERROR(c, "智能体不存在")
return
}
role := model.ChatApp{
Name: src.Name,
Tid: src.Tid,
UserId: userId,
SystemPrompt: src.SystemPrompt,
HelloMsg: src.HelloMsg,
Icon: src.Icon,
Enable: true,
SortNum: src.SortNum,
ModelId: src.ModelId,
}
if err := h.DB.Create(&role).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{"id": role.Id})
}
// UpdateApp 更新用户聊天应用(仅允许更新自己创建的)
func (h *ChatAppHandler) UpdateApp(c *gin.Context) {
userId := h.GetLoginUserId(c)
if userId == 0 {
resp.NotAuth(c)
return
}
var data vo.ChatApp
if err := c.ShouldBindJSON(&data); err != nil || data.Id == 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
var role model.ChatApp
if err := h.DB.First(&role, data.Id).Error; err != nil {
resp.ERROR(c, "智能体不存在")
return
}
if role.UserId != userId {
resp.ERROR(c, "无权限修改该智能体")
return
}
updates := map[string]interface{}{
"name": data.Name,
"hello_msg": data.HelloMsg,
"icon": data.Icon,
"model_id": data.ModelId,
"system_prompt": data.SystemPrompt,
}
if err := h.DB.Model(&role).Updates(updates).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, nil)
}
// UpdateWorkArea 更新用户工作区应用列表(存为应用 id 数组)
func (h *ChatAppHandler) UpdateWorkArea(c *gin.Context) {
userId := h.GetLoginUserId(c)
if userId == 0 {
resp.NotAuth(c)
return
}
var body struct {
Ids []uint `json:"ids"`
}
if err := c.ShouldBindJSON(&body); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if err := h.DB.Model(&model.User{}).Where("id = ?", userId).Update("chat_roles_json", utils.JsonEncode(body.Ids)).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, nil)
}
// Remove 删除用户智能体(仅允许删除自己创建的)
func (h *ChatAppHandler) Remove(c *gin.Context) {
userId := h.GetLoginUserId(c)
if userId == 0 {
resp.NotAuth(c)
return
}
var body struct {
Id uint `json:"id"`
}
_ = c.ShouldBindJSON(&body)
if body.Id == 0 {
body.Id = uint(h.GetInt(c, "id", 0))
}
if body.Id == 0 {
resp.ERROR(c, types.InvalidArgs)
return
}
var role model.ChatApp
if err := h.DB.First(&role, body.Id).Error; err != nil {
resp.ERROR(c, "智能体不存在")
return
}
if role.UserId != userId {
resp.ERROR(c, "无权限删除该智能体")
return
}
if err := h.DB.Delete(&role).Error; err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, nil)
} }
+74 -30
View File
@@ -19,6 +19,7 @@ import (
"geekai/service" "geekai/service"
"geekai/service/moderation" "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"
@@ -65,44 +66,50 @@ 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 userService *service.UserService
moderationManager *moderation.ServiceManager moderationManager *moderation.ServiceManager
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, moderationManager *moderation.ServiceManager) *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, userService: userService,
moderationManager: moderationManager, moderationManager: moderationManager,
userLocks: types.NewUserLockManager(),
soraService: soraService,
} }
} }
// RegisterRoutes 注册路由 // RegisterRoutes 注册路由
func (h *ChatHandler) RegisterRoutes() { func (h *ChatHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/chat/") group := h.App.Engine.Group("/api/chat/")
group.GET("detail", h.Detail)
// 聊天接口不需要授权(已在authConfig中配置) group.GET("history", h.History)
group.Any("message", h.Chat)
// 其他接口需要用户授权 // 其他接口需要用户授权
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{ {
group.Any("message", h.Chat)
group.GET("list", h.List) group.GET("list", h.List)
group.GET("detail", h.Detail)
group.POST("update", h.Update) group.POST("update", h.Update)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("history", h.History)
group.GET("clear", h.Clear) group.GET("clear", h.Clear)
group.POST("tokens", h.Tokens) group.POST("tokens", h.Tokens)
group.GET("stop", h.StopGenerate) group.GET("stop", h.StopGenerate)
group.POST("tts", h.TextToSpeech) group.POST("tts", h.TextToSpeech)
} }
} }
// Chat 处理聊天请求 // Chat 处理聊天请求
@@ -120,6 +127,14 @@ func (h *ChatHandler) Chat(c *gin.Context) {
return 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()) ctx, cancel := context.WithCancel(c.Request.Context())
defer cancel() defer cancel()
@@ -258,13 +273,13 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
chatCtx := make([]any, 0) chatCtx := make([]any, 0)
messages := make([]any, 0) messages := make([]any, 0)
if h.App.SysConfig.Base.EnableContext { if h.App.SysConfig.Base.EnableContext {
_ = utils.JsonDecode(input.ChatRole.Context, &messages) _ = utils.JsonDecode(input.ChatRole.SystemPrompt, &messages)
if h.App.SysConfig.Base.ContextDeep > 0 { if h.App.SysConfig.Base.ContextDeep > 0 {
var historyMessages []model.ChatMessage var historyMessages []model.ChatMessage
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId) dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
if input.LastMsgId > 0 { // 重新生成逻辑 if input.LastMsgId > 0 { // 重新生成和编辑逻辑
var lastMessage model.ChatMessage var lastMessage model.ChatMessage
err = dbSession.Where("id <= ?", input.LastMsgId).Where("type", types.PromptMsg).First(&lastMessage).Error err = dbSession.Where("id < ?", input.LastMsgId).Where("type", types.ReplyMsg).Order("id DESC").First(&lastMessage).Error
if err != nil { if err != nil {
input.LastMsgId = 0 input.LastMsgId = 0
} else { } else {
@@ -272,7 +287,7 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
} }
dbSession = dbSession.Where("id < ?", input.LastMsgId) dbSession = dbSession.Where("id < ?", input.LastMsgId)
// 删除对应的聊天记录 // 删除对应的聊天记录
h.DB.Debug().Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{}) 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 err = dbSession.Limit(h.App.SysConfig.Base.ContextDeep).Order("id DESC").Find(&historyMessages).Error
if err == nil { if err == nil {
@@ -330,14 +345,14 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
for _, file := range input.Files { for _, file := range input.Files {
logger.Debugf("detected file: %+v", file.URL) logger.Debugf("detected file: %+v", file.URL)
// 处理图片 // 处理图片
if isImageURL(file.URL) { if isImageURL(file.URL) || isVideoURL(file.URL) {
imgList = append(imgList, gin.H{ imgList = append(imgList, gin.H{
"type": "image_url", "type": "image_url",
"image_url": gin.H{ "image_url": gin.H{
"url": file.URL, "url": file.URL,
}, },
}) })
} else { } else if isTextURL(file.URL) {
// 处理文件,提取文件内容 // 处理文件,提取文件内容
content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost) content, err := utils.ReadFileContent(file.URL, h.App.Config.TikaHost)
if err != nil { if err != nil {
@@ -398,23 +413,43 @@ func isImageURL(url string) bool {
".svg": true, ".svg": true,
".ico": true, ".ico": true,
} }
return validImageExts[ext]
}
if !validImageExts[ext] { // 判断是个链接是否是文本链接
func isTextURL(url string) bool {
// 检查是否是有效的URL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return false return false
} }
// 发送HEAD请求检查Content-Type // 检查文件扩展名
client := &http.Client{ ext := strings.ToLower(path.Ext(url))
Timeout: 5 * time.Second, validTextExts := map[string]bool{
".txt": true,
".doc": true,
".docx": true,
".pdf": true,
} }
resp, err := client.Head(url) return validTextExts[ext]
if err != nil { }
// 判断是个链接是否是视频
func isVideoURL(url string) bool {
// 检查是否是有效的URL
if !strings.HasPrefix(url, "http://") && !strings.HasPrefix(url, "https://") {
return false return false
} }
defer resp.Body.Close()
contentType := resp.Header.Get("Content-Type") // 检查文件扩展名
return strings.HasPrefix(contentType, "image/") 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 数量
@@ -495,11 +530,6 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, input
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 var apiURL string
p, _ := url.Parse(apiKey.ApiURL) p, _ := url.Parse(apiKey.ApiURL)
@@ -634,6 +664,19 @@ 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 {
logger.Error("failed to download video: ", err)
pushMessage(c, ChatEventError, "视频下载失败:"+err.Error())
} else {
files = append(files, *video)
}
}
historyReplyMsg := model.ChatMessage{ historyReplyMsg := model.ChatMessage{
UserId: userVo.Id, UserId: userVo.Id,
ChatId: input.ChatId, ChatId: input.ChatId,
@@ -642,7 +685,7 @@ func (h *ChatHandler) saveChatHistory(
Icon: input.ChatRole.Icon, Icon: input.ChatRole.Icon,
Content: utils.JsonEncode(vo.MsgContent{ Content: utils.JsonEncode(vo.MsgContent{
Text: message.Content, Text: message.Content,
Files: input.Files, Files: files,
}), }),
Tokens: replyTokens, Tokens: replyTokens,
TotalTokens: totalTokens, TotalTokens: totalTokens,
@@ -666,6 +709,7 @@ func (h *ChatHandler) saveChatHistory(
if err != nil { if err != nil {
content.Text = historyReplyMsg.Content content.Text = historyReplyMsg.Content
} }
content.Files = files
messageVo.Content = content messageVo.Content = content
messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix() messageVo.CreatedAt = historyReplyMsg.CreatedAt.Unix()
messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix() messageVo.UpdatedAt = historyReplyMsg.UpdatedAt.Unix()
+8
View File
@@ -212,6 +212,14 @@ 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),
})
}
} }
pushMessage(c, "text", errMsg) pushMessage(c, "text", errMsg)
} }
+34 -14
View File
@@ -8,8 +8,10 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"errors"
"geekai/core" "geekai/core"
"geekai/service" "geekai/core/types"
"geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"geekai/utils/resp" "geekai/utils/resp"
@@ -20,11 +22,16 @@ import (
type ConfigHandler struct { type ConfigHandler struct {
BaseHandler BaseHandler
licenseService *service.LicenseService uploaderManager *oss.UploaderManager
sysConfig *types.SystemConfig
} }
func NewConfigHandler(app *core.AppServer, db *gorm.DB, licenseService *service.LicenseService) *ConfigHandler { func NewConfigHandler(app *core.AppServer, db *gorm.DB, uploaderManager *oss.UploaderManager, sysConfig *types.SystemConfig) *ConfigHandler {
return &ConfigHandler{BaseHandler: BaseHandler{App: app, DB: db}, licenseService: licenseService} return &ConfigHandler{
BaseHandler: BaseHandler{App: app, DB: db},
uploaderManager: uploaderManager,
sysConfig: sysConfig,
}
} }
// RegisterRoutes 注册路由 // RegisterRoutes 注册路由
@@ -33,31 +40,44 @@ func (h *ConfigHandler) RegisterRoutes() {
// 无需授权的接口 // 无需授权的接口
group.GET("get", h.Get) group.GET("get", h.Get)
group.GET("license", h.License) group.GET("oss/thumb", h.GetOssThumbTemplate)
} }
// 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("name", key).First(&config) err := h.DB.Where("name", key).First(&config).Error
if res.Error != nil { if errors.Is(err, gorm.ErrRecordNotFound) {
resp.ERROR(c, res.Error.Error()) resp.SUCCESS(c, nil)
return return
} }
var value map[string]any
err := utils.JsonDecode(config.Value, &value)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
var value map[string]any
err = utils.JsonDecode(config.Value, &value)
if err != nil {
resp.ERROR(c, err.Error())
return
}
if key == types.ConfigKeyWxGzh {
delete(value, "secret")
delete(value, "token")
delete(value, "encoding_aes_key")
}
resp.SUCCESS(c, value) resp.SUCCESS(c, value)
} }
// License 获取 License 配置 // GetOssThumbTemplate 获取当前存储引擎的缩略图模板
func (h *ConfigHandler) License(c *gin.Context) { func (h *ConfigHandler) GetOssThumbTemplate(c *gin.Context) {
license := h.licenseService.GetLicense() template := h.uploaderManager.GetThumbTemplate()
resp.SUCCESS(c, license.Configs) resp.SUCCESS(c, gin.H{
"template": template,
"active": h.sysConfig.OSS.Active,
})
} }
+17 -20
View File
@@ -13,7 +13,7 @@ import (
"geekai/core" "geekai/core"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/dalle" "geekai/service/image"
"geekai/service/oss" "geekai/service/oss"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo" "geekai/store/vo"
@@ -31,7 +31,7 @@ import (
type FunctionHandler struct { type FunctionHandler struct {
BaseHandler BaseHandler
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
dallService *dalle.Service imageService *image.Service
userService *service.UserService userService *service.UserService
} }
@@ -40,7 +40,7 @@ func NewFunctionHandler(
db *gorm.DB, db *gorm.DB,
config *types.AppConfig, config *types.AppConfig,
manager *oss.UploaderManager, manager *oss.UploaderManager,
dallService *dalle.Service, imageService *image.Service,
userService *service.UserService) *FunctionHandler { userService *service.UserService) *FunctionHandler {
return &FunctionHandler{ return &FunctionHandler{
BaseHandler: BaseHandler{ BaseHandler: BaseHandler{
@@ -48,7 +48,7 @@ func NewFunctionHandler(
DB: db, DB: db,
}, },
uploadManager: manager, uploadManager: manager,
dallService: dallService, imageService: imageService,
userService: userService, userService: userService,
} }
} }
@@ -61,7 +61,7 @@ func (h *FunctionHandler) RegisterRoutes() {
// 需要用户授权的接口 // 需要用户授权的接口
group.POST("weibo", h.WeiBo) group.POST("weibo", h.WeiBo)
group.POST("zaobao", h.ZaoBao) group.POST("zaobao", h.ZaoBao)
group.POST("dalle3", h.Dall3) group.POST("image3", h.Image3)
} }
type resVo struct { type resVo struct {
@@ -176,8 +176,8 @@ func (h *FunctionHandler) ZaoBao(c *gin.Context) {
resp.SUCCESS(c, strings.Join(builder, "\n\n")) resp.SUCCESS(c, strings.Join(builder, "\n\n"))
} }
// Dall3 DallE3 AI // Image3 AI 图像生成
func (h *FunctionHandler) Dall3(c *gin.Context) { func (h *FunctionHandler) Image3(c *gin.Context) {
if err := h.checkAuth(c); err != nil { if err := h.checkAuth(c); err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -209,25 +209,22 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
return return
} }
// create dall task // create image task
prompt := utils.InterfaceToString(params["prompt"]) prompt := utils.InterfaceToString(params["prompt"])
task := types.DallTask{ task := types.ImageTask{
UserId: user.Id, UserId: user.Id,
Prompt: prompt, Prompt: prompt,
ModelId: chatModel.Id, ModelId: chatModel.Id,
ModelName: chatModel.Value, ModelValue: chatModel.Value,
ModelName: chatModel.Name,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
N: 1,
Quality: "standard",
Size: "1024x1024",
Style: "vivid",
Power: chatModel.Power, Power: chatModel.Power,
} }
job := model.DallJob{ job := model.ImageJob{
UserId: user.Id, UserId: user.Id,
Prompt: prompt, Prompt: prompt,
Power: chatModel.Power, Power: chatModel.Power,
TaskInfo: utils.JsonEncode(task), Params: utils.JsonEncode(task),
} }
err := h.DB.Create(&job).Error err := h.DB.Create(&job).Error
if err != nil { if err != nil {
@@ -236,7 +233,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
} }
task.Id = job.Id task.Id = job.Id
content, err := h.dallService.Image(task, true) content, err := h.imageService.Image(task, true)
if err != nil { if err != nil {
resp.ERROR(c, "任务执行失败:"+err.Error()) resp.ERROR(c, "任务执行失败:"+err.Error())
return return
@@ -3,7 +3,7 @@ package handler
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved. // * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license // * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file. // * that can be found in LICENSE file.
// * @Author yangjian102621@163.com // * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -13,7 +13,7 @@ import (
"geekai/core/middleware" "geekai/core/middleware"
"geekai/core/types" "geekai/core/types"
"geekai/service" "geekai/service"
"geekai/service/dalle" "geekai/service/image"
"geekai/service/moderation" "geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/store/model" "geekai/store/model"
@@ -25,17 +25,17 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type DallJobHandler struct { type ImageJobHandler struct {
BaseHandler BaseHandler
dallService *dalle.Service imageService *image.Service
uploader *oss.UploaderManager uploader *oss.UploaderManager
userService *service.UserService userService *service.UserService
moderationManager *moderation.ServiceManager moderationManager *moderation.ServiceManager
} }
func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *DallJobHandler { func NewImageJobHandler(app *core.AppServer, db *gorm.DB, service *image.Service, manager *oss.UploaderManager, userService *service.UserService, moderationManager *moderation.ServiceManager) *ImageJobHandler {
return &DallJobHandler{ return &ImageJobHandler{
dallService: service, imageService: service,
uploader: manager, uploader: manager,
userService: userService, userService: userService,
moderationManager: moderationManager, moderationManager: moderationManager,
@@ -47,8 +47,8 @@ func NewDallJobHandler(app *core.AppServer, db *gorm.DB, service *dalle.Service,
} }
// RegisterRoutes 注册路由 // RegisterRoutes 注册路由
func (h *DallJobHandler) RegisterRoutes() { func (h *ImageJobHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/dall/") group := h.App.Engine.Group("/api/image/")
// 公开接口,不需要授权 // 公开接口,不需要授权
group.GET("imgWall", h.ImgWall) group.GET("imgWall", h.ImgWall)
@@ -65,8 +65,8 @@ func (h *DallJobHandler) RegisterRoutes() {
} }
// Image 创建一个绘画任务 // Image 创建一个绘画任务
func (h *DallJobHandler) Image(c *gin.Context) { func (h *ImageJobHandler) Image(c *gin.Context) {
var data types.DallTask var data types.ImageTask
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" { if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
@@ -82,7 +82,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
// 记录违规内容 // 记录违规内容
moderation := model.Moderation{ moderation := model.Moderation{
UserId: h.GetLoginUserId(c), UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceDalle, Source: types.ModerationSourceImage,
Input: data.Prompt, Input: data.Prompt,
Result: utils.JsonEncode(moderationResult), Result: utils.JsonEncode(moderationResult),
} }
@@ -114,23 +114,23 @@ 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.ImageTask{
UserId: uint(userId), UserId: uint(userId),
ModelId: chatModel.Id, ModelId: chatModel.Id,
ModelName: chatModel.Value, ModelName: chatModel.Name,
ModelValue: chatModel.Value,
Image: data.Image, 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.Base.AssistantModelId,
Power: chatModel.Power, Power: chatModel.Power,
} }
job := model.DallJob{ job := model.ImageJob{
UserId: uint(userId), UserId: uint(userId),
Prompt: data.Prompt, Prompt: data.Prompt,
Power: chatModel.Power, Power: chatModel.Power,
TaskInfo: utils.JsonEncode(task), Params: utils.JsonEncode(task),
} }
res := h.DB.Create(&job) res := h.DB.Create(&job)
if res.Error != nil { if res.Error != nil {
@@ -139,12 +139,12 @@ func (h *DallJobHandler) Image(c *gin.Context) {
} }
task.Id = job.Id task.Id = job.Id
h.dallService.PushTask(task) h.imageService.PushTask(task)
// 扣减算力 // 扣减算力
err = h.userService.DecreasePower(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 {
@@ -155,7 +155,7 @@ func (h *DallJobHandler) Image(c *gin.Context) {
} }
// ImgWall 照片墙 // ImgWall 照片墙
func (h *DallJobHandler) ImgWall(c *gin.Context) { func (h *ImageJobHandler) ImgWall(c *gin.Context) {
page := h.GetInt(c, "page", 0) page := h.GetInt(c, "page", 0)
pageSize := h.GetInt(c, "page_size", 0) pageSize := h.GetInt(c, "page_size", 0)
err, jobs := h.getData(true, 0, page, pageSize, true) err, jobs := h.getData(true, 0, page, pageSize, true)
@@ -167,8 +167,8 @@ func (h *DallJobHandler) ImgWall(c *gin.Context) {
resp.SUCCESS(c, jobs) resp.SUCCESS(c, jobs)
} }
// JobList 获取 SD 任务列表 // JobList 获取 Image 任务列表
func (h *DallJobHandler) JobList(c *gin.Context) { func (h *ImageJobHandler) JobList(c *gin.Context) {
finish := h.GetBool(c, "finish") finish := h.GetBool(c, "finish")
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
page := h.GetInt(c, "page", 0) page := h.GetInt(c, "page", 0)
@@ -185,7 +185,7 @@ func (h *DallJobHandler) JobList(c *gin.Context) {
} }
// JobList 获取任务列表 // JobList 获取任务列表
func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) { func (h *ImageJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
if finish { if finish {
@@ -205,21 +205,22 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
} }
// 统计总数 // 统计总数
var total int64 var total int64
session.Model(&model.DallJob{}).Count(&total) session.Model(&model.ImageJob{}).Count(&total)
var items []model.DallJob var items []model.ImageJob
res := session.Find(&items) res := session.Find(&items)
if res.Error != nil { if res.Error != nil {
return res.Error, vo.Page{} return res.Error, vo.Page{}
} }
var jobs = make([]vo.DallJob, 0) var jobs = make([]vo.ImageJob, 0)
for _, item := range items { for _, item := range items {
var job vo.DallJob var job vo.ImageJob
err := utils.CopyObject(item, &job) err := utils.CopyObject(item, &job)
if err != nil { if err != nil {
continue continue
} }
job.CreatedAt = item.CreatedAt.Unix()
jobs = append(jobs, job) jobs = append(jobs, job)
} }
@@ -227,10 +228,10 @@ func (h *DallJobHandler) getData(finish bool, userId uint, page int, pageSize in
} }
// Remove remove task image // Remove remove task image
func (h *DallJobHandler) Remove(c *gin.Context) { func (h *ImageJobHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
var job model.DallJob var job model.ImageJob
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil { if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在") resp.ERROR(c, "记录不存在")
return return
@@ -253,12 +254,12 @@ func (h *DallJobHandler) Remove(c *gin.Context) {
} }
// Publish 发布/取消发布图片到画廊显示 // Publish 发布/取消发布图片到画廊显示
func (h *DallJobHandler) Publish(c *gin.Context) { func (h *ImageJobHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0) id := h.GetInt(c, "id", 0)
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.DallJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error err := h.DB.Model(&model.ImageJob{Id: uint(id), UserId: userId}).UpdateColumn("publish", action).Error
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
@@ -267,7 +268,7 @@ func (h *DallJobHandler) Publish(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
func (h *DallJobHandler) GetModels(c *gin.Context) { func (h *ImageJobHandler) GetModels(c *gin.Context) {
var models []model.ChatModel var models []model.ChatModel
err := h.DB.Where("type", "img").Where("enabled", true).Find(&models).Error err := h.DB.Where("type", "img").Where("enabled", true).Find(&models).Error
if err != nil { if err != nil {
+99 -212
View File
@@ -1,6 +1,7 @@
package handler package handler
import ( import (
"errors"
"fmt" "fmt"
"geekai/core" "geekai/core"
"geekai/core/middleware" "geekai/core/middleware"
@@ -38,49 +39,28 @@ func NewJimengHandler(app *core.AppServer, jimengService *jimeng.Service, db *go
// RegisterRoutes 注册路由,新增统一任务接口 // RegisterRoutes 注册路由,新增统一任务接口
func (h *JimengHandler) RegisterRoutes() { func (h *JimengHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/jimeng/") 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.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{ {
group.POST("task", h.CreateTask) group.POST("task", h.CreateTask)
group.GET("power-config", h.GetPowerConfig)
group.POST("jobs", h.Jobs) group.POST("jobs", h.Jobs)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("retry", h.Retry) group.GET("retry", h.Retry)
} }
} }
// JimengTaskRequest 统一任务请求结构体
// 支持所有生图和生成视频类型
type JimengTaskRequest struct {
TaskType string `json:"task_type" binding:"required"`
Prompt string `json:"prompt"`
ImageInput string `json:"image_input"`
ImageUrls []string `json:"image_urls"`
BinaryDataBase64 []string `json:"binary_data_base64"`
Scale float64 `json:"scale"`
Width int `json:"width"`
Height int `json:"height"`
Gpen float64 `json:"gpen"`
Skin float64 `json:"skin"`
SkinUnifi float64 `json:"skin_unifi"`
GenMode string `json:"gen_mode"`
Seed int64 `json:"seed"`
UsePreLLM bool `json:"use_pre_llm"`
TemplateId string `json:"template_id"`
AspectRatio string `json:"aspect_ratio"`
}
// CreateTask 统一任务创建接口 // CreateTask 统一任务创建接口
func (h *JimengHandler) CreateTask(c *gin.Context) { func (h *JimengHandler) CreateTask(c *gin.Context) {
var req JimengTaskRequest var req types.JimengTaskRequest
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
// 文本审核 // 文本审核
if h.App.SysConfig.Moderation.Enable { if h.App.SysConfig.Moderation.Enable && req.Prompt != "" {
moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt) moderationResult, err := h.moderationManager.GetService().Moderate(req.Prompt)
if err != nil { if err != nil {
logger.Error("failed to moderate content: ", err) logger.Error("failed to moderate content: ", err)
@@ -103,136 +83,21 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
} }
// 新增:除图像特效外,其他任务类型必须有提示词 if req.Prompt == "" && len(req.ImageUrls) == 0 {
if req.TaskType != "image_effects" && req.Prompt == "" { resp.ERROR(c, "提示词和图片不能同时为空")
resp.ERROR(c, "提示词不能为空")
return 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 req.Width == 0 { // 获取算力消耗
req.Width = 1328 powerCost, err := h.getTaskPower(req)
} if err != nil {
if req.Height == 0 { resp.ERROR(c, "计算任务消耗积分失败: "+err.Error())
req.Height = 1328
}
if req.Seed == 0 {
req.Seed = -1
}
var powerCost int
var taskType model.JMTaskType
var params map[string]any
var reqKey string
var modelName string
switch req.TaskType {
case "text_to_image":
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToImage)
taskType = model.JMTaskTypeTextToImage
reqKey = jimeng.ReqKeyTextToImage
modelName = "即梦文生图"
if req.Scale == 0 {
req.Scale = 2.5
}
params = map[string]any{
"seed": req.Seed,
"scale": req.Scale,
"width": req.Width,
"height": req.Height,
"use_pre_llm": req.UsePreLLM,
}
case "image_to_image":
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToImage)
taskType = model.JMTaskTypeImageToImage
reqKey = jimeng.ReqKeyImageToImagePortrait
modelName = "即梦图生图"
if req.Gpen == 0 {
req.Gpen = 0.4
}
if req.Skin == 0 {
req.Skin = 0.3
}
if req.GenMode == "" {
if req.Prompt != "" {
req.GenMode = jimeng.GenModeCreative
} else {
req.GenMode = jimeng.GenModeReference
}
}
params = map[string]any{
"image_input": req.ImageInput,
"width": req.Width,
"height": req.Height,
"gpen": req.Gpen,
"skin": req.Skin,
"skin_unifi": req.SkinUnifi,
"gen_mode": req.GenMode,
"seed": req.Seed,
}
case "image_edit":
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEdit)
taskType = model.JMTaskTypeImageEdit
reqKey = jimeng.ReqKeyImageEdit
modelName = "即梦图像编辑"
if req.Scale == 0 {
req.Scale = 0.5
}
params = map[string]any{
"seed": req.Seed,
"scale": req.Scale,
}
params["image_urls"] = []string{req.ImageInput}
case "image_effects":
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageEffects)
taskType = model.JMTaskTypeImageEffects
reqKey = jimeng.ReqKeyImageEffects
modelName = "即梦图像特效"
if req.Width == 0 {
req.Width = 1328
}
if req.Height == 0 {
req.Height = 1328
}
params = map[string]any{
"image_input1": req.ImageInput,
"template_id": req.TemplateId,
"width": req.Width,
"height": req.Height,
}
case "text_to_video":
powerCost = h.getPowerFromConfig(model.JMTaskTypeTextToVideo)
taskType = model.JMTaskTypeTextToVideo
reqKey = jimeng.ReqKeyTextToVideo
modelName = "即梦文生视频"
if req.AspectRatio == "" {
req.AspectRatio = jimeng.AspectRatio16_9
}
params = map[string]any{
"seed": req.Seed,
"aspect_ratio": req.AspectRatio,
}
case "image_to_video":
powerCost = h.getPowerFromConfig(model.JMTaskTypeImageToVideo)
taskType = model.JMTaskTypeImageToVideo
reqKey = jimeng.ReqKeyImageToVideo
modelName = "即梦图生视频"
params = map[string]any{
"seed": req.Seed,
"aspect_ratio": req.AspectRatio,
}
if len(req.ImageUrls) > 0 {
params["image_urls"] = req.ImageUrls
}
if len(req.BinaryDataBase64) > 0 {
params["binary_data_base64"] = req.BinaryDataBase64
}
default:
resp.ERROR(c, "不支持的任务类型")
return return
} }
@@ -240,16 +105,9 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost)) resp.ERROR(c, fmt.Sprintf("算力不足,需要%d算力", powerCost))
return return
} }
req.Power = powerCost
taskReq := &jimeng.CreateTaskRequest{ job, err := h.jimengService.CreateTask(user.Id, &req)
Type: taskType,
Prompt: req.Prompt,
Params: params,
ReqKey: reqKey,
Power: powerCost,
}
job, err := h.jimengService.CreateTask(user.Id, taskReq)
if err != nil { if err != nil {
logger.Errorf("create jimeng task failed: %v", err) logger.Errorf("create jimeng task failed: %v", err)
resp.ERROR(c, "创建任务失败") resp.ERROR(c, "创建任务失败")
@@ -258,11 +116,42 @@ func (h *JimengHandler) CreateTask(c *gin.Context) {
h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{ h.userService.DecreasePower(user.Id, powerCost, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: "jimeng", Model: job.ReqKey,
Remark: fmt.Sprintf("%s,任务ID%d", modelName, job.Id), Remark: h.getTaskRemark(req, job.Id),
}) })
resp.SUCCESS(c, job) 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 获取任务列表 // Jobs 获取任务列表
@@ -287,17 +176,13 @@ func (h *JimengHandler) Jobs(c *gin.Context) {
switch req.Filter { switch req.Filter {
case "image": case "image":
query = query.Where("type IN (?)", []model.JMTaskType{ query = query.Where("type = ?", types.JMTaskTypeImage)
model.JMTaskTypeTextToImage,
model.JMTaskTypeImageToImage,
model.JMTaskTypeImageEdit,
model.JMTaskTypeImageEffects,
})
case "video": case "video":
query = query.Where("type IN (?)", []model.JMTaskType{ query = query.Where("type = ?", types.JMTaskTypeVideo)
model.JMTaskTypeTextToVideo, case "virtual_human":
model.JMTaskTypeImageToVideo, query = query.Where("type = ?", types.JMTaskTypeVirtualHuman)
}) case "action_transfer":
query = query.Where("type = ?", types.JMTaskTypeActionTransfer)
} }
if len(req.Ids) > 0 { if len(req.Ids) > 0 {
@@ -357,34 +242,17 @@ func (h *JimengHandler) Remove(c *gin.Context) {
} }
// 正在运行中的任务不能删除 // 正在运行中的任务不能删除
if job.Status == model.JMTaskStatusGenerating || job.Status == model.JMTaskStatusInQueue { if job.Status == types.JMTaskStatusGenerating || job.Status == types.JMTaskStatusInQueue {
resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力") resp.ERROR(c, "正在运行中的任务不能删除,否则无法退回算力")
return return
} }
tx := h.DB.Begin() if err := h.DB.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
if err := tx.Where("id = ? AND user_id = ?", jobId, user.Id).Delete(&model.JimengJob{}).Error; err != nil {
logger.Errorf("delete jimeng job failed: %v", err) logger.Errorf("delete jimeng job failed: %v", err)
resp.ERROR(c, "删除任务失败") resp.ERROR(c, "删除任务失败")
return return
} }
// 失败任务删除后退回算力
if job.Status != model.JMTaskStatusFailed {
err = h.userService.IncreasePower(user.Id, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "jimeng",
Remark: fmt.Sprintf("删除任务,退回%d算力", job.Power),
})
if err != nil {
resp.ERROR(c, "退回算力失败")
tx.Rollback()
return
}
}
tx.Commit()
resp.SUCCESS(c, gin.H{}) resp.SUCCESS(c, gin.H{})
} }
@@ -411,13 +279,13 @@ func (h *JimengHandler) Retry(c *gin.Context) {
} }
// 只有失败的任务才能重试 // 只有失败的任务才能重试
if job.Status != model.JMTaskStatusFailed { if job.Status != types.JMTaskStatusFailed {
resp.ERROR(c, "只有失败的任务才能重试") resp.ERROR(c, "只有失败的任务才能重试")
return return
} }
// 重置任务状态 // 重置任务状态
if err := h.jimengService.UpdateJobStatus(uint(jobId), model.JMTaskStatusInQueue, ""); err != nil { if err := h.jimengService.UpdateJobStatus(uint(jobId), types.JMTaskStatusInQueue, ""); err != nil {
logger.Errorf("reset job status failed: %v", err) logger.Errorf("reset job status failed: %v", err)
resp.ERROR(c, "重置任务状态失败") resp.ERROR(c, "重置任务状态失败")
return return
@@ -433,25 +301,49 @@ func (h *JimengHandler) Retry(c *gin.Context) {
resp.SUCCESS(c, gin.H{"message": "重试任务已提交"}) resp.SUCCESS(c, gin.H{"message": "重试任务已提交"})
} }
// getPowerFromConfig 从配置中获取指定类型的算力消耗 func (h *JimengHandler) getTaskPower(req types.JimengTaskRequest) (int, error) {
func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int { logger.Debugf("getTaskPower req: %+v", req)
config := h.App.SysConfig.Jimeng config := h.App.SysConfig.Jimeng
basePower, ok := config.Powers[req.ReqKey]
switch taskType { if !ok || basePower <= 0 {
case model.JMTaskTypeTextToImage: return 0, errors.New("未配置模型积分或配置不合法")
return config.Power.TextToImage }
case model.JMTaskTypeImageToImage: switch req.TaskType {
return config.Power.ImageToImage case types.JMTaskTypeImage:
case model.JMTaskTypeImageEdit: return basePower, nil
return config.Power.ImageEdit case types.JMTaskTypeVideo:
case model.JMTaskTypeImageEffects: if req.Duration == 0 {
return config.Power.ImageEffects return 0, errors.New("视频时长不能为0")
case model.JMTaskTypeTextToVideo: }
return config.Power.TextToVideo return basePower * req.Duration, nil
case model.JMTaskTypeImageToVideo: case types.JMTaskTypeVirtualHuman:
return config.Power.ImageToVideo 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: default:
return 10 return 0, errors.New("任务类型不支持")
} }
} }
@@ -459,11 +351,6 @@ func (h *JimengHandler) getPowerFromConfig(taskType model.JMTaskType) int {
func (h *JimengHandler) GetPowerConfig(c *gin.Context) { func (h *JimengHandler) GetPowerConfig(c *gin.Context) {
config := h.App.SysConfig.Jimeng config := h.App.SysConfig.Jimeng
resp.SUCCESS(c, gin.H{ resp.SUCCESS(c, gin.H{
"text_to_image": config.Power.TextToImage, "powers": config.Powers,
"image_to_image": config.Power.ImageToImage,
"image_edit": config.Power.ImageEdit,
"image_effects": config.Power.ImageEffects,
"text_to_video": config.Power.TextToVideo,
"image_to_video": config.Power.ImageToVideo,
}) })
} }
+18
View File
@@ -30,6 +30,7 @@ func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler {
func (h *MenuHandler) RegisterRoutes() { func (h *MenuHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/menu/") group := h.App.Engine.Group("/api/menu/")
group.GET("list", h.List) group.GET("list", h.List)
group.GET("list/all", h.ListAll)
} }
// List 数据列表 // List 数据列表
@@ -54,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)
}
+116 -6
View File
@@ -63,6 +63,7 @@ func (h *MidJourneyHandler) RegisterRoutes() {
group.POST("image", h.Image) group.POST("image", h.Image)
group.POST("upscale", h.Upscale) group.POST("upscale", h.Upscale)
group.POST("variation", h.Variation) group.POST("variation", h.Variation)
group.POST("modal", h.Modal)
group.GET("jobs", h.JobList) group.GET("jobs", h.JobList)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("publish", h.Publish) group.GET("publish", h.Publish)
@@ -82,7 +83,31 @@ func (h *MidJourneyHandler) preCheck(c *gin.Context) bool {
} }
return true return true
}
// preCheckPower 检查用户算力是否 >= required,不足时写 ERROR 并返回 false
func (h *MidJourneyHandler) preCheckPower(c *gin.Context, required int) bool {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return false
}
if required <= 0 {
required = h.App.SysConfig.Base.MjActionPower
}
if user.Power < required {
resp.ERROR(c, "当前用户剩余算力不足以完成本次操作!")
return false
}
return true
}
// mjActionPower 取分项算力,若未配置则回退 MjActionPower
func mjActionPower(base int, fallback int) int {
if base > 0 {
return base
}
return fallback
} }
// Image 创建一个绘画任务 // Image 创建一个绘画任务
@@ -109,7 +134,14 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
if !h.preCheck(c) { // 按任务类型计算所需算力
power := h.App.SysConfig.Base.MjPower
if data.TaskType == types.TaskBlend.String() {
power = mjActionPower(h.App.SysConfig.Base.MjBlendPower, h.App.SysConfig.Base.MjActionPower)
} else if data.TaskType == types.TaskSwapFace.String() {
power = mjActionPower(h.App.SysConfig.Base.MjSwapFacePower, h.App.SysConfig.Base.MjActionPower)
}
if !h.preCheckPower(c, power) {
return return
} }
@@ -215,7 +247,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
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.Base.MjPower, Power: power,
CreatedAt: time.Now(), CreatedAt: time.Now(),
} }
opt := "绘图" opt := "绘图"
@@ -264,7 +296,8 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
return return
} }
if !h.preCheck(c) { power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, h.App.SysConfig.Base.MjActionPower)
if !h.preCheckPower(c, power) {
return return
} }
@@ -286,7 +319,7 @@ func (h *MidJourneyHandler) Upscale(c *gin.Context) {
TaskId: taskId, TaskId: taskId,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
Progress: 0, Progress: 0,
Power: h.App.SysConfig.Base.MjActionPower, Power: power,
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 {
@@ -319,7 +352,8 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
return return
} }
if !h.preCheck(c) { power := mjActionPower(h.App.SysConfig.Base.MjUpscalePower, h.App.SysConfig.Base.MjActionPower)
if !h.preCheckPower(c, power) {
return return
} }
@@ -342,7 +376,7 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
TaskId: taskId, TaskId: taskId,
TaskInfo: utils.JsonEncode(task), TaskInfo: utils.JsonEncode(task),
Progress: 0, Progress: 0,
Power: h.App.SysConfig.Base.MjActionPower, Power: power,
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 {
@@ -366,6 +400,81 @@ func (h *MidJourneyHandler) Variation(c *gin.Context) {
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// modalReq 局部重绘请求参数
type modalReq struct {
TaskId string `json:"task_id"` // 原图任务 IDmessage_id
ChannelId string `json:"channel_id"` // 渠道 ID
Prompt string `json:"prompt"` // 提示词
MaskBase64 string `json:"mask_base64,omitempty"` // 蒙版 base64,可选
}
// Modal 提交局部重绘(inpaint
func (h *MidJourneyHandler) Modal(c *gin.Context) {
var data modalReq
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
if data.TaskId == "" || data.ChannelId == "" {
resp.ERROR(c, "task_id 与 channel_id 必填")
return
}
if data.Prompt == "" {
resp.ERROR(c, "请填写局部重绘提示词")
return
}
power := mjActionPower(h.App.SysConfig.Base.MjModalPower, h.App.SysConfig.Base.MjActionPower)
if !h.preCheckPower(c, power) {
return
}
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, _ := h.snowflake.Next(true)
// 原图 message_id 必须传入 API,同时写入 TaskId/MessageId 避免序列化 omitempty 丢失
task := types.MjTask{
Type: types.TaskModal,
UserId: userId,
ChannelId: data.ChannelId,
TaskId: data.TaskId,
MessageId: data.TaskId,
Prompt: data.Prompt,
MaskBase64: data.MaskBase64,
Mode: h.App.SysConfig.Base.MjMode,
}
job := model.MidJourneyJob{
Type: types.TaskModal.String(),
ChannelId: data.ChannelId,
UserId: uint(userId),
TaskId: taskId,
TaskInfo: utils.JsonEncode(task),
Progress: 0,
Prompt: data.Prompt,
Power: power,
CreatedAt: time.Now(),
}
if res := h.DB.Create(&job); res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "添加任务失败:"+res.Error.Error())
return
}
task.Id = job.Id
h.mjService.PushTask(task)
err := h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "mid-journey",
Remark: fmt.Sprintf("局部重绘操作,任务ID%s", job.TaskId),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// ImgWall 照片墙 // ImgWall 照片墙
func (h *MidJourneyHandler) ImgWall(c *gin.Context) { func (h *MidJourneyHandler) ImgWall(c *gin.Context) {
page := h.GetInt(c, "page", 0) page := h.GetInt(c, "page", 0)
@@ -432,6 +541,7 @@ func (h *MidJourneyHandler) getData(finish bool, userId uint, page int, pageSize
if err != nil { if err != nil {
continue continue
} }
job.CreatedAt = item.CreatedAt.Unix()
jobs = append(jobs, job) jobs = append(jobs, job)
} }
return nil, vo.NewPage(total, page, pageSize, jobs) return nil, vo.NewPage(total, page, pageSize, jobs)
+7 -2
View File
@@ -138,6 +138,7 @@ func (h *PaymentHandler) SyncOrders() error {
} }
case payment.PayChannelAL: case payment.PayChannelAL:
res, err = h.alipayService.Query(order.OrderNo) res, err = h.alipayService.Query(order.OrderNo)
logger.Debugf("支付宝支付订单状态:%+v", res)
if err != nil { if err != nil {
logger.Errorf("error with query order info: %v", err) logger.Errorf("error with query order info: %v", err)
continue continue
@@ -215,7 +216,7 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
data.Domain = h.config.WxPay.Domain data.Domain = h.config.WxPay.Domain
} }
notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain) notifyURL = fmt.Sprintf("%s/api/payment/notify/wxpay", data.Domain)
payURL, err = h.wxpayService.Pay(payment.PayRequest{ params := payment.PayRequest{
OutTradeNo: orderNo, OutTradeNo: orderNo,
TotalFee: fmt.Sprintf("%d", int(amount*100)), TotalFee: fmt.Sprintf("%d", int(amount*100)),
Subject: product.Name, Subject: product.Name,
@@ -223,7 +224,11 @@ func (h *PaymentHandler) CreateOrder(c *gin.Context) {
ClientIP: c.ClientIP(), ClientIP: c.ClientIP(),
Device: data.Device, Device: data.Device,
PayWay: payment.PayWayWX, PayWay: payment.PayWayWX,
}) }
if data.Device == "mobile" {
params.OpenID = user.OpenId
}
payURL, err = h.wxpayService.Pay(params)
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
+547
View File
@@ -0,0 +1,547 @@
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"
"errors"
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/ppt"
"geekai/utils"
"geekai/utils/resp"
"net/http"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// PPTTaskHandler 用户侧 PPT 生成任务处理器(薄层:参数、鉴权、调用 PptService)
type PPTTaskHandler struct {
BaseHandler
snowflake *service.Snowflake
pptService *ppt.PptService
}
// NewPPTTaskHandler 创建 PPT 任务处理器
func NewPPTTaskHandler(app *core.AppServer, db *gorm.DB, snowflake *service.Snowflake, pptService *ppt.PptService) *PPTTaskHandler {
return &PPTTaskHandler{
BaseHandler: BaseHandler{App: app, DB: db},
snowflake: snowflake,
pptService: pptService,
}
}
// RegisterRoutes 注册 PPT 任务相关路由
func (h *PPTTaskHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/v1/tasks")
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{
group.POST("generate-slides", h.CreateTask)
group.POST("generate-slides/from-file", h.CreateTaskFromFile)
group.GET("", h.ListTasks) // 当前用户任务列表,须在 :task_id 前注册
group.GET(":task_id/export", h.ExportTask)
group.POST(":task_id/resume", h.ResumeTask)
group.POST(":task_id/slides/:slide_index/edit-image", h.EditSlideImage)
group.PATCH(":task_id/slides/:slide_index/active-image", h.SetActiveSlideImage)
group.GET(":task_id", h.GetTask)
group.DELETE(":task_id", h.DeleteTask)
}
}
type createPPTTaskRequest struct {
Content string `json:"content"`
Prompt string `json:"prompt"`
Language string `json:"language"` // 如 zh-CN, en,约束分镜输出语言
Pages int `json:"pages"` // 目标页数,0 表示用配置默认值
Mode string `json:"mode"` // 生成模式:detailed=详细演示文稿,slides=演示用幻灯片,空则默认 slides
}
type createPPTTaskFromFileRequest struct {
FileURL string `json:"file_url"`
Prompt string `json:"prompt"`
Language string `json:"language"` // 如 zh-CN, en,约束分镜输出语言
Pages int `json:"pages"` // 目标页数,0 表示用配置默认值
Mode string `json:"mode"` // 生成模式:detailed=详细演示文稿,slides=演示用幻灯片,空则默认 slides
}
// CreateTask 创建 PPT 生成任务(异步)
func (h *PPTTaskHandler) CreateTask(c *gin.Context) {
var req createPPTTaskRequest
if err := c.ShouldBindJSON(&req); err != nil || req.Content == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
taskID, err := h.snowflake.Next(true)
if err != nil {
resp.ERROR(c, "生成任务ID失败:"+err.Error())
return
}
task, cfg, err := h.pptService.BuildPendingTask(taskID, user.Id, int(user.Power), req.Content, req.Prompt, req.Language, req.Mode, req.Pages)
if err != nil {
if errors.Is(err, ppt.ErrInsufficientPower) {
resp.ERROR(c, "当前用户算力不足以完成本次 PPT 生成任务!")
return
}
resp.ERROR(c, "加载 PPT 配置或校验失败:"+err.Error())
return
}
h.pptService.CreateTask(c.Request.Context(), task, cfg)
go h.pptService.RunTask(context.Background(), task, cfg)
resp.SUCCESS(c, map[string]any{
"task_id": taskID,
"status": ppt.TaskStatusPending,
})
}
// CreateTaskFromFile 创建 PPT 生成任务(基于上传材料文本提炼)
// 支持文件:PDF、Worddoc/docx)、TXT、Markdownmd/markdown
func (h *PPTTaskHandler) CreateTaskFromFile(c *gin.Context) {
var req createPPTTaskFromFileRequest
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.FileURL) == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
fileURL := strings.TrimSpace(req.FileURL)
// 去掉 query,避免 ".pdf?xxx" 这种导致 ext 识别失败
urlNoQuery := strings.Split(fileURL, "?")[0]
ext := strings.ToLower(filepath.Ext(urlNoQuery))
if ext == "" {
resp.ERROR(c, "不支持的文件格式")
return
}
allowedExts := map[string]bool{
".pdf": true,
".doc": true,
".docx": true,
".txt": true,
".md": true,
".markdown": true,
}
if !allowedExts[ext] {
resp.ERROR(c, "不支持的文件格式")
return
}
designPrompt := strings.TrimSpace(req.Prompt)
language := strings.TrimSpace(req.Language)
if language == "" {
language = "zh-CN"
}
mode := strings.TrimSpace(req.Mode)
pages := req.Pages
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
taskID, err := h.snowflake.Next(true)
if err != nil {
resp.ERROR(c, "生成任务ID失败:"+err.Error())
return
}
// 1) 下载文件并提取原始文本(Notebook 风格提炼前的输入)
rawText, err := h.extractMaterialTextFromURL(c.Request.Context(), fileURL, ext)
if err != nil || strings.TrimSpace(rawText) == "" {
if err == nil {
err = errors.New("empty extracted text")
}
resp.ERROR(c, "读取文件失败:"+err.Error())
return
}
// 2) 校验算力与页数,组装待写入的 Task(先在提炼前做 power 检查)
task, cfg, err := h.pptService.BuildPendingTask(taskID, user.Id, int(user.Power), rawText, designPrompt, language, mode, pages)
if err != nil {
if errors.Is(err, ppt.ErrInsufficientPower) {
resp.ERROR(c, "当前用户算力不足以完成本次 PPT 生成任务!")
return
}
resp.ERROR(c, "加载 PPT 配置或校验失败:"+err.Error())
return
}
// 3) NotebookLM 风格提炼:rawText -> PPT 可用的 content(大纲/结构化要点)
notebookContent, err := h.pptService.GenerateNotebookContent(c.Request.Context(), cfg, rawText, designPrompt, language)
if err != nil || strings.TrimSpace(notebookContent) == "" {
if err == nil {
err = errors.New("empty notebook content")
}
resp.ERROR(c, "提炼材料失败:"+err.Error())
return
}
task.Content = notebookContent
// 4) 落库 + 异步生成分镜与图片
err = h.pptService.CreateTask(c.Request.Context(), task, cfg)
if err != nil {
resp.ERROR(c, "创建任务失败:"+err.Error())
return
}
go h.pptService.RunTask(context.Background(), task, cfg)
resp.SUCCESS(c, map[string]any{
"task_id": taskID,
"status": ppt.TaskStatusPending,
})
}
func (h *PPTTaskHandler) extractMaterialTextFromURL(ctx context.Context, fileURL string, ext string) (string, error) {
if strings.TrimSpace(fileURL) == "" {
return "", errors.New("empty file url")
}
switch ext {
case ".txt", ".md", ".markdown":
b, status, err := utils.FetchURLBytes(ctx, fileURL, "", 30*time.Second, 2, 8<<20)
if err != nil {
// status=0 时通常是请求阶段错误(例如 TLS 握手超时)
return "", fmt.Errorf("download file failed: status=%d: %w", status, err)
}
return strings.TrimSpace(string(b)), nil
default:
// PDF/Word 等走 Tika
return utils.ReadFileContent(fileURL, h.App.Config.TikaHost)
}
}
// ListTasks 当前用户的 PPT 任务列表(分页)
func (h *PPTTaskHandler) ListTasks(c *gin.Context) {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
slice, total := h.pptService.ListUserTasks(c.Request.Context(), user.Id, page, pageSize)
jobs := make([]map[string]any, 0, len(slice))
for _, t := range slice {
job := t.TaskSummaryMap()
job["prompt"] = t.Prompt
if t.ErrorMessage != "" {
job["error_message"] = t.ErrorMessage
}
jobs = append(jobs, job)
}
resp.SUCCESS(c, map[string]any{
"jobs": jobs,
"total": total,
"page": page,
"page_size": pageSize,
})
}
// ExportTask 导出任务幻灯片为 PDF 或 PPTX(按图片逐页)
func (h *PPTTaskHandler) ExportTask(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
ef, ok := ppt.ParseExportFormat(c.Query("format"))
if !ok {
resp.ERROR(c, "format 参数无效,支持 pdf 或 pptx")
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
task, exists := h.pptService.GetTask(taskID)
if !exists {
resp.ERROR(c, "任务不存在")
return
}
if user.Id != task.UserID {
resp.NotAuth(c)
return
}
if task.Status != ppt.TaskStatusCompleted {
resp.ERROR(c, "仅已完成任务可导出")
return
}
ossCfg := types.OSSConfig{}
if h.App.SysConfig != nil {
ossCfg = h.App.SysConfig.OSS
}
data, err := ppt.BuildExportBytes(c.Request.Context(), task.Slides, ef, ossCfg, h.App.Config)
if err != nil {
resp.ERROR(c, err.Error())
return
}
base := ppt.SanitizeExportBaseName(task.Title, task.TaskID)
filename := base + ppt.ExportFileExt(ef)
c.Header("Content-Disposition", ppt.ContentDispositionAttachment(filename))
c.Data(http.StatusOK, ppt.ExportMimeType(ef), data)
}
// ResumeTask 继续生成缺图页(POST /api/v1/tasks/:task_id/resume
func (h *PPTTaskHandler) ResumeTask(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
err = h.pptService.ResumeTask(c.Request.Context(), taskID, user.Id)
if err != nil {
if errors.Is(err, ppt.ErrPptTaskNotFound) {
resp.ERROR(c, "任务不存在")
return
}
if errors.Is(err, ppt.ErrPptTaskBusy) {
resp.ERROR(c, "任务正在处理中,请稍后再试")
return
}
if errors.Is(err, ppt.ErrPptTaskNotResumable) {
resp.ERROR(c, "当前任务无法继续生成(已完成或分镜数据不完整)")
return
}
if errors.Is(err, ppt.ErrInsufficientPower) {
resp.ERROR(c, "当前用户算力不足以完成剩余图片生成")
return
}
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, map[string]any{
"task_id": taskID,
"status": ppt.TaskStatusProcessing,
})
}
type editSlideImageRequest struct {
Prompt string `json:"prompt"`
}
type activeSlideImageRequest struct {
VersionIndex int `json:"version_index"`
}
// EditSlideImage 图生图编辑当前页(基于激活图)
func (h *PPTTaskHandler) EditSlideImage(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
slideIndexStr := strings.TrimSpace(c.Param("slide_index"))
if taskID == "" || slideIndexStr == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
slideIndex, err := strconv.Atoi(slideIndexStr)
if err != nil || slideIndex < 1 {
resp.ERROR(c, types.InvalidArgs)
return
}
var req editSlideImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
ossCfg := types.OSSConfig{}
if h.App.SysConfig != nil {
ossCfg = h.App.SysConfig.OSS
}
slides, err := h.pptService.EditSlideImage(c.Request.Context(), taskID, user.Id, slideIndex, req.Prompt, ossCfg, h.App.Config)
if err != nil {
if errors.Is(err, ppt.ErrPptTaskNotFound) {
resp.ERROR(c, "任务不存在")
return
}
if errors.Is(err, ppt.ErrPptSlideNotFound) {
resp.ERROR(c, "幻灯片不存在")
return
}
if errors.Is(err, ppt.ErrPptSlideNoImage) {
resp.ERROR(c, "该页暂无配图,无法编辑")
return
}
if errors.Is(err, ppt.ErrInsufficientPower) {
resp.ERROR(c, "当前用户算力不足以完成本次编辑")
return
}
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, map[string]any{"slides": slides})
}
// SetActiveSlideImage 切换当前页激活的历史版本
func (h *PPTTaskHandler) SetActiveSlideImage(c *gin.Context) {
taskID := strings.TrimSpace(c.Param("task_id"))
slideIndexStr := strings.TrimSpace(c.Param("slide_index"))
if taskID == "" || slideIndexStr == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
slideIndex, err := strconv.Atoi(slideIndexStr)
if err != nil || slideIndex < 1 {
resp.ERROR(c, types.InvalidArgs)
return
}
var req activeSlideImageRequest
if err := c.ShouldBindJSON(&req); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
slides, err := h.pptService.SetActiveSlideVersion(taskID, user.Id, slideIndex, req.VersionIndex)
if err != nil {
if errors.Is(err, ppt.ErrPptTaskNotFound) {
resp.ERROR(c, "任务不存在")
return
}
if errors.Is(err, ppt.ErrPptSlideNotFound) {
resp.ERROR(c, "幻灯片不存在")
return
}
if errors.Is(err, ppt.ErrPptInvalidVersionIndex) {
resp.ERROR(c, "无效的历史版本序号")
return
}
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, map[string]any{"slides": slides})
}
// GetTask 查询 PPT 任务进度
func (h *PPTTaskHandler) GetTask(c *gin.Context) {
taskId := c.Param("task_id")
if taskId == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
task, ok := h.pptService.GetTask(taskId)
if !ok {
resp.ERROR(c, "任务不存在")
return
}
user, err := h.GetLoginUser(c)
if err != nil || user.Id != task.UserID {
resp.NotAuth(c)
return
}
percentage := 0
if task.Total > 0 {
percentage = int(float64(task.Completed) / float64(task.Total) * 100)
}
resp.SUCCESS(c, map[string]any{
"task_id": task.TaskID,
"status": task.Status,
"progress": map[string]any{
"total_slides": task.Total,
"completed_slides": task.Completed,
"percentage": percentage,
},
"slides": task.Slides,
"error_message": task.ErrorMessage,
"content": task.Content,
"prompt": task.Prompt,
"title": task.Title,
"thumb": task.Thumb,
})
}
// DeleteTask 删除用户 PPT 任务(仅允许 completed / failed),同时删除该任务生成的图片对象。
func (h *PPTTaskHandler) DeleteTask(c *gin.Context) {
taskID := c.Param("task_id")
if taskID == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return
}
if err := h.pptService.DeleteTask(taskID, user.Id); err != nil {
if errors.Is(err, ppt.ErrPptTaskNotFound) {
resp.ERROR(c, "任务不存在")
return
}
if errors.Is(err, ppt.ErrPptTaskNotDeletable) {
resp.ERROR(c, "仅允许删除已完成/已失败任务")
return
}
// 前端会统一在文案里拼接“删除失败:”,这里避免重复。
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{"message": "删除成功"})
}
+4 -3
View File
@@ -179,7 +179,7 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
} }
apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL) apiURL := fmt.Sprintf("%s/v1/chat/completions", apiKey.ApiURL)
logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, "advanced-voice") logger.Infof("Sending %s request, API KEY:%s, PROXY: %s, Model: %s", apiKey.ApiURL, apiURL, apiKey.ProxyURL, "advanced-voice")
r, err := client.R().SetHeader("Body-Type", "application/json"). r, err := client.R().SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+apiKey.Value). SetHeader("Authorization", "Bearer "+apiKey.Value).
SetBody(types.ApiRequest{ SetBody(types.ApiRequest{
Model: "advanced-voice", Model: "advanced-voice",
@@ -221,11 +221,12 @@ func (h *RealtimeHandler) VoiceChat(c *gin.Context) {
return return
} }
logger.Infof("Response: %v", response.Choices[0].Message.Content) replyText := utils.NormalizeAssistantContent(response.Choices[0].Message.Content)
logger.Infof("Response: %v", replyText)
// 提取链接 // 提取链接
re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`) re := regexp.MustCompile(`\[(.*?)\]\((.*?)\)`)
links := re.FindAllStringSubmatch(response.Choices[0].Message.Content, -1) links := re.FindAllStringSubmatch(replyText, -1)
var url = "" var url = ""
if len(links) > 0 { if len(links) > 0 {
url = links[0][2] url = links[0][2]
-328
View File
@@ -1,328 +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 (
"fmt"
"geekai/core"
"geekai/core/middleware"
"geekai/core/types"
"geekai/service"
"geekai/service/moderation"
"geekai/service/oss"
"geekai/service/sd"
"geekai/store"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"time"
"github.com/gin-gonic/gin"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
type SdJobHandler struct {
BaseHandler
redis *redis.Client
sdService *sd.Service
uploader *oss.UploaderManager
snowflake *service.Snowflake
leveldb *store.LevelDB
userService *service.UserService
moderationManager *moderation.ServiceManager
}
func NewSdJobHandler(app *core.AppServer,
db *gorm.DB,
service *sd.Service,
manager *oss.UploaderManager,
snowflake *service.Snowflake,
userService *service.UserService,
levelDB *store.LevelDB,
moderationManager *moderation.ServiceManager) *SdJobHandler {
return &SdJobHandler{
sdService: service,
uploader: manager,
snowflake: snowflake,
leveldb: levelDB,
userService: userService,
moderationManager: moderationManager,
BaseHandler: BaseHandler{
App: app,
DB: db,
},
}
}
// 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 {
user, err := h.GetLoginUser(c)
if err != nil {
resp.NotAuth(c)
return false
}
if user.Power < h.App.SysConfig.Base.SdPower {
resp.ERROR(c, "当前用户剩余算力不足以完成本次绘画!")
return false
}
return true
}
// Image 创建一个绘画任务
func (h *SdJobHandler) Image(c *gin.Context) {
if !h.preCheck(c) {
return
}
var data types.SdTaskParams
if err := c.ShouldBindJSON(&data); err != nil || data.Prompt == "" {
resp.ERROR(c, types.InvalidArgs)
return
}
if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil {
logger.Error("failed to moderate content: ", err)
}
if moderationResult.Flagged {
// 记录违规内容
moderation := model.Moderation{
UserId: h.GetLoginUserId(c),
Source: types.ModerationSourceSD,
Input: data.Prompt,
Result: utils.JsonEncode(moderationResult),
}
err = h.DB.Create(&moderation).Error
if err != nil {
logger.Error("failed to save moderation: ", err)
}
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return
}
}
if data.Width <= 0 {
data.Width = 512
}
if data.Height <= 0 {
data.Height = 512
}
if data.CfgScale <= 0 {
data.CfgScale = 7
}
if data.Seed == 0 {
data.Seed = -1
}
if data.Steps <= 0 {
data.Steps = 20
}
if data.Sampler == "" {
data.Sampler = "Euler a"
}
idValue, _ := c.Get(types.LoginUserID)
userId := utils.IntValue(utils.InterfaceToString(idValue), 0)
taskId, err := h.snowflake.Next(true)
if err != nil {
resp.ERROR(c, "error with generate task id: "+err.Error())
return
}
task := types.SdTask{
Type: types.TaskImage,
Params: types.SdTaskParams{
TaskId: taskId,
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
Steps: data.Steps,
Sampler: data.Sampler,
FaceFix: data.FaceFix,
CfgScale: data.CfgScale,
Seed: data.Seed,
Height: data.Height,
Width: data.Width,
HdFix: data.HdFix,
HdRedrawRate: data.HdRedrawRate,
HdScale: data.HdScale,
HdScaleAlg: data.HdScaleAlg,
HdSteps: data.HdSteps,
},
UserId: userId,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
}
job := model.SdJob{
UserId: uint(userId),
Type: types.TaskImage.String(),
TaskId: taskId,
Params: utils.JsonEncode(task.Params),
TaskInfo: utils.JsonEncode(task),
Prompt: data.Prompt,
Progress: 0,
Power: h.App.SysConfig.Base.SdPower,
CreatedAt: time.Now(),
}
res := h.DB.Create(&job)
if res.Error != nil {
resp.ERROR(c, "error with save job: "+res.Error.Error())
return
}
task.Id = int(job.Id)
h.sdService.PushTask(task)
// update user's power
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume,
Model: "stable-diffusion",
Remark: fmt.Sprintf("绘图操作,任务ID%s", job.TaskId),
})
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
// ImgWall 照片墙
func (h *SdJobHandler) ImgWall(c *gin.Context) {
page := h.GetInt(c, "page", 0)
pageSize := h.GetInt(c, "page_size", 0)
err, jobs := h.getData(true, 0, page, pageSize, true)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, jobs)
}
// JobList 获取 SD 任务列表
func (h *SdJobHandler) JobList(c *gin.Context) {
finish := h.GetBool(c, "finish")
userId := h.GetLoginUserId(c)
page := h.GetInt(c, "page", 0)
pageSize := h.GetInt(c, "page_size", 0)
publish := h.GetBool(c, "publish")
err, jobs := h.getData(finish, userId, page, pageSize, publish)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, jobs)
}
// JobList 获取 MJ 任务列表
func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int, publish bool) (error, vo.Page) {
session := h.DB.Session(&gorm.Session{})
if finish {
session = session.Where("progress >= ?", 100).Order("id DESC")
} else {
session = session.Where("progress < ?", 100).Order("id ASC")
}
if userId > 0 {
session = session.Where("user_id = ?", userId)
}
if publish {
session = session.Where("publish", publish)
}
if page > 0 && pageSize > 0 {
offset := (page - 1) * pageSize
session = session.Offset(offset).Limit(pageSize)
}
// 统计总数
var total int64
session.Model(&model.SdJob{}).Count(&total)
var items []model.SdJob
res := session.Find(&items)
if res.Error != nil {
return res.Error, vo.Page{}
}
var jobs = make([]vo.SdJob, 0)
for _, item := range items {
var job vo.SdJob
err := utils.CopyObject(item, &job)
if err != nil {
continue
}
jobs = append(jobs, job)
}
return nil, vo.NewPage(total, page, pageSize, jobs)
}
// Remove remove task image
func (h *SdJobHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
var job model.SdJob
if res := h.DB.Where("id = ? AND user_id = ?", id, userId).First(&job); res.Error != nil {
resp.ERROR(c, "记录不存在")
return
}
// 删除任务
err := h.DB.Delete(&job).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
// remove image
err = h.uploader.GetUploadHandler().Delete(job.ImgURL)
if err != nil {
logger.Error("remove image failed: ", err)
}
resp.SUCCESS(c)
}
// Publish 发布/取消发布图片到画廊显示
func (h *SdJobHandler) Publish(c *gin.Context) {
id := h.GetInt(c, "id", 0)
userId := h.GetLoginUserId(c)
action := h.GetBool(c, "action") // 发布动作,true => 发布,false => 取消分享
err := h.DB.Model(&model.SdJob{Id: uint(id), UserId: uint(userId)}).UpdateColumn("publish", action).Error
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c)
}
+22 -18
View File
@@ -125,9 +125,9 @@ func (h *SunoHandler) Create(c *gin.Context) {
if data.SongId != "" && data.Type == 3 { if data.SongId != "" && data.Type == 3 {
var song model.SunoJob var song model.SunoJob
if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil { if err := h.DB.Where("song_id = ?", data.SongId).First(&song).Error; err == nil {
data.Instrumental = song.Instrumental data.Instrumental = song.Params.Instrumental
data.Model = song.ModelName data.Model = song.Params.Model
data.Tags = song.Tags data.Tags = song.Params.Tags
} }
// 拼接歌词 // 拼接歌词
var refSong model.SunoJob var refSong model.SunoJob
@@ -153,22 +153,26 @@ func (h *SunoHandler) Create(c *gin.Context) {
// 插入数据库 // 插入数据库
job := model.SunoJob{ job := model.SunoJob{
UserId: uint(task.UserId), UserId: uint(task.UserId),
Prompt: data.Prompt, Prompt: data.Prompt,
Instrumental: data.Instrumental, Params: vo.SunoParam{
ModelName: data.Model, Prompt: data.Prompt,
TaskInfo: utils.JsonEncode(task), Instrumental: data.Instrumental,
Tags: data.Tags, Tags: data.Tags,
Title: data.Title, ExtendSecs: data.ExtendSecs,
Type: data.Type, Lyrics: data.Lyrics,
RefSongId: data.RefSongId, Model: data.Model,
RefTaskId: data.RefTaskId, },
ExtendSecs: data.ExtendSecs, Title: data.Title,
Power: h.App.SysConfig.Base.SunoPower, Type: data.Type,
SongId: utils.RandString(32), RefSongId: data.RefSongId,
RefTaskId: data.RefTaskId,
Power: h.App.SysConfig.Base.SunoPower,
SongId: utils.RandString(32),
} }
if data.Lyrics != "" { if data.Lyrics != "" {
job.Prompt = data.Lyrics job.Prompt = data.Lyrics
job.Params.Prompt = data.Lyrics
} }
tx := h.DB.Create(&job) tx := h.DB.Create(&job)
if tx.Error != nil { if tx.Error != nil {
@@ -183,8 +187,8 @@ func (h *SunoHandler) Create(c *gin.Context) {
// update user's power // update user's power
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: job.ModelName, Model: job.Params.Model,
Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.ModelName), Remark: fmt.Sprintf("Suno 文生歌曲,%s", job.Params.Model),
CreatedAt: time.Now(), CreatedAt: time.Now(),
}) })
if err != nil { if err != nil {
+148 -26
View File
@@ -35,11 +35,11 @@ type UserHandler struct {
searcher *xdb.Searcher searcher *xdb.Searcher
redis *redis.Client redis *redis.Client
levelDB *store.LevelDB levelDB *store.LevelDB
licenseService *service.LicenseService
captchaService *service.CaptchaService captchaService *service.CaptchaService
userService *service.UserService userService *service.UserService
wxLoginService *service.WxLoginService wxLoginService *service.WxLoginService
ipSearcher *xdb.Searcher ipSearcher *xdb.Searcher
wechatService *service.WxGzhService
} }
func NewUserHandler( func NewUserHandler(
@@ -51,17 +51,17 @@ func NewUserHandler(
captcha *service.CaptchaService, captcha *service.CaptchaService,
userService *service.UserService, userService *service.UserService,
wxLoginService *service.WxLoginService, wxLoginService *service.WxLoginService,
ipSearcher *xdb.Searcher, wechatService *service.WxGzhService,
licenseService *service.LicenseService) *UserHandler { 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,
levelDB: levelDB, levelDB: levelDB,
captchaService: captcha, captchaService: captcha,
licenseService: licenseService,
userService: userService, userService: userService,
wxLoginService: wxLoginService, wxLoginService: wxLoginService,
wechatService: wechatService,
ipSearcher: ipSearcher, ipSearcher: ipSearcher,
} }
} }
@@ -78,6 +78,7 @@ func (h *UserHandler) RegisterRoutes() {
group.POST("login/callback", h.WxLoginCallback) group.POST("login/callback", h.WxLoginCallback)
group.GET("login/status", h.GetWxLoginState) group.GET("login/status", h.GetWxLoginState)
group.GET("logout", h.Logout) group.GET("logout", h.Logout)
group.POST("wxAuthLogin", h.WxAuthLogin)
// 需要用户授权的接口 // 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
@@ -132,14 +133,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" {
@@ -327,14 +320,25 @@ func (h *UserHandler) GetWxLoginState(c *gin.Context) {
} }
// createNewUser 创建新用户 // createNewUser 创建新用户
func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.User, error) { func (h *UserHandler) createNewUser(user model.User, code string) (model.User, error) {
if user.OpenId != "" { if user.OpenId != "" {
user.Platform = "wechat" user.Platform = "wechat"
user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6)) // 如果未设置昵称,则生成默认昵称
user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8)) if user.Nickname == "" {
user.Password = "geekai123" user.Nickname = fmt.Sprintf("微信用户@%d", utils.RandomNumber(6))
}
// 如果未设置用户名,则生成默认用户名
if user.Username == "" {
user.Username = fmt.Sprintf("wx@%d", utils.RandomNumber(8))
}
// 如果未设置密码,则生成默认密码
if user.Password == "" {
user.Password = "geekai123"
}
} else { } else {
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6)) if user.Nickname == "" {
user.Nickname = fmt.Sprintf("用户@%d", utils.RandomNumber(6))
}
if user.Username == "" || user.Password == "" { if user.Username == "" || user.Password == "" {
return user, fmt.Errorf("用户名或密码不能为空") return user, fmt.Errorf("用户名或密码不能为空")
} }
@@ -343,9 +347,11 @@ func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.U
salt := utils.RandString(8) salt := utils.RandString(8)
user.Salt = salt user.Salt = salt
user.Password = utils.GenPassword(user.Password, salt) user.Password = utils.GenPassword(user.Password, salt)
user.Avatar = "/images/avatar/user.png" // 如果未设置头像,则使用默认头像
if user.Avatar == "" {
user.Avatar = "/images/avatar/user.png"
}
user.Status = true user.Status = true
user.ChatRoles = utils.JsonEncode([]string{"gpt"})
user.ChatConfig = "{}" user.ChatConfig = "{}"
user.ChatModels = "{}" user.ChatModels = "{}"
user.Power = h.App.SysConfig.Base.InitPower user.Power = h.App.SysConfig.Base.InitPower
@@ -353,19 +359,22 @@ func (h *UserHandler) createNewUser(user model.User, inviteCode string) (model.U
// 创建用户 // 创建用户
tx := h.DB.Begin() tx := h.DB.Begin()
if err := tx.Create(&user).Error; err != nil { if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return user, err return user, err
} }
// 记录邀请关系 // 记录邀请关系
if inviteCode != "" { if code != "" {
inviteCode := model.InviteCode{} var inviteCode model.InviteCode
err := h.DB.Where("code = ?", inviteCode).First(&inviteCode).Error err := h.DB.Where("code = ?", code).First(&inviteCode).Error
if err != nil { if err != nil {
logger.Error("无效的邀请码:", err.Error())
tx.Rollback()
return user, fmt.Errorf("无效的邀请码") return user, fmt.Errorf("无效的邀请码")
} }
// 增加邀请数量 // 增加邀请数量
h.DB.Model(&model.InviteCode{}).Where("code = ?", inviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1)) h.DB.Model(&model.InviteCode{}).Where("code = ?", code).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.Base.InvitePower > 0 { if h.App.SysConfig.Base.InvitePower > 0 {
err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{ err := h.userService.IncreasePower(inviteCode.UserId, h.App.SysConfig.Base.InvitePower, model.PowerLog{
Type: types.PowerInvite, Type: types.PowerInvite,
@@ -479,6 +488,20 @@ func (h *UserHandler) Session(c *gin.Context) {
h.DB.Model(&user).UpdateColumn("vip", false) h.DB.Model(&user).UpdateColumn("vip", false)
} }
userVo.Id = user.Id userVo.Id = user.Id
// 工作区应用 ID 列表(历史可能为 key 数组,仅解析数字 ID)
if user.ChatRoles != "" {
var raw []interface{}
if utils.JsonDecode(user.ChatRoles, &raw) == nil {
for _, v := range raw {
if n, ok := v.(float64); ok && n >= 0 {
userVo.ChatRoles = append(userVo.ChatRoles, uint(n))
}
}
}
}
if userVo.ChatRoles == nil {
userVo.ChatRoles = []uint{}
}
resp.SUCCESS(c, userVo) resp.SUCCESS(c, userVo)
} }
@@ -491,6 +514,7 @@ type userProfile struct {
Power int `json:"power"` Power int `json:"power"`
ExpiredTime int64 `json:"expired_time"` ExpiredTime int64 `json:"expired_time"`
Vip bool `json:"vip"` Vip bool `json:"vip"`
GemIds []uint `json:"gem_ids"`
} }
func (h *UserHandler) Profile(c *gin.Context) { func (h *UserHandler) Profile(c *gin.Context) {
@@ -510,6 +534,19 @@ func (h *UserHandler) Profile(c *gin.Context) {
} }
profile.Id = user.Id profile.Id = user.Id
if user.GemIds != "" {
var raw []interface{}
if utils.JsonDecode(user.GemIds, &raw) == nil {
for _, v := range raw {
if n, ok := v.(float64); ok {
profile.GemIds = append(profile.GemIds, uint(n))
}
}
}
}
if profile.GemIds == nil {
profile.GemIds = []uint{}
}
resp.SUCCESS(c, profile) resp.SUCCESS(c, profile)
} }
@@ -528,6 +565,12 @@ func (h *UserHandler) ProfileUpdate(c *gin.Context) {
h.DB.First(&user, user.Id) h.DB.First(&user, user.Id)
user.Avatar = data.Avatar user.Avatar = data.Avatar
user.Nickname = data.Nickname user.Nickname = data.Nickname
if data.GemIds != nil {
if len(data.GemIds) > 8 {
data.GemIds = data.GemIds[:8]
}
user.GemIds = utils.JsonEncode(data.GemIds)
}
res := h.DB.Updates(&user) res := h.DB.Updates(&user)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "更新用户信息失败") resp.ERROR(c, "更新用户信息失败")
@@ -592,13 +635,14 @@ func (h *UserHandler) ResetPass(c *gin.Context) {
session := h.DB.Session(&gorm.Session{}) session := h.DB.Session(&gorm.Session{})
var key string var key string
if data.Type == "email" { switch data.Type {
case "email":
session = session.Where("email", data.Email) session = session.Where("email", data.Email)
key = CodeStorePrefix + data.Email key = CodeStorePrefix + data.Email
} else if data.Type == "mobile" { case "mobile":
session = session.Where("mobile", data.Mobile) session = session.Where("mobile", data.Mobile)
key = CodeStorePrefix + data.Mobile key = CodeStorePrefix + data.Mobile
} else { default:
resp.ERROR(c, "验证类别错误") resp.ERROR(c, "验证类别错误")
return return
} }
@@ -730,3 +774,81 @@ func (h *UserHandler) SignIn(c *gin.Context) {
} }
resp.SUCCESS(c) resp.SUCCESS(c)
} }
// 微信公众号 小程序授权登录
func (h *UserHandler) WxAuthLogin(c *gin.Context) {
var data struct {
Code string `json:"code"`
InviteCode string `json:"invite_code"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
// 根据 code 获取 openid
openID, accessToken, err := h.wechatService.GetOpenIDByCode(data.Code)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 获取微信用户昵称头像
userInfo, err := h.wechatService.GetUserInfo(accessToken, openID)
if err != nil {
resp.ERROR(c, err.Error())
return
}
nickname := userInfo["nickname"].(string)
headimgurl := userInfo["headimgurl"].(string)
// 查询用户是否存在
var user model.User
h.DB.Where("openid = ?", openID).First(&user)
if user.Id > 0 {
// 用户存在,更新用户信息并登录
user.Nickname = nickname
user.Avatar = headimgurl
if err := h.DB.Save(&user).Error; err != nil {
resp.ERROR(c, "更新用户信息失败")
return
}
token, err := h.doLogin(&user, c.ClientIP())
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
return
}
// 用户不存在,创建新用户
user = model.User{
OpenId: openID,
Nickname: nickname,
Avatar: headimgurl,
}
// 被邀请人也获得赠送算力
if data.InviteCode != "" {
user.Power = h.App.SysConfig.Base.InitPower * 2
}
user, err = h.createNewUser(user, data.InviteCode)
if err != nil {
resp.ERROR(c, err.Error())
return
}
// 自动登录
token, err := h.doLogin(&user, c.ClientIP())
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{"token": token, "user_id": user.Id, "username": user.Username})
}
+127 -155
View File
@@ -20,7 +20,6 @@ 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"
@@ -54,33 +53,50 @@ func (h *VideoHandler) RegisterRoutes() {
// 需要用户授权的接口 // 需要用户授权的接口
group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis)) group.Use(middleware.UserAuthMiddleware(h.App.Config.Session.SecretKey, h.App.Redis))
{ {
group.POST("luma/create", h.LumaCreate) group.POST("create", h.Create)
group.POST("keling/create", h.KeLingCreate)
group.GET("list", h.List) group.GET("list", h.List)
group.GET("remove", h.Remove) group.GET("remove", h.Remove)
group.GET("publish", h.Publish) group.GET("publish", h.Publish)
group.GET("power-config", h.GetPowerConfig) // 获取算力配置
group.GET("power-by-key", h.GetPowerByPriceKey) // 根据 priceKey 获取算力
} }
} }
func (h *VideoHandler) LumaCreate(c *gin.Context) { type VideoTaskRequest struct {
Provider string `json:"provider"` // 服务提供商(不带版本号:veo, sora)
Model string `json:"model"` // 模型标识(带版本号:veo-2.0, sora-2.0
Prompt string `json:"prompt"` // 提示词
Params map[string]any `json:"params"` // 模型特定参数
PriceKey string `json:"price_key"` // 价格键(如 "fixed", "5_720P" 等)
}
var data struct { // Create 统一的创建视频任务接口
Prompt string `json:"prompt"` func (h *VideoHandler) Create(c *gin.Context) {
FirstFrameImg string `json:"first_frame_img,omitempty"` var data VideoTaskRequest
EndFrameImg string `json:"end_frame_img,omitempty"`
ExpandPrompt bool `json:"expand_prompt,omitempty"`
Loop bool `json:"loop,omitempty"`
}
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
} }
// 检查 Prompt 长度
// 验证必填字段
if data.Provider == "" {
resp.ERROR(c, "provider 不能为空")
return
}
if data.Model == "" {
resp.ERROR(c, "model 不能为空")
return
}
if data.Prompt == "" { if data.Prompt == "" {
resp.ERROR(c, "prompt is needed") resp.ERROR(c, "prompt 不能为空")
return
}
if data.PriceKey == "" {
resp.ERROR(c, "price_key 不能为空")
return return
} }
// 文本审查
if h.App.SysConfig.Moderation.Enable { if h.App.SysConfig.Moderation.Enable {
moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt) moderationResult, err := h.moderationManager.GetService().Moderate(data.Prompt)
if err != nil { if err != nil {
@@ -101,138 +117,45 @@ func (h *VideoHandler) LumaCreate(c *gin.Context) {
resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!") resp.ERROR(c, "当前创作内容包含敏感词,请重新输入!")
return 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.Base.LumaPower { // 计算算力
resp.ERROR(c, "您的算力不足,请充值后再试!") power, err := video.CalculatePower(h.DB, data.Model, data.PriceKey)
return
}
userId := int(h.GetLoginUserId(c))
params := types.LumaVideoParams{
PromptOptimize: data.ExpandPrompt,
Loop: data.Loop,
StartImgURL: data.FirstFrameImg,
EndImgURL: data.EndFrameImg,
}
task := types.VideoTask{
UserId: userId,
Type: types.VideoLuma,
Prompt: data.Prompt,
Params: params,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
}
// 插入数据库
job := model.VideoJob{
UserId: uint(userId),
Type: types.VideoLuma,
Prompt: data.Prompt,
Power: h.App.SysConfig.Base.LumaPower,
TaskInfo: utils.JsonEncode(task),
}
tx := h.DB.Create(&job)
if tx.Error != nil {
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: "luma",
Remark: fmt.Sprintf("Luma 文生视频,任务ID%d", job.Id),
})
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
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 { if user.Power < power {
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.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{ task := types.VideoTask{
UserId: userId, UserId: userId,
Type: types.VideoKeLing, Type: data.Provider, // provider 作为 type
Prompt: data.Prompt, Prompt: data.Prompt,
Params: params, Params: data.Params,
TranslateModelId: h.App.SysConfig.Base.AssistantModelId, TranslateModelId: h.App.SysConfig.Base.AssistantModelId,
Channel: data.Channel,
} }
// 插入数据库 // 插入数据库
job := model.VideoJob{ job := model.VideoJob{
UserId: uint(userId), UserId: uint(userId),
Type: types.VideoKeLing, Type: data.Provider,
Prompt: data.Prompt, Prompt: data.Prompt,
Power: power, Power: power,
TaskInfo: utils.JsonEncode(task), Params: utils.JsonEncode(task),
} }
tx := h.DB.Create(&job) tx := h.DB.Create(&job)
if tx.Error != nil { if tx.Error != nil {
@@ -244,17 +167,52 @@ func (h *VideoHandler) KeLingCreate(c *gin.Context) {
task.Id = job.Id task.Id = job.Id
h.videoService.PushTask(task) h.videoService.PushTask(task)
// update user's power // 扣减算力
err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{ err = h.userService.DecreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerConsume, Type: types.PowerConsume,
Model: "keling", Model: data.Provider,
Remark: fmt.Sprintf("keling 文生视频,任务ID%d", job.Id), Remark: fmt.Sprintf("%s 视频生成,任务ID%d", data.Provider, job.Id),
}) })
if err != nil { if err != nil {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
resp.SUCCESS(c)
resp.SUCCESS(c, gin.H{"job_id": job.Id})
}
// GetPowerConfig 获取算力配置
func (h *VideoHandler) GetPowerConfig(c *gin.Context) {
config, err := video.GetVideoConfig(h.DB)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, config.VideoPowers)
}
// GetPowerByPriceKey 根据 modelKey 和 priceKey 获取算力值
func (h *VideoHandler) GetPowerByPriceKey(c *gin.Context) {
modelKey := c.Query("model_key")
priceKey := c.Query("price_key")
if modelKey == "" {
resp.ERROR(c, "model_key 不能为空")
return
}
if priceKey == "" {
resp.ERROR(c, "price_key 不能为空")
return
}
power, err := video.CalculatePower(h.DB, modelKey, priceKey)
if err != nil {
resp.ERROR(c, err.Error())
return
}
resp.SUCCESS(c, gin.H{"power": power})
} }
func (h *VideoHandler) List(c *gin.Context) { func (h *VideoHandler) List(c *gin.Context) {
@@ -268,7 +226,7 @@ func (h *VideoHandler) List(c *gin.Context) {
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("status", types.VideoStatusSuccess)
} else { } else {
session = session.Where("user_id", userId) session = session.Where("user_id", userId)
} }
@@ -296,36 +254,51 @@ func (h *VideoHandler) List(c *gin.Context) {
continue continue
} }
item.CreatedAt = v.CreatedAt.Unix() item.CreatedAt = v.CreatedAt.Unix()
if item.VideoURL == "" { // 解析任务详情(用于前端展示标签)
item.VideoURL = v.WaterURL if v.Params != "" {
}
// 解析任务详情
if item.Type == types.VideoKeLing {
task := types.VideoTask{} task := types.VideoTask{}
err = utils.JsonDecode(v.TaskInfo, &task) if err := utils.JsonDecode(v.Params, &task); err == nil {
if err != nil { // 默认从 params map 中提取常用字段
continue if paramsMap, ok := task.Params.(map[string]any); ok {
} if item.Params == nil {
var params types.KeLingVideoParams item.Params = make(map[string]any)
err = utils.JsonDecode(utils.JsonEncode(task.Params), &params) }
if err != nil { if _, ok := item.Params["task_type"]; !ok {
continue if taskType, ok := paramsMap["task_type"]; ok {
} item.Params["task_type"] = taskType
item.RawData = map[string]interface{}{ }
"task_type": params.TaskType, }
"model": params.Model, if _, ok := item.Params["model"]; !ok {
"cfg_scale": params.CfgScale, if modelKey, ok := paramsMap["model"]; ok {
"mode": params.Mode, item.Params["model"] = modelKey
"aspect_ratio": params.AspectRatio, }
"duration": params.Duration, }
"model_name": fmt.Sprintf("%s_%s_%s", params.Model, params.Mode, params.Duration), if _, ok := item.Params["duration"]; !ok {
} if duration, ok := paramsMap["duration"]; ok {
item.Params["duration"] = duration
// 如果视频URL不为空,则设置为生成成功 }
if item.VideoURL != "" { }
item.Progress = 100 if _, ok := item.Params["size"]; !ok {
if size, ok := paramsMap["size"]; ok {
item.Params["size"] = size
} else if size, ok := paramsMap["resolution"].(string); ok {
item.Params["size"] = size
}
}
if _, ok := item.Params["mode"]; !ok {
if mode, ok := paramsMap["mode"]; ok {
item.Params["mode"] = mode
}
}
if _, ok := item.Params["sound"]; !ok {
if sound, ok := paramsMap["sound"]; ok {
item.Params["sound"] = sound
}
}
}
} }
} }
items = append(items, item) items = append(items, item)
} }
@@ -341,9 +314,9 @@ func (h *VideoHandler) Remove(c *gin.Context) {
resp.ERROR(c, err.Error()) resp.ERROR(c, err.Error())
return return
} }
// 只有失败或者超时的任务才能删除 // 只有失败的任务才能删除
if !(job.Progress == service.FailTaskProgress || time.Now().After(job.CreatedAt.Add(time.Minute*30))) { if job.Status != types.VideoStatusFailed {
resp.ERROR(c, "只有失败和超时(30分钟)的任务才能删除!") resp.ERROR(c, "只有失败的任务才能删除!")
return return
} }
@@ -355,7 +328,6 @@ func (h *VideoHandler) Remove(c *gin.Context) {
} }
// 删除文件 // 删除文件
_ = h.uploader.GetUploadHandler().Delete(job.CoverURL)
_ = h.uploader.GetUploadHandler().Delete(job.VideoURL) _ = h.uploader.GetUploadHandler().Delete(job.VideoURL)
resp.SUCCESS(c) resp.SUCCESS(c)
+95
View File
@@ -0,0 +1,95 @@
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 (
"crypto/sha1"
"encoding/hex"
"geekai/core"
"geekai/utils/resp"
"log"
"net/http"
"sort"
"strings"
"github.com/gin-gonic/gin"
)
type WxGzhHandler struct {
BaseHandler
}
func NewWxGzhHandler(server *core.AppServer) *WxGzhHandler {
return &WxGzhHandler{
BaseHandler: BaseHandler{
App: server,
},
}
}
func (h *WxGzhHandler) RegisterRoutes() {
group := h.App.Engine.Group("/api/wx/")
group.GET("verify", h.WechatVerify)
}
// 处理微信服务器验证请求
func (h *WxGzhHandler) WechatVerify(c *gin.Context) {
logger.Info("WechatVerify")
// 只处理 GET 请求
if c.Request.Method != "GET" {
resp.ERROR(c, "Method Not Allowed")
return
}
// 解析 URL 参数
signature := c.Query("signature")
timestamp := c.Query("timestamp")
nonce := c.Query("nonce")
echostr := c.Query("echostr")
// 验证参数完整性
if signature == "" || timestamp == "" || nonce == "" || echostr == "" {
log.Println("Missing parameters")
resp.ERROR(c, "Missing parameters")
return
}
// 验证签名
if validateSignature(signature, h.App.SysConfig.WxGzh.Token, timestamp, nonce) {
// 验证成功,返回 echostr(必须是纯文本)
c.String(http.StatusOK, echostr)
log.Println("Token verification success")
} else {
// 验证失败
resp.ERROR(c, "Forbidden: Invalid signature")
log.Println("Token verification failed")
}
}
func validateSignature(signature, token, timestamp, nonce string) bool {
// 1. 将 token、timestamp、nonce 按字典序排序
strs := []string{token, timestamp, nonce}
sort.Strings(strs)
// 2. 拼接字符串
joined := strings.Join(strs, "")
// 3. 计算 SHA1 哈希
hash := sha1.New()
hash.Write([]byte(joined))
hashed := hex.EncodeToString(hash.Sum(nil))
// 4. 与 signature 比对
return hashed == signature
}
// 创建微信菜单
func (h *WxGzhHandler) CreateMenu(c *gin.Context) {
resp.SUCCESS(c, "创建菜单成功")
}
+1 -1
View File
@@ -1,4 +1,4 @@
package logger package log
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved. // * Copyright 2023 The Geek-AI Authors. All rights reserved.
+48 -40
View File
@@ -14,21 +14,21 @@ import (
"geekai/core/types" "geekai/core/types"
"geekai/handler" "geekai/handler"
"geekai/handler/admin" "geekai/handler/admin"
logger2 "geekai/logger" "geekai/log"
"geekai/service" "geekai/service"
"geekai/service/dalle" "geekai/service/image"
"geekai/service/jimeng" "geekai/service/jimeng"
"geekai/service/mj" "geekai/service/mj"
"geekai/service/moderation" "geekai/service/moderation"
"geekai/service/oss" "geekai/service/oss"
"geekai/service/payment" "geekai/service/payment"
"geekai/service/sd" "geekai/service/ppt"
"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"
"io" "io"
"log"
"os" "os"
"os/signal" "os/signal"
"runtime/debug" "runtime/debug"
@@ -42,7 +42,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
//go:embed res //go:embed res
var xdbFS embed.FS var xdbFS embed.FS
@@ -88,7 +88,7 @@ func main() {
fx.Provide(func() *types.AppConfig { fx.Provide(func() *types.AppConfig {
config, err := core.LoadConfig(configFile) config, err := core.LoadConfig(configFile)
if err != nil { if err != nil {
log.Fatal(err) logger.Fatal(err)
} }
config.Path = configFile config.Path = configFile
return config return config
@@ -135,16 +135,23 @@ func main() {
fx.Provide(handler.NewSmsHandler), fx.Provide(handler.NewSmsHandler),
fx.Provide(handler.NewRedeemHandler), fx.Provide(handler.NewRedeemHandler),
fx.Provide(handler.NewCaptchaHandler), fx.Provide(handler.NewCaptchaHandler),
fx.Provide(func(db *gorm.DB, userService *service.UserService, uploadManager *oss.UploaderManager) *ppt.PptService {
return ppt.NewPptService(db, userService, uploadManager)
}),
fx.Provide(handler.NewPPTTaskHandler),
fx.Invoke(func(ppt *ppt.PptService) {
ppt.RecoverStaleProcessingTasks()
}),
fx.Provide(handler.NewMidJourneyHandler), fx.Provide(handler.NewMidJourneyHandler),
fx.Provide(handler.NewChatModelHandler), fx.Provide(handler.NewChatModelHandler),
fx.Provide(handler.NewSdJobHandler),
fx.Provide(handler.NewPaymentHandler), fx.Provide(handler.NewPaymentHandler),
fx.Provide(handler.NewOrderHandler), fx.Provide(handler.NewOrderHandler),
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(handler.NewJimengHandler),
fx.Provide(service.NewWxGzhService),
fx.Provide(handler.NewWxGzhHandler),
fx.Provide(service.NewMigrationService), fx.Provide(service.NewMigrationService),
fx.Invoke(func(migrationService *service.MigrationService) { fx.Invoke(func(migrationService *service.MigrationService) {
migrationService.StartMigrate() migrationService.StartMigrate()
@@ -163,18 +170,15 @@ func main() {
fx.Provide(admin.NewOrderHandler), fx.Provide(admin.NewOrderHandler),
fx.Provide(admin.NewPowerLogHandler), fx.Provide(admin.NewPowerLogHandler),
fx.Provide(admin.NewAdminJimengHandler), fx.Provide(admin.NewAdminJimengHandler),
fx.Provide(admin.NewVideoHandler),
fx.Provide(admin.NewSunoHandler),
fx.Provide(admin.NewPPTHandler),
// 邮件服务 // 邮件服务
fx.Provide(service.NewSmtpService), fx.Provide(service.NewSmtpService),
// License 服务 // Image 服务
fx.Provide(service.NewLicenseService), fx.Provide(image.NewService),
fx.Invoke(func(licenseService *service.LicenseService) { fx.Invoke(func(s *image.Service) {
licenseService.SyncLicense()
}),
// Dalle 服务
fx.Provide(dalle.NewService),
fx.Invoke(func(s *dalle.Service) {
s.Run() s.Run()
s.DownloadImages() s.DownloadImages()
s.CheckTaskStatus() s.CheckTaskStatus()
@@ -189,12 +193,8 @@ func main() {
s.DownloadImages() s.DownloadImages()
}), }),
// Stable Diffusion 机器人 // Sora service
fx.Provide(sd.NewService), fx.Provide(sora.NewSoraService),
fx.Invoke(func(s *sd.Service, config *types.AppConfig) {
s.Run()
s.CheckTaskStatus()
}),
fx.Provide(suno.NewService), fx.Provide(suno.NewService),
fx.Invoke(func(s *suno.Service) { fx.Invoke(func(s *suno.Service) {
@@ -221,13 +221,10 @@ func main() {
// 创建短信服务 // 创建短信服务
fx.Provide(sms.NewAliYunSmsService), fx.Provide(sms.NewAliYunSmsService),
fx.Provide(sms.NewBaoSmsService), fx.Provide(sms.NewBaoSmsService),
fx.Provide(sms.NewTencentSmsService),
fx.Provide(sms.NewSmsManager), fx.Provide(sms.NewSmsManager),
fx.Provide(func(config *types.SystemConfig) *service.CaptchaService { fx.Provide(service.NewCaptchaService),
return service.NewCaptchaService(config.Captcha) fx.Provide(service.NewWxLoginService),
}),
fx.Provide(func(config *types.SystemConfig, client *redis.Client) *service.WxLoginService {
return service.NewWxLoginService(config.WxLogin, client)
}),
// 支付服务 // 支付服务
fx.Provide(payment.NewAlipayService), fx.Provide(payment.NewAlipayService),
@@ -239,6 +236,7 @@ func main() {
fx.Provide(oss.NewMiniOss), fx.Provide(oss.NewMiniOss),
fx.Provide(oss.NewQiNiuOss), fx.Provide(oss.NewQiNiuOss),
fx.Provide(oss.NewAliYunOss), fx.Provide(oss.NewAliYunOss),
fx.Provide(oss.NewTencentOss),
fx.Provide(oss.NewUploaderManager), fx.Provide(oss.NewUploaderManager),
// 用户服务 // 用户服务
@@ -273,15 +271,15 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) { fx.Invoke(func(s *core.AppServer, h *handler.CaptchaHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.PPTTaskHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) { fx.Invoke(func(s *core.AppServer, h *handler.RedeemHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) { fx.Invoke(func(s *core.AppServer, h *handler.MidJourneyHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Invoke(func(s *core.AppServer, h *handler.SdJobHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ConfigHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
@@ -372,8 +370,8 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) { fx.Invoke(func(s *core.AppServer, h *handler.MarkMapHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Provide(handler.NewDallJobHandler), fx.Provide(handler.NewImageJobHandler),
fx.Invoke(func(s *core.AppServer, h *handler.DallJobHandler) { fx.Invoke(func(s *core.AppServer, h *handler.ImageJobHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Provide(handler.NewSunoHandler), fx.Provide(handler.NewSunoHandler),
@@ -392,6 +390,15 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) { fx.Invoke(func(s *core.AppServer, h *admin.AdminJimengHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Invoke(func(s *core.AppServer, h *admin.VideoHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.SunoHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, h *admin.PPTHandler) {
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) {
h.RegisterRoutes() h.RegisterRoutes()
@@ -408,6 +415,11 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) { fx.Invoke(func(s *core.AppServer, h *handler.PromptHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
// 微信公众号路由
fx.Invoke(func(s *core.AppServer, h *handler.WxGzhHandler) {
h.RegisterRoutes()
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) { fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() { go func() {
err := s.Run(db) err := s.Run(db)
@@ -433,10 +445,6 @@ func main() {
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) { fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
h.RegisterRoutes() h.RegisterRoutes()
}), }),
fx.Provide(admin.NewMediaHandler),
fx.Invoke(func(s *core.AppServer, h *admin.MediaHandler) {
h.RegisterRoutes()
}),
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) {
h.RegisterRoutes() h.RegisterRoutes()
@@ -445,7 +453,7 @@ func main() {
// 启动应用程序 // 启动应用程序
go func() { go func() {
if err := app.Start(context.Background()); err != nil { if err := app.Start(context.Background()); err != nil {
log.Fatal(err) logger.Fatal(err)
} }
}() }()
@@ -458,7 +466,7 @@ func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
if err := app.Stop(ctx); err != nil { if err := app.Stop(ctx); err != nil {
log.Fatal(err) logger.Fatal(err)
} }
} }
-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

+3 -3
View File
@@ -20,9 +20,9 @@ type CaptchaService struct {
client *req.Client client *req.Client
} }
func NewCaptchaService(captchaConfig types.CaptchaConfig) *CaptchaService { func NewCaptchaService(sysConfig *types.SystemConfig) *CaptchaService {
return &CaptchaService{ return &CaptchaService{
config: captchaConfig, config: sysConfig.Captcha,
client: req.C().SetTimeout(10 * time.Second), client: req.C().SetTimeout(10 * time.Second),
} }
} }
@@ -35,7 +35,7 @@ func (s *CaptchaService) GetConfig() types.CaptchaConfig {
return s.config return s.config
} }
func (s *CaptchaService) Get() (interface{}, error) { func (s *CaptchaService) Get() (any, error) {
url := fmt.Sprintf("%s/api/captcha/get", types.GeekAPIURL) 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().
@@ -1,4 +1,4 @@
package dalle package image
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved. // * Copyright 2023 The Geek-AI Authors. All rights reserved.
@@ -10,13 +10,13 @@ package dalle
import ( import (
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
"geekai/service" "geekai/service"
"geekai/service/oss" "geekai/service/oss"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"strings" "io"
"time" "time"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
@@ -25,9 +25,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
// DALL-E 绘画服务 // Image Generation Service
type Service struct { type Service struct {
httpClient *req.Client httpClient *req.Client
@@ -41,50 +41,50 @@ func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Clien
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("Image_Task_Queue", redisCli),
uploadManager: manager, uploadManager: manager,
userService: userService, userService: userService,
} }
} }
// PushTask push a new mj task in to task queue // PushTask push a new image task in to task queue
func (s *Service) PushTask(task types.DallTask) { func (s *Service) PushTask(task types.ImageTask) {
logger.Infof("add a new DALL-E task to the task list: %+v", task) logger.Infof("add a new Image generation task to the task list: %+v", task)
if err := s.taskQueue.RPush(task); err != nil { if err := s.taskQueue.RPush(task); err != nil {
logger.Errorf("push dall-e task to queue failed: %v", err) logger.Errorf("push image task to queue failed: %v", err)
} }
} }
func (s *Service) Run() { func (s *Service) Run() {
// 将数据库中未提交的任务加载到队列 // 将数据库中未提交的任务加载到队列
var jobs []model.DallJob var jobs []model.ImageJob
s.db.Where("progress", 0).Find(&jobs) s.db.Where("progress", 0).Find(&jobs)
for _, v := range jobs { for _, v := range jobs {
var task types.DallTask var task types.ImageTask
err := utils.JsonDecode(v.TaskInfo, &task) err := utils.JsonDecode(v.Params, &task)
if err != nil { if err != nil {
logger.Errorf("decode task info with error: %v", err) logger.Errorf("decode task params with error: %v", err)
continue continue
} }
task.Id = v.Id task.Id = v.Id
s.PushTask(task) s.PushTask(task)
} }
logger.Info("Starting DALL-E job consumer...") logger.Info("Starting Image generation job consumer...")
go func() { go func() {
for { for {
var task types.DallTask var task types.ImageTask
err := s.taskQueue.LPop(&task) err := s.taskQueue.LPop(&task)
if err != nil { if err != nil {
logger.Errorf("taking task with error: %v", err) logger.Errorf("taking task with error: %v", err)
continue continue
} }
logger.Infof("handle a new DALL-E task: %+v", task) logger.Infof("handle a new Image generation task: %+v", task)
go func() { 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.ImageJob{Id: task.Id}).UpdateColumns(map[string]interface{}{
"progress": service.FailTaskProgress, "progress": service.FailTaskProgress,
"err_msg": err.Error(), "err_msg": err.Error(),
}) })
@@ -98,10 +98,8 @@ type imgReq struct {
Model string `json:"model"` Model string `json:"model"`
Image []string `json:"image,omitempty"` Image []string `json:"image,omitempty"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
N int `json:"n,omitempty"` AspectRatio string `json:"aspect_ratio,omitempty"`
Size string `json:"size,omitempty"` Size string `json:"size,omitempty"`
Quality string `json:"quality,omitempty"`
Style string `json:"style,omitempty"`
ResponseFormat string `json:"response_format,omitempty"` ResponseFormat string `json:"response_format,omitempty"`
} }
@@ -116,21 +114,21 @@ 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.ImageTask, sync bool) (string, error) {
logger.Debugf("绘画参数:%+v", task) logger.Debugf("绘画参数:%+v", task)
var chatModel model.ChatModel var chatModel model.ChatModel
if task.ModelId > 0 { if task.ModelId > 0 {
s.db.Where("id", task.ModelId).First(&chatModel) s.db.Where("id", task.ModelId).First(&chatModel)
} else { } else {
s.db.Where("value", task.ModelName).First(&chatModel) s.db.Where("value", task.ModelValue).First(&chatModel)
} }
// get image generation API KEY // get image generation API KEY
@@ -139,7 +137,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
if chatModel.KeyId > 0 { if chatModel.KeyId > 0 {
session = session.Where("id = ?", chatModel.KeyId) session = session.Where("id = ?", chatModel.KeyId)
} else { } else {
session = session.Where("type = ?", "dalle") session = session.Where("type = ?", "image")
} }
err := session.Order("last_used_at ASC").First(&apiKey).Error err := session.Order("last_used_at ASC").First(&apiKey).Error
if err != nil { if err != nil {
@@ -153,16 +151,16 @@ 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: task.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 { if len(task.Image) > 0 {
reqBody.Prompt = fmt.Sprintf("%s, %s", strings.Join(task.Image, " "), task.Prompt) 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)
@@ -182,10 +180,15 @@ 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)
} }
if len(res.Data) == 0 && r.Body != nil {
body, _ := io.ReadAll(r.Body)
return "", fmt.Errorf("%s", string(body))
}
// 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": task.Prompt, "prompt": task.Prompt,
} }
@@ -202,7 +205,7 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
} }
data["org_url"] = imgURL data["org_url"] = imgURL
// update task progress // update task progress
err = s.db.Model(&model.DallJob{Id: task.Id}).UpdateColumns(data).Error err = s.db.Model(&model.ImageJob{Id: task.Id}).UpdateColumns(data).Error
if err != nil { if err != nil {
return "", fmt.Errorf("err with update database: %v", err) return "", fmt.Errorf("err with update database: %v", err)
} }
@@ -217,10 +220,10 @@ func (s *Service) Image(task types.DallTask, sync bool) (string, error) {
func (s *Service) CheckTaskStatus() { func (s *Service) CheckTaskStatus() {
go func() { go func() {
logger.Info("Running DALL-E task status checking ...") logger.Info("Running Image generation task status checking ...")
for { for {
// 检查未完成任务进度 // 检查未完成任务进度
var jobs []model.DallJob var jobs []model.ImageJob
s.db.Where("progress < ?", 100).Find(&jobs) s.db.Where("progress < ?", 100).Find(&jobs)
for _, job := range jobs { for _, job := range jobs {
// 超时的任务标记为失败 // 超时的任务标记为失败
@@ -234,8 +237,8 @@ func (s *Service) CheckTaskStatus() {
// 找出失败的任务,并恢复其扣减算力 // 找出失败的任务,并恢复其扣减算力
s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
for _, job := range jobs { for _, job := range jobs {
var task types.DallTask var task types.ImageTask
err := utils.JsonDecode(job.TaskInfo, &task) err := utils.JsonDecode(job.Params, &task)
if err != nil { if err != nil {
continue continue
} }
@@ -257,7 +260,7 @@ func (s *Service) CheckTaskStatus() {
func (s *Service) DownloadImages() { func (s *Service) DownloadImages() {
go func() { go func() {
var items []model.DallJob var items []model.ImageJob
for { for {
res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items) res := s.db.Where("img_url = ? AND progress = ?", "", 100).Find(&items)
if res.Error != nil { if res.Error != nil {
@@ -294,7 +297,7 @@ func (s *Service) downloadImage(jobId uint, orgURL string) (string, error) {
} }
// update img_url // update img_url
res := s.db.Model(&model.DallJob{Id: jobId}).UpdateColumn("img_url", imgURL) res := s.db.Model(&model.ImageJob{Id: jobId}).UpdateColumn("img_url", imgURL)
if res.Error != nil { if res.Error != nil {
return "", err return "", err
} }
+137 -28
View File
@@ -1,15 +1,21 @@
package jimeng package jimeng
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/volcengine/volc-sdk-golang/base" "github.com/volcengine/volc-sdk-golang/base"
"github.com/volcengine/volc-sdk-golang/service/visual" "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客户端 // Client 即梦API客户端
@@ -50,6 +56,22 @@ func (c *Client) UpdateConfig(config types.JimengConfig) error {
"Version": []string{"2022-08-31"}, "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": { "CVProcess": {
Method: http.MethodPost, Method: http.MethodPost,
Path: "/", Path: "/",
@@ -71,6 +93,22 @@ func (c *Client) UpdateConfig(config types.JimengConfig) error {
return c.testConnection() 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连接 // testConnection 测试即梦AI连接
func (c *Client) testConnection() error { func (c *Client) testConnection() error {
@@ -80,7 +118,7 @@ func (c *Client) testConnection() error {
TaskId: "test_task_id_12345", TaskId: "test_task_id_12345",
} }
_, err := c.QueryTask(testReq) _, err := c.QueryTask(testReq, ASyncActionGetResult)
// 即使任务不存在,只要不是认证错误就说明连接正常 // 即使任务不存在,只要不是认证错误就说明连接正常
if err != nil { if err != nil {
// 检查是否是认证错误 // 检查是否是认证错误
@@ -94,7 +132,7 @@ func (c *Client) testConnection() error {
} }
// SubmitTask 提交异步任务 // SubmitTask 提交异步任务
func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error) { func (c *Client) SubmitTask(req map[string]any) (*SubmitTaskResponse, error) {
// 直接将请求转为map[string]interface{} // 直接将请求转为map[string]interface{}
reqBodyBytes, err := json.Marshal(req) reqBodyBytes, err := json.Marshal(req)
if err != nil { if err != nil {
@@ -103,9 +141,14 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error)
// 直接使用序列化后的字节 // 直接使用序列化后的字节
jsonBody := reqBodyBytes jsonBody := reqBodyBytes
action := ASyncActionSubmit
if v, ok := req["action"]; ok {
action = v.(string)
delete(req, "action")
}
// 调用SDK的JSON方法 // 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncSubmitTask", nil, string(jsonBody)) respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
if err != nil { if err != nil {
return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err) return nil, fmt.Errorf("submit task failed (status: %d): %w", statusCode, err)
} }
@@ -118,11 +161,70 @@ func (c *Client) SubmitTask(req *SubmitTaskRequest) (*SubmitTaskResponse, error)
return nil, fmt.Errorf("unmarshal response failed: %w", err) return nil, fmt.Errorf("unmarshal response failed: %w", err)
} }
// 检查响应错误代码
if err := HandleResponseError(result.Code, result.Message); err != nil {
return nil, err
}
return &result, nil 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 查询任务结果 // QueryTask 查询任务结果
func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) { func (c *Client) QueryTask(req *QueryTaskRequest, action string) (*QueryTaskResponse, error) {
// 序列化请求 // 序列化请求
jsonBody, err := json.Marshal(req) jsonBody, err := json.Marshal(req)
if err != nil { if err != nil {
@@ -130,7 +232,7 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
} }
// 调用SDK的JSON方法 // 调用SDK的JSON方法
respBody, statusCode, err := c.visual.Client.Json("CVSync2AsyncGetResult", nil, string(jsonBody)) respBody, statusCode, err := c.visual.Client.Json(action, nil, string(jsonBody))
if err != nil { if err != nil {
return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err) return nil, fmt.Errorf("query task failed (status: %d): %w", statusCode, err)
} }
@@ -143,30 +245,37 @@ func (c *Client) QueryTask(req *QueryTaskRequest) (*QueryTaskResponse, error) {
return nil, fmt.Errorf("unmarshal response failed: %w", err) return nil, fmt.Errorf("unmarshal response failed: %w", err)
} }
return &result, nil // 检查响应错误代码
} if err := HandleResponseError(result.Code, result.Message); err != nil {
return nil, err
// SubmitSyncTask 提交同步任务(仅用于文生图)
func (c *Client) SubmitSyncTask(req *SubmitTaskRequest) (*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("CVProcess", nil, string(jsonBody))
if err != nil {
return nil, fmt.Errorf("submit sync task failed (status: %d): %w", statusCode, err)
}
logger.Infof("Jimeng SubmitSyncTask Response: %s", string(respBody))
// 解析响应,同步任务直接返回结果
var result QueryTaskResponse
if err := json.Unmarshal(respBody, &result); err != nil {
return nil, fmt.Errorf("unmarshal response failed: %w", err)
} }
return &result, nil 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
}
@@ -4,12 +4,14 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv" "strings"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
logger2 "geekai/logger" "geekai/core/types"
"geekai/log"
"geekai/service"
"geekai/service/oss" "geekai/service/oss"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
@@ -18,33 +20,35 @@ import (
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
// Service 即梦服务(合并了消费者功能) // Service 即梦服务(合并了消费者功能)
type Service struct { type Service struct {
db *gorm.DB db *gorm.DB
redis *redis.Client redis *redis.Client
taskQueue *store.RedisQueue taskQueue *store.RedisQueue
client *Client client *Client
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
running bool running bool
uploader *oss.UploaderManager uploader *oss.UploaderManager
userService *service.UserService
} }
// NewService 创建即梦服务 // NewService 创建即梦服务
func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client) *Service { func NewService(db *gorm.DB, redisCli *redis.Client, uploader *oss.UploaderManager, client *Client, userService *service.UserService) *Service {
taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli) taskQueue := store.NewRedisQueue("JimengTaskQueue", redisCli)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
return &Service{ return &Service{
db: db, db: db,
redis: redisCli, redis: redisCli,
taskQueue: taskQueue, taskQueue: taskQueue,
client: client, client: client,
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
running: false, running: false,
uploader: uploader, uploader: uploader,
userService: userService,
} }
} }
@@ -95,35 +99,29 @@ func (s *Service) processNextTask() {
if err := s.ProcessTask(jobId); err != nil { if err := s.ProcessTask(jobId); err != nil {
logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err) logger.Errorf("process jimeng task failed: job_id=%d, error=%v", jobId, err)
s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, err.Error()) s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, err.Error())
} else { } else {
logger.Infof("Jimeng task processed successfully: job_id=%d", jobId) logger.Infof("Jimeng task processed successfully: job_id=%d", jobId)
} }
} }
// CreateTask 创建任务 // CreateTask 创建任务
func (s *Service) CreateTask(userId uint, req *CreateTaskRequest) (*model.JimengJob, error) { func (s *Service) CreateTask(userId uint, req *types.JimengTaskRequest) (*model.JimengJob, error) {
// 生成任务ID // 生成任务ID
taskId := utils.RandString(20) taskId := utils.RandString(20)
// 序列化任务参数
paramsJson, err := json.Marshal(req.Params)
if err != nil {
return nil, fmt.Errorf("marshal task params failed: %w", err)
}
// 创建任务记录 // 创建任务记录
job := &model.JimengJob{ job := &model.JimengJob{
UserId: userId, UserId: userId,
TaskId: taskId, TaskId: taskId,
Type: req.Type, Type: req.TaskType,
ReqKey: req.ReqKey, ReqKey: req.ReqKey,
Prompt: req.Prompt, Prompt: req.Prompt,
TaskParams: string(paramsJson), Params: utils.JsonEncode(req),
Status: model.JMTaskStatusInQueue, Status: types.JMTaskStatusSubmited,
Power: req.Power, Power: req.Power,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
// 保存到数据库 // 保存到数据库
@@ -148,25 +146,71 @@ func (s *Service) ProcessTask(jobId uint) error {
} }
// 更新任务状态为处理中 // 更新任务状态为处理中
if err := s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, ""); err != nil { if err := s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, ""); err != nil {
return fmt.Errorf("update job status failed: %w", err) return fmt.Errorf("update job status failed: %w", err)
} }
// 解析任务参数
var req types.JimengTaskRequest
err := utils.JsonDecode(job.Params, &req)
if err != nil {
return fmt.Errorf("parse task params failed: %w", err)
}
// 构建请求并提交任务 // 构建请求并提交任务
req, err := s.buildTaskRequest(&job) params, err := s.buildTaskRequest(&req)
if err != nil { if err != nil {
return s.handleTaskError(job.Id, fmt.Sprintf("build task request failed: %v", err)) return s.handleTaskError(job.Id, fmt.Sprintf("build task request failed: %v", err))
} }
logger.Infof("提交即梦任务: %+v", req) // 数字人任务,先识别主体
if req.TaskType == types.JMTaskTypeVirtualHuman {
if err := s.client.AvatarRecognition(req.ImageUrls[0], req.RecognizeKey); err != nil {
return s.handleTaskError(job.Id, fmt.Sprintf("avatar recognition failed: %v", err))
}
}
// 提交异步任务 // 同步任务 ,后台执行
resp, err := s.client.SubmitTask(req) if req.ReqKey == DoubaoSeedream40ReqKey {
go func() {
resp, err := s.client.SubmitSyncImageTask(req)
if err != nil {
_ = s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
return
}
logger.Infof("同步任务提交成功: %+v", resp)
// 更新原始数据
rawData, _ := json.Marshal(resp)
updates := map[string]any{
"raw_data": string(rawData),
}
if resp.Error != nil {
updates["status"] = types.JMTaskStatusFailed
updates["err_msg"] = resp.Error.Message
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
return
}
// 更新任务状态
updates["status"] = types.JMTaskStatusSuccess
// 下载图片
imgUrl, err := s.uploader.GetUploadHandler().PutUrlFile(*resp.Data[0].Url, ".png", false)
if err == nil {
updates["img_url"] = imgUrl
}
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
}()
return nil
}
logger.Debugf("提交即梦任务: %+v", params)
// 异步任务 ,前台执行
resp, err := s.client.SubmitTask(params)
if err != nil { if err != nil {
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err)) return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %v", err))
} }
if resp.Code != 10000 { if resp.Code != CodeSuccess {
return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message)) return s.handleTaskError(job.Id, fmt.Sprintf("submit task failed: %s", resp.Message))
} }
@@ -175,6 +219,7 @@ func (s *Service) ProcessTask(jobId uint) error {
if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{ if err := s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(map[string]any{
"task_id": resp.Data.TaskId, "task_id": resp.Data.TaskId,
"raw_data": string(rawData), "raw_data": string(rawData),
"status": types.JMTaskStatusInQueue, // 把任务状态改成排队中,以便开启轮询
"updated_at": time.Now(), "updated_at": time.Now(),
}).Error; err != nil { }).Error; err != nil {
logger.Errorf("update jimeng job task_id failed: %v", err) logger.Errorf("update jimeng job task_id failed: %v", err)
@@ -184,172 +229,53 @@ func (s *Service) ProcessTask(jobId uint) error {
} }
// buildTaskRequest 构建任务请求(统一的参数解析) // buildTaskRequest 构建任务请求(统一的参数解析)
func (s *Service) buildTaskRequest(job *model.JimengJob) (*SubmitTaskRequest, error) { func (s *Service) buildTaskRequest(req *types.JimengTaskRequest) (map[string]any, error) {
// 解析任务参数
var params map[string]any var params map[string]any
if err := json.Unmarshal([]byte(job.TaskParams), &params); err != nil { err := utils.JsonDecode(utils.JsonEncode(req), &params)
if err != nil {
return nil, fmt.Errorf("parse task params failed: %w", err) return nil, fmt.Errorf("parse task params failed: %w", err)
} }
// 把 size 转成 width 和 height
// 构建基础请求 if size, ok := params["size"]; ok {
req := &SubmitTaskRequest{ if sizeStr, ok := size.(string); ok {
ReqKey: job.ReqKey, if strings.Contains(sizeStr, "x") {
Prompt: job.Prompt, sizes := strings.Split(sizeStr, "x")
} params["width"] = sizes[0]
params["height"] = sizes[1]
// 根据任务类型设置特定参数
switch job.Type {
case model.JMTaskTypeTextToImage:
s.setTextToImageParams(req, params)
case model.JMTaskTypeImageToImage:
s.setImageToImageParams(req, params)
case model.JMTaskTypeImageEdit:
s.setImageEditParams(req, params)
case model.JMTaskTypeImageEffects:
s.setImageEffectsParams(req, params)
case model.JMTaskTypeTextToVideo:
s.setTextToVideoParams(req, params)
case model.JMTaskTypeImageToVideo:
s.setImageToVideoParams(req, params)
default:
return nil, fmt.Errorf("unsupported task type: %s", job.Type)
}
return req, nil
}
// setTextToImageParams 设置文生图参数
func (s *Service) setTextToImageParams(req *SubmitTaskRequest, params map[string]any) {
if seed, ok := params["seed"]; ok {
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil {
req.Seed = seedVal
}
}
if scale, ok := params["scale"]; ok {
if scaleVal, ok := scale.(float64); ok {
req.Scale = scaleVal
}
}
if width, ok := params["width"]; ok {
if widthVal, ok := width.(float64); ok {
req.Width = int(widthVal)
}
}
if height, ok := params["height"]; ok {
if heightVal, ok := height.(float64); ok {
req.Height = int(heightVal)
}
}
if usePreLlm, ok := params["use_pre_llm"]; ok {
if usePreLlmVal, ok := usePreLlm.(bool); ok {
req.UsePreLLM = usePreLlmVal
}
}
}
// setImageToImageParams 设置图生图参数
func (s *Service) setImageToImageParams(req *SubmitTaskRequest, params map[string]any) {
if imageInput, ok := params["image_input"].(string); ok {
req.ImageInput = imageInput
}
if gpen, ok := params["gpen"]; ok {
if gpenVal, ok := gpen.(float64); ok {
req.Gpen = gpenVal
}
}
if skin, ok := params["skin"]; ok {
if skinVal, ok := skin.(float64); ok {
req.Skin = skinVal
}
}
if skinUnifi, ok := params["skin_unifi"]; ok {
if skinUnifiVal, ok := skinUnifi.(float64); ok {
req.SkinUnifi = skinUnifiVal
}
}
if genMode, ok := params["gen_mode"].(string); ok {
req.GenMode = genMode
}
s.setCommonParams(req, params) // 复用通用参数
}
// setImageEditParams 设置图像编辑参数
func (s *Service) setImageEditParams(req *SubmitTaskRequest, params map[string]any) {
if imageUrls, ok := params["image_urls"].([]any); ok {
for _, url := range imageUrls {
if urlStr, ok := url.(string); ok {
req.ImageUrls = append(req.ImageUrls, urlStr)
} }
} }
delete(params, "size")
} }
if binaryData, ok := params["binary_data_base64"].([]any); ok {
for _, data := range binaryData {
if dataStr, ok := data.(string); ok {
req.BinaryDataBase64 = append(req.BinaryDataBase64, dataStr)
}
}
}
if scale, ok := params["scale"]; ok {
if scaleVal, ok := scale.(float64); ok {
req.Scale = scaleVal
}
}
s.setCommonParams(req, params)
}
// setImageEffectsParams 设置图像特效参数 // duration 转成 frames
func (s *Service) setImageEffectsParams(req *SubmitTaskRequest, params map[string]any) { if duration, ok := params["duration"]; ok {
if imageInput1, ok := params["image_input1"].(string); ok { if v, ok := duration.(int); ok {
req.ImageInput1 = imageInput1 params["frames"] = v*24 + 1
} } else if v, ok := duration.(float64); ok {
if templateId, ok := params["template_id"].(string); ok { params["frames"] = int(v*24) + 1
req.TemplateId = templateId
}
if width, ok := params["width"]; ok {
if widthVal, ok := width.(float64); ok {
req.Width = int(widthVal)
} }
delete(params, "duration")
} }
if height, ok := params["height"]; ok {
if heightVal, ok := height.(float64); ok {
req.Height = int(heightVal)
}
}
}
// setTextToVideoParams 设置文生视频参数 // 单独处理图片特效任务
func (s *Service) setTextToVideoParams(req *SubmitTaskRequest, params map[string]any) { if req.ReqKey == ImageEffectReqKey {
if aspectRatio, ok := params["aspect_ratio"].(string); ok { params["image_input1"] = req.ImageUrls[0]
req.AspectRatio = aspectRatio delete(params, "image_urls")
} }
s.setCommonParams(req, params)
}
// setImageToVideoParams 设置图生视频参数 // 动作迁移,数字人任务参数处理
func (s *Service) setImageToVideoParams(req *SubmitTaskRequest, params map[string]any) { if req.TaskType == types.JMTaskTypeVirtualHuman || req.TaskType == types.JMTaskTypeActionTransfer {
s.setImageEditParams(req, params) // 复用图像编辑的参数设置 params["image_url"] = req.ImageUrls[0]
if aspectRatio, ok := params["aspect_ratio"].(string); ok { delete(params, "image_urls")
req.AspectRatio = aspectRatio }
if req.RecognizeKey != "" {
delete(params, "recognize_key")
} }
}
// setCommonParams 设置通用参数(seed, width, height等) // 删除多余参数,剩下的就是各个任务自己专有参数了
func (s *Service) setCommonParams(req *SubmitTaskRequest, params map[string]any) { delete(params, "type")
if seed, ok := params["seed"]; ok { delete(params, "power")
if seedVal, err := strconv.ParseInt(fmt.Sprintf("%.0f", seed), 10, 64); err == nil { return params, nil
req.Seed = seedVal
}
}
if width, ok := params["width"]; ok {
if widthVal, ok := width.(float64); ok {
req.Width = int(widthVal)
}
}
if height, ok := params["height"]; ok {
if heightVal, ok := height.(float64); ok {
req.Height = int(heightVal)
}
}
} }
// pollTaskStatus 轮询任务状态 // pollTaskStatus 轮询任务状态
@@ -357,12 +283,10 @@ func (s *Service) pollTaskStatus() {
for { for {
var jobs []model.JimengJob var jobs []model.JimengJob
s.db.Where("status IN (?)", []model.JMTaskStatus{model.JMTaskStatusGenerating, model.JMTaskStatusInQueue}).Find(&jobs) // 找出排队中和处理中的任务进行轮询
if len(jobs) == 0 { s.db.Where("status IN (?)", []types.JMTaskStatus{
logger.Debugf("no jimeng task to poll, sleep 10s") types.JMTaskStatusGenerating,
time.Sleep(10 * time.Second) types.JMTaskStatusInQueue}).Find(&jobs)
continue
}
for _, job := range jobs { for _, job := range jobs {
// 任务超时处理 // 任务超时处理
@@ -371,12 +295,17 @@ func (s *Service) pollTaskStatus() {
continue continue
} }
// 豆包生图 4.0 是同步任务,不需要轮询
if job.ReqKey == DoubaoSeedream40ReqKey {
continue
}
// 查询任务状态 // 查询任务状态
resp, err := s.client.QueryTask(&QueryTaskRequest{ resp, err := s.client.QueryTask(&QueryTaskRequest{
ReqKey: job.ReqKey, ReqKey: job.ReqKey,
TaskId: job.TaskId, TaskId: job.TaskId,
ReqJson: `{"return_url":true}`, ReqJson: `{"return_url":true}`,
}) }, ASyncActionGetResult)
if err != nil { if err != nil {
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", err.Error())) s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", err.Error()))
@@ -387,13 +316,13 @@ func (s *Service) pollTaskStatus() {
rawData, _ := json.Marshal(resp) rawData, _ := json.Marshal(resp)
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Update("raw_data", string(rawData)) s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Update("raw_data", string(rawData))
if resp.Code != 10000 { if resp.Code != CodeSuccess {
s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", resp.Message)) s.handleTaskError(job.Id, fmt.Sprintf("query task failed: %s", resp.Message))
continue continue
} }
switch resp.Data.Status { switch resp.Data.Status {
case model.JMTaskStatusDone: case types.JMTaskStatusDone:
// 判断任务是否成功 // 判断任务是否成功
if resp.Message != "Success" { if resp.Message != "Success" {
s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage)) s.handleTaskError(job.Id, fmt.Sprintf("task failed: %s", resp.Data.AlgorithmBaseResp.StatusMessage))
@@ -402,7 +331,7 @@ func (s *Service) pollTaskStatus() {
// 任务完成,更新结果 // 任务完成,更新结果
updates := map[string]any{ updates := map[string]any{
"status": model.JMTaskStatusSuccess, "status": types.JMTaskStatusSuccess,
"updated_at": time.Now(), "updated_at": time.Now(),
} }
@@ -425,15 +354,15 @@ func (s *Service) pollTaskStatus() {
} }
s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates) s.db.Model(&model.JimengJob{}).Where("id = ?", job.Id).Updates(updates)
case model.JMTaskStatusInQueue, model.JMTaskStatusGenerating: case types.JMTaskStatusInQueue, types.JMTaskStatusGenerating:
// 任务处理中 // 任务处理中
s.UpdateJobStatus(job.Id, model.JMTaskStatusGenerating, "") s.UpdateJobStatus(job.Id, types.JMTaskStatusGenerating, "")
case model.JMTaskStatusNotFound: case types.JMTaskStatusNotFound:
// 任务未找到 // 任务未找到
s.handleTaskError(job.Id, "task not found") s.handleTaskError(job.Id, "task not found")
case model.JMTaskStatusExpired: case types.JMTaskStatusExpired:
continue continue
default: default:
logger.Warnf("unknown task status: %s", resp.Data.Status) logger.Warnf("unknown task status: %s", resp.Data.Status)
@@ -441,6 +370,21 @@ func (s *Service) pollTaskStatus() {
} }
// 找出失败的任务,并恢复其扣减算力
s.db.Where("status = ?", types.JMTaskStatusFailed).Where("power > ?", 0).Find(&jobs)
for _, job := range jobs {
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: job.ReqKey,
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%d", job.Id),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
} }
@@ -448,7 +392,7 @@ func (s *Service) pollTaskStatus() {
} }
// UpdateJobStatus 更新任务状态 // UpdateJobStatus 更新任务状态
func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg string) error { func (s *Service) UpdateJobStatus(jobId uint, status types.JMTaskStatus, errMsg string) error {
updates := map[string]any{ updates := map[string]any{
"status": status, "status": status,
"updated_at": time.Now(), "updated_at": time.Now(),
@@ -462,7 +406,7 @@ func (s *Service) UpdateJobStatus(jobId uint, status model.JMTaskStatus, errMsg
// handleTaskError 处理任务错误 // handleTaskError 处理任务错误
func (s *Service) handleTaskError(jobId uint, errMsg string) error { func (s *Service) handleTaskError(jobId uint, errMsg string) error {
logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg) logger.Errorf("Jimeng task error (job_id: %d): %s", jobId, errMsg)
return s.UpdateJobStatus(jobId, model.JMTaskStatusFailed, errMsg) return s.UpdateJobStatus(jobId, types.JMTaskStatusFailed, errMsg)
} }
// PushTaskToQueue 推送任务到队列(用于手动重试) // PushTaskToQueue 推送任务到队列(用于手动重试)
@@ -473,8 +417,8 @@ func (s *Service) PushTaskToQueue(jobId uint) error {
// GetTaskStats 获取任务统计信息 // GetTaskStats 获取任务统计信息
func (s *Service) GetTaskStats() (map[string]any, error) { func (s *Service) GetTaskStats() (map[string]any, error) {
type StatResult struct { type StatResult struct {
Status string `json:"status"` Status types.JMTaskStatus `json:"status"`
Count int64 `json:"count"` Count int64 `json:"count"`
} }
var stats []StatResult var stats []StatResult
@@ -496,7 +440,7 @@ func (s *Service) GetTaskStats() (map[string]any, error) {
for _, stat := range stats { for _, stat := range stats {
result["total"] = result["total"].(int64) + stat.Count result["total"] = result["total"].(int64) + stat.Count
result[stat.Status] = stat.Count result[string(stat.Status)] = stat.Count
} }
return result, nil return result, nil
+74 -83
View File
@@ -1,43 +1,9 @@
package jimeng package jimeng
import "geekai/store/model" import (
"geekai/core/types"
// ReqKey 常量定义
const (
ReqKeyTextToImage = "high_aes_general_v30l_zt2i" // 文生图
ReqKeyImageToImagePortrait = "i2i_portrait_photo" // 图生图人像写真
ReqKeyImageEdit = "seededit_v3.0" // 图像编辑
ReqKeyImageEffects = "i2i_multi_style_zx2x" // 图像特效
ReqKeyTextToVideo = "jimeng_vgfm_t2v_l20" // 文生视频
ReqKeyImageToVideo = "jimeng_vgfm_i2v_l20" // 图生视频
) )
// SubmitTaskRequest 提交任务请求
type SubmitTaskRequest struct {
ReqKey string `json:"req_key"`
// 文生图参数
Prompt string `json:"prompt,omitempty"`
Seed int64 `json:"seed,omitempty"`
Scale float64 `json:"scale,omitempty"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
UsePreLLM bool `json:"use_pre_llm,omitempty"`
// 图生图参数
ImageInput string `json:"image_input,omitempty"`
ImageUrls []string `json:"image_urls,omitempty"`
BinaryDataBase64 []string `json:"binary_data_base64,omitempty"`
Gpen float64 `json:"gpen,omitempty"`
Skin float64 `json:"skin,omitempty"`
SkinUnifi float64 `json:"skin_unifi,omitempty"`
GenMode string `json:"gen_mode,omitempty"`
// 图像编辑参数
// 图像特效参数
ImageInput1 string `json:"image_input1,omitempty"`
TemplateId string `json:"template_id,omitempty"`
// 视频生成参数
AspectRatio string `json:"aspect_ratio,omitempty"`
}
// SubmitTaskResponse 提交任务响应 // SubmitTaskResponse 提交任务响应
type SubmitTaskResponse struct { type SubmitTaskResponse struct {
Code int `json:"code"` Code int `json:"code"`
@@ -73,7 +39,7 @@ type QueryTaskResponse struct {
ImageUrls []string `json:"image_urls"` ImageUrls []string `json:"image_urls"`
VideoUrl string `json:"video_url"` VideoUrl string `json:"video_url"`
RespData string `json:"resp_data"` RespData string `json:"resp_data"`
Status model.JMTaskStatus `json:"status"` Status types.JMTaskStatus `json:"status"`
LlmResult string `json:"llm_result"` LlmResult string `json:"llm_result"`
PeResult string `json:"pe_result"` PeResult string `json:"pe_result"`
PredictTagsResult string `json:"predict_tags_result"` PredictTagsResult string `json:"predict_tags_result"`
@@ -83,9 +49,73 @@ type QueryTaskResponse struct {
} `json:"data"` } `json:"data"`
} }
const CodeSuccess = 10000
// 即梦AI错误代码常量
const (
// 成功
ECSuccess = 10000
// 请求参数错误 (50200-50215)
ECReqInvalidArgs = 50200 // 参数错误
ECReqMissingArgs = 50201 // 缺少参数
ECParseArgs = 50204 // 参数类型错误/参数缺失
ECImageSizeLimited = 50205 // 图像尺寸超过限制
ECImageEmpty = 50206 // 请求参数中没有获取到图像
ECImageDecodeError = 50207 // 图像解码错误
ECVideoEmpty = 50209 // 请求参数中没有获取到视频
ECVideoDecodeError = 50210 // 视频解码错误
ECVideoSizeLimited = 50211 // 视频尺寸超过限制
ECReqBodySizeLimited = 50213 // 请求Body过大
ECVideoTimeTooLong = 50214 // 输入视频时长过大
ECRPCProcess = 50215 // 请求处理失败
// 算法服务错误 (60102-60208)
ECJPFaceDetect = 60102 // 算法服务需要输入人脸图,但未检测到
ECFSLeaderRiskError = 60208 // 输入图片中包含敏感信息,未通过审核
// 权限和系统错误 (50400-50501)
ECAuth = 50400 // 权限校验失败
ECReqMethod = 50402 // 访问的接口不存在
ECReqLimit = 50429 // 超过调用QPS限制
ECInternal = 50500 // 服务器内部错误
ECRPCInternal = 50501 // 服务器内部RPC错误
)
// 错误代码到错误信息的映射
var errorCodeMessages = map[int]string{
// 成功
ECSuccess: "请求成功",
// 请求参数错误
ECReqInvalidArgs: "参数错误,检查入参及MIME类型",
ECReqMissingArgs: "缺少参数,检查入参及MIME类型",
ECParseArgs: "参数类型错误/参数缺失,检查入参及MIME类型",
ECImageSizeLimited: "图像尺寸超过限制,参考接口文档入参要求部分",
ECImageEmpty: "请求参数中没有获取到图像,检查入参",
ECImageDecodeError: "图像解码错误:没有获取到图像或者通过image_base64参数传递图像是base64解码错误,检查输出图片或检查base64是否错误携带前缀",
ECVideoEmpty: "请求参数中没有获取到视频。输入为视频时可能返回此错误,检查入参",
ECVideoDecodeError: "视频解码错误。输入为视频时可能返回此错误,检查输入视频是否不正确",
ECVideoSizeLimited: "视频尺寸超过限制。输入为视频时可能返回此错误,检查输入视频大小",
ECReqBodySizeLimited: "请求Body过大,超出接口限制,检查请求Body大小",
ECVideoTimeTooLong: "输入视频时长过大,检查输入视频时长",
ECRPCProcess: "由于输入的图片、视频、参数等不满足要求,导致请求处理失败。若接口文档中有具体说明,优先参考其具体含义,按照具体服务说明进行检查",
// 算法服务错误
ECJPFaceDetect: "算法服务需要输入人脸图,但未检测到,检查输入图片是否包含人脸",
ECFSLeaderRiskError: "输入图片中包含敏感信息,未通过审核",
// 权限和系统错误
ECAuth: "权限校验失败,请检查是否已创建应用并开通服务或签名,参考接入指南及快速接入",
ECReqMethod: "访问的接口不存在,检查入参",
ECReqLimit: "超过调用QPS限制,购买QPS增项包",
ECInternal: "服务器内部错误,提工单",
ECRPCInternal: "服务器内部RPC错误,提工单",
}
// CreateTaskRequest 创建任务请求 // CreateTaskRequest 创建任务请求
type CreateTaskRequest struct { type CreateTaskRequest struct {
Type model.JMTaskType `json:"type"` Type types.JMTaskType `json:"type"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
Params map[string]any `json:"params"` Params map[string]any `json:"params"`
ReqKey string `json:"req_key"` ReqKey string `json:"req_key"`
@@ -93,53 +123,14 @@ type CreateTaskRequest struct {
Power int `json:"power,omitempty"` Power int `json:"power,omitempty"`
} }
// LogoInfo 水印信息
type LogoInfo struct {
AddLogo bool `json:"add_logo"`
Position int `json:"position"`
Language int `json:"language"`
Opacity float64 `json:"opacity"`
LogoTextContent string `json:"logo_text_content"`
}
// ReqJsonConfig 查询配置
type ReqJsonConfig struct {
ReturnUrl bool `json:"return_url"`
LogoInfo *LogoInfo `json:"logo_info,omitempty"`
}
// ImageEffectTemplate 图像特效模板
const ( const (
TemplateIdFelt3DPolaroid = "felt_3d_polaroid" // 毛毡3d拍立得风格 ImageEffectReqKey = "i2i_multi_style_zx2x"
TemplateIdMyWorld = "my_world" // 像素世界风 DoubaoSeedream40ReqKey = "doubao-seedream-4-0-250828"
TemplateIdMyWorldUniversal = "my_world_universal" // 像素世界-万物通用版
TemplateIdPlasticBubbleFigure = "plastic_bubble_figure" // 盲盒玩偶风
TemplateIdPlasticBubbleFigureCartoon = "plastic_bubble_figure_cartoon_text" // 塑料泡罩人偶-文字卡头版
TemplateIdFurryDreamDoll = "furry_dream_doll" // 毛绒玩偶风
TemplateIdMicroLandscapeMiniWorld = "micro_landscape_mini_world" // 迷你世界玩偶风
TemplateIdMicroLandscapeProfessional = "micro_landscape_mini_world_professional" // 微型景观小世界-职业版
TemplateIdAcrylicOrnaments = "acrylic_ornaments" // 亚克力挂饰
TemplateIdFeltKeychain = "felt_keychain" // 毛毡钥匙扣
TemplateIdLofiPixelCharacter = "lofi_pixel_character_mini_card" // Lofi像素人物小卡
TemplateIdAngelFigurine = "angel_figurine" // 天使形象手办
TemplateIdLyingInFluffyBelly = "lying_in_fluffy_belly" // 躺在毛茸茸肚皮里
TemplateIdGlassBall = "glass_ball" // 玻璃球
) )
// AspectRatio 视频宽高比
const ( const (
AspectRatio16_9 = "16:9" // 1280×720 ASyncActionSubmit = "CVSync2AsyncSubmitTask" // 异步提交任务
AspectRatio9_16 = "9:16" // 720×1280 SyncActionSubmit = "CVSubmitTask" // 同步提交任务
AspectRatio1_1 = "1:1" // 960×960 ASyncActionGetResult = "CVSync2AsyncGetResult" // 异步获取结果
AspectRatio4_3 = "4:3" // 960×720 SyncActionGetResult = "CVGetResult" // 同步获取结果
AspectRatio3_4 = "3:4" // 720×960
AspectRatio21_9 = "21:9" // 1680×720
AspectRatio9_21 = "9:21" // 720×1680
)
// GenMode 生成模式
const (
GenModeCreative = "creative" // 提示词模式
GenModeReference = "reference" // 全参考模式
GenModeReferenceChar = "reference_char" // 人物参考模式
) )
-224
View File
@@ -1,224 +0,0 @@
package service
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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 (
"errors"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/utils"
"strings"
"time"
"github.com/imroc/req/v3"
"github.com/shirou/gopsutil/host"
"gorm.io/gorm"
)
type LicenseService struct {
license *types.License
urlWhiteList []string
machineId string
db *gorm.DB
}
func NewLicenseService(sysConfig *types.SystemConfig, db *gorm.DB) *LicenseService {
var machineId string
info, err := host.Info()
if err == nil {
machineId = info.HostID
}
logger.Infof("License: %+v", sysConfig.License)
return &LicenseService{
license: &sysConfig.License,
machineId: machineId,
db: db,
}
}
type License struct {
Name string `json:"name"`
License string `json:"license"`
MachineId string `json:"mid"`
ActiveAt int64 `json:"active_at"`
ExpiredAt int64 `json:"expired_at"`
UserNum int `json:"user_num"`
Configs types.LicenseConfig `json:"configs"`
}
// ActiveLicense 激活 License
func (s *LicenseService) ActiveLicense(license string) error {
var res struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
Data License `json:"data"`
}
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/active")
response, err := req.C().R().
SetBody(map[string]string{"license": license, "machine_id": s.machineId}).
SetSuccessResult(&res).Post(apiURL)
if err != nil {
return fmt.Errorf("发送激活请求失败: %v", err)
}
if response.IsErrorState() {
return fmt.Errorf("发送激活请求失败:%v", response.Status)
}
if res.Code != types.Success {
return fmt.Errorf("激活失败:%v", res.Message)
}
if res.Data.ExpiredAt > 0 && res.Data.ExpiredAt < time.Now().Unix() {
return fmt.Errorf("License 已过期")
}
s.license = &types.License{
Key: license,
MachineId: s.machineId,
Configs: res.Data.Configs,
ExpiredAt: res.Data.ExpiredAt,
IsActive: true,
}
// 保存 License 到数据库
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
if err != nil {
return fmt.Errorf("保存 License 到数据库失败: %v", err)
}
return nil
}
// SyncLicense 定期同步 License
func (s *LicenseService) SyncLicense() {
go func() {
retryCounter := 0
for {
license, err := s.fetchLicense()
if err != nil {
retryCounter++
if retryCounter < 5 {
logger.Debug(err)
}
s.license.IsActive = false
} else {
s.license = license
// 保存 License 到数据库
err = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).UpdateColumn("value", utils.JsonEncode(s.license)).Error
if err != nil {
logger.Errorf("保存 License 到数据库失败: %v", err)
}
}
urls, err := s.fetchUrlWhiteList()
if err == nil {
s.urlWhiteList = urls
}
time.Sleep(time.Second * 10)
}
}()
}
func (s *LicenseService) fetchLicense() (*types.License, error) {
var res struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
Data License `json:"data"`
}
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/check")
response, err := req.C().R().
SetBody(map[string]string{"license": s.license.Key, "machine_id": s.machineId}).
SetSuccessResult(&res).Post(apiURL)
if err != nil {
return nil, fmt.Errorf("License 同步失败: %v", err)
}
if response.IsErrorState() {
return nil, fmt.Errorf("License 同步失败:%v", response.Status)
}
if res.Code != types.Success {
return nil, fmt.Errorf("License 同步失败:%v", res.Message)
}
return &types.License{
Key: res.Data.License,
MachineId: res.Data.MachineId,
Configs: res.Data.Configs,
ExpiredAt: res.Data.ExpiredAt,
IsActive: true,
}, nil
}
func (s *LicenseService) fetchUrlWhiteList() ([]string, error) {
var res struct {
Code types.BizCode `json:"code"`
Message string `json:"message"`
Data []string `json:"data"`
}
apiURL := fmt.Sprintf("%s/%s", types.GeekAPIURL, "api/license/urls")
response, err := req.C().R().SetSuccessResult(&res).Get(apiURL)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
if response.IsErrorState() {
return nil, fmt.Errorf("发送请求失败:%v", response.Status)
}
if res.Code != types.Success {
return nil, fmt.Errorf("获取白名单失败:%v", res.Message)
}
return res.Data, nil
}
// GetLicense 获取许可信息
func (s *LicenseService) GetLicense() *types.License {
if s.license == nil {
var config model.Config
s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyLicense).First(&config)
if config.Value != "" {
utils.JsonDecode(config.Value, &s.license)
}
}
return s.license
}
func (s *LicenseService) SetLicense(licenseKey string) {
s.license.Key = licenseKey
}
// IsValidApiURL 判断是否合法的中转 URL
func (s *LicenseService) IsValidApiURL(uri string) error {
// 获得许可授权的直接放行
if s.license.IsActive {
if s.license.MachineId != s.machineId {
return errors.New("系统使用了盗版的许可证书")
}
if time.Now().Unix() > s.license.ExpiredAt {
return errors.New("系统许可证书已经过期")
}
return nil
}
if len(s.urlWhiteList) == 0 {
urls, err := s.fetchUrlWhiteList()
if err == nil {
s.urlWhiteList = urls
}
}
for _, v := range s.urlWhiteList {
if strings.HasPrefix(uri, v) {
return nil
}
}
return fmt.Errorf("当前 API 地址 %s 不在白名单列表当中。", uri)
}
+421 -58
View File
@@ -12,7 +12,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"geekai/store"
"geekai/store/model" "geekai/store/model"
"strings" "strings"
@@ -29,61 +28,27 @@ const (
// MigrationService 配置迁移服务 // MigrationService 配置迁移服务
type MigrationService struct { type MigrationService struct {
db *gorm.DB db *gorm.DB
redisClient *redis.Client redisClient *redis.Client
appConfig *types.AppConfig appConfig *types.AppConfig
levelDB *store.LevelDB
licenseService *LicenseService
} }
func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig, levelDB *store.LevelDB, licenseService *LicenseService) *MigrationService { func NewMigrationService(db *gorm.DB, redisClient *redis.Client, appConfig *types.AppConfig) *MigrationService {
return &MigrationService{ return &MigrationService{
db: db, db: db,
redisClient: redisClient, redisClient: redisClient,
appConfig: appConfig, appConfig: appConfig,
levelDB: levelDB,
licenseService: licenseService,
} }
} }
func (s *MigrationService) StartMigrate() { func (s *MigrationService) StartMigrate() {
// 表结构迁移必须在业务服务启动前完成,避免新表和新列尚未创建就被后台任务查询。
s.TableMigration()
go func() { go func() {
s.MigrateConfig(s.appConfig) s.MigrateConfig(s.appConfig)
s.TableMigration()
s.MigrateLicense()
}() }()
} }
// 迁移 License
func (s *MigrationService) MigrateLicense() {
key := "migrate:license"
if s.redisClient.Get(context.Background(), key).Val() == "1" {
logger.Info("License 已迁移,跳过迁移")
return
}
logger.Info("开始迁移 License...")
var license types.License
err := s.levelDB.Get(types.LicenseKey, &license)
if err != nil {
license = types.License{
Key: "",
MachineId: "",
Configs: types.LicenseConfig{UserNum: 0, DeCopy: false},
ExpiredAt: 0,
IsActive: false,
}
}
logger.Infof("迁移 License: %+v", license)
if err := s.saveConfig(types.ConfigKeyLicense, license); err != nil {
logger.Errorf("迁移 License 失败: %v", err)
return
}
s.licenseService.SetLicense(license.Key)
logger.Info("迁移 License 完成")
s.redisClient.Set(context.Background(), key, "1", 0)
}
// 迁移配置内容 // 迁移配置内容
func (s *MigrationService) MigrateConfigContent() error { func (s *MigrationService) MigrateConfigContent() error {
// 用户协议 // 用户协议
@@ -157,25 +122,218 @@ func (s *MigrationService) MigrateConfigContent() error {
return nil return nil
} }
// 数据表迁移 // fullTableMigration 第一步:全量表迁移,同步所有表结构(新增表、新增字段、字段类型)
func (s *MigrationService) TableMigration() { // 适用于首次安装或导入旧版数据库后同步 schema,AutoMigrate 会补齐缺失的表和列
// 新数据表 func (s *MigrationService) fullTableMigration() {
s.db.AutoMigrate(&model.Moderation{}) logger.Info("执行全量表迁移(同步 schema...")
models := []any{
// 订单字段整理 &model.Config{},
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") { &model.AdminUser{},
s.db.Migrator().RenameColumn(&model.Order{}, "pay_type", "channel") &model.ChatApp{},
&model.ApiKey{},
&model.AppType{},
&model.ChatModel{},
&model.User{},
&model.ChatItem{},
&model.ChatMessage{},
&model.Order{},
&model.Product{},
&model.Function{},
&model.Menu{},
&model.InviteCode{},
&model.InviteLog{},
&model.Redeem{},
&model.PowerLog{},
&model.File{},
&model.UserLoginLog{},
&model.MidJourneyJob{},
&model.SunoJob{},
&model.VideoJob{},
&model.JimengJob{},
&model.PPTJob{},
&model.Moderation{},
&model.ImageJob{},
} }
if !s.db.Migrator().HasColumn(&model.Order{}, "checked") { if err := s.db.AutoMigrate(models...); err != nil {
s.db.Migrator().AddColumn(&model.Order{}, "checked") logger.Errorf("全量表迁移失败: %v", err)
return
}
logger.Info("全量表迁移完成")
}
// fixTableConstraints 第一步之后:根据模型定义修复各表的主键、自增和关键索引
// 主要解决初始化 SQL 中缺少 AUTO_INCREMENT 或 PRIMARY KEY 导致插入失败的问题
func (s *MigrationService) fixTableConstraints() {
logger.Info("开始修复各表的主键、自增属性和索引...")
// 当前数据库名
var dbName string
if err := s.db.Raw("SELECT DATABASE()").Scan(&dbName).Error; err != nil {
logger.Errorf("获取当前数据库名失败: %v", err)
return
}
if dbName == "" {
logger.Warn("当前连接未选择数据库,跳过约束修复")
return
} }
// 重命名 config 表字段 // === 修复所有包含 id 字段的表的主键 + 自增 ===
type columnInfo struct {
TableName string `gorm:"column:TABLE_NAME"`
ColumnName string `gorm:"column:COLUMN_NAME"`
ColumnKey string `gorm:"column:COLUMN_KEY"`
Extra string `gorm:"column:EXTRA"`
DataType string `gorm:"column:DATA_TYPE"`
}
var idColumns []columnInfo
if err := s.db.Raw(`
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_KEY, EXTRA, DATA_TYPE
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = ? AND COLUMN_NAME = 'id'
`, dbName).Scan(&idColumns).Error; err != nil {
logger.Errorf("查询各表 id 字段信息失败: %v", err)
} else {
for _, col := range idColumns {
if col.ColumnName == "" {
continue
}
// 检查当前表是否已经存在主键
type pkInfo struct {
ColumnName string `gorm:"column:COLUMN_NAME"`
}
var pkColumns []pkInfo
if err := s.db.Raw(`
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'
`, dbName, col.TableName).Scan(&pkColumns).Error; err != nil {
logger.Errorf("查询表 %s 的主键信息失败: %v", col.TableName, err)
continue
}
hasPK := len(pkColumns) > 0
pkOnIdOnly := hasPK && len(pkColumns) == 1 && pkColumns[0].ColumnName == "id"
needAlter := false
switch {
case !hasPK:
// 没有任何主键:允许将 id 设置为自增主键
if col.ColumnKey != "PRI" || col.Extra == "" || !strings.Contains(col.Extra, "auto_increment") {
needAlter = true
}
case pkOnIdOnly:
// 只有 id 作为主键:只补充自增属性
if col.Extra == "" || !strings.Contains(col.Extra, "auto_increment") {
needAlter = true
}
default:
// 已存在非 id 或组合主键:避免破坏原有主键,直接跳过
logger.Infof("表 %s 已存在非 id 主键,跳过 id 自增主键修复", col.TableName)
}
if !needAlter {
continue
}
// 维持原来的数据类型,避免与历史 SQL 冲突
dataType := col.DataType
if dataType == "" {
dataType = "int"
}
alterSQL := fmt.Sprintf(
"ALTER TABLE `%s` MODIFY COLUMN id %s NOT NULL AUTO_INCREMENT PRIMARY KEY",
col.TableName,
dataType,
)
if err := s.db.Exec(alterSQL).Error; err != nil {
logger.Errorf("修复表 %s 的 id 自增主键失败: %v", col.TableName, err)
} else {
logger.Infof("已修复表 %s 的 id 为 AUTO_INCREMENT PRIMARY KEY", col.TableName)
}
}
}
// === 为 geekai_users.username 补充唯一索引(根据模型 uniqueIndex 定义) ===
var usernameUniqueCount int64
err := s.db.Raw(`
SELECT COUNT(1)
FROM INFORMATION_SCHEMA.STATISTICS
WHERE TABLE_SCHEMA = ?
AND TABLE_NAME = 'geekai_users'
AND COLUMN_NAME = 'username'
AND NON_UNIQUE = 0
`, dbName).Scan(&usernameUniqueCount).Error
if err != nil {
logger.Errorf("检查 geekai_users.username 唯一索引失败: %v", err)
} else if usernameUniqueCount == 0 {
// 索引名尽量固定,避免重复创建
if err := s.db.Exec("ALTER TABLE geekai_users ADD UNIQUE KEY idx_geekai_users_username (username)").Error; err != nil {
logger.Errorf("创建 geekai_users.username 唯一索引失败: %v", err)
} else {
logger.Info("已为 geekai_users.username 创建唯一索引 idx_geekai_users_username")
}
} else {
logger.Info("geekai_users.username 唯一索引已存在,跳过创建")
}
logger.Info("关键表主键、自增和索引修复完成")
}
// incrementalTableMigration 第二步:增量迁移,仅处理删除字段与数据迁移
// AutoMigrate 不会删除列,故需在此显式 DropColumn;字段重命名需先拷贝数据再删除旧列
func (s *MigrationService) incrementalTableMigration() {
logger.Info("执行增量迁移(删除字段 + 数据迁移)...")
// ========== 字段重命名:全量迁移已添加新列,需将旧列数据拷贝到新列后删除旧列 ==========
// ChatApp(geekai_chat_roles): context_json -> system_prompt 历史数据迁移
if s.db.Migrator().HasColumn(&model.ChatApp{}, "context_json") {
// 将旧列 context_json 的值拷贝到 system_promptNULL 转为空字符串,保证 NOT NULL 约束)
if err := s.db.Exec(`
UPDATE geekai_chat_roles
SET system_prompt = IFNULL(NULLIF(TRIM(COALESCE(context_json, '')), ''), '')
`).Error; err != nil {
logger.Errorf("迁移 geekai_chat_roles.context_json -> system_prompt 失败: %v", err)
} else {
if err := s.db.Migrator().DropColumn(&model.ChatApp{}, "context_json"); err != nil {
logger.Errorf("删除 geekai_chat_roles.context_json 失败: %v", err)
} else {
logger.Info("geekai_chat_roles: context_json 已迁移至 system_prompt 并删除旧列")
}
}
}
// ChatApp: 将 user_id 为 NULL 的历史记录置为 0(系统内置)
if s.db.Migrator().HasColumn(&model.ChatApp{}, "user_id") {
if err := s.db.Exec(`UPDATE geekai_chat_roles SET user_id = 0 WHERE user_id IS NULL`).Error; err != nil {
logger.Errorf("初始化 geekai_chat_roles.user_id 失败: %v", err)
}
}
// ChatApp(geekai_chat_roles): 删除 marker 列(应用仅通过 id 区分)
var hasMarker int
if s.db.Raw("SELECT COUNT(1) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'geekai_chat_roles' AND COLUMN_NAME = 'marker'").Scan(&hasMarker).Error == nil && hasMarker > 0 {
// 先删除可能存在的唯一索引(不同版本 SQL 索引名不同)
for _, idxName := range []string{"marker", "idx_chatgpt_chat_roles_marker", "idx_chatgpt_chat_roles_key", "idx_geekai_chat_roles_marker"} {
_ = s.db.Exec(fmt.Sprintf("ALTER TABLE geekai_chat_roles DROP INDEX `%s`", idxName)).Error
}
if err := s.db.Exec("ALTER TABLE geekai_chat_roles DROP COLUMN marker").Error; err != nil {
logger.Errorf("删除 geekai_chat_roles.marker 失败: %v", err)
} else {
logger.Info("geekai_chat_roles: 已删除 marker 列")
}
}
// Config: config_json -> value, marker -> name
if s.db.Migrator().HasColumn(&model.Config{}, "config_json") { if s.db.Migrator().HasColumn(&model.Config{}, "config_json") {
s.db.Migrator().RenameColumn(&model.Config{}, "config_json", "value") s.db.Exec("UPDATE geekai_configs SET `value` = config_json WHERE config_json IS NOT NULL AND config_json != ''")
s.db.Migrator().DropColumn(&model.Config{}, "config_json")
} }
if s.db.Migrator().HasColumn(&model.Config{}, "marker") { if s.db.Migrator().HasColumn(&model.Config{}, "marker") {
s.db.Migrator().RenameColumn(&model.Config{}, "marker", "name") s.db.Exec("UPDATE geekai_configs SET `name` = marker WHERE marker IS NOT NULL AND marker != ''")
s.db.Migrator().DropColumn(&model.Config{}, "marker")
} }
if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") { if s.db.Migrator().HasIndex(&model.Config{}, "idx_chatgpt_configs_key") {
s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key") s.db.Migrator().DropIndex(&model.Config{}, "idx_chatgpt_configs_key")
@@ -184,7 +342,40 @@ func (s *MigrationService) TableMigration() {
s.db.Migrator().DropIndex(&model.Config{}, "marker") s.db.Migrator().DropIndex(&model.Config{}, "marker")
} }
// 手动删除字段 // Order: pay_type -> channel
if s.db.Migrator().HasColumn(&model.Order{}, "pay_type") {
s.db.Exec("UPDATE geekai_orders SET channel = pay_type WHERE pay_type IS NOT NULL AND pay_type != ''")
s.db.Migrator().DropColumn(&model.Order{}, "pay_type")
}
// JimengJob: task_params -> params
if s.db.Migrator().HasColumn(&model.JimengJob{}, "task_params") {
s.db.Exec("UPDATE geekai_jimeng_jobs SET params = task_params WHERE task_params IS NOT NULL AND task_params != ''")
s.db.Migrator().DropColumn(&model.JimengJob{}, "task_params")
}
// VideoJob: task_info -> params, raw_data -> output
if s.db.Migrator().HasColumn(&model.VideoJob{}, "task_info") {
s.db.Exec("UPDATE geekai_video_jobs SET params = task_info WHERE task_info IS NOT NULL AND task_info != ''")
s.db.Migrator().DropColumn(&model.VideoJob{}, "task_info")
}
if s.db.Migrator().HasColumn(&model.VideoJob{}, "raw_data") {
s.db.Exec("UPDATE geekai_video_jobs SET `output` = raw_data WHERE raw_data IS NOT NULL AND raw_data != ''")
s.db.Migrator().DropColumn(&model.VideoJob{}, "raw_data")
}
// SunoJob: task_info -> params, raw_data -> output
if s.db.Migrator().HasColumn(&model.SunoJob{}, "task_info") {
s.db.Exec("UPDATE geekai_suno_jobs SET params = task_info WHERE task_info IS NOT NULL AND task_info != ''")
s.db.Migrator().DropColumn(&model.SunoJob{}, "task_info")
}
if s.db.Migrator().HasColumn(&model.SunoJob{}, "raw_data") {
s.db.Exec("UPDATE geekai_suno_jobs SET `output` = raw_data WHERE raw_data IS NOT NULL AND raw_data != ''")
s.db.Migrator().DropColumn(&model.SunoJob{}, "raw_data")
}
// ========== 删除不再使用的字段 ==========
if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") { if s.db.Migrator().HasColumn(&model.Order{}, "deleted_at") {
s.db.Migrator().DropColumn(&model.Order{}, "deleted_at") s.db.Migrator().DropColumn(&model.Order{}, "deleted_at")
} }
@@ -215,6 +406,100 @@ func (s *MigrationService) TableMigration() {
if s.db.Migrator().HasColumn(&model.Product{}, "url") { if s.db.Migrator().HasColumn(&model.Product{}, "url") {
s.db.Migrator().DropColumn(&model.Product{}, "url") s.db.Migrator().DropColumn(&model.Product{}, "url")
} }
if s.db.Migrator().HasColumn(&model.VideoJob{}, "water_url") {
s.db.Migrator().DropColumn(&model.VideoJob{}, "water_url")
}
if s.db.Migrator().HasColumn(&model.VideoJob{}, "cover_url") {
s.db.Migrator().DropColumn(&model.VideoJob{}, "cover_url")
}
if s.db.Migrator().HasColumn(&model.VideoJob{}, "prompt_ext") {
s.db.Migrator().DropColumn(&model.VideoJob{}, "prompt_ext")
}
if s.db.Migrator().HasColumn(&model.SunoJob{}, "instrumental") {
s.db.Migrator().DropColumn(&model.SunoJob{}, "instrumental")
}
if s.db.Migrator().HasColumn(&model.SunoJob{}, "tags") {
s.db.Migrator().DropColumn(&model.SunoJob{}, "tags")
}
if s.db.Migrator().HasColumn(&model.SunoJob{}, "extend_secs") {
s.db.Migrator().DropColumn(&model.SunoJob{}, "extend_secs")
}
if s.db.Migrator().HasColumn(&model.SunoJob{}, "model_name") {
s.db.Migrator().DropColumn(&model.SunoJob{}, "model_name")
}
// ========== 数据迁移:根据业务逻辑更新现有数据 ==========
// video_job: 根据 progress 填充 status
if s.db.Migrator().HasColumn(&model.VideoJob{}, "status") {
s.db.Exec(`UPDATE geekai_video_jobs SET status = CASE
WHEN progress < 100 THEN 'in_progress'
WHEN progress = 100 THEN 'success'
WHEN progress = 101 THEN 'failed'
WHEN progress = 102 THEN 'downloading'
ELSE 'pending'
END WHERE status = '' OR status IS NULL`)
}
// suno_job: 从 output 提取 tags/model_name 填入 params
s.migrateSunoJobData()
logger.Info("增量迁移完成")
}
// TableMigration 数据表迁移入口:先全量同步 schema,再增量删除字段并迁移数据
func (s *MigrationService) TableMigration() {
s.fullTableMigration()
s.fixTableConstraints()
s.incrementalTableMigration()
s.migrateChatAppSystemPromptFromJSON()
}
// migrateChatAppSystemPromptFromJSON 将智能体 system_prompt 字段中历史 JSON 数组
// 解析后取出 role 为 system 的 content,覆盖回 system_prompt(纯文本)
func (s *MigrationService) migrateChatAppSystemPromptFromJSON() {
key := "migrate:chat_app_system_prompt_json"
if s.redisClient.Get(context.Background(), key).Val() == "1" {
logger.Info("ChatApp system_prompt JSON 已迁移,跳过")
return
}
logger.Info("开始迁移智能体 system_prompt 历史 JSON 数据...")
var apps []model.ChatApp
if err := s.db.Find(&apps).Error; err != nil {
logger.Errorf("查询 ChatApp 失败: %v", err)
return
}
updated := 0
for i := range apps {
raw := strings.TrimSpace(apps[i].SystemPrompt)
if raw == "" {
continue
}
if len(raw) < 2 || raw[0] != '[' {
continue
}
var messages []types.Message
if err := json.Unmarshal([]byte(raw), &messages); err != nil {
continue
}
var systemContent string
for _, m := range messages {
if strings.ToLower(strings.TrimSpace(m.Role)) == "system" && m.Content != "" {
systemContent = m.Content
break
}
}
if err := s.db.Model(&model.ChatApp{}).Where("id = ?", apps[i].Id).Update("system_prompt", systemContent).Error; err != nil {
logger.Warnf("更新 ChatApp id=%d system_prompt 失败: %v", apps[i].Id, err)
continue
}
updated++
}
logger.Infof("智能体 system_prompt JSON 迁移完成,共更新 %d 条", updated)
s.redisClient.Set(context.Background(), key, "1", 0)
} }
// 迁移配置数据 // 迁移配置数据
@@ -308,6 +593,14 @@ func (s *MigrationService) migrateCommunicationConfig(config *types.AppConfig) e
"sign": config.SMS.Bao.Sign, "sign": config.SMS.Bao.Sign,
"code_template": config.SMS.Bao.CodeTemplate, "code_template": config.SMS.Bao.CodeTemplate,
}, },
"tencent": map[string]any{
"secret_id": config.SMS.Tencent.SecretId,
"secret_key": config.SMS.Tencent.SecretKey,
"sms_sdk_app_id": config.SMS.Tencent.SmsSdkAppId,
"sign": config.SMS.Tencent.Sign,
"code_temp_id": config.SMS.Tencent.CodeTempId,
"region": config.SMS.Tencent.Region,
},
} }
return s.saveConfig(types.ConfigKeySms, smsConfig) return s.saveConfig(types.ConfigKeySms, smsConfig)
} }
@@ -340,3 +633,73 @@ func (s *MigrationService) saveConfig(key string, config any) error {
logger.Infof("成功迁移配置 %s", key) logger.Infof("成功迁移配置 %s", key)
return nil return nil
} }
// migrateSunoJobData 合并 suno_job 数据:从 Output 原始数据中解析出 tags 和 model_name 填入 params 字段
func (s *MigrationService) migrateSunoJobData() {
key := "migrate:suno_job_data"
if s.redisClient.Get(context.Background(), key).Val() == "1" {
logger.Info("SunoJob 数据已合并,跳过迁移")
return
}
logger.Info("开始合并 SunoJob 数据...")
// 查询所有有 output 数据的记录
var jobs []model.SunoJob
if err := s.db.Where("output != ? AND output != ''", "").Find(&jobs).Error; err != nil {
logger.Errorf("查询 SunoJob 数据失败: %v", err)
return
}
updatedCount := 0
for _, job := range jobs {
if job.Output == "" {
continue
}
// 解析 Output JSON 数据
var outputData struct {
Metadata struct {
Tags string `json:"tags"`
} `json:"metadata"`
ModelName string `json:"model_name"`
}
if err := json.Unmarshal([]byte(job.Output), &outputData); err != nil {
logger.Warnf("解析 Output 数据失败 (ID: %d): %v", job.Id, err)
continue
}
// 检查是否需要更新 params
needUpdate := false
params := job.Params
// 如果 params 中的 tags 为空,但 output 中有 tags,则更新
if params.Tags == "" && outputData.Metadata.Tags != "" {
params.Tags = outputData.Metadata.Tags
// 修复 tags 字段过长导致更新失败
if len(params.Tags) > 255 {
params.Tags = params.Tags[:255]
}
needUpdate = true
}
// 如果 params 中的 model 为空,但 output 中有 model_name,则更新
if params.Model == "" && outputData.ModelName != "" {
params.Model = outputData.ModelName
needUpdate = true
}
// 如果需要更新,则保存
if needUpdate {
if err := s.db.Model(&model.SunoJob{}).Where("id = ?", job.Id).Update("params", params).Error; err != nil {
logger.Errorf("更新 SunoJob 数据失败 (ID: %d): %v", job.Id, err)
continue
}
updatedCount++
}
}
logger.Infof("SunoJob 数据合并完成,共更新 %d 条记录", updatedCount)
s.redisClient.Set(context.Background(), key, "1", 0)
}
+33 -17
View File
@@ -12,23 +12,24 @@ import (
"errors" "errors"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
"geekai/service"
"geekai/store/model" "geekai/store/model"
"geekai/utils" "geekai/utils"
"github.com/imroc/req/v3"
"gorm.io/gorm"
"io" "io"
"time" "time"
"github.com/imroc/req/v3"
"gorm.io/gorm"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
var logger = log.GetLogger()
// Client MidJourney client // Client MidJourney client
type Client struct { type Client struct {
client *req.Client client *req.Client
licenseService *service.LicenseService db *gorm.DB
db *gorm.DB
} }
type ImageReq struct { type ImageReq struct {
@@ -75,13 +76,10 @@ type QueryRes struct {
SubmitTime int `json:"submitTime"` SubmitTime int `json:"submitTime"`
} }
var logger = logger2.GetLogger() func NewClient(db *gorm.DB) *Client {
func NewClient(licenseService *service.LicenseService, db *gorm.DB) *Client {
return &Client{ return &Client{
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"), client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
licenseService: licenseService, db: db,
db: db,
} }
} }
@@ -185,6 +183,28 @@ func (c *Client) Variation(task types.MjTask) (ImageRes, error) {
return c.doRequest(body, apiPath, task.ChannelId) return c.doRequest(body, apiPath, task.ChannelId)
} }
// Modal 提交局部重绘(inpaint/ ZOOM,请求体 taskId 必填(提交成功返回的 taskId,不是查询结果里的 messageId),prompt、maskBase64 可选
func (c *Client) Modal(task types.MjTask) (ImageRes, error) {
apiPath := fmt.Sprintf("mj-%s/mj/submit/modal", task.Mode)
taskId := task.TaskId
if taskId == "" {
taskId = task.MessageId
}
if taskId == "" {
return ImageRes{}, fmt.Errorf("modal 任务缺少原图 taskId(提交成功返回的 ID")
}
body := map[string]any{
"taskId": taskId,
}
if task.Prompt != "" {
body["prompt"] = task.Prompt
}
if task.MaskBase64 != "" {
body["maskBase64"] = task.MaskBase64
}
return c.doRequest(body, apiPath, task.ChannelId)
}
func (c *Client) doRequest(body interface{}, apiPath string, channel string) (ImageRes, error) { func (c *Client) doRequest(body interface{}, apiPath string, channel string) (ImageRes, error) {
var res ImageRes var res ImageRes
session := c.db.Session(&gorm.Session{}).Where("type", "mj").Where("enabled", true) session := c.db.Session(&gorm.Session{}).Where("type", "mj").Where("enabled", true)
@@ -198,10 +218,6 @@ func (c *Client) doRequest(body interface{}, apiPath string, channel string) (Im
return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err) return ImageRes{}, fmt.Errorf("no available MidJourney api key: %v", err)
} }
if err = c.licenseService.IsValidApiURL(apiKey.ApiURL); err != nil {
return ImageRes{}, err
}
apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath) apiURL := fmt.Sprintf("%s/%s", apiKey.ApiURL, apiPath)
logger.Info("API URL: ", apiURL) logger.Info("API URL: ", apiURL)
r, err := req.C().R(). r, err := req.C().R().
+3
View File
@@ -97,6 +97,9 @@ func (s *Service) Run() {
case types.TaskSwapFace: case types.TaskSwapFace:
res, err = s.client.SwapFace(task) res, err = s.client.SwapFace(task)
break break
case types.TaskModal:
res, err = s.client.Modal(task)
break
} }
if err != nil || (res.Code != 1 && res.Code != 22) { if err != nil || (res.Code != 1 && res.Code != 22) {
@@ -2,8 +2,6 @@ package moderation
import ( import (
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger"
) )
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -13,8 +11,6 @@ import (
// * @Author yangjian102621@163.com // * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
var logger = logger2.GetLogger()
type Service interface { type Service interface {
Moderate(text string) (types.ModerationResult, error) Moderate(text string) (types.ModerationResult, error)
} }
+167
View File
@@ -0,0 +1,167 @@
package oss
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"geekai/core/types"
"geekai/utils"
"net/http"
"net/url"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/tencentyun/cos-go-sdk-v5"
)
type TencentOss struct {
config types.TencentOssConfig
client *cos.Client
proxyURL string
}
func NewTencentOss(sysConfig *types.SystemConfig, appConfig *types.AppConfig) (*TencentOss, error) {
s := &TencentOss{
proxyURL: appConfig.ProxyURL,
}
err := s.UpdateConfig(sysConfig.OSS.Tencent)
if err != nil {
logger.Warnf("腾讯云COS初始化失败: %v", err)
}
return s, nil
}
func (s *TencentOss) UpdateConfig(config types.TencentOssConfig) error {
if config.Bucket == "" || config.Region == "" || config.SecretId == "" || config.SecretKey == "" {
// 配置不完整时不初始化客户端
s.config = config
return nil
}
// 构建 COS 客户端 URL
cosURL := fmt.Sprintf("https://%s.cos.%s.myqcloud.com", config.Bucket, config.Region)
u, err := url.Parse(cosURL)
if err != nil {
return fmt.Errorf("error parsing COS URL: %v", err)
}
// 创建 COS 客户端
b := &cos.BaseURL{BucketURL: u}
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: config.SecretId,
SecretKey: config.SecretKey,
},
})
s.client = client
s.config = config
return nil
}
func (s TencentOss) PutFile(ctx *gin.Context, name string) (File, error) {
// 解析表单
file, err := ctx.FormFile(name)
if err != nil {
return File{}, err
}
// 打开上传文件
src, err := file.Open()
if err != nil {
return File{}, err
}
defer src.Close()
fileExt := filepath.Ext(file.Filename)
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), fileExt)
// 上传文件
_, err = s.client.Object.Put(ctx, objectKey, src, nil)
if err != nil {
return File{}, err
}
// 生成文件 URL
fileURL := s.generateURL(objectKey)
return File{
Name: file.Filename,
ObjKey: objectKey,
URL: fileURL,
Ext: fileExt,
Size: file.Size,
}, nil
}
func (s TencentOss) PutUrlFile(fileURL string, ext string, useProxy bool) (string, error) {
var fileData []byte
var err error
if useProxy {
fileData, err = utils.DownloadImage(fileURL, s.proxyURL)
} else {
fileData, err = utils.DownloadImage(fileURL, "")
}
if err != nil {
return "", fmt.Errorf("error with download image: %v", err)
}
parse, err := url.Parse(fileURL)
if err != nil {
return "", fmt.Errorf("error with parse image URL: %v", err)
}
if ext == "" {
ext = filepath.Ext(parse.Path)
}
objectKey := fmt.Sprintf("%d%s", time.Now().UnixMicro(), ext)
// 上传文件字节数据
_, err = s.client.Object.Put(context.Background(), objectKey, bytes.NewReader(fileData), nil)
if err != nil {
return "", err
}
return s.generateURL(objectKey), nil
}
func (s TencentOss) PutBase64(base64Img string) (string, error) {
imageData, err := base64.StdEncoding.DecodeString(base64Img)
if err != nil {
return "", fmt.Errorf("error decoding base64:%v", err)
}
objectKey := fmt.Sprintf("%d.png", time.Now().UnixMicro())
// 上传文件字节数据
_, err = s.client.Object.Put(context.Background(), objectKey, bytes.NewReader(imageData), nil)
if err != nil {
return "", err
}
return s.generateURL(objectKey), nil
}
func (s TencentOss) Delete(fileURL string) error {
var objectKey string
if strings.HasPrefix(fileURL, "http") {
objectKey = filepath.Base(fileURL)
} else {
objectKey = fileURL
}
_, err := s.client.Object.Delete(context.Background(), objectKey)
return err
}
// generateURL 生成文件访问 URL
func (s TencentOss) generateURL(objectKey string) string {
if s.config.Domain != "" {
// 使用自定义域名
return fmt.Sprintf("%s/%s", strings.TrimSuffix(s.config.Domain, "/"), objectKey)
}
// 使用 COS 默认域名
return fmt.Sprintf("https://%s.cos.%s.myqcloud.com/%s", s.config.Bucket, s.config.Region, objectKey)
}
var _ Uploader = TencentOss{}
+1
View File
@@ -13,6 +13,7 @@ const Local = "local"
const Minio = "minio" const Minio = "minio"
const QiNiu = "qiniu" const QiNiu = "qiniu"
const AliYun = "aliyun" const AliYun = "aliyun"
const Tencent = "tencent"
type File struct { type File struct {
Name string `json:"name"` Name string `json:"name"`
+89 -13
View File
@@ -8,32 +8,41 @@ package oss
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"fmt"
"geekai/core/types" "geekai/core/types"
"strings"
logger2 "geekai/logger" "geekai/log"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
// 默认缩略图模板(本地存储格式)
const DefaultThumbTemplate = "?imageView2/4/w/{width}/h/{height}/q/75"
type UploaderManager struct { type UploaderManager struct {
local *LocalStorage local *LocalStorage
aliyun *AliYunOss aliyun *AliYunOss
mini *MiniOss mini *MiniOss
qiniu *QiNiuOss qiniu *QiNiuOss
active string tencent *TencentOss
active string
ossConfig types.OSSConfig // 保存当前OSS配置
} }
func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliyun *AliYunOss, mini *MiniOss, qiniu *QiNiuOss) (*UploaderManager, error) { func NewUploaderManager(sysConfig *types.SystemConfig, local *LocalStorage, aliyun *AliYunOss, mini *MiniOss, qiniu *QiNiuOss, tencent *TencentOss) (*UploaderManager, error) {
if sysConfig.OSS.Active == "" { if sysConfig.OSS.Active == "" {
sysConfig.OSS.Active = Local sysConfig.OSS.Active = Local
} }
return &UploaderManager{ return &UploaderManager{
active: sysConfig.OSS.Active, active: sysConfig.OSS.Active,
local: local, local: local,
aliyun: aliyun, aliyun: aliyun,
mini: mini, mini: mini,
qiniu: qiniu, qiniu: qiniu,
tencent: tencent,
ossConfig: sysConfig.OSS,
}, nil }, nil
} }
@@ -47,6 +56,8 @@ func (m *UploaderManager) GetUploadHandler() Uploader {
return m.mini return m.mini
case QiNiu: case QiNiu:
return m.qiniu return m.qiniu
case Tencent:
return m.tencent
} }
return m.local return m.local
} }
@@ -61,6 +72,71 @@ func (m *UploaderManager) UpdateConfig(config types.OSSConfig) {
m.mini.UpdateConfig(config.Minio) m.mini.UpdateConfig(config.Minio)
case QiNiu: case QiNiu:
m.qiniu.UpdateConfig(config.QiNiu) m.qiniu.UpdateConfig(config.QiNiu)
case Tencent:
m.tencent.UpdateConfig(config.Tencent)
} }
m.active = config.Active m.active = config.Active
m.ossConfig = config
}
// GetThumbURL 根据原始图片URL和尺寸生成缩略图URL
// 如果模板为空,使用默认模板(本地存储格式)
// 如果明确设置为空字符串(表示不支持缩略图),返回原图URL
func (m *UploaderManager) GetThumbURL(originalURL string, width, height int) string {
var template string
// 根据当前激活的存储引擎获取对应的模板
switch m.active {
case Local:
template = m.ossConfig.Local.ThumbTemplate
case AliYun:
template = m.ossConfig.AliYun.ThumbTemplate
case Minio:
template = m.ossConfig.Minio.ThumbTemplate
case QiNiu:
template = m.ossConfig.QiNiu.ThumbTemplate
case Tencent:
template = m.ossConfig.Tencent.ThumbTemplate
default:
template = m.ossConfig.Local.ThumbTemplate
}
// 如果模板为空,使用默认模板(兼容旧配置)
if template == "" {
template = DefaultThumbTemplate
}
// 替换变量
thumbURL := strings.ReplaceAll(template, "{width}", fmt.Sprintf("%d", width))
thumbURL = strings.ReplaceAll(thumbURL, "{height}", fmt.Sprintf("%d", height))
// 拼接原始URL和缩略图参数
return originalURL + thumbURL
}
// GetThumbTemplate 获取当前存储引擎的缩略图模板
func (m *UploaderManager) GetThumbTemplate() string {
var template string
switch m.active {
case Local:
template = m.ossConfig.Local.ThumbTemplate
case AliYun:
template = m.ossConfig.AliYun.ThumbTemplate
case Minio:
template = m.ossConfig.Minio.ThumbTemplate
case QiNiu:
template = m.ossConfig.QiNiu.ThumbTemplate
case Tencent:
template = m.ossConfig.Tencent.ThumbTemplate
default:
template = m.ossConfig.Local.ThumbTemplate
}
// 如果模板为空,返回默认模板
if template == "" {
return DefaultThumbTemplate
}
return template
} }
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"context" "context"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
"net/http" "net/http"
"os" "os"
@@ -24,7 +24,7 @@ type AlipayService struct {
config *types.AlipayConfig config *types.AlipayConfig
} }
var logger = logger2.GetLogger() var logger = log.GetLogger()
func NewAlipayService(sysConfig *types.SystemConfig) (*AlipayService, error) { func NewAlipayService(sysConfig *types.SystemConfig) (*AlipayService, error) {
config := sysConfig.Payment.Alipay config := sysConfig.Payment.Alipay
+13 -1
View File
@@ -9,6 +9,7 @@ package payment
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"geekai/utils" "geekai/utils"
@@ -88,7 +89,18 @@ func (s *WxPayService) Pay(params PayRequest) (string, error) {
if wxRsp.Code != wechat.Success { if wxRsp.Code != wechat.Success {
return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error) return "", fmt.Errorf("error status with generating pay url: %v", wxRsp.Error)
} }
return wxRsp.Response.PrepayId, nil // 签名
payParams, err := s.client.PaySignOfJSAPI(s.config.AppId, wxRsp.Response.PrepayId)
if err != nil {
return "", fmt.Errorf("error with generating jsapi pay sign: %v", err)
}
payParamsBytes, err := json.Marshal(payParams)
if err != nil {
return "", fmt.Errorf("error with marshaling pay params: %v", err)
}
return string(payParamsBytes), nil
} else if params.Device == "pc" { } else if params.Device == "pc" {
wxRsp, err := s.client.V3TransactionNative(context.Background(), bm) wxRsp, err := s.client.V3TransactionNative(context.Background(), bm)
if err != nil { if err != nil {
Binary file not shown.
+404
View File
@@ -0,0 +1,404 @@
package ppt
import (
"bytes"
"context"
_ "embed"
"fmt"
"geekai/core/types"
"image"
_ "image/gif"
"io"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strings"
"time"
"unicode"
"github.com/jung-kurt/gofpdf/v2"
"github.com/ktye/pptx"
_ "golang.org/x/image/webp"
)
//go:embed embed/minimal.pptx
var minimalPptxTemplate []byte
const (
exportHTTPTimeout = 60 * time.Second
// 必须与 embed/minimal.pptx 中 p:sldSz 一致(当前模板为 4:310\"×7.5\"
slideEmuW pptx.Dimension = 9144000
slideEmuH pptx.Dimension = 6858000
)
// ExportFormat 导出类型
type ExportFormat string
const (
ExportFormatPDF ExportFormat = "pdf"
ExportFormatPPTX ExportFormat = "pptx"
)
// ExportMimeType 返回 Content-Type
func ExportMimeType(f ExportFormat) string {
switch f {
case ExportFormatPDF:
return "application/pdf"
case ExportFormatPPTX:
return "application/vnd.openxmlformats-officedocument.presentationml.presentation"
default:
return "application/octet-stream"
}
}
// ExportFileExt 返回文件扩展名(含点)
func ExportFileExt(f ExportFormat) string {
switch f {
case ExportFormatPDF:
return ".pdf"
case ExportFormatPPTX:
return ".pptx"
default:
return ""
}
}
// ParseExportFormat 解析 query format
func ParseExportFormat(s string) (ExportFormat, bool) {
switch strings.ToLower(strings.TrimSpace(s)) {
case "pdf":
return ExportFormatPDF, true
case "pptx", "ppt":
return ExportFormatPPTX, true
default:
return "", false
}
}
// SanitizeExportBaseName 用于下载文件名的主体(不含扩展名)
func SanitizeExportBaseName(title, taskID string) string {
s := strings.TrimSpace(title)
repl := strings.NewReplacer(
"/", "_", "\\", "_", ":", "_", "*", "_", "?", "_", "\"", "_", "<", "_", ">", "_", "|", "_",
)
s = repl.Replace(s)
var b strings.Builder
for _, r := range s {
if r == unicode.ReplacementChar || r < 32 {
continue
}
b.WriteRune(r)
}
s = strings.TrimSpace(b.String())
if s == "" {
s = strings.TrimSpace(taskID)
}
if len([]rune(s)) > 120 {
rs := []rune(s)
s = string(rs[:120])
}
return s
}
// ContentDispositionAttachment RFC 5987,兼容旧客户端
func ContentDispositionAttachment(filename string) string {
ascii := filename
for _, r := range filename {
if r > 127 || r == '"' || r == '\\' {
ascii = "export" + strings.ToLower(filepath.Ext(filename))
if ascii == "export" {
ascii = "export.bin"
}
break
}
}
return fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, ascii, url.PathEscape(filename))
}
// mapLocalUploadFile 将站点相对路径或完整 BaseURL 前缀映射为本地文件路径(local OSS)
func mapLocalUploadFile(raw string, local types.LocalStorageConfig) (string, bool) {
raw = strings.TrimSpace(raw)
if raw == "" || local.BasePath == "" {
return "", false
}
bp := filepath.Clean(local.BasePath)
bu := strings.TrimSuffix(strings.TrimSpace(local.BaseURL), "/")
if bu != "" && strings.HasPrefix(raw, bu) {
suffix := strings.TrimPrefix(strings.TrimPrefix(raw, bu), "/")
return filepath.Join(bp, suffix), true
}
if strings.HasPrefix(raw, local.BaseURL) {
return filepath.Join(bp, strings.TrimPrefix(raw, local.BaseURL)), true
}
return "", false
}
func originFromBaseURL(baseURL string) string {
u, err := url.Parse(strings.TrimSpace(baseURL))
if err != nil || u.Scheme == "" || u.Host == "" {
return ""
}
return u.Scheme + "://" + u.Host
}
func defaultOriginFromListen(listen string) string {
listen = strings.TrimSpace(listen)
if listen == "" {
return ""
}
host, port, err := net.SplitHostPort(listen)
if err != nil {
if strings.HasPrefix(listen, ":") {
return "http://127.0.0.1" + listen
}
return ""
}
if host == "0.0.0.0" || host == "::" || host == "" {
host = "127.0.0.1"
}
return "http://" + net.JoinHostPort(host, port)
}
// resolveAbsoluteImageURL 将可能为相对路径的地址转为可 HTTP 访问的绝对 URL
func resolveAbsoluteImageURL(raw string, local types.LocalStorageConfig, app *types.AppConfig) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return raw
}
if u, err := url.Parse(raw); err == nil && u.Scheme != "" && u.Host != "" {
return raw
}
if strings.HasPrefix(raw, "//") {
return "https:" + raw
}
origin := originFromBaseURL(local.BaseURL)
if origin == "" && app != nil {
origin = originFromBaseURL(app.StaticUrl)
}
if origin == "" && app != nil {
origin = defaultOriginFromListen(app.Listen)
}
if origin == "" {
return raw
}
if strings.HasPrefix(raw, "/") {
return strings.TrimSuffix(origin, "/") + raw
}
return strings.TrimSuffix(origin, "/") + "/" + raw
}
// BuildExportBytes 按幻灯片顺序拉取图片并生成 PDF 或 PPTX(oss/app 用于解析相对路径图片 URL)
func BuildExportBytes(ctx context.Context, slides []SlideData, format ExportFormat, oss types.OSSConfig, app *types.AppConfig) ([]byte, error) {
raws, err := fetchSlideImages(ctx, slides, oss, app)
if err != nil {
return nil, err
}
if len(raws) == 0 {
return nil, fmt.Errorf("没有可导出的幻灯片图片")
}
switch format {
case ExportFormatPDF:
return buildPDF(raws)
case ExportFormatPPTX:
return buildPPTX(raws)
default:
return nil, fmt.Errorf("不支持的导出格式")
}
}
func fetchSlideImages(ctx context.Context, slides []SlideData, oss types.OSSConfig, app *types.AppConfig) ([][]byte, error) {
cp := append([]SlideData(nil), slides...)
sort.Slice(cp, func(i, j int) bool { return cp[i].SlideIndex < cp[j].SlideIndex })
client := &http.Client{Timeout: exportHTTPTimeout}
var out [][]byte
for _, s := range cp {
u := strings.TrimSpace(s.ImageURL)
if u == "" {
continue
}
var body []byte
if oss.Active == "local" {
if fp, ok := mapLocalUploadFile(u, oss.Local); ok {
b, err := os.ReadFile(fp)
if err == nil {
if _, err := decodeImageBytes(b); err == nil {
body = b
}
}
}
}
if len(body) == 0 {
absURL := resolveAbsoluteImageURL(u, oss.Local, app)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, absURL, nil)
if err != nil {
return nil, fmt.Errorf("幻灯片 %d: %w", s.SlideIndex, err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("幻灯片 %d 下载失败: %w", s.SlideIndex, err)
}
body, err = io.ReadAll(io.LimitReader(resp.Body, 32<<20))
_ = resp.Body.Close()
if err != nil {
return nil, fmt.Errorf("幻灯片 %d 读取失败: %w", s.SlideIndex, err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("幻灯片 %d 下载失败: HTTP %d", s.SlideIndex, resp.StatusCode)
}
}
if _, err := decodeImageBytes(body); err != nil {
return nil, fmt.Errorf("幻灯片 %d 不是有效图片: %v", s.SlideIndex, err)
}
out = append(out, body)
}
return out, nil
}
func decodeImageBytes(b []byte) (image.Image, error) {
m, _, err := image.Decode(bytes.NewReader(b))
return m, err
}
// fitSlideBoundsEMU 在幻灯片 EMU 框(与模板 p:sldSz 一致)内按原图比例 contain 居中(不裁切)
func fitSlideBoundsEMU(imgW, imgH int) (x, y, w, h pptx.Dimension) {
if imgW <= 0 || imgH <= 0 {
return 0, 0, slideEmuW, slideEmuH
}
sw := float64(slideEmuW)
sh := float64(slideEmuH)
iw := float64(imgW)
ih := float64(imgH)
scale := sw / iw
if ih*scale > sh {
scale = sh / ih
}
wf := iw * scale
hf := ih * scale
w = pptx.Dimension(wf + 0.5)
h = pptx.Dimension(hf + 0.5)
x = pptx.Dimension((sw-wf)*0.5 + 0.5)
y = pptx.Dimension((sh-hf)*0.5 + 0.5)
return x, y, w, h
}
func buildPDF(images [][]byte) ([]byte, error) {
pdf := gofpdf.New("L", "mm", "A4", "")
pdf.SetMargins(0, 0, 0)
pdf.SetAutoPageBreak(false, 0)
// 像素 → mm(按 96 DPI),再按页面对比缩放以 contain 放入整页
const pxPerMM = 96.0 / 25.4
for i, raw := range images {
im, err := decodeImageBytes(raw)
if err != nil {
return nil, fmt.Errorf("第 %d 页: %w", i+1, err)
}
b := im.Bounds()
pxW := float64(b.Dx())
pxH := float64(b.Dy())
if pxW <= 0 || pxH <= 0 {
return nil, fmt.Errorf("第 %d 页: 图片尺寸无效", i+1)
}
pdf.AddPage()
pageW, pageH := pdf.GetPageSize()
imgWmm := pxW / pxPerMM
imgHmm := pxH / pxPerMM
scale := pageW / imgWmm
if imgHmm*scale > pageH {
scale = pageH / imgHmm
}
w := imgWmm * scale
h := imgHmm * scale
x := (pageW - w) / 2
y := (pageH - h) / 2
name := fmt.Sprintf("slide%d", i)
opt := gofpdf.ImageOptions{ReadDpi: false}
tp := sniffImageType(raw)
if tp != "" {
opt.ImageType = tp
}
if pdf.RegisterImageOptionsReader(name, opt, bytes.NewReader(raw)) == nil {
return nil, fmt.Errorf("第 %d 页: 无法写入 PDF 图片", i+1)
}
pdf.ImageOptions(name, x, y, w, h, false, opt, 0, "")
}
var buf bytes.Buffer
if err := pdf.Output(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func sniffImageType(b []byte) string {
if len(b) < 12 {
return ""
}
switch {
case len(b) >= 2 && b[0] == 0xFF && b[1] == 0xD8:
return "jpg"
case len(b) >= 8 && string(b[0:8]) == "\x89PNG\r\n\x1a\n":
return "png"
case len(b) >= 6 && string(b[0:6]) == "GIF87a" || string(b[0:6]) == "GIF89a":
return "gif"
case len(b) >= 12 && string(b[0:4]) == "RIFF" && string(b[8:12]) == "WEBP":
return "webp"
default:
return ""
}
}
func buildPPTX(images [][]byte) ([]byte, error) {
if len(minimalPptxTemplate) == 0 {
return nil, fmt.Errorf("内置 PPT 模板缺失")
}
tmp, err := os.CreateTemp("", "ppt-export-*.pptx")
if err != nil {
return nil, err
}
path := tmp.Name()
if _, err := tmp.Write(minimalPptxTemplate); err != nil {
_ = tmp.Close()
_ = os.Remove(path)
return nil, err
}
if err := tmp.Close(); err != nil {
_ = os.Remove(path)
return nil, err
}
defer func() { _ = os.Remove(path) }()
f, err := pptx.Open(path)
if err != nil {
return nil, err
}
for _, raw := range images {
im, err := decodeImageBytes(raw)
if err != nil {
f.Abort()
return nil, err
}
b := im.Bounds()
ex, ey, ew, eh := fitSlideBoundsEMU(b.Dx(), b.Dy())
slide := pptx.Slide{
Images: []pptx.Image{
pptx.NewImage(im, ex, ey, ew, eh),
},
}
if err := f.Add(slide); err != nil {
f.Abort()
return nil, err
}
}
if err := f.Close(); err != nil {
return nil, err
}
return os.ReadFile(path)
}
+263
View File
@@ -0,0 +1,263 @@
package ppt
import (
"context"
"fmt"
"geekai/core/types"
"geekai/log"
"net/http"
"time"
"github.com/imroc/req/v3"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"golang.org/x/time/rate"
)
var imageLogger = log.GetLogger()
// ImageGenerator 图片生成适配器接口
type ImageGenerator interface {
Provider() string
Generate(ctx context.Context, prompt string) (string, error)
// GenerateWithReference 图生图;referenceImages 为公网 URL 或 data:image/...;base64,...(本地文件应在调用前经 PrepareReferenceInputsForImg2Img 转换)
GenerateWithReference(ctx context.Context, prompt string, referenceImages []string) (string, error)
}
// Nano Banana 适配器(OpenAI DALL-E 风格 API
type nanoBananaImageGenerator struct {
client *req.Client
cfg types.PPTConfig
limiter *rate.Limiter
}
// Seedream 适配器(火山引擎 arkruntime SDK
type seedreamImageGenerator struct {
cfg types.PPTConfig
limiter *rate.Limiter
}
// NewImageGenerator 根据配置创建对应的图片生成适配器
func NewImageGenerator(cfg types.PPTConfig) (ImageGenerator, error) {
qps := cfg.QPSLimit
if qps <= 0 {
qps = 1
}
limiter := rate.NewLimiter(rate.Limit(qps), 1)
switch cfg.ActiveImageProvider {
case types.PPTImageProviderNanoBanana:
if cfg.NanoBananaApiURL == "" || cfg.NanoBananaApiKey == "" {
return nil, fmt.Errorf("nano banana api not configured")
}
return &nanoBananaImageGenerator{
client: req.C().SetTimeout(3 * time.Minute),
cfg: cfg,
limiter: limiter,
}, nil
case types.PPTImageProviderSeedream:
if cfg.SeedreamBaseURL == "" || cfg.SeedreamApiKey == "" || cfg.SeedreamModel == "" {
return nil, fmt.Errorf("seedream api not configured")
}
return &seedreamImageGenerator{
cfg: cfg,
limiter: limiter,
}, nil
default:
return nil, fmt.Errorf("unsupported image provider: %s", cfg.ActiveImageProvider)
}
}
func (g *nanoBananaImageGenerator) Provider() string {
return string(types.PPTImageProviderNanoBanana)
}
// nanoBananaReq 按 OpenAI DALL-E 风格 / Nano-banana API 文档
type nanoBananaReq struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
ResponseFormat string `json:"response_format,omitempty"` // url 或 b64_json
AspectRatio string `json:"aspect_ratio,omitempty"` // 1:1, 4:3, 3:4, 16:9, 9:16, 2:3, 3:2, 4:5, 5:4, 21:9
Image []string `json:"image,omitempty"` // 参考图 url 或 b64
}
// nanoBananaRes 响应为 data[].urlDALL-E 风格)
type nanoBananaRes struct {
Data []struct {
URL string `json:"url,omitempty"`
B64JSON string `json:"b64_json,omitempty"`
} `json:"data"`
}
type nanoBananaErr struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
func (g *nanoBananaImageGenerator) buildReqBody(prompt string, referenceImages []string) nanoBananaReq {
modelName := g.cfg.NanoBananaModel
if modelName == "" {
modelName = "nano-banana"
}
reqBody := nanoBananaReq{
Model: modelName,
Prompt: prompt,
}
if len(referenceImages) > 0 {
reqBody.Image = referenceImages
}
if g.cfg.NanoBananaResponseFormat != "" {
reqBody.ResponseFormat = g.cfg.NanoBananaResponseFormat
} else {
reqBody.ResponseFormat = "url"
}
if g.cfg.NanoBananaAspectRatio != "" {
reqBody.AspectRatio = g.cfg.NanoBananaAspectRatio
} else {
reqBody.AspectRatio = "16:9"
}
return reqBody
}
func (g *nanoBananaImageGenerator) Generate(ctx context.Context, prompt string) (string, error) {
return g.GenerateWithReference(ctx, prompt, nil)
}
func (g *nanoBananaImageGenerator) GenerateWithReference(ctx context.Context, prompt string, referenceImages []string) (string, error) {
reqBody := g.buildReqBody(prompt, referenceImages)
var (
result nanoBananaRes
errRes nanoBananaErr
)
do := func() (int, error) {
if err := g.limiter.Wait(ctx); err != nil {
return 0, err
}
imageLogger.Infof("nano banana generate image, api: %s", g.cfg.NanoBananaApiURL)
r, err := g.client.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+g.cfg.NanoBananaApiKey).
SetBody(reqBody).
SetSuccessResult(&result).
SetErrorResult(&errRes).
Post(g.cfg.NanoBananaApiURL)
if err != nil {
return 0, err
}
if r.IsErrorState() {
return r.StatusCode, fmt.Errorf("nano banana error: %s, %s", r.Status, errRes.Error.Message)
}
if len(result.Data) == 0 || result.Data[0].URL == "" {
return r.StatusCode, fmt.Errorf("nano banana returned empty data")
}
return r.StatusCode, nil
}
if err := callWithRetry(ctx, do); err != nil {
return "", err
}
return result.Data[0].URL, nil
}
func (g *seedreamImageGenerator) Provider() string {
return string(types.PPTImageProviderSeedream)
}
func (g *seedreamImageGenerator) Generate(ctx context.Context, prompt string) (string, error) {
return g.GenerateWithReference(ctx, prompt, nil)
}
func (g *seedreamImageGenerator) GenerateWithReference(ctx context.Context, prompt string, referenceImages []string) (string, error) {
if err := g.limiter.Wait(ctx); err != nil {
return "", err
}
client := arkruntime.NewClientWithApiKey(g.cfg.SeedreamApiKey, arkruntime.WithBaseUrl(g.cfg.SeedreamBaseURL))
size := g.cfg.SeedreamSize
if size == "" {
size = "1920x1080"
}
responseFormat := g.cfg.SeedreamResponseType
if responseFormat == "" {
responseFormat = "url"
}
generateReq := model.GenerateImagesRequest{
Model: g.cfg.SeedreamModel,
Prompt: prompt,
Size: volcengine.String(size),
ResponseFormat: volcengine.String(responseFormat),
Watermark: volcengine.Bool(g.cfg.SeedreamWatermark),
}
if len(referenceImages) > 0 {
generateReq.Image = referenceImages
}
var lastErr error
for attempt := 0; attempt < 3; attempt++ {
if attempt > 0 {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(time.Duration(attempt) * 2 * time.Second):
}
}
imageLogger.Infof("seedream generate image, api: %s", g.cfg.SeedreamBaseURL)
if err := generateReq.NormalizeImages(); err != nil {
return "", fmt.Errorf("seedream normalize images: %w", err)
}
resp, err := client.GenerateImages(ctx, generateReq)
if err != nil {
lastErr = fmt.Errorf("seedream error: %w", err)
continue
}
if resp.Data == nil || len(resp.Data) == 0 {
lastErr = fmt.Errorf("seedream returned empty data")
continue
}
if resp.Data[0].Url == nil || *resp.Data[0].Url == "" {
lastErr = fmt.Errorf("seedream returned empty url")
continue
}
return *resp.Data[0].Url, nil
}
return "", lastErr
}
// callWithRetry 对 429 错误做指数退避重试
func callWithRetry(ctx context.Context, fn func() (int, error)) error {
var (
retries = 3
backoffs = []time.Duration{2 * time.Second, 4 * time.Second, 8 * time.Second}
lastError error
)
for i := 0; i < retries; i++ {
status, err := fn()
if err == nil {
return nil
}
lastError = err
// 仅对 429 做指数退避重试
if status != http.StatusTooManyRequests || i == retries-1 {
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(backoffs[i]):
}
}
return lastError
}
+373
View File
@@ -0,0 +1,373 @@
package ppt
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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"
"encoding/json"
"fmt"
"geekai/core/types"
"geekai/log"
"strings"
"time"
"github.com/imroc/req/v3"
)
var logger = log.GetLogger()
// slidePlan LLM 输出的分镜结构
type slidePlan struct {
SlideIndex int `json:"slide_index"`
Theme string `json:"theme"`
Title string `json:"title"`
Points []string `json:"points"`
ImagePrompt string `json:"image_prompt"`
}
// systemPrompt 图文并茂幻灯片:每页有插图且画面上含与主题一致的文字,文字量与生成模式相关
const systemPrompt = `
# Role
你是一位顶级的专业演示文稿(PPT)策划专家和 AI 图像提示词(Prompt)工程师。任务是根据用户提供的「内容大纲」或「设计要求」,生成一套逻辑清晰、视觉风格高度统一的幻灯片分镜数据。目标是生成**图文并茂**的幻灯片:每页既有文字又有插图,图片上直接呈现与本页内容一致的文字,而不是留白让用户后加文字。
# Rules
1. 全局风格锚定:根据大纲推断或遵循用户要求的全局视觉风格。所有配图必须严格遵循此风格。
2. 结构化拆解:合理拆分为多张幻灯片,单页最多 3-4 个简短要点。
3. 视觉转译(图文并茂):
- 为每页构思的 image_prompt 既要描述**插图画面**,也要明确**画面上应出现的文字**(如本页标题、要点或短句),与当页 theme、title、points 内容一致。不要描述留白或“用于排版文字的空间”。
- 图片中出现的所有文字必须使用与 theme、title、points **相同的语言**(即本次请求指定的输出语言)。
- 图片上文字的量由「生成模式」决定(见用户输入中的模式说明):
- **详细演示文稿**:图片上的说明文字可适当多一些,如本页要点、一两句说明。
- **演示用幻灯片**:图片上的文字尽量精简,如仅主标题或少量关键词,便于演讲时配合口述。
- image_prompt 须包含前缀「[全局风格描述]」,并清晰描述画面中的插图与文字内容(含具体要出现的文字及其语言),不要包含“不要在图片中生成任何文字”的约束。
4. 严格输出合法的纯 JSON 数组:
[
{"slide_index": 1, "theme": "...", "title": "...", "points": ["..."], "image_prompt": "..."}
]
禁止输出任何 Markdown 标记或多余文本。`
// notebookSystemPrompt 文档提炼:NotebookLM 风格输出 PPT 可用大纲文本(纯文本/Markdown)。
const notebookSystemPrompt = `
# Role
你是一位“NotebookLM 风格”的专业文档理解与提炼助手。任务是基于用户提供的「原始文档文本」和「设计要求」,提炼出可用于制作 PPT 的结构化大纲内容。
# Output Requirements
1. 输出必须是纯文本/Markdown(允许使用标题与列表),禁止输出任何 JSON。
2. 禁止输出代码块(不要出现代码块语法)。
3. 不要输出解释过程、不要复述提示词。
4. 大纲必须是“内容大纲/要点”,用于后续继续拆分成幻灯片,而不是直接输出最终幻灯片分镜。
# Rules
1. 文档优先:尽可能从原始文档中提取信息与措辞;若文档缺失关键点,则给出合理补全的“建议方向”,并明确标注为“(建议)”。
2. 贴合设计要求:根据设计要求调整大纲的语气、侧重点、术语风格,使内容更符合目标受众与整体风格。
3. 结构清晰:使用分层标题(例如:# 总主题、## 模块/章节、### 要点),并为每个模块给出 2-4 个要点句(可直接用于 PPT 每页标题/要点)。
4. 语言一致:所有输出语言必须与本次请求指定的语言一致。
`
// LLMClient 分镜 LLM 客户端
type LLMClient struct {
httpClient *req.Client
}
func NewLLMClient() *LLMClient {
return &LLMClient{
httpClient: req.C().SetTimeout(2 * time.Minute),
}
}
// GenerateSlides 调用大模型生成分镜列表。language 约束输出语言,mode 约束图中文字量,maxPages 约束恰好生成 N 页。
func (c *LLMClient) GenerateSlides(ctx context.Context, cfg types.PPTConfig, content, prompt, language, mode string, maxPages int) ([]slidePlan, error) {
if cfg.OutlineLLMApiURL == "" {
return nil, fmt.Errorf("outline LLM api url is empty")
}
if cfg.OutlineLLMApiKey == "" {
return nil, fmt.Errorf("outline LLM api key is empty")
}
if maxPages <= 0 {
maxPages = 10
}
if mode != "detailed" && mode != "slides" {
mode = "slides"
}
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
// 动态 system:加入页数约束
systemContent := systemPrompt + fmt.Sprintf("\n\n# 页数约束\n请将内容拆分为恰好 %d 页的幻灯片分镜,保证逻辑完整、故事线连贯,不要多也不要少。输出 JSON 数组长度必须为 %d。", maxPages, maxPages)
// 组装用户输入
userContent := fmt.Sprintf("下面是用户提供的演示文稿大纲内容:\n\n%s", content)
if prompt != "" {
userContent = fmt.Sprintf("%s\n\n额外的设计要求:%s", userContent, prompt)
}
if language != "" {
langHint := "中文"
if language == "en" || language == "en-US" {
langHint = "英文"
} else if language == "zh-CN" || language == "zh" {
langHint = "中文"
} else {
langHint = "语言代码 " + language + " 对应的语言"
}
userContent = fmt.Sprintf("%s\n\n请用%s输出所有分镜内容(theme、title、points、image_prompt 等均使用该语言;图片中出现的文字也必须是%s)。", userContent, langHint, langHint)
}
modeHint := "演示用幻灯片"
if mode == "detailed" {
modeHint = "详细演示文稿"
}
userContent = fmt.Sprintf("%s\n\n本次生成模式为:%s。请按上述规则控制每页插图中文字的量。", userContent, modeHint)
modelName := cfg.OutlineLLMModel
if modelName == "" {
modelName = "gpt-5.2"
}
body := map[string]any{
"model": modelName,
"messages": []message{
{Role: "user", Content: systemContent + "\n\n" + userContent},
},
"temperature": 0.8,
}
var respBody struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
logger.Infof("generate PPT slides with outline LLM, api: %s", cfg.OutlineLLMApiURL)
r, err := c.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+cfg.OutlineLLMApiKey).
SetBody(body).
SetSuccessResult(&respBody).
Post(cfg.OutlineLLMApiURL)
if err != nil {
return nil, fmt.Errorf("request outline LLM failed: %v", err)
}
if r.IsErrorState() {
return nil, fmt.Errorf("outline LLM returned error status: %s", r.Status)
}
if len(respBody.Choices) == 0 {
return nil, fmt.Errorf("outline LLM returned empty choices")
}
contentStr := respBody.Choices[0].Message.Content
var plans []slidePlan
if err := json.Unmarshal([]byte(contentStr), &plans); err != nil {
return nil, fmt.Errorf("parse outline LLM json failed: %v, raw: %s", err, contentStr)
}
return plans, nil
}
// GenerateNotebookContent 调用文档提炼 LLM,把 rawDocText -> PPT 可用的 content(大纲/结构化要点)。
func (c *LLMClient) GenerateNotebookContent(ctx context.Context, cfg types.PPTConfig, rawDocText, designPrompt, language string) (string, error) {
if cfg.OutlineLLMApiURL == "" {
return "", fmt.Errorf("outline LLM api url is empty")
}
if cfg.OutlineLLMApiKey == "" {
return "", fmt.Errorf("outline LLM api key is empty")
}
rawDocText = strings.TrimSpace(rawDocText)
if rawDocText == "" {
return "", fmt.Errorf("rawDocText is empty")
}
// 对超长输入做保守截断,避免请求体过大或上下文溢出。
// 这里按“字符数”截断,真实 token 仍可能超出,但作为兜底足够。
const maxChars = 25000
runes := []rune(rawDocText)
if len(runes) > maxChars {
rawDocText = string(runes[:maxChars])
}
langHint := "中文"
if language == "en" || language == "en-US" {
langHint = "英文"
} else if language == "zh-CN" || language == "zh" {
langHint = "中文"
} else if language != "" {
langHint = "语言代码 " + language + " 对应的语言"
}
maxSlides := cfg.MaxSlidesPerTask
if maxSlides <= 0 {
maxSlides = 10
}
userContent := fmt.Sprintf("原始文档文本如下(可能很长):\n\n%s", rawDocText)
if strings.TrimSpace(designPrompt) != "" {
userContent = fmt.Sprintf("%s\n\n设计要求(风格/受众/侧重点等):\n%s", userContent, designPrompt)
}
userContent = fmt.Sprintf(
"%s\n\n请用%s输出 PPT 大纲内容。该大纲应便于拆分为不超过 %d 页的 PPT。",
userContent,
langHint,
maxSlides,
)
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
systemContent := notebookSystemPrompt + fmt.Sprintf("\n\n# 语言约束\n输出语言:%s。", langHint)
modelName := cfg.OutlineLLMModel
if modelName == "" {
modelName = "gpt-4o-mini"
}
body := map[string]any{
"model": modelName,
"messages": []message{
{Role: "user", Content: systemContent + "\n\n" + userContent},
},
"temperature": 0.4,
}
var respBody struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
logger.Infof("generate PPT content with outline LLM, api: %s", cfg.OutlineLLMApiURL)
r, err := c.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+cfg.OutlineLLMApiKey).
SetBody(body).
SetSuccessResult(&respBody).
Post(cfg.OutlineLLMApiURL)
if err != nil {
return "", fmt.Errorf("request outline LLM failed: %v", err)
}
if r.IsErrorState() {
return "", fmt.Errorf("outline LLM returned error status: %s", r.Status)
}
if len(respBody.Choices) == 0 {
return "", fmt.Errorf("outline LLM returned empty choices")
}
return strings.TrimSpace(respBody.Choices[0].Message.Content), nil
}
// titleSystemPrompt 根据大纲生成用于任务列表的短标题(单行纯文本)
const titleSystemPrompt = `
# Role
你是「演示文稿命名助手」。用户会提供一份 PPT 内容大纲(可能含 Markdown)。请根据大纲主题与受众,生成一个**适合出现在任务列表中的短标题**。
# Output Rules
1. 只输出**一行**纯文本,不要换行、不要编号、不要引号包裹。
2. 长度建议 **20 个字以内**(中文)或 **8 个英文单词以内**;若大纲极长,仍只给概括性标题。
3. 输出语言必须与本次指定的「输出语言」一致。
4. 不要输出“标题:”“Title:”等前缀,不要复述本说明。
`
// GeneratePPTTitle 调用大模型根据 content 生成列表用短标题;失败时由调用方降级。
func (c *LLMClient) GeneratePPTTitle(ctx context.Context, cfg types.PPTConfig, content, language string) (string, error) {
if cfg.OutlineLLMApiURL == "" {
return "", fmt.Errorf("outline LLM api url is empty")
}
if cfg.OutlineLLMApiKey == "" {
return "", fmt.Errorf("outline LLM api key is empty")
}
content = strings.TrimSpace(content)
if content == "" {
return "", fmt.Errorf("content is empty")
}
const maxChars = 10000
runes := []rune(content)
if len(runes) > maxChars {
content = string(runes[:maxChars])
}
langHint := "中文"
if language == "en" || language == "en-US" {
langHint = "英文"
} else if language == "zh-CN" || language == "zh" {
langHint = "中文"
} else if language != "" {
langHint = "语言代码 " + language + " 对应的语言"
}
type message struct {
Role string `json:"role"`
Content string `json:"content"`
}
userContent := fmt.Sprintf("输出语言:%s。\n\n下面是用户提供的 PPT 大纲内容,请只返回列表标题:\n\n%s", langHint, content)
systemContent := titleSystemPrompt + fmt.Sprintf("\n\n# 语言约束\n请用%s撰写标题。", langHint)
modelName := cfg.OutlineLLMModel
if modelName == "" {
modelName = "gpt-4o-mini"
}
body := map[string]any{
"model": modelName,
"messages": []message{
{Role: "user", Content: systemContent + "\n\n" + userContent},
},
"temperature": 0.5,
}
var respBody struct {
Choices []struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
} `json:"choices"`
}
logger.Infof("generate PPT list title with outline LLM, api: %s", cfg.OutlineLLMApiURL)
r, err := c.httpClient.R().
SetContext(ctx).
SetHeader("Content-Type", "application/json").
SetHeader("Authorization", "Bearer "+cfg.OutlineLLMApiKey).
SetBody(body).
SetSuccessResult(&respBody).
Post(cfg.OutlineLLMApiURL)
if err != nil {
return "", fmt.Errorf("request outline LLM failed: %v", err)
}
if r.IsErrorState() {
return "", fmt.Errorf("outline LLM returned error status: %s", r.Status)
}
if len(respBody.Choices) == 0 {
return "", fmt.Errorf("outline LLM returned empty choices")
}
raw := strings.TrimSpace(respBody.Choices[0].Message.Content)
if idx := strings.IndexAny(raw, "\r\n"); idx >= 0 {
raw = strings.TrimSpace(raw[:idx])
}
raw = strings.Trim(raw, `"'「」`)
return raw, nil
}
+884
View File
@@ -0,0 +1,884 @@
package ppt
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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"
"errors"
"fmt"
"geekai/core/types"
"geekai/service"
"geekai/service/oss"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"sort"
"strings"
"sync"
"time"
"golang.org/x/sync/errgroup"
"gorm.io/gorm"
)
// ErrInsufficientPower 用户算力不足以完成本次 PPT 任务(由 handler 映射文案)
var ErrInsufficientPower = errors.New("insufficient power for ppt task")
var (
// ErrPptTaskNotFound 表示任务不存在
ErrPptTaskNotFound = errors.New("ppt task not found")
// ErrPptTaskNotDeletable 表示任务状态不允许删除
ErrPptTaskNotDeletable = errors.New("ppt task not deletable")
// ErrPptTaskBusy 任务正在处理中
ErrPptTaskBusy = errors.New("ppt task is processing")
// ErrPptTaskNotResumable 无法继续生成(无分镜占位或已完成)
ErrPptTaskNotResumable = errors.New("ppt task cannot be resumed")
// ErrPptSlideNotFound 指定 slide_index 不存在
ErrPptSlideNotFound = errors.New("ppt slide not found")
// ErrPptSlideNoImage 该页尚无配图
ErrPptSlideNoImage = errors.New("ppt slide has no image")
// ErrPptInvalidVersionIndex 历史版本下标无效
ErrPptInvalidVersionIndex = errors.New("invalid slide version index")
)
// TaskStatus 任务状态
type TaskStatus string
const (
TaskStatusPending TaskStatus = "pending"
TaskStatusProcessing TaskStatus = "processing"
TaskStatusCompleted TaskStatus = "completed"
TaskStatusFailed TaskStatus = "failed"
)
// SlideData 单页 PPT 数据
type SlideData struct {
SlideIndex int `json:"slide_index"`
Theme string `json:"theme"`
Title string `json:"title"`
Points []string `json:"points"`
ImagePrompt string `json:"image_prompt"`
ImageURL string `json:"image_url"`
ImageHistory []vo.PPTSlideImageVersion `json:"image_history,omitempty"`
}
// Task PPT 生成任务(用于业务层与 API 返回,持久化在 DB)
type Task struct {
TaskID string `json:"task_id"`
UserID uint `json:"user_id"`
Status TaskStatus `json:"status"`
Content string `json:"content"`
Prompt string `json:"prompt"`
Language string `json:"language"`
Mode string `json:"mode"`
Pages int `json:"pages"`
Total int `json:"total_slides"`
Completed int `json:"completed_slides"`
Slides []SlideData `json:"slides"`
Title string `json:"title"`
Thumb string `json:"thumb"`
ErrorMessage string `json:"error_message"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TaskSummaryMap 返回任务摘要 map(公共字段),供 handler 补充独有字段后返回。
func (t *Task) TaskSummaryMap() map[string]any {
return map[string]any{
"task_id": t.TaskID,
"status": t.Status,
"total_slides": t.Total,
"completed_slides": t.Completed,
"created_at": t.CreatedAt.Unix(),
"updated_at": t.UpdatedAt.Unix(),
"title": t.Title,
"thumb": t.Thumb,
}
}
// Progress 任务进度信息
type Progress struct {
Total int `json:"total_slides"`
Completed int `json:"completed_slides"`
}
// PptService PPT 任务与生成流程(持久化、LLM 分镜、生图、转存、算力)
type PptService struct {
db *gorm.DB
userService *service.UserService
uploadManager *oss.UploaderManager
llm *LLMClient
// slidesLock 全局互斥:串行化 slides JSON 的写库,避免并发覆盖(写库极短,可接受排队)
slidesLock sync.Mutex
}
// NewPptService 创建 PptService
func NewPptService(db *gorm.DB, userService *service.UserService, uploadManager *oss.UploaderManager) *PptService {
return &PptService{
db: db,
userService: userService,
uploadManager: uploadManager,
llm: NewLLMClient(),
}
}
// GenerateNotebookContent 将原始文档文本提炼成 PPT 可用的大纲 content。
func (s *PptService) GenerateNotebookContent(ctx context.Context, cfg types.PPTConfig, rawDocText, designPrompt, language string) (string, error) {
if s.llm == nil {
s.llm = NewLLMClient()
}
return s.llm.GenerateNotebookContent(ctx, cfg, rawDocText, designPrompt, language)
}
func slideToVO(s SlideData) vo.PPTSlideData {
return vo.PPTSlideData{
SlideIndex: s.SlideIndex,
Theme: s.Theme,
Title: s.Title,
Points: s.Points,
ImagePrompt: s.ImagePrompt,
ImageURL: s.ImageURL,
ImageHistory: vo.PPTSlideImageVersions(s.ImageHistory),
}
}
func voToSlide(s vo.PPTSlideData) SlideData {
return SlideData{
SlideIndex: s.SlideIndex,
Theme: s.Theme,
Title: s.Title,
Points: s.Points,
ImagePrompt: s.ImagePrompt,
ImageURL: s.ImageURL,
ImageHistory: []vo.PPTSlideImageVersion(s.ImageHistory),
}
}
func voSlidesToBiz(slides vo.PPTSlides) []SlideData {
out := make([]SlideData, len(slides))
for i, sv := range slides {
out[i] = voToSlide(sv)
}
return out
}
func voSlidesToBizNormalized(slides vo.PPTSlides) []SlideData {
out := make([]SlideData, len(slides))
for i, sv := range slides {
sd := voToSlide(sv)
normalizeSlideImageHistory(&sd)
out[i] = sd
}
return out
}
// normalizeSlideImageHistory 旧数据仅有 image_url 时补全 image_history,便于前端展示历史
func normalizeSlideImageHistory(s *SlideData) {
if strings.TrimSpace(s.ImageURL) != "" && len(s.ImageHistory) == 0 {
s.ImageHistory = []vo.PPTSlideImageVersion{
{ImageURL: s.ImageURL, Prompt: strings.TrimSpace(s.ImagePrompt)},
}
}
}
// DerivePPTThumbFromSlides 按 slide_index 升序取第一张有图 URL(与 vo.PPTSlides 规则一致)
func DerivePPTThumbFromSlides(slides []SlideData) string {
if len(slides) == 0 {
return ""
}
cp := make([]SlideData, len(slides))
copy(cp, slides)
sort.Slice(cp, func(i, j int) bool {
return cp[i].SlideIndex < cp[j].SlideIndex
})
for _, s := range cp {
if strings.TrimSpace(s.ImageURL) != "" {
return s.ImageURL
}
}
return ""
}
func truncateTitleRunes(s string, max int) string {
if max <= 0 {
return s
}
r := []rune(s)
if len(r) <= max {
return s
}
return string(r[:max])
}
func taskToModel(t *Task) *model.PPTJob {
now := time.Now()
job := &model.PPTJob{
TaskId: t.TaskID,
UserId: t.UserID,
Status: string(t.Status),
ErrMsg: t.ErrorMessage,
Prompt: t.Prompt,
Title: t.Title,
Thumb: t.Thumb,
Content: t.Content,
Params: vo.PPTParams{Language: t.Language, Mode: t.Mode, Pages: t.Pages},
Slides: nil,
TotalSlides: t.Total,
CompletedSlides: t.Completed,
CreatedAt: now,
UpdatedAt: now,
}
if t.CreatedAt.IsZero() {
job.CreatedAt = now
job.UpdatedAt = now
} else {
job.CreatedAt = t.CreatedAt
job.UpdatedAt = t.UpdatedAt
}
return job
}
func modelToTask(j *model.PPTJob) *Task {
slides := voSlidesToBizNormalized(j.Slides)
return &Task{
TaskID: j.TaskId,
UserID: j.UserId,
Status: TaskStatus(j.Status),
Content: j.Content,
Prompt: j.Prompt,
Title: j.Title,
Thumb: j.Thumb,
Language: j.Params.Language,
Mode: j.Params.Mode,
Pages: j.Params.Pages,
Total: j.TotalSlides,
Completed: j.CompletedSlides,
Slides: slides,
ErrorMessage: j.ErrMsg,
CreatedAt: j.CreatedAt,
UpdatedAt: j.UpdatedAt,
}
}
// BuildPendingTask 校验算力与页数,组装待写入的 Task(未落库)
func (s *PptService) BuildPendingTask(taskID string, userID uint, userPower int, content, prompt, language, mode string, reqPages int) (*Task, types.PPTConfig, error) {
cfg, err := s.loadPPTConfig()
if err != nil {
return nil, cfg, err
}
// 目标页数:用户指定时取 min(请求页数, 服务端上限);未指定(0)时按服务端上限作为默认生成规模
effectivePages := cfg.MaxSlidesPerTask
if reqPages > 0 {
effectivePages = reqPages
if effectivePages > cfg.MaxSlidesPerTask {
effectivePages = cfg.MaxSlidesPerTask
}
}
estimatePower := effectivePages * cfg.PowerCostPerSlide
if estimatePower > 0 && userPower < estimatePower {
return nil, cfg, ErrInsufficientPower
}
effectiveMode := mode
if effectiveMode != "detailed" && effectiveMode != "slides" {
effectiveMode = "slides"
}
task := &Task{
TaskID: taskID,
UserID: userID,
Status: TaskStatusPending,
Content: content,
Prompt: prompt,
Language: language,
Pages: effectivePages,
Mode: effectiveMode,
}
return task, cfg, nil
}
// CreateTask 创建新任务并写入数据库(调用大模型生成列表标题后落库)
func (s *PptService) CreateTask(ctx context.Context, task *Task, cfg types.PPTConfig) error {
if s.llm == nil {
s.llm = NewLLMClient()
}
title, err := s.llm.GeneratePPTTitle(ctx, cfg, task.Content, task.Language)
if err != nil {
logger.Warnf("GeneratePPTTitle failed task_id=%s: %v", task.TaskID, err)
title = "未命名演示文稿"
} else {
title = strings.TrimSpace(title)
if title == "" {
title = "未命名演示文稿"
}
}
task.Title = truncateTitleRunes(title, 255)
task.CreatedAt = time.Now()
task.UpdatedAt = task.CreatedAt
task.Status = TaskStatusPending
job := taskToModel(task)
return s.db.Create(job).Error
}
// GetTask 从数据库获取任务
func (s *PptService) GetTask(taskID string) (*Task, bool) {
var job model.PPTJob
err := s.db.Where("task_id = ?", taskID).First(&job).Error
if err != nil || job.TaskId == "" {
return nil, false
}
return modelToTask(&job), true
}
// UpdateStatus 更新任务状态
func (s *PptService) UpdateStatus(taskID string, status TaskStatus) {
s.db.Model(&model.PPTJob{}).Where("task_id = ?", taskID).
Updates(map[string]interface{}{"status": string(status), "updated_at": time.Now()})
}
func countSlidesWithImage(slides []SlideData) int {
n := 0
for _, sl := range slides {
if strings.TrimSpace(sl.ImageURL) != "" {
n++
}
}
return n
}
func slidePlansToOutlines(plans []slidePlan) []SlideData {
out := make([]SlideData, len(plans))
for i, p := range plans {
out[i] = SlideData{
SlideIndex: p.SlideIndex,
Theme: p.Theme,
Title: p.Title,
Points: p.Points,
ImagePrompt: p.ImagePrompt,
ImageURL: "",
}
}
sort.Slice(out, func(i, j int) bool {
return out[i].SlideIndex < out[j].SlideIndex
})
return out
}
// saveSlidesOutline 分镜一出即落库:每页含 theme/title/points/image_promptimage_url 为空
func (s *PptService) saveSlidesOutline(taskID string, total int, slides []SlideData) error {
s.slidesLock.Lock()
defer s.slidesLock.Unlock()
voSlides := make(vo.PPTSlides, len(slides))
for i := range slides {
voSlides[i] = slideToVO(slides[i])
}
completed := countSlidesWithImage(slides)
return s.db.Model(&model.PPTJob{}).Where("task_id = ?", taskID).Updates(map[string]interface{}{
"slides": voSlides,
"total_slides": total,
"completed_slides": completed,
"updated_at": time.Now(),
}).Error
}
// ApplySlideImage 按 slide_index 原地写入 image_url,并刷新 completed_slides、thumb
func (s *PptService) ApplySlideImage(taskID string, slide SlideData) error {
s.slidesLock.Lock()
defer s.slidesLock.Unlock()
var job model.PPTJob
if err := s.db.Where("task_id = ?", taskID).First(&job).Error; err != nil {
return err
}
slides := job.Slides
found := false
for i := range slides {
if slides[i].SlideIndex == slide.SlideIndex {
slides[i].ImageURL = slide.ImageURL
if strings.TrimSpace(slide.ImageURL) != "" && len(slides[i].ImageHistory) == 0 {
slides[i].ImageHistory = vo.PPTSlideImageVersions{
{ImageURL: slide.ImageURL, Prompt: strings.TrimSpace(slide.ImagePrompt)},
}
}
found = true
break
}
}
if !found {
return fmt.Errorf("slide index %d not found", slide.SlideIndex)
}
job.Slides = slides
biz := voSlidesToBiz(slides)
return s.refreshJobMeta(&job, biz)
}
// refreshJobMeta 刷新 job 的 CompletedSlides/Thumb/UpdatedAt 并 Save。
// 调用前必须已持有 slidesLock。
func (s *PptService) refreshJobMeta(job *model.PPTJob, biz []SlideData) error {
job.CompletedSlides = countSlidesWithImage(biz)
job.Thumb = DerivePPTThumbFromSlides(biz)
job.UpdatedAt = time.Now()
return s.db.Save(job).Error
}
func (s *PptService) validateSlideOutline(task *Task) error {
if task.Total <= 0 {
return ErrPptTaskNotResumable
}
if len(task.Slides) < task.Total {
return ErrPptTaskNotResumable
}
seen := make(map[int]bool, len(task.Slides))
for _, sl := range task.Slides {
seen[sl.SlideIndex] = true
}
for i := 1; i <= task.Total; i++ {
if !seen[i] {
return ErrPptTaskNotResumable
}
}
return nil
}
func slidesNeedingImages(task *Task) []SlideData {
var need []SlideData
for _, sl := range task.Slides {
if strings.TrimSpace(sl.ImageURL) == "" {
need = append(need, sl)
}
}
sort.Slice(need, func(i, j int) bool {
return need[i].SlideIndex < need[j].SlideIndex
})
return need
}
func (s *PptService) userPower(userID uint) (int, error) {
var u model.User
if err := s.db.Where("id = ?", userID).First(&u).Error; err != nil {
return 0, err
}
return u.Power, nil
}
// runSlideImageJobs 为给定幻灯片列表并发生图(每张成功后 ApplySlideImage
func (s *PptService) runSlideImageJobs(ctx context.Context, task *Task, cfg types.PPTConfig, generator ImageGenerator, slides []SlideData) error {
if len(slides) == 0 {
return nil
}
if cfg.MaxConcurrentRequests <= 0 {
cfg.MaxConcurrentRequests = 3
}
group, ctx := errgroup.WithContext(ctx)
group.SetLimit(cfg.MaxConcurrentRequests)
for _, item := range slides {
slide := item
group.Go(func() error {
imgURL, err := generator.Generate(ctx, slide.ImagePrompt)
if err != nil {
return err
}
storedURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(imgURL, ".png", false)
if err != nil {
return fmt.Errorf("转存图片失败:%w", err)
}
full := slide
full.ImageURL = storedURL
if err := s.ApplySlideImage(task.TaskID, full); err != nil {
return err
}
if cfg.PowerCostPerSlide > 0 {
err = s.userService.DecreasePower(task.UserID, cfg.PowerCostPerSlide, model.PowerLog{
Type: types.PowerConsume,
Model: generator.Provider(),
Remark: fmt.Sprintf("PPT 任务 %s 第 %d 页图片生成", task.TaskID, slide.SlideIndex),
})
if err != nil {
return fmt.Errorf("扣减算力失败:%v", err)
}
}
return nil
})
}
return group.Wait()
}
// startSlideImageGenerationAsync 在后台为 missing 页并发生图;若 setProcessingBeforeRun 为 true 则先置为 processing(用户主动 resume)。
func (s *PptService) startSlideImageGenerationAsync(task *Task, missing []SlideData, setProcessingBeforeRun bool) error {
if len(missing) == 0 {
return nil
}
cfg, err := s.loadPPTConfig()
if err != nil {
return err
}
cost := len(missing) * cfg.PowerCostPerSlide
if cost > 0 {
power, err := s.userPower(task.UserID)
if err != nil {
return err
}
if power < cost {
return ErrInsufficientPower
}
}
generator, err := NewImageGenerator(cfg)
if err != nil {
return fmt.Errorf("初始化图片生成器失败:%w", err)
}
if setProcessingBeforeRun {
s.UpdateStatus(task.TaskID, TaskStatusProcessing)
}
t := task
go func() {
bg := context.Background()
if err := s.runSlideImageJobs(bg, t, cfg, generator, missing); err != nil {
s.MarkAsFailed(task.TaskID, fmt.Sprintf("图片生成失败:%v", err))
return
}
s.UpdateStatus(task.TaskID, TaskStatusCompleted)
}()
return nil
}
// RecoverStaleProcessingTasks 进程启动时扫描 DB 中仍为 processing 且存在缺图页的任务,重新拉起生图协程(用于服务中断后的恢复)。
func (s *PptService) RecoverStaleProcessingTasks() {
var jobs []model.PPTJob
if err := s.db.Where("status = ?", string(TaskStatusProcessing)).Find(&jobs).Error; err != nil {
logger.Warnf("PPT recover: list processing jobs failed: %v", err)
return
}
for i := range jobs {
task := modelToTask(&jobs[i])
missing := slidesNeedingImages(task)
if len(missing) == 0 {
s.UpdateStatus(task.TaskID, TaskStatusCompleted)
logger.Infof("PPT recover: task %s was processing but all slides had images, marked completed", task.TaskID)
continue
}
if err := s.validateSlideOutline(task); err != nil {
logger.Warnf("PPT recover: task %s skip (invalid outline): %v", task.TaskID, err)
continue
}
if err := s.startSlideImageGenerationAsync(task, missing, false); err != nil {
if errors.Is(err, ErrInsufficientPower) {
logger.Warnf("PPT recover: task %s skip (insufficient power for %d slides)", task.TaskID, len(missing))
continue
}
logger.Warnf("PPT recover: task %s failed to restart: %v", task.TaskID, err)
continue
}
logger.Infof("PPT recover: restarted image generation for task %s (%d slides)", task.TaskID, len(missing))
}
}
// ResumeTask 继续为缺图页生图(需完整分镜占位;processing 时返回 ErrPptTaskBusy
func (s *PptService) ResumeTask(ctx context.Context, taskID string, userID uint) error {
task, ok := s.GetTask(taskID)
if !ok {
return ErrPptTaskNotFound
}
if task.UserID != userID {
return ErrPptTaskNotFound
}
if task.Status == TaskStatusProcessing {
return ErrPptTaskBusy
}
if task.Status == TaskStatusCompleted {
return ErrPptTaskNotResumable
}
if err := s.validateSlideOutline(task); err != nil {
return err
}
missing := slidesNeedingImages(task)
if len(missing) == 0 {
s.UpdateStatus(taskID, TaskStatusCompleted)
return nil
}
return s.startSlideImageGenerationAsync(task, missing, true)
}
// MarkAsFailed 标记任务失败
func (s *PptService) MarkAsFailed(taskID string, msg string) {
s.db.Model(&model.PPTJob{}).Where("task_id = ?", taskID).
Updates(map[string]interface{}{"status": string(TaskStatusFailed), "err_msg": msg, "updated_at": time.Now()})
}
// DeleteTask 删除任务并删除关联生成图片
// 仅允许删除 completed / failed 状态的任务,避免并发任务生成过程被打断。
func (s *PptService) DeleteTask(taskID string, userID uint) error {
var job model.PPTJob
if err := s.db.Where("task_id = ? AND user_id = ?", taskID, userID).First(&job).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) || job.TaskId == "" {
return ErrPptTaskNotFound
}
return err
}
if job.Status != string(TaskStatusCompleted) && job.Status != string(TaskStatusFailed) {
return ErrPptTaskNotDeletable
}
// 删除所有幻灯片对应的图片对象。
uploader := s.uploadManager.GetUploadHandler()
for _, slide := range job.Slides {
if slide.ImageURL == "" {
continue
}
if err := uploader.Delete(slide.ImageURL); err != nil {
// 图片可能已过期/不存在/对象已被清理,此时不应阻断“删除任务记录”的主流程。
// 这里只记录日志,确保数据库记录被删除后前端认为任务删除成功。
logger.Warnf("delete ppt image failed (task_id=%s, url=%s): %v", taskID, slide.ImageURL, err)
}
}
// 最后删除任务记录(slides 会随之从数据库消失)。
return s.db.Where("task_id = ? AND user_id = ?", taskID, userID).Delete(&model.PPTJob{}).Error
}
// List 返回所有任务列表(从数据库按创建时间倒序),供调用方按用户/状态过滤与分页
func (s *PptService) List() []*Task {
var jobs []model.PPTJob
s.db.Order("created_at DESC").Find(&jobs)
tasks := make([]*Task, 0, len(jobs))
for i := range jobs {
tasks = append(tasks, modelToTask(&jobs[i]))
}
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].CreatedAt.After(tasks[j].CreatedAt)
})
return tasks
}
// ListUserTasks 当前用户的任务分页列表(缺 title/thumb 时补全并写库)
func (s *PptService) ListUserTasks(ctx context.Context, userID uint, page, pageSize int) ([]*Task, int) {
all := s.List()
filtered := make([]*Task, 0, len(all))
for _, t := range all {
if t.UserID == userID {
filtered = append(filtered, t)
}
}
total := len(filtered)
start := (page - 1) * pageSize
if start > total {
start = total
}
end := start + pageSize
if end > total {
end = total
}
slice := filtered[start:end]
cfg, cfgErr := s.loadPPTConfig()
if cfgErr != nil {
logger.Warnf("ListUserTasks loadPPTConfig: %v", cfgErr)
}
if s.llm == nil {
s.llm = NewLLMClient()
}
for _, t := range slice {
s.ensureTaskMeta(ctx, t, cfg)
}
return slice, total
}
// EnsureTaskMeta 对外暴露标题/缩略图补全逻辑,供管理端列表/详情复用。
func (s *PptService) EnsureTaskMeta(ctx context.Context, task *Task) {
if task == nil {
return
}
cfg, cfgErr := s.loadPPTConfig()
if cfgErr != nil {
logger.Warnf("EnsureTaskMeta loadPPTConfig: %v", cfgErr)
}
if s.llm == nil {
s.llm = NewLLMClient()
}
s.ensureTaskMeta(ctx, task, cfg)
}
func (s *PptService) ensureTaskMeta(ctx context.Context, task *Task, cfg types.PPTConfig) {
updates := map[string]interface{}{}
if strings.TrimSpace(task.Title) == "" && strings.TrimSpace(task.Content) != "" {
title := "未命名演示文稿"
if cfg.OutlineLLMApiURL != "" && cfg.OutlineLLMApiKey != "" {
ti, err := s.llm.GeneratePPTTitle(ctx, cfg, task.Content, task.Language)
if err != nil {
logger.Warnf("ensureTaskMeta GeneratePPTTitle task_id=%s: %v", task.TaskID, err)
} else {
ti = strings.TrimSpace(ti)
if ti != "" {
title = truncateTitleRunes(ti, 255)
}
}
}
task.Title = title
updates["title"] = task.Title
}
if task.Thumb == "" && len(task.Slides) > 0 {
thumb := DerivePPTThumbFromSlides(task.Slides)
if thumb != "" {
task.Thumb = thumb
updates["thumb"] = thumb
}
}
if len(updates) > 0 {
updates["updated_at"] = time.Now()
_ = s.db.Model(&model.PPTJob{}).Where("task_id = ?", task.TaskID).Updates(updates).Error
}
}
// ListAdminJobs 管理后台任务列表(筛选 + 分页)
func (s *PptService) ListAdminJobs(ctx context.Context, page, pageSize, filterUserID int, status string) ([]*Task, int) {
items := s.List()
filtered := make([]*Task, 0, len(items))
for _, t := range items {
if filterUserID > 0 && int(t.UserID) != filterUserID {
continue
}
if status != "" && string(t.Status) != status {
continue
}
filtered = append(filtered, t)
}
total := len(filtered)
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 20
}
start := (page - 1) * pageSize
if start > total {
start = total
}
end := start + pageSize
if end > total {
end = total
}
slice := filtered[start:end]
for _, t := range slice {
s.EnsureTaskMeta(ctx, t)
}
return slice, total
}
// Stats 任务状态统计(管理后台)
func (s *PptService) Stats() (total, completed, processing, failed, pending int64) {
for _, t := range s.List() {
total++
switch t.Status {
case TaskStatusCompleted:
completed++
case TaskStatusProcessing:
processing++
case TaskStatusFailed:
failed++
case TaskStatusPending:
pending++
}
}
return
}
// RunTask 执行 PPT 生成:分镜、并发生图、转存图片、扣算力
func (s *PptService) RunTask(ctx context.Context, task *Task, cfg types.PPTConfig) {
s.UpdateStatus(task.TaskID, TaskStatusProcessing)
maxPages := task.Pages
if maxPages <= 0 {
maxPages = cfg.MaxSlidesPerTask
}
plans, err := s.llm.GenerateSlides(ctx, cfg, task.Content, task.Prompt, task.Language, task.Mode, maxPages)
if err != nil {
s.MarkAsFailed(task.TaskID, fmt.Sprintf("生成分镜失败:%v", err))
return
}
if len(plans) == 0 {
s.MarkAsFailed(task.TaskID, "分镜结果为空")
return
}
total := len(plans)
if cfg.MaxSlidesPerTask > 0 && total > cfg.MaxSlidesPerTask {
plans = plans[:cfg.MaxSlidesPerTask]
total = len(plans)
}
outlines := slidePlansToOutlines(plans)
if err := s.saveSlidesOutline(task.TaskID, total, outlines); err != nil {
s.MarkAsFailed(task.TaskID, fmt.Sprintf("保存分镜占位失败:%v", err))
return
}
generator, err := NewImageGenerator(cfg)
if err != nil {
s.MarkAsFailed(task.TaskID, fmt.Sprintf("初始化图片生成器失败:%v", err))
return
}
if err := s.runSlideImageJobs(ctx, task, cfg, generator, outlines); err != nil {
s.MarkAsFailed(task.TaskID, fmt.Sprintf("图片生成失败:%v", err))
return
}
s.UpdateStatus(task.TaskID, TaskStatusCompleted)
}
func (s *PptService) loadPPTConfig() (types.PPTConfig, error) {
var cfgModel model.Config
var pptCfg types.PPTConfig
err := s.db.Where("name", types.ConfigKeyPPT).First(&cfgModel).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
pptCfg.MaxSlidesPerTask = 30
pptCfg.MaxConcurrentRequests = 3
pptCfg.QPSLimit = 1
pptCfg.PowerCostPerSlide = 0
return pptCfg, nil
}
return pptCfg, err
}
err = utils.JsonDecode(cfgModel.Value, &pptCfg)
if err != nil {
return pptCfg, err
}
legacyMax10 := pptCfg.MaxSlidesPerTask == 10
if pptCfg.MaxSlidesPerTask <= 0 {
pptCfg.MaxSlidesPerTask = 30
}
if legacyMax10 {
// 与前端 PPT 页数控件 max=30 对齐;历史默认 10 会导致用户选择 12/15 仍被截断为 10
pptCfg.MaxSlidesPerTask = 30
}
if pptCfg.MaxConcurrentRequests <= 0 {
pptCfg.MaxConcurrentRequests = 3
}
if pptCfg.QPSLimit <= 0 {
pptCfg.QPSLimit = 1
}
if legacyMax10 {
val := utils.JsonEncode(pptCfg)
_ = s.db.Model(&model.Config{}).Where("name = ?", types.ConfigKeyPPT).Update("value", val)
}
return pptCfg, nil
}
+50
View File
@@ -0,0 +1,50 @@
package ppt
import (
"encoding/base64"
"fmt"
"geekai/core/types"
"net/http"
"net/url"
"os"
"strings"
)
// PrepareReferenceInputsForImg2Img 将幻灯片参考图转为第三方 API 可消费的输入:本地存储时读文件并转为 data URI(base64),公网 URL 原样传递。
func PrepareReferenceInputsForImg2Img(rawURL string, oss types.OSSConfig, app *types.AppConfig) ([]string, error) {
rawURL = strings.TrimSpace(rawURL)
if rawURL == "" {
return nil, fmt.Errorf("empty reference image url")
}
if strings.HasPrefix(rawURL, "data:") {
return []string{rawURL}, nil
}
if oss.Active == "local" {
if fp, ok := mapLocalUploadFile(rawURL, oss.Local); ok {
b, err := os.ReadFile(fp)
if err != nil {
return nil, fmt.Errorf("read local reference image: %w", err)
}
if _, err := decodeImageBytes(b); err != nil {
return nil, fmt.Errorf("reference is not a valid image: %w", err)
}
mime := http.DetectContentType(b)
if !strings.HasPrefix(mime, "image/") {
mime = "image/png"
}
dataURI := fmt.Sprintf("data:%s;base64,%s", mime, base64.StdEncoding.EncodeToString(b))
return []string{dataURI}, nil
}
}
if u, err := url.Parse(rawURL); err == nil && u.Scheme != "" && u.Host != "" {
return []string{rawURL}, nil
}
abs := resolveAbsoluteImageURL(rawURL, oss.Local, app)
if strings.TrimSpace(abs) == "" {
return nil, fmt.Errorf("cannot resolve reference image url")
}
return []string{abs}, nil
}
+182
View File
@@ -0,0 +1,182 @@
package ppt
import (
"context"
"fmt"
"geekai/core/types"
"geekai/store/model"
"geekai/store/vo"
"strings"
)
// EditSlideImage 基于当前激活图做图生图,追加 image_history 并将 image_url 设为新版。
func (s *PptService) EditSlideImage(ctx context.Context, taskID string, userID uint, slideIndex int, prompt string, oss types.OSSConfig, app *types.AppConfig) ([]SlideData, error) {
prompt = strings.TrimSpace(prompt)
if prompt == "" {
return nil, fmt.Errorf("请输入修改说明")
}
task, ok := s.GetTask(taskID)
if !ok {
return nil, ErrPptTaskNotFound
}
if task.UserID != userID {
return nil, ErrPptTaskNotFound
}
refURL := ""
for _, sl := range task.Slides {
if sl.SlideIndex == slideIndex {
normalizeSlideImageHistory(&sl)
refURL = strings.TrimSpace(sl.ImageURL)
break
}
}
if refURL == "" {
if slideExists(task.Slides, slideIndex) {
return nil, ErrPptSlideNoImage
}
return nil, ErrPptSlideNotFound
}
cfg, err := s.loadPPTConfig()
if err != nil {
return nil, err
}
power, err := s.userPower(userID)
if err != nil {
return nil, err
}
if cfg.PowerCostPerSlide > 0 && power < cfg.PowerCostPerSlide {
return nil, ErrInsufficientPower
}
generator, err := NewImageGenerator(cfg)
if err != nil {
return nil, err
}
refInputs, err := PrepareReferenceInputsForImg2Img(refURL, oss, app)
if err != nil {
return nil, fmt.Errorf("准备参考图失败:%w", err)
}
imgURL, err := generator.GenerateWithReference(ctx, prompt, refInputs)
if err != nil {
return nil, err
}
storedURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(imgURL, ".png", false)
if err != nil {
return nil, fmt.Errorf("转存图片失败:%w", err)
}
if err := s.applySlideImageEdit(taskID, slideIndex, storedURL, prompt); err != nil {
return nil, err
}
if cfg.PowerCostPerSlide > 0 {
err = s.userService.DecreasePower(userID, cfg.PowerCostPerSlide, model.PowerLog{
Type: types.PowerConsume,
Model: generator.Provider(),
Remark: fmt.Sprintf("PPT 任务 %s 第 %d 页图生图编辑", taskID, slideIndex),
})
if err != nil {
return nil, fmt.Errorf("扣减算力失败:%v", err)
}
}
task2, _ := s.GetTask(taskID)
return task2.Slides, nil
}
func slideExists(slides []SlideData, slideIndex int) bool {
for _, sl := range slides {
if sl.SlideIndex == slideIndex {
return true
}
}
return false
}
func (s *PptService) applySlideImageEdit(taskID string, slideIndex int, newURL string, editPrompt string) error {
s.slidesLock.Lock()
defer s.slidesLock.Unlock()
var job model.PPTJob
if err := s.db.Where("task_id = ?", taskID).First(&job).Error; err != nil {
return err
}
slides := job.Slides
found := false
for i := range slides {
if slides[i].SlideIndex != slideIndex {
continue
}
found = true
sd := voToSlide(slides[i])
normalizeSlideImageHistory(&sd)
if strings.TrimSpace(sd.ImageURL) == "" {
return ErrPptSlideNoImage
}
sd.ImageHistory = append(sd.ImageHistory, vo.PPTSlideImageVersion{
ImageURL: newURL,
Prompt: editPrompt,
})
sd.ImageURL = newURL
slides[i] = slideToVO(sd)
break
}
if !found {
return ErrPptSlideNotFound
}
job.Slides = slides
biz := voSlidesToBiz(slides)
return s.refreshJobMeta(&job, biz)
}
// SetActiveSlideVersion 将 image_url 切换为 image_history[versionIndex]。
func (s *PptService) SetActiveSlideVersion(taskID string, userID uint, slideIndex int, versionIndex int) ([]SlideData, error) {
task, ok := s.GetTask(taskID)
if !ok {
return nil, ErrPptTaskNotFound
}
if task.UserID != userID {
return nil, ErrPptTaskNotFound
}
s.slidesLock.Lock()
defer s.slidesLock.Unlock()
var job model.PPTJob
if err := s.db.Where("task_id = ?", taskID).First(&job).Error; err != nil {
return nil, err
}
slides := job.Slides
found := false
for i := range slides {
if slides[i].SlideIndex != slideIndex {
continue
}
found = true
sd := voToSlide(slides[i])
normalizeSlideImageHistory(&sd)
hist := sd.ImageHistory
if versionIndex < 0 || versionIndex >= len(hist) {
return nil, ErrPptInvalidVersionIndex
}
sd.ImageURL = hist[versionIndex].ImageURL
slides[i] = slideToVO(sd)
break
}
if !found {
return nil, ErrPptSlideNotFound
}
job.Slides = slides
biz := voSlidesToBiz(slides)
if err := s.refreshJobMeta(&job, biz); err != nil {
return nil, err
}
out := voSlidesToBizNormalized(job.Slides)
return out, nil
}
-299
View File
@@ -1,299 +0,0 @@
package sd
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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/types"
logger2 "geekai/logger"
"geekai/service"
"geekai/service/oss"
"geekai/store"
"geekai/store/model"
"geekai/utils"
"time"
"github.com/go-redis/redis/v8"
"github.com/imroc/req/v3"
"gorm.io/gorm"
)
var logger = logger2.GetLogger()
// SD 绘画服务
type Service struct {
httpClient *req.Client
taskQueue *store.RedisQueue
db *gorm.DB
uploadManager *oss.UploaderManager
userService *service.UserService
}
func NewService(db *gorm.DB, manager *oss.UploaderManager, redisCli *redis.Client, userService *service.UserService) *Service {
return &Service{
httpClient: req.C(),
taskQueue: store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli),
db: db,
uploadManager: manager,
userService: userService,
}
}
func (s *Service) Run() {
// 将数据库中未提交的人物加载到队列
var jobs []model.SdJob
s.db.Where("progress", 0).Find(&jobs)
for _, v := range jobs {
var task types.SdTask
err := utils.JsonDecode(v.TaskInfo, &task)
if err != nil {
logger.Errorf("decode task info with error: %v", err)
continue
}
task.Id = int(v.Id)
s.PushTask(task)
}
logger.Infof("Starting Stable-Diffusion job consumer")
go func() {
for {
var task types.SdTask
err := s.taskQueue.LPop(&task)
if err != nil {
logger.Errorf("taking task with error: %v", err)
continue
}
// translate prompt
if utils.HasChinese(task.Params.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.Prompt), task.TranslateModelId)
if err == nil {
task.Params.Prompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
// translate negative prompt
if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt), task.TranslateModelId)
if err == nil {
task.Params.NegPrompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
logger.Infof("handle a new Stable-Diffusion task: %+v", task)
err = s.Txt2Img(task)
if err != nil {
logger.Error("绘画任务执行失败:", err.Error())
// update the task progress
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(map[string]interface{}{
"progress": service.FailTaskProgress,
"err_msg": err.Error(),
})
continue
}
}
}()
}
// Txt2ImgReq 文生图请求实体
type Txt2ImgReq struct {
Prompt string `json:"prompt"`
NegativePrompt string `json:"negative_prompt"`
Seed int64 `json:"seed,omitempty"`
Steps int `json:"steps"`
CfgScale float32 `json:"cfg_scale"`
Width int `json:"width"`
Height int `json:"height"`
SamplerName string `json:"sampler_name"`
Scheduler string `json:"scheduler"`
EnableHr bool `json:"enable_hr,omitempty"`
HrScale int `json:"hr_scale,omitempty"`
HrUpscaler string `json:"hr_upscaler,omitempty"`
HrSecondPassSteps int `json:"hr_second_pass_steps,omitempty"`
DenoisingStrength float32 `json:"denoising_strength,omitempty"`
ForceTaskId string `json:"force_task_id,omitempty"`
}
// Txt2ImgResp 文生图响应实体
type Txt2ImgResp struct {
Images []string `json:"images"`
Parameters struct {
} `json:"parameters"`
Info string `json:"info"`
}
// TaskProgressResp 任务进度响应实体
type TaskProgressResp struct {
Progress float64 `json:"progress"`
EtaRelative float64 `json:"eta_relative"`
}
// Txt2Img 文生图 API
func (s *Service) Txt2Img(task types.SdTask) error {
body := Txt2ImgReq{
Prompt: task.Params.Prompt,
NegativePrompt: task.Params.NegPrompt,
Steps: task.Params.Steps,
CfgScale: task.Params.CfgScale,
Width: task.Params.Width,
Height: task.Params.Height,
SamplerName: task.Params.Sampler,
Scheduler: task.Params.Scheduler,
ForceTaskId: task.Params.TaskId,
}
if task.Params.Seed > 0 {
body.Seed = task.Params.Seed
}
if task.Params.HdFix {
body.EnableHr = true
body.HrScale = task.Params.HdScale
body.HrUpscaler = task.Params.HdScaleAlg
body.HrSecondPassSteps = task.Params.HdSteps
body.DenoisingStrength = task.Params.HdRedrawRate
}
var res Txt2ImgResp
var errChan = make(chan error)
var apiKey model.ApiKey
err := s.db.Where("type", "sd").Where("enabled", true).Order("last_used_at ASC").First(&apiKey).Error
if err != nil {
return fmt.Errorf("no available Stable-Diffusion api key: %v", err)
}
apiURL := fmt.Sprintf("%s/sdapi/v1/txt2img", apiKey.ApiURL)
logger.Infof("send image request to %s", apiURL)
// send a request to sd api endpoint
go func() {
response, err := s.httpClient.R().
SetHeader("Authorization", apiKey.Value).
SetBody(body).
SetSuccessResult(&res).
Post(apiURL)
if err != nil {
errChan <- err
return
}
if response.IsErrorState() {
errChan <- fmt.Errorf("error http code status: %v", response.Status)
return
}
// update the last used time
apiKey.LastUsedAt = time.Now().Unix()
s.db.Updates(&apiKey)
// 保存 Base64 图片
imgURL, err := s.uploadManager.GetUploadHandler().PutBase64(res.Images[0])
if err != nil {
errChan <- fmt.Errorf("error with upload image: %v", err)
return
}
// 获取绘画真实的 seed
var info map[string]interface{}
err = utils.JsonDecode(res.Info, &info)
if err != nil {
errChan <- fmt.Errorf("error with decode task response: %v", err)
return
}
task.Params.Seed = int64(utils.IntValue(utils.InterfaceToString(info["seed"]), -1))
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumns(model.SdJob{ImgURL: imgURL, Params: utils.JsonEncode(task.Params), Prompt: task.Params.Prompt})
errChan <- nil
}()
// waiting for task finish
for {
select {
case err := <-errChan:
if err != nil {
return err
}
// task finished
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
return nil
default:
resp, err := s.checkTaskProgress(apiKey)
// 更新任务进度
if err == nil && resp.Progress > 0 {
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
}
time.Sleep(time.Second)
}
}
}
// 执行任务
func (s *Service) checkTaskProgress(apiKey model.ApiKey) (*TaskProgressResp, error) {
apiURL := fmt.Sprintf("%s/sdapi/v1/progress?skip_current_image=false", apiKey.ApiURL)
var res TaskProgressResp
response, err := s.httpClient.R().
SetHeader("Authorization", apiKey.Value).
SetSuccessResult(&res).
Get(apiURL)
if err != nil {
return nil, err
}
if response.IsErrorState() {
return nil, fmt.Errorf("error http code status: %v", response.Status)
}
return &res, nil
}
func (s *Service) PushTask(task types.SdTask) {
logger.Debugf("add a new MidJourney task to the task list: %+v", task)
if err := s.taskQueue.RPush(task); err != nil {
logger.Errorf("push sd task to queue failed: %v", err)
}
}
// CheckTaskStatus 检查任务状态,自动删除过期或者失败的任务
func (s *Service) CheckTaskStatus() {
go func() {
logger.Info("Running Stable-Diffusion task status checking ...")
for {
var jobs []model.SdJob
res := s.db.Where("progress < ?", 100).Find(&jobs)
if res.Error != nil {
time.Sleep(5 * time.Second)
continue
}
for _, job := range jobs {
// 5 分钟还没完成的任务标记为失败
if time.Since(job.CreatedAt) > time.Minute*5 {
job.Progress = service.FailTaskProgress
job.ErrMsg = "任务超时"
s.db.Updates(&job)
}
}
// 找出失败的任务,并恢复其扣减算力
s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
for _, job := range jobs {
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund,
Model: "stable-diffusion",
Remark: fmt.Sprintf("任务失败,退回算力。任务ID%d Err: %s", job.Id, job.ErrMsg),
})
if err != nil {
continue
}
// 更新任务状态
s.db.Model(&job).UpdateColumn("power", 0)
}
time.Sleep(time.Second * 5)
}
}()
}
+4 -10
View File
@@ -8,14 +8,14 @@ package sms
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ // * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import ( import (
"context"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
"geekai/utils" "geekai/utils"
"io"
"net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time"
) )
type BaoSmsService struct { type BaoSmsService struct {
@@ -58,15 +58,9 @@ func (s *BaoSmsService) SendVerifyCode(mobile string, code int) error {
params.Set("c", content) params.Set("c", content)
apiURL := fmt.Sprintf("https://%s/sms?%s", s.domain, params.Encode()) apiURL := fmt.Sprintf("https://%s/sms?%s", s.domain, params.Encode())
response, err := http.Get(apiURL) body, status, err := utils.FetchURLBytes(context.Background(), apiURL, "", 30*time.Second, 2, 2<<20)
if err != nil { if err != nil {
return err return fmt.Errorf("smsbao request failed: status=%d: %w", status, err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
return err
} }
result := string(body) result := string(body)
logger.Debugf("send SmsBao result: %v", errMsg[result]) logger.Debugf("send SmsBao result: %v", errMsg[result])
+1
View File
@@ -9,6 +9,7 @@ package sms
const Ali = "aliyun" const Ali = "aliyun"
const Bao = "bao" const Bao = "bao"
const Tencent = "tencent"
type Service interface { type Service interface {
SendVerifyCode(mobile string, code int) error SendVerifyCode(mobile string, code int) error
+15 -9
View File
@@ -9,23 +9,25 @@ package sms
import ( import (
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
) )
type SmsManager struct { type SmsManager struct {
aliyun *AliYunSmsService aliyun *AliYunSmsService
bao *BaoSmsService bao *BaoSmsService
active string tencent *TencentSmsService
active string
} }
var logger = logger2.GetLogger() var logger = log.GetLogger()
func NewSmsManager(sysConfig *types.SystemConfig, aliyun *AliYunSmsService, bao *BaoSmsService) (*SmsManager, error) { func NewSmsManager(sysConfig *types.SystemConfig, aliyun *AliYunSmsService, bao *BaoSmsService, tencent *TencentSmsService) (*SmsManager, error) {
return &SmsManager{ return &SmsManager{
active: sysConfig.SMS.Active, active: sysConfig.SMS.Active,
aliyun: aliyun, aliyun: aliyun,
bao: bao, bao: bao,
tencent: tencent,
}, nil }, nil
} }
@@ -35,6 +37,8 @@ func (m *SmsManager) GetService() Service {
return m.aliyun return m.aliyun
case Bao: case Bao:
return m.bao return m.bao
case Tencent:
return m.tencent
} }
return nil return nil
} }
@@ -49,6 +53,8 @@ func (m *SmsManager) UpdateConfig(config types.SMSConfig) {
m.aliyun.UpdateConfig(config.Ali) m.aliyun.UpdateConfig(config.Ali)
case Bao: case Bao:
m.bao.UpdateConfig(config.Bao) m.bao.UpdateConfig(config.Bao)
case Tencent:
m.tencent.UpdateConfig(config.Tencent)
} }
m.active = config.Active m.active = config.Active
} }
+127
View File
@@ -0,0 +1,127 @@
package sms
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * 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/types"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
)
type TencentSmsService struct {
config types.SmsConfigTencent
client *sms.Client
region string
}
func NewTencentSmsService(sysConfig *types.SystemConfig) (*TencentSmsService, error) {
config := sysConfig.SMS.Tencent
region := config.Region
if region == "" {
region = "ap-guangzhou" // 默认使用广州地区
}
s := TencentSmsService{
config: config,
region: region,
}
if sysConfig.SMS.Active == Tencent {
err := s.UpdateConfig(config)
if err != nil {
logger.Errorf("腾讯云短信初始化失败: %v", err)
}
}
return &s, nil
}
func (s *TencentSmsService) UpdateConfig(config types.SmsConfigTencent) error {
if config.SecretId == "" || config.SecretKey == "" {
// 配置不完整时不初始化客户端
s.config = config
if config.Region != "" {
s.region = config.Region
} else {
s.region = "ap-guangzhou"
}
return nil
}
region := config.Region
if region == "" {
region = "ap-guangzhou"
}
// 创建凭证
credential := common.NewCredential(
config.SecretId,
config.SecretKey,
)
// 创建客户端配置
cpf := profile.NewClientProfile()
cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com"
// 创建客户端
client, err := sms.NewClient(credential, region, cpf)
if err != nil {
return fmt.Errorf("failed to create client: %v", err)
}
s.client = client
s.config = config
s.region = region
return nil
}
// SendVerifyCode 发送验证码短信
// 注意:腾讯云后台配置的短信模板内容应与配置中的 code_template 一致
// 模板只需要1个参数:{1} 表示验证码,例如:{1}为您的验证码,请于5分钟内填写,如非本人操作,请忽略本短信。
func (s *TencentSmsService) SendVerifyCode(mobile string, code int) error {
if s.client == nil {
return fmt.Errorf("腾讯云短信服务未初始化")
}
// 创建发送短信请求
request := sms.NewSendSmsRequest()
request.SmsSdkAppId = common.StringPtr(s.config.SmsSdkAppId)
request.SignName = common.StringPtr(s.config.Sign)
request.TemplateId = common.StringPtr(s.config.CodeTempId)
request.PhoneNumberSet = common.StringPtrs([]string{mobile})
request.TemplateParamSet = common.StringPtrs([]string{fmt.Sprintf("%d", code), "5"})
// 发送短信
response, err := s.client.SendSms(request)
if err != nil {
return fmt.Errorf("failed to send SMS: %v", err)
}
// 检查响应
if response.Response == nil {
return fmt.Errorf("failed to send SMS: response is nil")
}
if len(response.Response.SendStatusSet) == 0 {
return fmt.Errorf("failed to send SMS: no send status")
}
sendStatus := response.Response.SendStatusSet[0]
if sendStatus.Code == nil || *sendStatus.Code != "Ok" {
message := "unknown error"
if sendStatus.Message != nil {
message = *sendStatus.Message
}
return fmt.Errorf("failed to send SMS: %s", message)
}
return nil
}
var _ Service = &TencentSmsService{}
+1 -1
View File
@@ -120,7 +120,7 @@ func (s *SmtpService) sendTLS(auth smtp.Auth, to string, subject string, body st
} }
_, _ = fmt.Fprintln(wc) _, _ = fmt.Fprintln(wc)
// 将邮件内容写入 // 将邮件内容写入
_, err = fmt.Fprintf(wc, body) _, err = fmt.Fprint(wc, body)
if err != nil { if err != nil {
return fmt.Errorf("error sending email: %v", err) return fmt.Errorf("error sending email: %v", err)
} }
+85
View File
@@ -0,0 +1,85 @@
package sora
import (
"context"
"encoding/json"
"errors"
"geekai/service/oss"
"geekai/store/vo"
"geekai/utils"
"path/filepath"
"regexp"
"time"
"geekai/log"
)
var logger = log.GetLogger()
type SoraService struct {
uploadManager *oss.UploaderManager
}
func NewSoraService(uploadManager *oss.UploaderManager) *SoraService {
return &SoraService{
uploadManager: uploadManager,
}
}
// 下载视频地址
func (s *SoraService) DownloadVideoURL(text string) (*vo.File, error) {
videoDataURL, err := s.ExtractVideoURL(text)
if err != nil {
return nil, err
}
// 用统一的超时/重试策略,避免“偶发 HTTPS 握手超时”直接导致失败
body, _, err := utils.FetchURLBytes(context.Background(), videoDataURL, "", 30*time.Second, 2, 2<<20)
if err != nil {
logger.Errorf("failed to get video data: %v", err)
return nil, err
}
// 解析视频下载地址
var videoData map[string]any
err = json.Unmarshal(body, &videoData)
if err != nil {
logger.Errorf("failed to unmarshal video data: %v", err)
return nil, err
}
if v, ok := videoData["url"].(string); ok && v != "" {
logger.Infof("try to download video: %s", v)
videoURL, err := s.uploadManager.GetUploadHandler().PutUrlFile(v, ".mp4", true)
if err != nil { // 如果上传失败,则返回原始错误
return nil, err
}
// 获取文件大小
size, _ := utils.GetFileSize(videoURL)
name := filepath.Base(videoURL)
return &vo.File{
Name: name,
ObjKey: name,
URL: videoURL,
Ext: ".mp4",
Size: size,
CreatedAt: time.Now().Unix(),
}, nil
}
return nil, errors.New("no video URL found: " + string(body))
}
// 从文本中提取视频URL
func (s *SoraService) ExtractVideoURL(text string) (string, error) {
// 提取原始 JSON 数据地址
//[原始数据](https://asyncdata.net/source/task_01k8pye324ef7t6heq6jyaxbfe) 类似这样的链接
re := regexp.MustCompile(`\[原始数据\]\((https?://.*?)\)`)
matches := re.FindAllStringSubmatch(text, 1)
if len(matches) == 0 {
return "", errors.New("no video URL found")
}
return matches[0][1], nil
}
+36 -16
View File
@@ -12,11 +12,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"geekai/core/types" "geekai/core/types"
logger2 "geekai/logger" "geekai/log"
"geekai/service" "geekai/service"
"geekai/service/oss" "geekai/service/oss"
"geekai/store" "geekai/store"
"geekai/store/model" "geekai/store/model"
"geekai/store/vo"
"geekai/utils" "geekai/utils"
"io" "io"
"time" "time"
@@ -27,7 +28,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var logger = logger2.GetLogger() var logger = log.GetLogger()
type Service struct { type Service struct {
httpClient *req.Client httpClient *req.Client
@@ -61,13 +62,24 @@ func (s *Service) Run() {
var jobs []model.SunoJob var jobs []model.SunoJob
s.db.Where("task_id", "").Where("progress", 0).Find(&jobs) s.db.Where("task_id", "").Where("progress", 0).Find(&jobs)
for _, v := range jobs { for _, v := range jobs {
var task types.SunoTask // 从 Params 中提取字段构建 task
err := utils.JsonDecode(v.TaskInfo, &task) task := types.SunoTask{
if err != nil { Id: v.Id,
logger.Errorf("decode task info with error: %v", err) UserId: int(v.UserId),
continue Channel: v.Channel,
Type: v.Type,
Title: v.Title,
RefTaskId: v.RefTaskId,
RefSongId: v.RefSongId,
Prompt: v.Params.Prompt,
Lyrics: v.Params.Lyrics,
Tags: v.Params.Tags,
Model: v.Params.Model,
Instrumental: v.Params.Instrumental,
ExtendSecs: v.Params.ExtendSecs,
SongId: v.SongId,
AudioURL: v.AudioURL,
} }
task.Id = v.Id
s.PushTask(task) s.PushTask(task)
} }
logger.Info("Starting Suno job consumer...") logger.Info("Starting Suno job consumer...")
@@ -335,15 +347,22 @@ func (s *Service) SyncTaskProgress() {
job.SongId = v.Id job.SongId = v.Id
job.Duration = int(v.Metadata.Duration) job.Duration = int(v.Metadata.Duration)
job.Prompt = v.Metadata.Prompt job.Prompt = v.Metadata.Prompt
// 设置 Params
tags := v.Metadata.Tags
// 修复 tags 字段过长导致插入数据库失败 // 修复 tags 字段过长导致插入数据库失败
if len(v.Metadata.Tags) > 255 { if len(tags) > 255 {
job.Tags = v.Metadata.Tags[:255] tags = tags[:255]
} else { }
job.Tags = v.Metadata.Tags job.Params = vo.SunoParam{
Prompt: v.Metadata.Prompt,
Tags: tags,
Model: v.ModelName,
Instrumental: job.Params.Instrumental, // 保持原任务参数
ExtendSecs: job.Params.ExtendSecs, // 保持原任务参数
} }
job.ModelName = v.ModelName job.Output = utils.JsonEncode(v)
job.RawData = utils.JsonEncode(v)
job.CoverURL = v.ImageLargeUrl job.CoverURL = v.ImageLargeUrl
job.AudioURL = v.AudioUrl job.AudioURL = v.AudioUrl
@@ -372,11 +391,12 @@ func (s *Service) SyncTaskProgress() {
} }
// 找出失败的任务,并恢复其扣减算力 // 找出失败的任务,并恢复其扣减算力
s.db.Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs) s.db.Select("id", "user_id", "power", "task_id", "err_msg", "params").
Where("progress", service.FailTaskProgress).Where("power > ?", 0).Find(&jobs)
for _, job := range jobs { for _, job := range jobs {
err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{ err := s.userService.IncreasePower(job.UserId, job.Power, model.PowerLog{
Type: types.PowerRefund, Type: types.PowerRefund,
Model: job.ModelName, Model: job.Params.Model,
Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID%sErr:%s", job.TaskId, job.ErrMsg), Remark: fmt.Sprintf("Suno 任务失败,退回算力。任务ID%sErr:%s", job.TaskId, job.ErrMsg),
}) })
if err != nil { if err != nil {
+2 -2
View File
@@ -1,6 +1,6 @@
package service package service
import logger2 "geekai/logger" import "geekai/log"
const FailTaskProgress = 101 const FailTaskProgress = 101
const ( const (
@@ -17,7 +17,7 @@ type NotifyMessage struct {
Type string `json:"type"` Type string `json:"type"`
} }
var logger = logger2.GetLogger() var logger = log.GetLogger()
const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]" const TranslatePromptTemplate = "Translate the following painting prompt words into English keyword phrases. Without any explanation, directly output the keyword phrases separated by commas. The content to be translated is: [%s]"

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