Compare commits

..
503 Commits
Author SHA1 Message Date
whyour fa6d57d809 修复shell发送通知 2022-04-09 11:13:08 +08:00
whyour 1060fc7476 修复并发日志目录 2022-04-06 12:18:16 +08:00
whyour 56bc38e9e7 更新readme 2022-04-05 23:24:27 +08:00
whyour 18be65d6fe 更新发布脚本 2022-04-05 23:17:03 +08:00
whyour 3409085195 更新发布脚本 2022-04-05 23:14:47 +08:00
whyour 17e17ac077 增加发布脚本 2022-04-05 23:11:48 +08:00
whyour c655e1da38 修复日志目录判断 2022-04-05 00:28:14 +08:00
whyour 0ea158724f 修复日志目录获取 2022-04-05 00:21:32 +08:00
whyour fb521498ff 修复task日志打印 2022-04-04 23:32:27 +08:00
whyour 399550ccad 修复日志创建目录 2022-04-04 22:40:02 +08:00
whyour c7c30c86f2 修复目录获取 2022-04-04 18:06:21 +08:00
whyour 5f6c0882d6 修改版本说明 2022-04-04 17:28:15 +08:00
whyour 6e05a4f3a5 更新版本号 v2.12.1 2022-04-04 17:24:42 +08:00
whyour ea8aa6a231 修改获取版本链接 2022-04-04 17:17:34 +08:00
whyour ae2570c677 修复shell日志路径截取 2022-04-03 22:31:11 +08:00
whyour c76d952d41 修改yarn命令 2022-04-03 21:49:20 +08:00
whyour cdf9fb24be 修复定时任务获取日志 2022-04-03 20:00:31 +08:00
whyour 2131e16781 删除console 2022-04-03 19:50:44 +08:00
whyour b0c8425639 修复日志路径拼接规则 2022-04-03 19:32:44 +08:00
whyour f0869567de 七牛云改为http访问 2022-04-01 08:29:16 +08:00
whyour be2a19949b 修复获取日志相对目录 2022-03-30 11:34:36 +08:00
whyour 8627cccb58 修改日志路径合并方式 2022-03-30 11:17:11 +08:00
whyour 4a8eaebf17 修复获取任务日志路径 2022-03-30 10:57:43 +08:00
kan3GitandGitHub e7edd0a5a4 tg发送消息改json (#1269)
发现有带&的消息都会漏掉&后面的内容,改为JSON发送
2022-03-26 17:23:54 +08:00
chen310andGitHub 9ccbea0a04 修复PushDeer推送Python版本错误 (#1270)
* 修复PushDeer推送Python版本错误
2022-03-26 17:20:02 +08:00
whyour ab715ef840 修复系统pushDeer通知 2022-03-26 17:17:13 +08:00
whyour 117822fe7c 修改任务最大内存占用200兆 2022-03-26 16:53:03 +08:00
whyour da922678a1 修复定时任务获取日志目录 2022-03-26 00:29:42 +08:00
whyour 133e9e0e1f 更新版本 v2.12.0 2022-03-24 08:47:35 +08:00
whyour 7cc6081d15 修复bot依赖安装目录 2022-03-19 16:44:00 +08:00
whyour 5a24f62a62 更新bot.sh 2022-03-17 23:45:37 +08:00
whyour be597355a7 修复任务详情展示脚本逻辑 2022-03-13 13:21:00 +08:00
whyour 51afcb6c5b 修复任务详情交互样式 2022-03-13 11:09:31 +08:00
whyour 92ebb70eaa 修复定时任务名称标签显示 2022-03-13 00:06:24 +08:00
whyour ab19f30342 修复系统更新时间弹窗 2022-03-13 00:03:58 +08:00
whyour fc9bc7bea2 修复任务列表名称标签提示 2022-03-12 23:57:38 +08:00
whyour 793da29f22 修复定时任务详情日志查看 2022-03-12 23:49:09 +08:00
whyour 5afe4da607 修复退出登录样式 2022-03-12 18:15:42 +08:00
whyour a7a56c03ce 修复退出登录和定时任务搜索 2022-03-12 16:14:18 +08:00
whyour 7639b588d8 修改退出登录样式 2022-03-10 23:58:57 +08:00
whyour 1d536cdd53 修改退出登录样式 2022-03-10 23:53:39 +08:00
whyour 9f08245b6d 修复编辑应用初始值 2022-03-10 22:37:44 +08:00
whyour ff9bf20e57 添加podman安装 2022-03-10 00:29:28 +08:00
whyour b201730100 修复任务详情样式 2022-03-09 23:40:59 +08:00
whyour 6ca098f9e2 修复任务详情手机样式 2022-03-09 22:32:50 +08:00
whyour e0eef05f5c 修复dockerfile 2022-03-09 00:24:36 +08:00
whyour 6398e5ec70 修复dockerfile 2022-03-09 00:17:33 +08:00
whyour 6f93219c44 任务详情脚本增加编辑功能 2022-03-08 00:11:37 +08:00
whyour 9b957ac019 修复任务详情样式 2022-03-07 23:56:15 +08:00
whyour e0d1b127aa 修改更新青龙日志 2022-03-07 23:47:20 +08:00
whyour 64072b3435 修改重置分支shell 2022-03-07 23:40:49 +08:00
whyour f81f7720f0 更新git pull参数 2022-03-07 23:27:16 +08:00
whyour 69fab610e6 修复编译静态资源目录 2022-03-07 23:21:18 +08:00
whyour d693e1f9ea 修改shell根目录获取,移除notify.sh 2022-03-07 23:16:36 +08:00
whyour 2a9ca67355 修复更新青龙命令 2022-03-07 22:33:15 +08:00
whyour 3fb01860d4 修复重置repo地址命令 2022-03-07 22:31:46 +08:00
whyour d92a46d09c 修复更新青龙命令 2022-03-07 22:27:04 +08:00
whyour ec605bfba4 修复shell更新命令 2022-03-06 11:27:48 +08:00
whyour d2f999a5e2 修改定时任务初始化查询条件 2022-03-06 00:07:59 +08:00
whyour 627f541f17 更新版本拷贝2.11.3以前版本数据到data目录 2022-03-06 00:07:08 +08:00
whyour c457dca555 自动更新定时任务2.11.3以前脚本路径 2022-03-06 00:01:03 +08:00
whyour ff3ec7395e 修改任务详情样式 2022-03-05 20:52:21 +08:00
whyour fdaba146ca 移除s390x支持,修改sqlite3包 2022-03-05 14:39:06 +08:00
whyour 31a6d30c23 修改sqlite3包 2022-03-05 13:42:45 +08:00
whyour fa3b185fd5 修改sqlite3包 2022-03-05 12:57:48 +08:00
whyour 101d4fb497 修改 sqlite3 依赖 2022-03-05 12:18:30 +08:00
whyour ccd308f5b0 修改update对应分支 2022-03-05 11:46:21 +08:00
whyour 999d9f8e01 任务详情添加日志列表,更新依赖 2022-03-05 01:24:52 +08:00
whyour 313db0d74e logo使用七牛云 2022-03-04 00:15:42 +08:00
whyour 634e9705a7 增加任务详情 2022-03-04 00:08:33 +08:00
NekoMioandGitHub c959cec738 增加PushDeer推送 #1161 (#1194)
* 增加PushDeer推送
2022-03-03 18:42:11 +08:00
whyour 95330e04a3 修复task修改环境变量时机 2022-03-02 21:37:45 +08:00
whyour a099d09af7 修改openapi获取token逻辑 2022-02-28 22:30:58 +08:00
whyour a0d0197ad7 修改error页面样式 2022-02-26 14:24:23 +08:00
whyour d49e711a34 修复error页面样式 2022-02-26 14:16:51 +08:00
whyour e3b8cd75b9 修改错误日志页面 2022-02-26 14:14:10 +08:00
whyour 7a240eaabb 修改默认跳转 2022-02-26 00:13:52 +08:00
whyour 01564e96d3 添加系统错误页面 2022-02-26 00:12:12 +08:00
whyour 03f4b5c219 修改public服务 2022-02-25 23:06:35 +08:00
whyour 99e1d44f7d 修复public服务 2022-02-25 22:53:24 +08:00
whyour 8c55659c18 增加2.10-2.11升级代码 2022-02-25 22:49:17 +08:00
whyour 3ea980b2f8 增加public服务,查询panel日志 2022-02-24 23:50:04 +08:00
whyour f5fc987b51 修改pane样式 2022-02-24 23:24:21 +08:00
whyour 9cfa4af2b2 更新git pull参数 2022-02-24 22:36:49 +08:00
whyour 3efdf1fa87 修复git reset 2022-02-24 22:22:33 +08:00
whyour 25fc832605 增加使用系统通知配置发送通知api 2022-02-24 22:10:03 +08:00
whyour a7e404c5d5 修改编辑器cdn 2022-02-20 21:36:01 +08:00
whyour 90b27f0564 修复sql查询 2022-02-20 10:15:17 +08:00
whyour 632440965d 修改杀死进程逻辑 2022-02-19 22:54:27 +08:00
whyour aedfac446b 版本文件改为七牛云存储 2022-02-19 22:48:06 +08:00
whyour 3372b0025a 修复定时删除日志 2022-02-19 17:36:49 +08:00
whyour 0112b92e77 调整数据目录 2022-02-19 13:08:14 +08:00
whyour 0caf53b147 修复删除环境变量无效 2022-02-17 11:14:43 +08:00
whyour 4d1354d5f4 更新版本 v2.11.3 2022-02-16 23:09:33 +08:00
whyour fc4562eca2 修改check命令,安装依赖使用代理
修改apk源地址
2022-02-16 23:06:12 +08:00
whyour 5b048a5510 修改logo地址 2022-02-16 22:56:54 +08:00
whyour d23049b2fe 移除esbuild 2022-02-11 13:52:12 +08:00
whyour caac5607b9 环境变量增加更新时间排序 2022-02-11 13:41:48 +08:00
Ghost LeeandGitHub fb2e69b12d 修复goCqHttpBotToken中存在&字符导致认证失败的问题 (#993)
修改goCqHttpBot鉴权方式,调整为使用Header模式
2022-02-08 18:45:24 +08:00
UkennandGitHub fc1838ff39 bark支持自定义推送图标 (#1146)
* bark添加自定义推送图标

* 脚本bark通知添加自定义推送图标
2022-02-08 18:21:37 +08:00
whyour 25689cf40f 修复定时任务排序 2022-02-01 22:33:18 +08:00
whyour c746a3d215 修复.env 2022-01-30 20:22:00 +08:00
whyour 548d924fa0 更新版本 v2.11.2 2022-01-30 18:25:18 +08:00
whyour 18a6be3ecd 修改placeholder 2022-01-30 17:39:27 +08:00
whyour afef0cb72f 更新dockerfile 2022-01-30 14:59:41 +08:00
whyour 1221b47eca 修改dockerfile 2022-01-30 14:03:57 +08:00
whyour a356eeb5da 修改base context 2022-01-30 12:37:20 +08:00
whyour 25bb0972b9 修改dockerfile 2022-01-30 12:31:49 +08:00
whyour 0914075cf6 修改定时任务唯一键 2022-01-30 10:11:47 +08:00
whyour 74b258e1b6 修改定时任务排序 2022-01-30 09:59:33 +08:00
whyour 27eb0a21e4 修改dockerfile 2022-01-29 14:23:19 +08:00
whyour d77c6d50d8 修改dockerfile 2022-01-29 12:53:21 +08:00
whyour 6a64c18a7b 修改dockerfile 2022-01-29 12:15:25 +08:00
whyour b97ff714ae 修复dockerfile 2022-01-29 11:45:06 +08:00
whyour 1f23eea805 更新dockerfile 2022-01-29 11:20:43 +08:00
whyour cb0cb7df21 修改dockerfile 2022-01-29 00:09:37 +08:00
whyour f3e8628664 移除prepare scripts 2022-01-28 23:46:17 +08:00
whyour 4758471f9f 修改sentry依赖 2022-01-28 23:19:39 +08:00
whyour c10bd5a549 修改sentry cli版本 2022-01-28 23:14:12 +08:00
whyour d53faf96eb 修改sentry依赖 2022-01-28 22:47:46 +08:00
whyour 2307b8ed1a 增加esbuild 2022-01-28 21:53:05 +08:00
whyour 711af8d4a5 修改dockfile 2022-01-28 20:54:00 +08:00
whyour 458c9c456b 修改dockerfile 2022-01-28 20:27:38 +08:00
whyour 29069df48c 修复dockerfile 2022-01-28 19:33:01 +08:00
whyour 300e84ae2c 修改dockerfile 2022-01-28 19:23:57 +08:00
whyour bf11768b92 修改base镜像 2022-01-27 23:09:29 +08:00
whyour 91bd1d9263 测试青龙基础镜像 2022-01-27 23:06:35 +08:00
whyour ac40f9ff12 测试最新alpine 2022-01-27 22:21:34 +08:00
whyour f930837220 修复获取克隆仓库状态 2022-01-27 22:15:45 +08:00
whyour 1d5e3acc93 去除sql执行日志 2022-01-27 00:56:37 +08:00
whyour 2aec1f9846 修改表唯一约束 2022-01-27 00:49:44 +08:00
whyour b56a681b98 python默认实时输出 2022-01-25 23:03:56 +08:00
whyour 2713419b32 修复修改任务状态本地免认证 2022-01-25 22:52:27 +08:00
whyour 764a3492cc 添加关于页 2022-01-24 23:17:12 +08:00
whyour 29fc471ac4 修复日志加载状态 2022-01-24 22:50:01 +08:00
whyour 54a597aad3 修改logo地址 2022-01-23 16:55:11 +08:00
whyour 778961d0b6 修改node基础镜像版本 2022-01-22 22:09:16 +08:00
whyour 14b01ac761 测试基础镜像node版本 2022-01-22 21:57:02 +08:00
whyour f87848c05b 修改darkreader版本 2022-01-22 21:09:01 +08:00
whyour 153e5a806d 增加sqlite连接池 2022-01-22 20:53:22 +08:00
whyour ee1b8a906f 移除cdn依赖 2022-01-22 20:25:22 +08:00
whyour 734685d45a 定时任务默认改为创建时间倒序 2022-01-21 22:38:13 +08:00
whyour e3b540c212 修复更新系统通知 2022-01-21 22:27:50 +08:00
whyour 201f578bbc 修复sqlite加载顺序 2022-01-20 22:19:39 +08:00
whyour ce9aa369d2 更新版本 v2.11.1 2022-01-20 19:11:41 +08:00
whyour c5c41c0859 修改cdn地址,增加重启服务延迟 2022-01-20 19:02:29 +08:00
whyour d3009f82dd 移除默认github代理,增加http或者socks代理参数 2022-01-19 23:29:55 +08:00
whyour 0eb9a1fdb0 定时任务默认倒序 2022-01-18 23:50:18 +08:00
whyour 83f6d9926a 修改迁移数据库逻辑 2022-01-18 22:56:12 +08:00
whyour 14134842b0 修复环境变量更新时间显示 2022-01-17 22:47:23 +08:00
whyour 4c094c4710 修复重置应用秘钥 2022-01-16 18:55:18 +08:00
whyour 3ac41075fd 修复停止任务后任务状态修改 2022-01-14 23:44:22 +08:00
whyour fdc97773d1 修改数据迁移逻辑 2022-01-14 23:17:16 +08:00
whyour 917a739595 修复环境变量搜索 2022-01-13 22:38:20 +08:00
ChiupamandGitHub 21475f81ae 支持运行pyc格式文件 (#1072) 2022-01-13 14:39:49 +08:00
whyour a2a9a0560c 修复更新定时任务 2022-01-13 14:37:56 +08:00
whyour b879846cdb 修复环境变量搜索 2022-01-13 13:15:00 +08:00
whyour a24011cd72 修复openapi token 查询 2022-01-13 12:02:36 +08:00
whyour 410dd2ecdc 修复openapi认证,环境变量排序 2022-01-13 11:13:04 +08:00
whyour 96b111b894 更新版本 v2.11.0 2022-01-12 23:43:32 +08:00
whyour 630572c8f9 更新task命令获取时间戳 2022-01-12 23:39:40 +08:00
whyour 6a381a6366 更新dockerfile 2022-01-12 00:55:14 +08:00
whyour fd04f12677 修复dockerfile 2022-01-11 00:44:22 +08:00
whyour 86b8cd17a4 修复dockerfile 2022-01-11 00:35:30 +08:00
whyour bb8ee01693 修复dockerfile 2022-01-10 23:35:47 +08:00
whyour 5fb0bca3af 修复dockerfile 2022-01-10 23:17:33 +08:00
whyour 1e4d1f373d 测试编译sqlite 2022-01-10 23:10:59 +08:00
whyour 87c78d0fcc 测试nodejs14 sqlite 2022-01-09 23:17:50 +08:00
whyour 041d2b7947 测试打包镜像时间 2022-01-09 12:38:20 +08:00
whyour 5e7b90d995 更新dockerfile 2022-01-08 23:56:45 +08:00
whyour fadc3dcb55 测试arm/v7 2022-01-08 23:29:20 +08:00
whyour 4eea091944 修改dockerfile 2022-01-08 22:49:17 +08:00
whyour 9f8d46f395 更新node gyp 2022-01-08 22:19:21 +08:00
whyour 2c54a057c9 修改node gyp版本 2022-01-08 21:48:32 +08:00
whyour e1e5cacc6d 添加node-pre-gyp依赖 2022-01-08 21:12:38 +08:00
whyour edf9066967 修复方法名称 2022-01-08 01:26:34 +08:00
whyour 7097d12b87 更新dockerfile 2022-01-08 01:10:10 +08:00
whyour 08aaeb6372 修复sqlite数据操作类型 2022-01-08 01:09:38 +08:00
kilo5hzandGitHub 4310f80ec0 添加标签功能 (#1026)
* 添加标签功能
2022-01-07 22:01:13 +08:00
whyour 01577f6494 dockerfile使用yarn install 2022-01-07 00:18:12 +08:00
whyour 9aa570b2c6 修改sqlite版本 2022-01-06 23:25:48 +08:00
whyour d611d7d698 修复param类型 2022-01-06 22:59:29 +08:00
whyour f05549b06c 使用sqlite替换nedb 2022-01-06 22:51:12 +08:00
ChiupamandGitHub 8c4db8dd40 支持自定义BOT仓库 (#1051) 2022-01-04 13:26:16 +08:00
whyour ddee69d291 更新版本 v2.10.13 2021-12-25 21:31:52 +08:00
whyour 28960c9fab 添加sentry-cli cdn 2021-12-25 20:55:13 +08:00
whyour 449301e519 修改热更新目录 2021-12-25 20:13:49 +08:00
whyour 28dab3dc2f 修改两步验证界面逻辑 2021-12-25 20:09:02 +08:00
whyour 7995d98db5 修改批量运行任务逻辑 2021-12-24 23:31:11 +08:00
whyour 1f319f3533 调试脚本增加停止,修复调试交互 2021-12-23 23:25:39 +08:00
whyour 7d2dbaa62d 修改文本 2021-12-23 13:39:16 +08:00
whyour ed5999526b 优化初始化通知设置交互 2021-12-21 23:49:52 +08:00
whyour 2409f75491 修复登录退出接口路径 2021-12-21 23:42:05 +08:00
whyour 0c840095d1 增加系统openapi 2021-12-21 23:22:34 +08:00
whyour 3eaf0b4856 system接口增加版本号,应用权限增加依赖管理 2021-12-21 22:53:19 +08:00
琚涛涛andGitHub 394735a5d9 优化定时任务名称展示效果,提升用户体验 (#1017) 2021-12-21 22:39:17 +08:00
whyour 5bfa10bfe7 修复任务跳转脚本参数,task运行脚本支持更多参数 2021-12-21 00:23:20 +08:00
whyour 3f12d9cbd5 更新版本号 v2.10.12 2021-12-09 23:24:10 +08:00
whyour 54d89754df 修复darkreader最新版bug 2021-12-09 23:22:17 +08:00
whyour 9470524615 更新readme 2021-12-06 23:42:14 +08:00
whyour b5faa6eaaa 修复logo样式 2021-12-06 11:18:19 +08:00
whyour eab476cfe8 更新版本号 v2.10.11 2021-12-05 23:15:49 +08:00
whyour b773def012 修复搜索环境变量urlencode 2021-12-05 23:04:26 +08:00
whyour a9f8b23a7a 修复内容区高度和logo样式 2021-11-29 23:02:42 +08:00
whyour 61bb6535fb 修复最后运行时长被覆盖 2021-11-29 21:49:32 +08:00
雪狐andGitHub eb0bdffe06 避免环境变量中出现"字符的时候提前截断字符串 (#966) 2021-11-29 21:43:29 +08:00
whyour e129485285 更新版本 v2.10.10 2021-11-27 20:43:18 +08:00
colinxuandGitHub aac7de172e 修复使用旧pushplus推送后结果解析失败的问题 (#958) 2021-11-27 20:33:49 +08:00
whyour e19d2412fc 检测更新增加强制更新 2021-11-27 20:19:49 +08:00
whyour 7cba2cf76c 依赖目录通知文件没有时,拷贝示例的通知文件 2021-11-27 17:41:26 +08:00
whyour 55d7a49dbc 修复deps映射目录 2021-11-27 17:19:50 +08:00
whyour f695864fd8 修复build args 2021-11-27 17:12:45 +08:00
whyour ae4883fbe7 测试yml 2021-11-27 17:05:55 +08:00
whyour 836dfd2861 添加deps目录软链 2021-11-27 16:20:24 +08:00
whyour 6e0523c6d7 修复删除JavaScript进程,修改依赖重新安装逻辑 2021-11-27 13:41:38 +08:00
whyour cd9ba084ae 修复调试运行时文件路径 2021-11-23 23:36:43 +08:00
phoenixandGitHub c8a0c0de67 fix nginx can not run without /run/nginx (#944)
alpine3.12 和 alpine3.14 兼容性问题
2021-11-23 18:32:22 +08:00
whyour 5dd8d9df91 更新版本 v2.10.9 2021-11-22 22:27:15 +08:00
whyour 2dbee94e85 默认拷贝deps目录下所有文件 2021-11-22 22:20:13 +08:00
风之凌殇andGitHub 9001797037 表格选择pageSize增加200/500/1000 (#934) 2021-11-22 19:26:02 +08:00
kilo5hzandGitHub 5e7104d5a3 系统通知增加gotify (#936) 2021-11-22 19:23:40 +08:00
whyour 7c33b22d63 修改docker基础镜像为alpine3.12,解决arm32无法启动 2021-11-22 19:22:39 +08:00
whyour b7cb1e379c 更新readme 2021-11-21 22:26:22 +08:00
whyour 274a6ce52e 修复调试功能 2021-11-20 01:06:25 +08:00
kilo5hzandwhyour 8079e4e7b8 点击任务名跳转脚本管理编辑脚本 (#929)
* 增加捷径:点击任务名跳转脚本管理编辑脚本

* 修正树节点选中,父节点自动展开
2021-11-20 01:06:25 +08:00
whyour 920efb8fab 更新readme图片链接 2021-11-20 01:06:25 +08:00
whyour 92aeb2b9da 更新readme 2021-11-20 01:06:25 +08:00
whyour ced76cf343 更新readme 2021-11-20 01:06:25 +08:00
whyour 3750d6ecd5 增加初始化依赖目录及文件 2021-11-20 01:06:25 +08:00
风之凌殇andwhyour 7444f30384 移除dockerfile中不必要的环境变量 (#925)
* 移除上次pr新增的不必要的环境变量
2021-11-20 01:06:16 +08:00
whyour b0fd5ba47f 更新readme 2021-11-17 19:22:40 +08:00
whyour cc098bc2e5 修复拷贝deps已存在依赖 2021-11-17 10:23:45 +08:00
whyour 1cfcf8d194 更新版本 v2.10.8 2021-11-16 14:36:13 +08:00
whyour d1e9f741d0 修复脚本管理删除、新建增加选择父目录 2021-11-16 14:18:02 +08:00
kilo5hzandGitHub 0cde9d07ee 增加gotify推送 (#905)
* 增加gotify推送
2021-11-15 23:04:55 +08:00
whyour a266425348 修复拷贝deps 2021-11-13 22:05:54 +08:00
whyour 6dbe02e321 修复dep目录 2021-11-13 21:54:45 +08:00
whyour 39979879c9 更新版本 v2.10.7 2021-11-13 21:21:35 +08:00
whyour ba3f604a5c 修复bot安装依赖判断 2021-11-13 20:58:27 +08:00
whyour 9572582401 修改拷贝依赖 2021-11-13 19:05:34 +08:00
whyour 2bcef0ba75 修复脚本管理搜索 2021-11-13 17:42:06 +08:00
whyour 31cd7c4007 外部依赖改为系统直接分发通知文件js和py版 2021-11-13 17:21:27 +08:00
whyour 1c4c1799b0 修复环境变量表格样式 2021-11-13 16:44:21 +08:00
whyour 2d558df15e 修复schedule服务 2021-11-13 12:34:49 +08:00
whyour 7b769da4e2 修复脚本管理树结构 2021-11-13 11:14:43 +08:00
whyour 4e95d62933 更新版本 v2.10.6 2021-11-13 00:03:12 +08:00
whyour 83255280b9 修复检查更新 2021-11-12 23:55:57 +08:00
whyour 2546cebdea 修复定时匹配 2021-11-12 23:42:11 +08:00
whyour f14a3fe0f1 修复脚本管理数据 2021-11-12 23:18:24 +08:00
whyour c3eb845be4 修改workflow变量 2021-11-12 22:09:02 +08:00
MiniersandGitHub 1eef77dca5 增加环境变量更新时间 (#887) 2021-11-12 21:55:03 +08:00
风之凌殇andGitHub d7d9f47fe5 增加在添加语义化版本tag时自动生成static并构建多个不同tag的容器的workflow,简化版本发布流程 (#888) 2021-11-12 21:54:42 +08:00
whyour fce6a996d9 更新版本号 v2.10.5 2021-11-12 14:23:16 +08:00
whyour 48a484d7de 修复文件判断引起服务异常 2021-11-12 14:22:21 +08:00
whyour 4b81d3c612 更新版本号 2021-11-12 13:18:32 +08:00
whyour 21b6edd7eb 修复entrypoint启动失败 2021-11-12 13:16:53 +08:00
whyour 9a43e022e9 修改版本说明序号 2021-11-11 23:04:29 +08:00
whyour 62bef3f13d 更新版本号 v2.10.3 2021-11-11 23:03:28 +08:00
whyour c87f5fa004 服务重启时依赖改为单独安装,避免同时失败 2021-11-11 22:56:47 +08:00
whyour 8d99f3ffbb 修复获取日志判断 2021-11-11 22:50:28 +08:00
whyour 100ca7b54b 单个数据库文件大于500兆时,裁剪文件,避免服务无法启动 2021-11-11 22:41:12 +08:00
whyour 4a2624f12a 脚本管理改成树结构 2021-11-11 00:10:05 +08:00
whyour 21b01a85a8 修复pushplus通知设置 2021-11-10 23:33:19 +08:00
whyour e039ae436e 修复依赖文件获取 2021-11-10 23:26:21 +08:00
whyour b13a1a45f8 修复repo依赖路径 2021-11-10 10:35:53 +08:00
whyour 667fe2bbba 修复shell语法 2021-11-09 23:55:50 +08:00
whyour f083871ca0 修复repo文件比对 2021-11-09 23:23:07 +08:00
whyour 782e1ba235 修改ql repo逻辑 2021-11-09 23:07:43 +08:00
whyour b92c6edc4e 添加动态引入 2021-11-09 22:34:24 +08:00
whyour bdf4657c73 修改退出下拉样式 2021-11-08 22:56:53 +08:00
whyour 8559438611 修改数据库加载 2021-11-08 22:47:46 +08:00
whyour 04045fd410 修复centos -x兼容性 2021-11-07 21:54:31 +08:00
whyour 1c71a9d8a8 回退shell [ 2021-11-07 19:18:36 +08:00
whyour 9a9ae9782c 修改pnpm安装源 2021-11-07 12:51:10 +08:00
whyour da2569850b 修复entrypoint 2021-11-07 11:25:18 +08:00
whyour b475035415 修复 arm 没有nginx conf.d 目录 2021-11-07 11:08:21 +08:00
OreoandGitHub 7baee27585 更新 Python 通知文件 (#832)
已在 Python3.6.x(云函数平台),3.8.x,3.10.0 测试通过
特性如下
1. 支持 11 种推送方式,未来还会继续添加
2. 多线程通知
3. 支持一言(一句话)和控制台输出开关
4. 对于现有推送方式,补充了配置变量
2021-11-06 20:45:53 +08:00
whyour 2cb2f1ce0a 更新dockerfile 2021-11-06 20:19:07 +08:00
whyour 8c1407e2a9 修改dockerfile 2021-11-06 19:04:01 +08:00
whyour 7bcb113085 更新docker yml 2021-11-06 18:58:18 +08:00
whyour 42dc27addf 修复任务日志结束时间展示 2021-11-06 00:50:44 +08:00
whyour aac30398c4 修复toLocaleString 0点显示24点 2021-11-05 23:56:16 +08:00
whyour ceae888372 修改shell语法 2021-11-05 23:39:22 +08:00
whyour 61df7050db 修复移动端展开导航 2021-11-04 19:24:55 +08:00
whyour af5561cbbf 更新版本号 v2.10.2 2021-11-04 18:10:43 +08:00
whyour bce30f922d 修复基础镜像pip3依赖 2021-11-04 18:10:14 +08:00
whyour d6ed394557 修复openapi路由 2021-11-04 18:07:34 +08:00
whyour 7b512a1681 更新版本号 v2.10.1 2021-11-04 15:09:07 +08:00
whyour 7af0597cd5 添加白屏时间及错误上报 2021-11-04 15:05:34 +08:00
whyour 2f5cf6d93c 修复check命令 2021-11-03 16:35:18 +08:00
whyour d672d2d104 修改失败任务日志 2021-11-01 23:06:23 +08:00
whyour c0d1fb1ef4 shell 中 [ 改为 [[ 2021-11-01 23:04:10 +08:00
whyour 3c882ad0ce 移动退出登录位置 2021-11-01 15:23:06 +08:00
whyour 58acd2eb68 修复表格滚动高度 2021-10-31 19:45:51 +08:00
whyour 590473e7d2 修复无法覆盖安装依赖 2021-10-31 17:34:19 +08:00
21139157c8 修改错误文字 (#809)
Co-authored-by: 14K <9112790+the14k@user.noreply.gitee.com>
2021-10-29 23:26:44 +08:00
whyour 8f8296ab64 支持服务端端口直接访问前端 2021-10-28 23:35:21 +08:00
whyour 40bf07d2a4 修复置顶后编辑置顶消失 2021-10-28 22:38:34 +08:00
whyour fd2b4fb86d 更新版本号 v2.10.0 2021-10-24 17:51:20 +08:00
whyour 783e867ef6 修复python移除依赖命令 2021-10-24 17:19:14 +08:00
whyour 3873d9f978 添加依赖管理 2021-10-24 17:07:16 +08:00
whyour 7dece938c8 依赖安装日志增加安装时间 2021-10-24 11:11:23 +08:00
whyour 0842d66c79 修复表格初次获取滚动高度 2021-10-23 23:44:11 +08:00
whyour d071e50d62 由于浏览器兼容性,移除dynamicImport 2021-10-23 23:31:46 +08:00
whyour 337f01aff1 修复python3任务无法停止 2021-10-23 23:22:08 +08:00
whyour 5614e41eb0 修复service引入路径 2021-10-23 18:46:24 +08:00
whyour 4f0fdbe453 初始化安装所有依赖 2021-10-23 18:44:25 +08:00
whyour 49c068db78 修改依赖安装结束标识 2021-10-23 18:27:20 +08:00
whyour fbba44d482 添加依赖管理 2021-10-23 18:23:32 +08:00
whyour 10c07b0a09 两步验证输入框自动聚焦 2021-10-21 22:25:15 +08:00
whyour 20893698ba 更新版本号 v2.9.10 2021-10-21 21:59:24 +08:00
longjuanandGitHub 31de659ed3 更新install.md (#789) 2021-10-21 21:55:26 +08:00
whyour 6c08a16c66 修改websocket日志 2021-10-21 21:52:06 +08:00
whyour 21e7178f48 修复table滚动高度 2021-10-21 19:13:38 +08:00
whyour aefd267eb2 更新monaco 2021-10-19 22:41:59 +08:00
whyour fb1a3980d4 检查密码默认值 2021-10-19 21:46:24 +08:00
whyour cfc467f8a5 修复tg系统通知认证参数 2021-10-18 19:18:46 +08:00
whyour 48120f012d 修复检查更新日志展示 2021-10-18 17:05:38 +08:00
whyour c907a78437 修复shell变量重复 2021-10-16 19:32:23 +08:00
whyour 915c0d09a9 升级umi 2021-10-16 17:21:19 +08:00
whyour 80638862cc 添加 yarn.lock 2021-10-16 17:07:05 +08:00
whyour a7350948cd 更新版本号 v2.9.9 2021-10-16 15:19:53 +08:00
whyour 1e1be013b5 脚本管理移动端支持添加脚本、编辑脚本 2021-10-16 15:11:23 +08:00
hanhh 4062584eae 移除无用代码 2021-10-16 13:39:18 +08:00
hanhh abedddffa6 修复表格样式 2021-10-16 10:53:14 +08:00
hanhh 38858ccdb2 修改pm2任务日志 2021-10-15 22:32:32 +08:00
hanhh d56e6359d0 修复更新日志匹配正则 2021-10-15 22:14:42 +08:00
hanhh 5886991464 放开ql update删除操作 2021-10-15 22:01:34 +08:00
hanhh 838326616b 更新版本号 v2.9.8-001 2021-10-15 11:47:32 +08:00
hanhh fcf80fe946 修复设置删除日志 2021-10-15 10:53:27 +08:00
phoenixandGitHub 2e63a7056c 修复nginx.conf错误 (#776)
Fix nginx: [emerg] unknown directive.
Log
`nginx: [emerg] unknown directive "client_body_buffer_size:" in /etc/nginx/nginx.conf:18
`
2021-10-15 10:40:58 +08:00
hanhh 7c807e3ec8 更新版本 2.9.8 2021-10-14 22:07:21 +08:00
hanhh b558aa0934 修复初始化流程 2021-10-14 22:00:36 +08:00
hanhh 38757758cb 增加日志删除频率设置 2021-10-14 10:58:54 +08:00
hanhh 84dd3d8fd1 修改版本更新执行命令 2021-10-13 23:50:25 +08:00
hanhh 19423c2d30 添加ql -l update 2021-10-13 23:45:46 +08:00
hanhh 98bba7d7fa 更新nginx配置文件 2021-10-13 23:31:38 +08:00
hanhh dcaa9ce000 修复websocket连接域名 2021-10-13 23:09:48 +08:00
hanhh 3cae44a136 修复版本号匹配正则 2021-10-13 22:53:10 +08:00
hanhh df8a3d2692 修复版本号匹配 2021-10-13 22:45:24 +08:00
hanhh 56e43f1808 移除远程版本更新链接 2021-10-13 22:41:59 +08:00
hanhh 2f6b95cccf 修复获取最后版本文件 2021-10-13 22:39:59 +08:00
hanhh ad9f6b2b64 修复获取最新版本号 2021-10-13 22:05:54 +08:00
hanhh 165c3dc976 容器启动忽略config和env中的错误 2021-10-13 14:25:38 +08:00
hanhh cbe6390b41 修复版本检查 2021-10-12 23:06:28 +08:00
hanhh 026eaf6606 完善检查更新操作 2021-10-12 22:54:17 +08:00
hanhh ee184f8e62 并发命令支持指定账号 2021-10-12 21:58:41 +08:00
hanhh 5745231dc9 添加系统更新操作和设置删除日志频率 2021-10-12 00:27:42 +08:00
hanhh cbdc3a9861 添加检查更新和删除日志频率操作 2021-10-08 23:02:45 +08:00
hanhh ba5c42ad6d 修复表格滚动样式和交互 2021-10-08 22:39:17 +08:00
hanhh b056a731a1 增加新版本检测 2021-10-07 15:26:25 +08:00
hanhh b4cd94f2d9 增加环境变量名称正则校验 2021-10-06 16:38:46 +08:00
hanhh 3b8cb43f02 添加新版本提醒 2021-10-06 16:22:53 +08:00
hanhh 82cb11c3c4 更新版本号 v2.9.7 2021-10-03 22:26:16 +08:00
SowevoandGitHub eb7522a90b 增加企业微信机器人推送的python版本 (#748) 2021-10-03 22:16:07 +08:00
hanhh 3898201b1e 修复登录两步提醒样式 2021-10-03 22:05:56 +08:00
hanhh bb6ab0c17b 修改初始化判断条件 2021-10-03 22:01:44 +08:00
hanhh 652c40ab1f 增加初始化过程 2021-10-03 20:58:55 +08:00
hanhh ca0d6215a9 修复entrypoint启动nginx配置 2021-09-30 23:49:19 +08:00
hanhh 61d155257c ql update添加patch 2021-09-30 19:14:40 +08:00
hanhh 91399f1a1f 移除check中init git 2021-09-30 19:08:13 +08:00
hanhh 2e738c5977 更新版本号 v2.9.6 2021-09-29 23:16:59 +08:00
hanhh c0bd51cf03 修复类型检查 2021-09-29 23:15:09 +08:00
hanhh ca5afe04f8 修复退出登录获取token出错 2021-09-29 23:09:13 +08:00
hanhh 47db0b9f4e 对比工具支持对比config目录下的文件 2021-09-29 22:48:49 +08:00
hanhh 27f17a377a 重启服务时自动压缩db 2021-09-29 21:40:35 +08:00
hanhh ecaf731ecf 修复loading状态位置 2021-09-29 21:19:25 +08:00
hanhh 9fba2a85f6 修复清空文件操作 2021-09-27 22:04:47 +08:00
hanhh 93f75be182 task和ql命令添加基础目录检测 2021-09-27 19:38:06 +08:00
hanhh d537901f59 测试动态加载 2021-09-25 12:11:13 +08:00
hanhh 260c1ed4a4 修改定时任务列表滚动宽度 2021-09-25 11:40:55 +08:00
hanhh ac2bd52463 修复status接口参数 2021-09-25 02:00:50 +08:00
hanhh 16c6f8fee1 修复并发参数 2021-09-25 01:51:18 +08:00
hanhh 8a0db21f17 更新版本号 2021-09-25 00:43:05 +08:00
hanhh 9ab08a71c0 增加登录设备展示 2021-09-25 00:42:03 +08:00
hanhh 381c32a3b4 修复更新任务状态api 2021-09-25 00:28:18 +08:00
hanhh 350c0f10c4 修复ql resettfa命令 2021-09-25 00:15:23 +08:00
hanhh d5b982f813 ql resettfa增加兼容字段 2021-09-24 23:33:52 +08:00
hanhh 6587375ff2 移除定时任务左右列固定n 2021-09-24 23:31:50 +08:00
hanhh f5ecb81bad ql bot移除自动更新远程仓库 2021-09-24 23:22:40 +08:00
hanhh 0141cd81c4 修复服务端ts类型检查 2021-09-24 22:49:14 +08:00
hanhh ff412c7497 更新版本号 2021-09-24 22:30:09 +08:00
hanhh 623f239f91 修复定时任务操作宽度 2021-09-24 22:28:25 +08:00
hanhh c7ba304c6a 支持多端登录 2021-09-24 22:24:39 +08:00
hanhh c8393604d1 修复task运行指定账号 2021-09-24 00:24:54 +08:00
hanhh c5d47a67aa 修复定时任务列表下次运行时间 2021-09-24 00:11:14 +08:00
hanhh 3a1423cd59 task支持指定账号运行 2021-09-23 23:59:53 +08:00
hanhh d57f4cf6cd 修改定时任务表格布局 2021-09-23 22:12:19 +08:00
hanhh af420942c4 添加最后运行时间和运行时长 2021-09-23 13:42:32 +08:00
hanhh eac4e5e66f 修复主题切换全局class未变化 2021-09-21 23:29:38 +08:00
hanhh b8822a98f7 定时任务添加下次运行时间 2021-09-21 20:11:39 +08:00
hanhh 95322b42c0 修改登录通知消息 2021-09-20 23:56:20 +08:00
hanhh 33f5a06d32 修复pushplus通知参数 2021-09-20 22:23:56 +08:00
hanhh a7e5126c92 修复新建文件 2021-09-20 00:59:38 +08:00
hanhh c62f3e5b8e 添加bak目录ignore 2021-09-20 00:47:58 +08:00
hanhh 5d3f413a05 修改测试通知内容 2021-09-19 23:10:58 +08:00
hanhh 644154ca38 更新版本号 2021-09-19 23:00:53 +08:00
hanhh cccce5ff74 修复通知 2021-09-19 23:00:21 +08:00
hanhh aa1fe98188 定时任务增加置顶功能 2021-09-19 22:25:57 +08:00
hanhh b6a78550d6 添加全局主题className 2021-09-19 21:27:20 +08:00
hanhh 70b7f2c493 脚本管理支持新增文件 2021-09-19 21:23:03 +08:00
hanhh ea44e5f209 修复pageTitle 2021-09-18 23:27:50 +08:00
hanhh 41ef87d9f8 任务详情日志添加loading 2021-09-18 16:33:12 +08:00
hanhh c0b4a70aa0 添加全局loading 2021-09-18 15:08:45 +08:00
hanhh 955875cb7a 修复关闭通知 2021-09-18 02:09:06 +08:00
hanhh 827e8571aa 添加通知设置页面 2021-09-18 01:44:55 +08:00
whyour aed08b0f59 添加通知api 2021-09-17 18:06:14 +08:00
hanhh 3ddaf41e27 更新通知服务 2021-09-17 00:35:27 +08:00
whyour 38a9cce6bf 更新通知服务 2021-09-16 21:01:29 +08:00
whyour ae255ef381 添加通知服务 2021-09-16 18:01:25 +08:00
prcxuandGitHub 4316bef01e 更新安装说明 (#705) 2021-09-15 11:18:58 +08:00
JH2628andGitHub 8491c5706d 修复config sample序号 (#694) 2021-09-14 19:25:00 +08:00
hanhh 7b7fab1f32 修改系统异常提醒 2021-09-13 00:12:44 +08:00
hanhh 5a6fccdc2f 更新tg链接 2021-09-12 20:28:43 +08:00
hanhh 23668be46d 更新版本号 2021-09-11 13:53:58 +08:00
hanhh f92f2c1666 修复写入任务错误日志逻辑 2021-09-11 13:53:24 +08:00
hanhh 895740a3cb 修改获取用户数据逻辑 2021-09-11 02:03:31 +08:00
hanhh 4c76c3231f 移除自动压缩db数据 2021-09-09 13:02:49 +08:00
hanhh ca9d3f9559 更新版本号 2021-09-08 12:26:21 +08:00
hanhh 3ca3c5e75c 修复开启两步失败提醒 2021-09-08 12:26:05 +08:00
hanhh c2c0b1dfdc 添加应用名称改为选填 2021-09-08 12:15:30 +08:00
hanhh c633774487 修复两步验证逻辑 2021-09-08 12:13:19 +08:00
hanhh c2d86fc517 更新版本号 2021-09-07 23:53:01 +08:00
hanhh d7c8d150ca 修改登陆日志title 2021-09-07 23:52:40 +08:00
L1n3andGitHub 6b8a4e65f2 添加安装说明 (#667) 2021-09-07 17:46:53 +08:00
LeonandGitHub 482e43dd1f 修复 Python 和 JavaScript Server酱和企业微信推送的环境变量不一致 (#673) 2021-09-07 17:46:14 +08:00
hanhh 1437babc6c 增加登录日志 2021-09-07 01:40:41 +08:00
Zy143LandGitHub 5ed882943a 修复python和JavaScript钉钉推送的环境变量不一致 (#659) 2021-09-05 23:53:18 +08:00
hanhh e678b3fb3a config文件读取添加黑名单 2021-09-05 23:48:58 +08:00
hanhh a5770bc4e4 添加环境变量命令input改为textarea 2021-09-05 23:38:36 +08:00
hanhh 59821bb939 修改ql check依赖安装逻辑 2021-09-03 22:30:24 +08:00
hanhh b58040be44 修改两步验证逻辑 2021-09-03 19:09:09 +08:00
hanhh be3b8ad52e 修复环境变量拖拽 2021-09-02 19:16:28 +08:00
hanhh 0fdaf97461 更新两步验证逻辑 2021-09-01 23:03:27 +08:00
L1n3andGitHub f5f036372f 添加 docker-compose.yml (#629) 2021-08-31 15:52:24 +08:00
hanhh 3fceb983e6 增加上次登录状态 2021-08-31 15:29:23 +08:00
hanhh ae6a39196d 增加ql resettfa禁用两步验证 2021-08-31 11:06:20 +08:00
hanhh 2810ff2753 更新版本号 2021-08-31 10:56:37 +08:00
hanhh 205c0cf8d3 两步验证失败增加ip记录 2021-08-31 10:54:46 +08:00
hanhh d9c801a091 添加两步验证 2021-08-30 23:37:26 +08:00
hanhh 0833aa3b30 增加nedb数据自定比对,测试对db文件不断增大的影响 2021-08-29 00:52:21 +08:00
hanhh 9c90bbc146 修复移动端无法打开脚本管理、任务日志 2021-08-29 00:34:47 +08:00
hanhh 231b387076 更换ip地址查询接口 2021-08-27 14:00:11 +08:00
hanhh 3de432652f 更换ip查询接口 2021-08-27 13:27:32 +08:00
hanhh cad714493e 更新版本号 2021-08-27 11:08:08 +08:00
hanhh 6a690a6108 更新nginx配置 2021-08-27 11:04:31 +08:00
hanhh 1b85c52557 修改新建应用placeholder 2021-08-27 10:52:49 +08:00
hanhh b37be92ea9 搜索时去掉前后空格 2021-08-27 10:47:30 +08:00
HeroWongandGitHub f95979f445 fix: 只有一页时不隐藏分页栏,确保用户可以更改 pagesize (#613) 2021-08-26 21:27:22 +08:00
hanhh 09e07218ae 添加openapi模块 2021-08-26 19:01:39 +08:00
hanhh 085f5a9451 更新版本号 2021-08-25 23:55:16 +08:00
hanhh eb4e935554 修复cron分隔符,修复登录获取ip逻辑 2021-08-25 23:54:42 +08:00
hanhh 57986cf425 修复登录倒计时颜色 2021-08-25 18:58:45 +08:00
hanhh 7bf14b9acc 更新版本号 2021-08-25 18:20:29 +08:00
hanhh 3ba0be34d9 修复6位cron判断 2021-08-25 18:19:22 +08:00
hanhh 0f828a3c75 修复群辉update之后403 2021-08-25 17:58:21 +08:00
hanhh 16a768a142 更新版本号 2021-08-24 18:21:19 +08:00
hanhh f380d20279 添加重置登录错误次数命令 2021-08-24 18:20:36 +08:00
hanhh be908ab36c 添加默认定时规则配置 2021-08-23 19:10:57 +08:00
hanhh b545454537 task传递参数给task_before和task_after 2021-08-23 15:58:25 +08:00
hanhh a5ff8dd1d6 更新readme图片 2021-08-23 15:31:13 +08:00
hanhh 59f23e518b 修复readme图片 2021-08-23 15:26:30 +08:00
hanhh 858b3f0176 更新版本号 2021-08-23 13:50:44 +08:00
hanhh 89b8494ad0 修复获取ip api失败时,无法登陆
修复禁用或者运行定时任务出现重复
2021-08-23 13:44:54 +08:00
hanhh 64ecd75bd5 修复定时任务搜索后页码 2021-08-23 12:29:29 +08:00
hanhh 138eb3393d 修复error_log文件读取 2021-08-23 11:41:34 +08:00
hanhh 1ad51d2925 更新版本号 2021-08-23 10:47:11 +08:00
hanhh 317e175bca 回退update.sh 2021-08-23 10:43:26 +08:00
hanhh 0e287aa7d1 修复task_before eval 2021-08-22 22:33:07 +08:00
hanhh 28018b705b 修复未产生日志文件cat报错 2021-08-22 22:02:31 +08:00
hanhh 46ec72d6be ql命令增加-l参数 2021-08-22 21:57:10 +08:00
hanhh 82494a87af 修改task.sh任务顺序 2021-08-22 20:54:40 +08:00
hanhh 95c19b447a 修复task语法 2021-08-22 18:57:52 +08:00
hanhh 9c6eae936a 修复task.sh执行语法 2021-08-22 18:21:31 +08:00
hanhh 77605a9360 修改task.sh中参数名 2021-08-22 16:05:58 +08:00
hanhh 82880c3f06 修复最后登录ip读取 2021-08-22 15:25:16 +08:00
hanhh 11143c771b 更新版本号 2021-08-22 15:23:58 +08:00
hanhh 0968eaa5a8 修复任务执行参数出错 2021-08-22 15:23:12 +08:00
hanhh 0687f9d472 修改登陆成功提示文本 2021-08-22 14:47:07 +08:00
hanhh 25f0f5951a 更新版本号 2021-08-22 14:07:43 +08:00
hanhh eec0ce65ad 修复获取访问ip逻辑 2021-08-22 14:07:00 +08:00
hanhh be4e100168 更新版本号 2021-08-22 12:59:54 +08:00
hanhh 59e4f1063c 修复登录重试判断 2021-08-22 12:59:34 +08:00
hanhh 359e21f02b 登录增加错误延迟提醒 2021-08-22 12:28:45 +08:00
hanhh 6894bd9c0e 登录增加最后登录时间地点,登录失败增加重试间隔 2021-08-22 01:22:23 +08:00
hanhh 7040f02823 默认定时改为随机时间 2021-08-21 19:26:37 +08:00
hanhh 41408de337 修复更新环境变量备注无法删除 2021-08-21 18:34:53 +08:00
hanhh 72e243dce8 增加命令行运行日志实时打印 2021-08-21 17:26:41 +08:00
hanhh 77d4840e2a 定时任务,环境变量添加状态筛选 2021-08-20 16:31:24 +08:00
hanhh 0d5961821c 更新版本号 2021-08-20 16:17:04 +08:00
hanhh f34980b173 移除console 2021-08-20 14:26:12 +08:00
hanhh 5b21a6e529 手动运行任务添加错误捕获 2021-08-20 10:59:59 +08:00
hanhh 6dbb737d4b 修复脚本管理编辑交互,修复ico无法加载 2021-08-19 23:46:26 +08:00
hanhh 76ecfbaaa2 更新版本号 2021-08-19 14:47:07 +08:00
hanhh d9aacbcb92 定时任务支持任务名,定时,命令排序
环境变量支持名称排序
2021-08-19 14:45:42 +08:00
hanhh e088325d95 脚本管理支持编辑删除文件 2021-08-19 14:18:25 +08:00
hanhh b29dd2c580 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:31:39 +08:00
hanhh 89e82fd5b0 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:26:33 +08:00
hanhh 9f0b671d42 修复手动运行任务进程 2021-08-17 22:06:04 +08:00
hanhh 7de7c724dc 更新版本号 2021-08-17 18:35:53 +08:00
hanhh 3525b1d92a 支持批量创建同名称环境变量,日志和脚本管理PC页支持宽度拖拽 2021-08-17 18:34:55 +08:00
hanhh 30cea84886 修改暗黑模式默认值 2021-08-16 18:05:52 +08:00
DerekandGitHub 0c2cc7f575 更正ql"本脚本用法"中的序号 (#552) 2021-08-16 17:17:45 +08:00
139 changed files with 3434 additions and 23351 deletions
+3 -3
View File
@@ -7,6 +7,6 @@ LOG_LEVEL='debug'
SECRET='whyour' SECRET='whyour'
QINIU_AK='' QINIU_AK = ''
QINIU_SK='' QINIU_SK = ''
QINIU_SCOPE='' QINIU_SCOPE = ''
-42
View File
@@ -1,42 +0,0 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: input
id: version
attributes:
label: Qinglong version
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Chat
url: https://t.me/jiao_long
about: Ask questions and discuss with other Qinglong users in real time.
- name: Questions & Discussions
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
about: Use GitHub discussions for message-board style questions and discussions.
@@ -1,40 +0,0 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
-31
View File
@@ -1,31 +0,0 @@
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
-7
View File
@@ -1,7 +0,0 @@
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
+20 -67
View File
@@ -17,47 +17,18 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitlab.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitee.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: pnpm/action-setup@v2 - uses: actions/setup-node@v1
with: with:
version: latest node-version: '14'
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: build front and back - name: build front and back
run: | run: |
pnpm install --frozen-lockfile npm i -g pnpm
pnpm install
pnpm build:front pnpm build:front
pnpm build:back pnpm build:back
@@ -65,9 +36,6 @@ jobs:
env: env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }} GITHUB_BRANCH: ${{ github.ref_name }}
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
run: | run: |
mkdir -p tmp mkdir -p tmp
cd ./tmp cd ./tmp
@@ -78,16 +46,6 @@ jobs:
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
mkdir -p ~/.ssh
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}"
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build: build:
needs: build-static needs: build-static
@@ -98,27 +56,26 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- uses: pnpm/action-setup@v2 - uses: actions/setup-node@v1
with: with:
version: latest node-version: '14'
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: Setup timezone - name: Set time zone
uses: zcong1993/setup-timezone@master uses: szenius/set-timezone@v1.0
with: with:
timezone: Asia/Shanghai timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v2 uses: docker/login-action@v1
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -126,7 +83,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v3
with: with:
images: | images: |
${{ github.repository }} ${{ github.repository }}
@@ -143,24 +100,20 @@ jobs:
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v1
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v3 uses: docker/build-push-action@v2
with: with:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
context: docker/ context: docker/
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+31
View File
@@ -0,0 +1,31 @@
name: PublishDockerImage
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Docker buildx image and push on master branch
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
+1
View File
@@ -5,6 +5,7 @@
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock /yarn.lock
/pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
+1 -2
View File
@@ -1,2 +1 @@
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/ sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
strict-peer-dependencies=false
+16 -11
View File
@@ -1,15 +1,20 @@
import { defineConfig } from '@umijs/max'; import { defineConfig } from 'umi';
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
antd: {}, layout: false,
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, nodeModulesTransform: {
favicons: ['/images/favicon.svg'], type: 'none',
mfsu: {
strategy: 'eager',
}, },
fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/g5.ico',
proxy: { proxy: {
'/api/public': { '/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
@@ -21,7 +26,7 @@ export default defineConfig({
ws: true, ws: true,
}, },
}, },
chainWebpack: ((config: any) => { chainWebpack: (config) => {
config.plugin('compression-webpack-plugin').use( config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({ new CompressionPlugin({
algorithm: 'gzip', algorithm: 'gzip',
@@ -30,13 +35,13 @@ export default defineConfig({
minRatio: 0.6, minRatio: 0.6,
}), }),
); );
}) as any, },
externals: { externals: {
react: 'window.React', react: 'window.React',
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
}, },
headScripts: [ scripts: [
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.production.min.js', 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react-dom/18.2.0/umd/react-dom.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
], ],
}); });
+18 -197
View File
@@ -1,201 +1,22 @@
Apache License MIT LICENSE
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION Copyright (c) 2021-present whyour
1. Definitions. Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
"License" shall mean the terms and conditions for use, reproduction, The above copyright notice and this permission notice shall be
and distribution as defined by Sections 1 through 9 of this document. included in all copies or substantial portions of the Software.
"Licensor" shall mean the copyright owner or entity authorized by THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
the copyright owner that is granting the License. EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
"Legal Entity" shall mean the union of the acting entity and all NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
other entities that control, are controlled by, or are under common LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
control with that entity. For the purposes of this definition, OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
"control" means (i) the power, direct or indirect, to cause the WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 WHYOUR <https://github.com/whyour>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-218
View File
@@ -1,218 +0,0 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center">
Timed task management panel with python3, javaScript, shell, typescript support
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
[简体中文](./README.md) | English
## Features
- Support for multiple scripting languages (python3, javaScript, shell, typescript)
- Support online management of scripts, environment variables, configuration files
- Support online view task log
- Support second-level task setting
- Support system level notification
- Support dark mode
- Support cell phone operation
## Deployment
### Local Deployment
```bash
# To be refined, see the development steps first (not supported on windows yet)
```
### Podman Deployment
1. podman installation
```bash
https://podman.io/getting-started/installation
```
2. start the container
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### Docker Deployment
1. docker installation
```bash
sudo curl -sSL get.docker.com | sh
```
2. configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. start the container
```bash
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### Docker-compose Deployment
1. docker-compose installation
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. start the container
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# start
docker-compose up -d
# stop
docker-compose down
```
3. access
Open your browser and visit http://{ip}:5700
## Use
1. built-in commands
```bash
# Update and restart Green Dragon
ql update
# Run custom scripts extra.sh
ql extra
# Adding a single script file
ql raw <file_url>
# Add a specific script for a single repository
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# Delete old logs
ql rmlog <days>
# Start bot
ql bot
# Detecting the Green Dragon environment and repairing it
ql check
# Reset the number of login errors
ql resetlet
# Disable two-step login
ql resettfa
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
```
2. parameter description
* file_url: Script address
* repo_url: Repository address
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
* branch: Pull the branch of the repository
* days: Number of days of logs to be kept
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Links
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Development
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Communication
[telegram channel](https://t.me/jiao_long)
## Name Origin
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
+14 -43
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png"> <img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a> </a>
</p> </p>
@@ -8,7 +8,7 @@
<div align="center"> <div align="center">
支持python3、javaScript、shell、typescript 的定时任务管理面板 支持python3、javaScript、shell、typescript 的定时任务管理面板A timed task management panel that supports typescript, javaScript, python3, and shell.
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url] [![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
@@ -22,7 +22,7 @@
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn) [![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
简体中文 | [English](./README-en.md) 简体中文 | [English](./README-en.md)
@@ -41,7 +41,7 @@
### 本机部署 ### 本机部署
```bash ```bash
# 待完善,可先参考开发步骤 (windows暂时不支持) # 待完善
``` ```
### podman 部署 ### podman 部署
@@ -57,10 +57,11 @@ https://podman.io/getting-started/installation
```bash ```bash
podman run -dit \ podman run -dit \
--network bridge \ --network bridge \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
``` ```
@@ -72,31 +73,11 @@ podman run -dit \
sudo curl -sSL get.docker.com | sh sudo curl -sSL get.docker.com | sh
``` ```
2. 配置国内镜像源 2. 启动容器
Configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. 启动容器
```bash ```bash
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql:/ql/data \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
@@ -124,10 +105,6 @@ docker-compose up -d
docker-compose down docker-compose down
``` ```
3. 访问
打开你的浏览器,访问 http://{ip}:5700
## 使用 ## 使用
1. 内置命令 1. 内置命令
@@ -140,7 +117,7 @@ ql extra
# 添加单个脚本文件 # 添加单个脚本文件
ql raw <file_url> ql raw <file_url>
# 添加单个仓库的指定脚本 # 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions> ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# 删除旧日志 # 删除旧日志
ql rmlog <days> ql rmlog <days>
# 启动tg-bot # 启动tg-bot
@@ -159,27 +136,21 @@ task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行 # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的) task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行 # 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number> task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
``` ```
2. 参数说明 2. 参数说明
* file_url: 脚本地址 * file_url: 脚本地址
* repo_url: 仓库地址 * repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 * whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 * blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 * dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
* extensions: 拉取仓库的文件后缀,多个竖线分割
* branch: 拉取仓库的分支 * branch: 拉取仓库的分支
* days: 需要保留的日志的天数 * days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径 * file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称 * env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号 * account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 链接 ## 链接
@@ -187,7 +158,7 @@ task -l <file_path>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org) - [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+3
View File
@@ -25,6 +25,7 @@ export default (app: Router) => {
}), }),
}); });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -50,6 +51,7 @@ export default (app: Router) => {
} }
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -74,6 +76,7 @@ export default (app: Router) => {
fs.writeFileSync(path, content); fs.writeFileSync(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
+24 -139
View File
@@ -2,7 +2,6 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import CronService from '../services/cron'; import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
@@ -10,141 +9,11 @@ const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/crons', route); app.use('/crons', route);
route.get(
'/views',
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
if (req.body.type === 1) {
return res.send({ code: 400, message: '参数错误' });
} else {
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
}
} catch (e) {
return next(e);
}
},
);
route.delete(
'/views',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/move',
celebrate({
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.move(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any); const data = await cronService.crontabs(req.query.searchValue as string);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -173,6 +42,7 @@ export default (app: Router) => {
return res.send({ code: 400, message: 'param schedule error' }); return res.send({ code: 400, message: 'param schedule error' });
} }
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -190,6 +60,7 @@ export default (app: Router) => {
const data = await cronService.run(req.body); const data = await cronService.run(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -207,6 +78,7 @@ export default (app: Router) => {
const data = await cronService.stop(req.body); const data = await cronService.stop(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -230,6 +102,7 @@ export default (app: Router) => {
); );
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -250,6 +123,7 @@ export default (app: Router) => {
const data = await cronService.addLabels(req.body.ids, req.body.labels); const data = await cronService.addLabels(req.body.ids, req.body.labels);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -267,6 +141,7 @@ export default (app: Router) => {
const data = await cronService.disabled(req.body); const data = await cronService.disabled(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -284,6 +159,7 @@ export default (app: Router) => {
const data = await cronService.enabled(req.body); const data = await cronService.enabled(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -303,6 +179,7 @@ export default (app: Router) => {
const data = await cronService.log(req.params.id); const data = await cronService.log(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -313,9 +190,9 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
labels: Joi.array().optional().allow(null), labels: Joi.array().optional().allow(null),
command: Joi.string().required(), command: Joi.string().optional(),
schedule: Joi.string().required(), schedule: Joi.string().optional(),
name: Joi.string().optional().allow(null), name: Joi.string().optional(),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
@@ -333,6 +210,7 @@ export default (app: Router) => {
return res.send({ code: 400, message: 'param schedule error' }); return res.send({ code: 400, message: 'param schedule error' });
} }
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -350,6 +228,7 @@ export default (app: Router) => {
const data = await cronService.remove(req.body); const data = await cronService.remove(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -367,6 +246,7 @@ export default (app: Router) => {
const data = await cronService.pin(req.body); const data = await cronService.pin(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -384,6 +264,7 @@ export default (app: Router) => {
const data = await cronService.unPin(req.body); const data = await cronService.unPin(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -398,6 +279,7 @@ export default (app: Router) => {
const data = await cronService.import_crontab(); const data = await cronService.import_crontab();
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -417,6 +299,7 @@ export default (app: Router) => {
const data = await cronService.getDb({ id: req.params.id }); const data = await cronService.getDb({ id: req.params.id });
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -428,10 +311,10 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.number().required()), ids: Joi.array().items(Joi.number().required()),
status: Joi.string().required(), status: Joi.string().required(),
pid: Joi.string().optional().allow(null), pid: Joi.string().optional(),
log_path: Joi.string().optional().allow(null), log_path: Joi.string().optional(),
last_running_time: Joi.number().optional().allow(null), last_running_time: Joi.number().optional(),
last_execution_time: Joi.number().optional().allow(null), last_execution_time: Joi.number().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -445,6 +328,7 @@ export default (app: Router) => {
}); });
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -464,6 +348,7 @@ export default (app: Router) => {
const data = await cronService.logs(req.params.id); const data = await cronService.logs(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
+7 -1
View File
@@ -38,6 +38,7 @@ export default (app: Router) => {
const data = await dependenceService.create(req.body); const data = await dependenceService.create(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -60,6 +61,7 @@ export default (app: Router) => {
const data = await dependenceService.update(req.body); const data = await dependenceService.update(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -77,6 +79,7 @@ export default (app: Router) => {
const data = await dependenceService.remove(req.body); const data = await dependenceService.remove(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -91,9 +94,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.remove(req.body, true); const data = await dependenceService.removeDb(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -113,6 +117,7 @@ export default (app: Router) => {
const data = await dependenceService.getDb({ id: req.params.id }); const data = await dependenceService.getDb({ id: req.params.id });
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -130,6 +135,7 @@ export default (app: Router) => {
const data = await dependenceService.reInstall(req.body); const data = await dependenceService.reInstall(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
+11 -50
View File
@@ -3,21 +3,8 @@ import { Container } from 'typedi';
import EnvService from '../services/env'; import EnvService from '../services/env';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/envs', route); app.use('/envs', route);
@@ -39,9 +26,7 @@ export default (app: Router) => {
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string() name: Joi.string().required(),
.required()
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
remarks: Joi.string().optional().allow(''), remarks: Joi.string().optional().allow(''),
}), }),
), ),
@@ -53,6 +38,7 @@ export default (app: Router) => {
const data = await envService.create(req.body); const data = await envService.create(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -75,6 +61,7 @@ export default (app: Router) => {
const data = await envService.update(req.body); const data = await envService.update(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -92,6 +79,7 @@ export default (app: Router) => {
const data = await envService.remove(req.body); const data = await envService.remove(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -109,11 +97,13 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.move(req.params.id, req.body); const data = await envService.move(req.params.id, req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -131,6 +121,7 @@ export default (app: Router) => {
const data = await envService.disabled(req.body); const data = await envService.disabled(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -148,6 +139,7 @@ export default (app: Router) => {
const data = await envService.enabled(req.body); const data = await envService.enabled(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -168,6 +160,7 @@ export default (app: Router) => {
const data = await envService.updateNames(req.body); const data = await envService.updateNames(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -184,42 +177,10 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.getDb({ id: req.params.id }); const data = await envService.getDb(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); logger.error('🔥 error: %o', e);
}
},
);
route.post(
'/upload',
upload.single('env'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
if (data.every((x) => x.name && x.value)) {
const result = await envService.create(
data.map((x) => ({
name: x.name,
value: x.value,
remarks: x.remarks,
})),
);
return res.send({ code: 200, data: result });
} else {
return res.send({
code: 400,
message: '文件缺少name或者value字段,参考导出文件格式',
});
}
} catch (e) {
return next(e); return next(e);
} }
}, },
-2
View File
@@ -8,7 +8,6 @@ import script from './script';
import open from './open'; import open from './open';
import dependence from './dependence'; import dependence from './dependence';
import system from './system'; import system from './system';
import subscription from './subscription';
export default () => { export default () => {
const app = Router(); const app = Router();
@@ -21,7 +20,6 @@ export default () => {
open(app); open(app);
dependence(app); dependence(app);
system(app); system(app);
subscription(app);
return app; return app;
}; };
+44 -48
View File
@@ -3,11 +3,8 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { emptyDir, getFileContentByName, readDirs } from '../config/util'; import { getFileContentByName } from '../config/util';
import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp'];
export default (app: Router) => { export default (app: Router) => {
app.use('/logs', route); app.use('/logs', route);
@@ -15,65 +12,64 @@ export default (app: Router) => {
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const result = readDirs(config.logPath, config.logPath, blacklist); const fileList = fs.readdirSync(config.logPath, 'utf-8');
res.send({ const dirs = [];
code: 200, for (let i = 0; i < fileList.length; i++) {
data: result, const stat = fs.lstatSync(config.logPath + fileList[i]);
}); if (stat.isDirectory()) {
const fileListTmp = fs.readdirSync(
`${config.logPath}/${fileList[i]}`,
'utf-8',
);
dirs.push({
name: fileList[i],
isDir: true,
files: fileListTmp.reverse(),
});
} else {
dirs.push({
name: fileList[i],
isDir: false,
files: [],
});
}
}
res.send({ code: 200, dirs });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}); });
route.get(
'/:dir/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { dir, file } = req.params;
const content = getFileContentByName(
`${config.logPath}/${dir}/${file}`,
);
res.send({ code: 200, data: content });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get( route.get(
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
if (blacklist.includes(req.path)) { const { file } = req.params;
return res.send({ code: 403, message: '暂无权限' }); const content = getFileContentByName(`${config.logPath}/${file}`);
}
const filePath = join(
config.logPath,
(req.query.path || '') as string,
req.params.file,
);
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
); );
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+7 -1
View File
@@ -16,6 +16,7 @@ export default (app: Router) => {
const data = await openService.list(); const data = await openService.list();
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -25,7 +26,7 @@ export default (app: Router) => {
'/apps', '/apps',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().optional().allow('').disallow('system'), name: Joi.string().optional().allow(''),
scopes: Joi.array().items(Joi.string().required()), scopes: Joi.array().items(Joi.string().required()),
}), }),
}), }),
@@ -36,6 +37,7 @@ export default (app: Router) => {
const data = await openService.create(req.body); const data = await openService.create(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -57,6 +59,7 @@ export default (app: Router) => {
const data = await openService.update(req.body); const data = await openService.update(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -74,6 +77,7 @@ export default (app: Router) => {
const data = await openService.remove(req.body); const data = await openService.remove(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -93,6 +97,7 @@ export default (app: Router) => {
const data = await openService.resetSecret(req.params.id); const data = await openService.resetSecret(req.params.id);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -113,6 +118,7 @@ export default (app: Router) => {
const result = await openService.authToken(req.query as any); const result = await openService.authToken(req.query as any);
return res.send(result); return res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
+102 -84
View File
@@ -1,10 +1,7 @@
import { import {
fileExist, fileExist,
getFileContentByName, getFileContentByName,
readDirs,
getLastModifyFilePath, getLastModifyFilePath,
readDir,
emptyDir,
} from '../config/util'; } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
@@ -12,47 +9,75 @@ import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path'; import path, { join } from 'path';
import ScriptService from '../services/script'; import ScriptService from '../services/script';
import multer from 'multer';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/scripts', route); app.use('/scripts', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get(
const logger: Logger = Container.get('logger'); '/files',
try { async (req: Request, res: Response, next: NextFunction) => {
let result = []; const logger: Logger = Container.get('logger');
const blacklist = ['node_modules', '.git']; try {
if (req.query.path) { const fileList = fs.readdirSync(config.scriptPath, 'utf-8');
const targetPath = path.join(
config.scriptPath, let result = [];
req.query.path as string, for (let i = 0; i < fileList.length; i++) {
); const fileOrDir = fileList[i];
result = readDir(targetPath, config.scriptPath, blacklist); const fPath = path.join(config.scriptPath, fileOrDir);
} else { const dirStat = fs.statSync(fPath);
result = readDirs(config.scriptPath, config.scriptPath, blacklist); if (['node_modules'].includes(fileOrDir)) {
continue;
}
if (dirStat.isDirectory()) {
const childFileList = fs.readdirSync(fPath, 'utf-8');
let children = [];
for (let j = 0; j < childFileList.length; j++) {
const childFile = childFileList[j];
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
const _fileExist = await fileExist(sPath);
if (_fileExist && fs.statSync(sPath).isFile()) {
const statObj = fs.statSync(sPath);
children.push({
title: childFile,
value: childFile,
key: `${fileOrDir}/${childFile}`,
mtime: statObj.mtimeMs,
parent: fileOrDir,
});
}
}
result.push({
title: fileOrDir,
value: fileOrDir,
key: fileOrDir,
mtime: dirStat.mtimeMs,
disabled: true,
children: children.sort((a, b) => b.mtime - a.mtime),
});
} else {
result.push({
title: fileOrDir,
value: fileOrDir,
key: fileOrDir,
mtime: dirStat.mtimeMs,
});
}
}
res.send({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
} }
res.send({ },
code: 200, );
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get( route.get(
'/:file', '/:file',
@@ -67,6 +92,7 @@ export default (app: Router) => {
const content = getFileContentByName(filePath); const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -74,19 +100,23 @@ export default (app: Router) => {
route.post( route.post(
'/', '/',
upload.single('file'), celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
content: Joi.string().allow(''),
originFilename: Joi.string().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path, content, originFilename, directory } = let { filename, path, content, originFilename } = req.body as {
req.body as { filename: string;
filename: string; path: string;
path: string; content: string;
content: string; originFilename: string;
originFilename: string; };
directory: string;
};
if (!path) { if (!path) {
path = config.scriptPath; path = config.scriptPath;
} }
@@ -99,29 +129,18 @@ export default (app: Router) => {
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
code: 430, code: 430,
message: '文件路径禁止访问', data: '文件路径禁止访问',
}); });
} }
if (req.file) {
fs.renameSync(req.file.path, join(path, req.file.filename));
return res.send({ code: 200 });
}
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
if (!originFilename) { if (!originFilename) {
originFilename = filename; originFilename = filename;
} }
const originFilePath = join( const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
path, const filePath = `${path}${filename.replace(/\//g, '')}`;
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) { if (fs.existsSync(originFilePath)) {
if (!fs.existsSync(config.bakPath)) {
fs.mkdirSync(config.bakPath);
}
fs.copyFileSync( fs.copyFileSync(
originFilePath, originFilePath,
`${config.bakPath}${originFilename.replace(/\//g, '')}`, `${config.bakPath}${originFilename.replace(/\//g, '')}`,
@@ -133,6 +152,7 @@ export default (app: Router) => {
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -144,7 +164,7 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().required().allow(''), content: Joi.string().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -159,6 +179,7 @@ export default (app: Router) => {
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -170,25 +191,20 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional()
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path, type } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
if (type === 'directory') { fs.unlinkSync(filePath);
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -219,6 +235,7 @@ export default (app: Router) => {
return next(err); return next(err);
}); });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -229,22 +246,22 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body; let { filename, path } = req.body as {
const { name, ext } = parse(filename); filename: string;
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); path: string;
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' }); };
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath); const result = await scriptService.runScript(filePath);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -255,21 +272,22 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body; let { filename, path } = req.body as {
const { name, ext } = parse(filename); filename: string;
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath); const result = await scriptService.stopScript(filePath);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
-285
View File
@@ -1,285 +0,0 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import SubscriptionService from '../services/subscription';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
app.use('/subscriptions', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.list(
req.query.searchValue as string,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional().allow('').allow(null),
interval_schedule: Joi.object({
type: Joi.string().required(),
value: Joi.number().min(1).required(),
})
.optional()
.allow('')
.allow(null),
name: Joi.string().optional().allow('').allow(null),
url: Joi.string().required(),
whitelist: Joi.string().optional().allow('').allow(null),
blacklist: Joi.string().optional().allow('').allow(null),
branch: Joi.string().optional().allow('').allow(null),
dependences: Joi.string().optional().allow('').allow(null),
pull_type: Joi.string().optional().allow('').allow(null),
pull_option: Joi.object().optional().allow('').allow(null),
extensions: Joi.string().optional().allow('').allow(null),
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(),
alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
return next(e);
}
},
);
route.put(
'/run',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.run(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.stop(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id/log',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.log(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional().allow('').allow(null),
interval_schedule: Joi.object().optional().allow('').allow(null),
name: Joi.string().optional().allow('').allow(null),
url: Joi.string().required(),
whitelist: Joi.string().optional().allow('').allow(null),
blacklist: Joi.string().optional().allow('').allow(null),
branch: Joi.string().optional().allow('').allow(null),
dependences: Joi.string().optional().allow('').allow(null),
pull_type: Joi.string().optional().allow('').allow(null),
pull_option: Joi.object().optional().allow('').allow(null),
schedule_type: Joi.string().optional().allow('').allow(null),
extensions: Joi.string().optional().allow('').allow(null),
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
typeof req.body.schedule === 'object' ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/status',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
status: Joi.string().required(),
pid: Joi.string().optional(),
log_path: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.status({
...req.body,
status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
});
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get(
'/:id/logs',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.logs(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
};
+5 -19
View File
@@ -7,9 +7,6 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { promiseExec } from '../config/util';
import dayjs from 'dayjs';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -25,19 +22,6 @@ export default (app: Router) => {
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1]; const version = currentVersionFile.match(versionRegx)![1];
const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
)
).replace('\n', '');
const lastCommitId = (
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
).replace('\n', '');
const branch = (
await promiseExec(
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
)
).replace('\n', '');
let isInitialized = true; let isInitialized = true;
if ( if (
@@ -53,9 +37,6 @@ export default (app: Router) => {
data: { data: {
isInitialized, isInitialized,
version, version,
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
}, },
}); });
} catch (e) { } catch (e) {
@@ -73,6 +54,7 @@ export default (app: Router) => {
const data = await systemService.getLogRemoveFrequency(); const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -94,6 +76,7 @@ export default (app: Router) => {
); );
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -108,6 +91,7 @@ export default (app: Router) => {
const result = await systemService.checkUpdate(); const result = await systemService.checkUpdate();
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -122,6 +106,7 @@ export default (app: Router) => {
const result = await systemService.updateSystem(); const result = await systemService.updateSystem();
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -142,6 +127,7 @@ export default (app: Router) => {
const result = await systemService.notify(req.body); const result = await systemService.notify(req.body);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
+15 -32
View File
@@ -1,26 +1,13 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import UserService from '../services/user'; import UserService from '../services/user';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import multer from 'multer'; import { getFileContentByName } from '../config/util';
import path from 'path';
import { v4 as uuidV4 } from 'uuid';
import config from '../config';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.uploadPath);
},
filename: function (req, file, cb) {
const ext = path.parse(file.originalname).ext;
const key = uuidV4();
cb(null, key + ext);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/user', route); app.use('/user', route);
@@ -39,6 +26,7 @@ export default (app: Router) => {
const data = await userService.login({ ...req.body }, req); const data = await userService.login({ ...req.body }, req);
return res.send(data); return res.send(data);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -53,6 +41,7 @@ export default (app: Router) => {
await userService.logout(req.platform); await userService.logout(req.platform);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -73,6 +62,7 @@ export default (app: Router) => {
await userService.updateUsernameAndPassword(req.body); await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' }); res.send({ code: 200, message: '更新成功' });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -87,7 +77,6 @@ export default (app: Router) => {
code: 200, code: 200,
data: { data: {
username: authInfo.username, username: authInfo.username,
avatar: authInfo.avatar,
twoFactorActivated: authInfo.twoFactorActivated, twoFactorActivated: authInfo.twoFactorActivated,
}, },
}); });
@@ -106,6 +95,7 @@ export default (app: Router) => {
const data = await userService.initTwoFactor(); const data = await userService.initTwoFactor();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -125,6 +115,7 @@ export default (app: Router) => {
const data = await userService.activeTwoFactor(req.body.code); const data = await userService.activeTwoFactor(req.body.code);
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -139,6 +130,7 @@ export default (app: Router) => {
const data = await userService.deactiveTwoFactor(); const data = await userService.deactiveTwoFactor();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -160,6 +152,7 @@ export default (app: Router) => {
const data = await userService.twoFactorLogin(req.body, req); const data = await userService.twoFactorLogin(req.body, req);
res.send(data); res.send(data);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -174,6 +167,7 @@ export default (app: Router) => {
const data = await userService.getLoginLog(); const data = await userService.getLoginLog();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -188,6 +182,7 @@ export default (app: Router) => {
const data = await userService.getNotificationMode(); const data = await userService.getNotificationMode();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -202,6 +197,7 @@ export default (app: Router) => {
const result = await userService.updateNotificationMode(req.body); const result = await userService.updateNotificationMode(req.body);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -222,6 +218,7 @@ export default (app: Router) => {
await userService.updateUsernameAndPassword(req.body); await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' }); res.send({ code: 200, message: '更新成功' });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, },
@@ -236,21 +233,7 @@ export default (app: Router) => {
const result = await userService.updateNotificationMode(req.body); const result = await userService.updateNotificationMode(req.body);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); logger.error('🔥 error: %o', e);
}
},
);
route.put(
'/avatar',
upload.single('avatar'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateAvatar(req.file!.filename);
res.send(result);
} catch (e) {
return next(e); return next(e);
} }
}, },
+10 -4
View File
@@ -1,23 +1,29 @@
import 'reflect-metadata'; // We need this in order to use @Decorators import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config'; import config from './config';
import express from 'express'; import express from 'express';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import path from 'path';
async function startServer() { async function startServer() {
const app = express(); const app = express();
await require('./loaders/db').default();
await require('./loaders/initFile').default(); await require('./loaders/initFile').default();
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await require('./loaders/app').default({ expressApp: app }); await require('./loaders/app').default({ expressApp: app });
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`); Logger.info(`
################################################
🛡️ Server listening on port: ${config.port} 🛡️
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
-1
View File
@@ -1 +0,0 @@
export const LOG_END_SYMBOL = '\n          ';
+3 -17
View File
@@ -4,21 +4,10 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
if (!process.env.QL_DIR) { const lastVersionFile = 'http://qn.whyour.cn/version.ts?v=2.12.1';
// 声明QL_DIR环境变量
let qlHomePath = path.join(__dirname, '../../');
// 生产环境
if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../');
}
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
}
const lastVersionFile = `https://qn.whyour.cn/version.ts`;
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const envFound = dotenv.config();
const rootPath = process.cwd();
const dataPath = path.join(rootPath, 'data/'); const dataPath = path.join(rootPath, 'data/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
@@ -26,7 +15,6 @@ const scriptPath = path.join(dataPath, 'scripts/');
const bakPath = path.join(dataPath, 'bak/'); const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/'); const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/');
const envFile = path.join(configPath, 'env.sh'); const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
@@ -70,7 +58,6 @@ export default {
confFile, confFile,
envFile, envFile,
dbPath, dbPath,
uploadPath,
configPath, configPath,
scriptPath, scriptPath,
samplePath, samplePath,
@@ -80,7 +67,6 @@ export default {
'cookie.sh', 'cookie.sh',
'crontab.list', 'crontab.list',
'env.sh', 'env.sh',
'token.json',
], ],
writePathList: [configPath, scriptPath], writePathList: [configPath, scriptPath],
bakPath, bakPath,
-184
View File
@@ -2,8 +2,6 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import got from 'got'; import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process';
import FormData from 'form-data';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -234,14 +232,6 @@ export async function fileExist(file: any) {
}); });
} }
export async function createFile(file: string, data: string = '') {
return new Promise((resolve) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);
resolve(true);
});
}
export async function concurrentRun( export async function concurrentRun(
fnList: Array<() => Promise<any>> = [], fnList: Array<() => Promise<any>> = [],
max = 5, max = 5,
@@ -278,177 +268,3 @@ export async function concurrentRun(
return replyList; return replyList;
} }
enum FileType {
'directory',
'file',
}
interface IFile {
title: string;
key: string;
type: 'directory' | 'file',
parent: string;
mtime: number;
children?: IFile[],
}
export function dirSort(a: IFile, b: IFile) {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1
}
export function readDirs(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
if (stats.isDirectory()) {
return {
title: file,
key,
type: 'directory',
parent: relativePath,
mtime: stats.mtime.getTime(),
children: readDirs(subPath, baseDir).sort(dirSort),
};
}
return {
title: file,
type: 'file',
isLeaf: true,
key,
parent: relativePath,
mtime: stats.mtime.getTime(),
};
});
return result.sort(dirSort);
}
export function readDir(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
return {
title: file,
type: stats.isDirectory() ? 'directory' : 'file',
key,
parent: relativePath,
};
});
return result;
}
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
export function parseHeaders(headers: string) {
if (!headers) return {};
const parsed: any = {};
let key;
let val;
let i;
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
}
export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
) {
if (!body) return '';
const parsed: any = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
+1 -1
View File
@@ -34,7 +34,7 @@ export class Crontab {
this.isDisabled = options.isDisabled || 0; this.isDisabled = options.isDisabled || 0;
this.log_path = options.log_path || ''; this.log_path = options.log_path || '';
this.isPinned = options.isPinned || 0; this.isPinned = options.isPinned || 0;
this.labels = options.labels || []; this.labels = options.labels || [''];
this.last_running_time = options.last_running_time || 0; this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0; this.last_execution_time = options.last_execution_time || 0;
} }
-62
View File
@@ -1,62 +0,0 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
export enum CronViewType {
'系统' = 1,
'个人',
}
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
property: string;
operation: string;
value: string;
}
export class CrontabView {
name?: string;
id?: number;
position?: number;
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
filterRelation?: 'and' | 'or';
type?: CronViewType;
constructor(options: CrontabView) {
this.name = options.name;
this.id = options.id;
this.position = options.position;
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
this.filterRelation = options.filterRelation;
this.type = options.type || CronViewType.;
}
}
interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
'CrontabView',
{
name: {
unique: 'name',
type: DataTypes.STRING,
},
position: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
filterRelation: {
type: DataTypes.STRING,
allowNull: true,
},
type: DataTypes.NUMBER,
},
);
+4 -4
View File
@@ -37,15 +37,15 @@ export enum DependenceTypes {
} }
export enum InstallDependenceCommandTypes { export enum InstallDependenceCommandTypes {
'pnpm add -g', 'npm i -g --force',
'pip3 install', 'pip3 install',
'apk add', 'apk add --no-cache -f',
} }
export enum unInstallDependenceCommandTypes { export enum unInstallDependenceCommandTypes {
'pnpm remove -g', 'npm uninstall -g --force',
'pip3 uninstall -y', 'pip3 uninstall -y',
'apk del', 'apk del -f',
} }
interface DependenceInstance interface DependenceInstance
+1 -4
View File
@@ -26,10 +26,7 @@ export enum EnvStatus {
'disabled', 'disabled',
} }
export const maxPosition = 9000000000000000; export const initEnvPosition = 9999999999;
export const initPosition = 4500000000000000;
export const stepPosition = 10000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {} interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', { export const EnvModel = sequelize.define<EnvInstance>('Env', {
+3 -10
View File
@@ -1,22 +1,15 @@
import { Sequelize, Transaction } from 'sequelize'; import { Sequelize } from 'sequelize';
import config from '../config/index'; import config from '../config/index';
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`, storage: `${config.dbPath}database.sqlite`,
logging: false, logging: false,
retry: {
max: 10,
match: ['SQLITE_BUSY: database is locked'],
},
pool: { pool: {
max: 5, max: 6,
min: 2, min: 0,
idle: 30000, idle: 30000,
acquire: 30000,
evict: 10000,
}, },
transactionType: Transaction.TYPES.IMMEDIATE,
}); });
export type ResponseType<T> = { code: number; data?: T; message?: string }; export type ResponseType<T> = { code: number; data?: T; message?: string };
+2 -39
View File
@@ -1,22 +1,16 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify', 'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan', 'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer', 'pushDeer' = 'pushDeer',
'bark' = 'bark', 'bark' = 'bark',
'chat' = 'chat',
'telegramBot' = 'telegramBot', 'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot', 'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp', 'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'email' = 'email', 'email' = 'email',
'feishu' = 'feishu',
'webhook' = 'webhook',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -41,17 +35,11 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class PushDeerNotification extends NotificationBaseInfo { export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = ''; public pushDeerKey = '';
public pushDeerUrl = '';
}
export class ChatNotification extends NotificationBaseInfo {
public chatUrl = '';
public chatToken = '';
} }
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'https://qn.whyour.cn/logo.png'; public barkIcon = 'http://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
} }
@@ -78,12 +66,6 @@ export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = ''; public weWorkAppKey = '';
} }
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
}
export class IGotNotification extends NotificationBaseInfo { export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = ''; public iGotPushKey = '';
} }
@@ -99,35 +81,16 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = ''; public emailPass: string = '';
} }
export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = '';
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
}
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification, PushDeerNotification,
ChatNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
WeWorkBotNotification, WeWorkBotNotification,
WeWorkAppNotification, WeWorkAppNotification,
AibotkNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
EmailNotification, EmailNotification {}
WebhookNotification,
LarkNotification {}
+1 -3
View File
@@ -13,7 +13,5 @@ export class SockMessage {
export type SockMessageType = export type SockMessageType =
| 'ping' | 'ping'
| 'installDependence' | 'installDependence'
| 'uninstallDependence'
| 'updateSystemVersion' | 'updateSystemVersion'
| 'manuallyRunScript' | 'manuallyRunScript';
| 'runSubscriptionEnd';
-109
View File
@@ -1,109 +0,0 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { SimpleIntervalSchedule } from 'toad-scheduler';
type SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;
export class Subscription {
id?: number;
name?: string;
type?: 'public-repo' | 'private-repo' | 'file';
schedule_type?: 'crontab' | 'interval';
schedule?: string;
interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };
url?: string;
whitelist?: string;
blacklist?: string;
dependences?: string;
branch?: string;
status?: SubscriptionStatus;
pull_type?: 'ssh-key' | 'user-pwd';
pull_option?:
| { private_key: string }
| { username: string; password: string };
pid?: number;
is_disabled?: 1 | 0;
log_path?: string;
alias: string;
command?: string;
extensions?: string;
sub_before?: string;
sub_after?: string;
proxy?: string;
constructor(options: Subscription) {
this.id = options.id;
this.name = options.name || options.alias;
this.type = options.type;
this.schedule = options.schedule;
this.status =
options.status && SubscriptionStatus[options.status]
? options.status
: SubscriptionStatus.idle;
this.url = options.url;
this.whitelist = options.whitelist;
this.blacklist = options.blacklist;
this.dependences = options.dependences;
this.branch = options.branch;
this.pull_type = options.pull_type;
this.pull_option = options.pull_option;
this.pid = options.pid;
this.is_disabled = options.is_disabled;
this.log_path = options.log_path;
this.schedule_type = options.schedule_type;
this.alias = options.alias;
this.interval_schedule = options.interval_schedule;
this.extensions = options.extensions;
this.sub_before = options.sub_before;
this.sub_after = options.sub_after;
this.proxy = options.proxy;
}
}
export enum SubscriptionStatus {
'running',
'idle',
'disabled',
'queued',
}
interface SubscriptionInstance
extends Model<Subscription, Subscription>,
Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
'Subscription',
{
name: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
url: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
schedule: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
interval_schedule: {
unique: 'compositeIndex',
type: DataTypes.JSON,
},
type: DataTypes.STRING,
whitelist: DataTypes.STRING,
blacklist: DataTypes.STRING,
status: DataTypes.NUMBER,
dependences: DataTypes.STRING,
extensions: DataTypes.STRING,
sub_before: DataTypes.STRING,
sub_after: DataTypes.STRING,
branch: DataTypes.STRING,
pull_type: DataTypes.STRING,
pull_option: DataTypes.JSON,
pid: DataTypes.NUMBER,
is_disabled: DataTypes.NUMBER,
log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true },
},
);
+5 -7
View File
@@ -1,13 +1,14 @@
import expressLoader from './express'; import expressLoader from './express';
import depInjectorLoader from './depInjector'; import dependencyInjectorLoader from './dependencyInjector';
import Logger from './logger'; import Logger from './logger';
import initData from './initData'; import initData from './initData';
import { Application } from 'express'; import { Application } from 'express';
import linkDeps from './deps'; import linkDeps from './deps';
import initTask from './initTask';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
await depInjectorLoader(); await dependencyInjectorLoader({
models: [],
});
Logger.info('✌️ Dependency Injector loaded'); Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp }); await expressLoader({ app: expressApp });
@@ -17,8 +18,5 @@ export default async ({ expressApp }: { expressApp: Application }) => {
Logger.info('✌️ init data loaded'); Logger.info('✌️ init data loaded');
await linkDeps(); await linkDeps();
Logger.info('✌️ link deps loaded'); Logger.info('✌️ link deps');
initTask();
Logger.info('✌️ init task loaded');
}; };
+9 -20
View File
@@ -6,11 +6,9 @@ import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open'; import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth'; import { AuthModel } from '../data/auth';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config';
import { sequelize } from '../data'; import { sequelize } from '../data';
import { fileExist } from '../config/util';
import config from '../config';
export default async () => { export default async () => {
try { try {
@@ -19,23 +17,14 @@ export default async () => {
await AppModel.sync(); await AppModel.sync();
await AuthModel.sync(); await AuthModel.sync();
await EnvModel.sync(); await EnvModel.sync();
await SubscriptionModel.sync(); await sequelize.sync();
await CrontabViewModel.sync();
// 初始化新增字段 // try {
try { // const queryInterface = sequelize.getQueryInterface();
await sequelize.query( // await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
'alter table CrontabViews add column filterRelation VARCHAR(255)', // await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
); // await queryInterface.addIndex('Apps', ['name'], { unique: true });
} catch (error) {} // } catch (error) { }
try {
await sequelize.query(
'alter table Subscriptions add column proxy VARCHAR(255)',
);
} catch (error) {}
try {
await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {}
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
@@ -1,8 +1,12 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import LoggerInstance from './logger'; import LoggerInstance from './logger';
export default () => { export default ({ models }: { models: { name: string; model: any }[] }) => {
try { try {
models.forEach((m) => {
Container.set(m.name, m.model);
});
Container.set('logger', LoggerInstance); Container.set('logger', LoggerInstance);
} catch (e) { } catch (e) {
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e); LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
-26
View File
@@ -2,7 +2,6 @@ import path from 'path';
import fs from 'fs'; import fs from 'fs';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import config from '../config/index'; import config from '../config/index';
import { promiseExec } from '../config/util';
function linkToNodeModule(src: string, dst?: string) { function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src); const target = path.join(config.rootPath, 'node_modules', dst || src);
@@ -17,32 +16,7 @@ function linkToNodeModule(src: string, dst?: string) {
}); });
} }
async function linkCommand() {
const commandPath = await promiseExec('which node');
const commandDir = path.dirname(commandPath);
const linkShell = [
{
src: 'update.sh',
dest: 'ql',
},
{
src: 'task.sh',
dest: 'task',
},
];
for (const link of linkShell) {
const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest);
if (fs.existsSync(target)) {
fs.unlinkSync(target);
}
fs.symlink(source, target, (err) => {});
}
}
export default async (src: string = 'deps') => { export default async (src: string = 'deps') => {
await linkCommand();
linkToNodeModule(src); linkToNodeModule(src);
const source = path.join(config.rootPath, src); const source = path.join(config.rootPath, src);
+15 -45
View File
@@ -1,9 +1,9 @@
import express, { Request, Response, NextFunction, Application } from 'express'; import { Request, Response, NextFunction, Application } from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import cors from 'cors'; import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import jwt, { UnauthorizedError } from 'express-jwt'; import jwt from 'express-jwt';
import fs from 'fs'; import fs from 'fs';
import { getPlatform, getToken } from '../config/util'; import { getPlatform, getToken } from '../config/util';
import Container from 'typedi'; import Container from 'typedi';
@@ -13,12 +13,10 @@ import UserService from '../services/user';
import handler from 'serve-handler'; import handler from 'serve-handler';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { errors } from 'celebrate';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
app.use(cors()); app.use(cors());
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => { app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) { if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
@@ -75,24 +73,29 @@ export default ({ app }: { app: Application }) => {
if ( if (
!headerToken && !headerToken &&
originPath && originPath &&
config.apiWhiteList.includes(originPath) config.apiWhiteList.includes(originPath) &&
originPath !== '/api/crons/status'
) {
return next();
}
const remoteAddress = req.socket.remoteAddress;
if (
remoteAddress === '::ffff:127.0.0.1' &&
originPath === '/api/crons/status'
) { ) {
return next(); return next();
} }
const data = fs.readFileSync(config.authConfigFile, 'utf8'); const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data && headerToken) { if (data) {
const { token = '', tokens = {} } = JSON.parse(data); const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
} }
const errorCode = headerToken ? 'invalid_token' : 'credentials_required'; const err: any = new Error('UnauthorizedError');
const errorMessage = headerToken err.status = 401;
? 'jwt malformed'
: 'No authorization token was found';
const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err); next(err);
}); });
@@ -130,7 +133,7 @@ export default ({ app }: { app: Application }) => {
next(err); next(err);
}); });
app.use(errors()); app.use(Sentry.Handlers.errorHandler());
app.use( app.use(
( (
@@ -149,39 +152,6 @@ export default ({ app }: { app: Application }) => {
}, },
); );
app.use(
(
err: Error & { errors: any[] },
req: Request,
res: Response,
next: NextFunction,
) => {
if (err.name.includes('Sequelize')) {
return res
.status(500)
.send({
code: 400,
message: `${err.name} ${err.message}`,
validation: err.errors,
})
.end();
}
return next(err);
},
);
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use( app.use(
( (
err: Error & { status: number }, err: Error & { status: number },
+47 -28
View File
@@ -4,31 +4,19 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import groupBy from 'lodash/groupBy'; import _ from 'lodash';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import config from '../config'; import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService);
// 初始化新增默认全部任务视图 const scheduleService = Container.get(ScheduleService);
CrontabViewModel.findAll({
where: { type: CronViewType., name: '全部任务' },
raw: true,
}).then((docs) => {
if (docs.length === 0) {
CrontabViewModel.create({
name: '全部任务',
type: CronViewType.,
position: initPosition / 2,
});
}
});
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update( await CrontabModel.update(
@@ -37,17 +25,18 @@ export default async () => {
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({ DependenceModel.findAll({ where: {} }).then(async (docs) => {
where: {}, const groups = _.groupBy(docs, 'type');
order: [['type', 'DESC']], for (const key in groups) {
raw: true, if (Object.prototype.hasOwnProperty.call(groups, key)) {
}).then(async (docs) => { const group = groups[key];
const groups = groupBy(docs, 'type'); const depIds = group.map((x) => x.id);
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a)); for (const dep of depIds) {
for (const key of keys) { if (dep) {
const group = groups[key]; await dependenceService.reInstall([dep]);
const depIds = group.map((x) => x.id); }
await dependenceService.reInstall(depIds as number[]); }
}
} }
}); });
@@ -130,4 +119,34 @@ export default async () => {
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await envService.set_envs(); await envService.set_envs();
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const cron = {
id: data.id,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
};
await scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
runImmediately: true,
});
}
}; };
function randomSchedule(from: number, to: number) {
const result: any[] = [];
const arr = [...Array(from).keys()];
let count = arr.length;
for (let i = 0; i < to; i++) {
const index = ~~(Math.random() * count) + i;
if (result.includes(arr[index])) {
continue;
}
result[i] = arr[index];
arr[index] = arr[i];
count--;
}
return result;
}
+1 -21
View File
@@ -1,24 +1,19 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import Logger from './logger'; import Logger from './logger';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
const rootPath = process.env.QL_DIR as string;; const rootPath = process.cwd();
const dataPath = path.join(rootPath, 'data/'); const dataPath = path.join(rootPath, 'data/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json'); const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh');
export default async () => { export default async () => {
const authFileExist = await fileExist(authConfigFile); const authFileExist = await fileExist(authConfigFile);
@@ -26,9 +21,6 @@ export default async () => {
const scriptDirExist = await fileExist(scriptPath); const scriptDirExist = await fileExist(scriptPath);
const logDirExist = await fileExist(logPath); const logDirExist = await fileExist(logPath);
const configDirExist = await fileExist(configPath); const configDirExist = await fileExist(configPath);
const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath);
if (!configDirExist) { if (!configDirExist) {
fs.mkdirSync(configPath); fs.mkdirSync(configPath);
@@ -50,18 +42,6 @@ export default async () => {
fs.mkdirSync(logPath); fs.mkdirSync(logPath);
} }
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
}
if (!sshDirExist) {
fs.mkdirSync(sshPath);
}
if (!bakDirExist) {
fs.mkdirSync(bakPath);
}
dotenv.config({ path: confFile }); dotenv.config({ path: confFile });
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
-51
View File
@@ -1,51 +0,0 @@
import { Container } from 'typedi';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
export default async () => {
const systemService = Container.get(SystemService);
const scheduleService = Container.get(ScheduleService);
const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `ts-node-transpile-only ${config.rootPath}/back/token.ts`;
const tokenFile = `${config.rootPath}static/build/token.js`;
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: NaN,
name: '生成token',
command: tokenCommand,
};
scheduleService.createIntervalTask(cron, {
days: 28,
});
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const cron = {
id: data.id,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
};
scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
});
}
// 运行所有订阅
const subs = await subscriptionService.list();
for (const sub of subs) {
await subscriptionService.handleTask(
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
}
};
+2 -9
View File
@@ -2,23 +2,16 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing'; import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config';
import fs from 'fs';
export default ({ expressApp }: { expressApp: Application }) => { export default ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({ Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1,
release: currentVersion, tracesSampleRate: 1.0,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+6 -2
View File
@@ -10,7 +10,7 @@ app.get('/api/public/panel/log', (req, res) => {
'pm2 logs panel --lines 500 --nostream --timestamp', 'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => { (err, stdout, stderr) => {
if (err || stderr) { if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr }); return res.send({ code: 400, data: err || stderr });
} }
return res.send({ code: 200, data: stdout }); return res.send({ code: 200, data: stdout });
}, },
@@ -22,7 +22,11 @@ app
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`); Logger.info(`
################################################
🛡️ Public listening on port: ${config.publicPort} 🛡️
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+7 -4
View File
@@ -18,15 +18,14 @@ const run = async () => {
_schedule && _schedule &&
_schedule.length > 5 && _schedule.length > 5 &&
task.status !== CrontabStatus.disabled && task.status !== CrontabStatus.disabled &&
!task.isDisabled && !task.isDisabled
task.schedule
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) { if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`; command = `task ${command}`;
} }
exec(`ID=${task.id} ${command}`); exec(command);
}); });
} }
} }
@@ -44,7 +43,11 @@ app
await require('./loaders/db').default(); await require('./loaders/db').default();
await run(); await run();
Logger.debug('定时任务服务启动成功!'); Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+44 -179
View File
@@ -10,8 +10,6 @@ import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class CronService { export default class CronService {
@@ -114,70 +112,9 @@ export default class CronService {
} }
} }
private formatViewQuery(query: any, viewQuery: any) { public async crontabs(searchText?: string): Promise<Crontab[]> {
if (viewQuery.filters && viewQuery.filters.length > 0) { let query = {};
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
if (!query[primaryOperate]) {
query[primaryOperate] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let q: any = {};
let operate2 = null;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
operate2 = Op.or;
break;
case 'NotReg':
operate = Op.notLike;
operate2 = Op.and;
break;
case 'In':
q[Op.or] = [
{
[property]: value,
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
: {},
];
break;
case 'Nin':
q[Op.and] = [
{
[property]: {
[Op.notIn]: value,
},
},
property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } }
: {},
];
break;
default:
break;
}
if (operate && operate2) {
q[property] = {
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
};
}
query[primaryOperate].push(q);
}
}
}
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) { if (searchText) {
if (!query[Op.and]) {
query[Op.and] = [];
}
let q: any = {};
const textArray = searchText.split(':'); const textArray = searchText.split(':');
switch (textArray[0]) { switch (textArray[0]) {
case 'name': case 'name':
@@ -185,11 +122,13 @@ export default class CronService {
case 'schedule': case 'schedule':
case 'label': case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0]; const column = textArray[0] === 'label' ? 'labels' : textArray[0];
q[column] = { query = {
[Op.or]: [ [column]: {
{ [Op.like]: `%${textArray[1]}%` }, [Op.or]: [
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` }, { [Op.like]: `%${textArray[1]}%` },
], { [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
],
},
}; };
break; break;
default: default:
@@ -199,96 +138,31 @@ export default class CronService {
{ [Op.like]: `%${encodeURIComponent(searchText)}%` }, { [Op.like]: `%${encodeURIComponent(searchText)}%` },
], ],
}; };
q[Op.or] = [ query = {
{ [Op.or]: [
name: reg, {
}, name: reg,
{ },
command: reg, {
}, command: reg,
{ },
schedule: reg, {
}, schedule: reg,
{ },
labels: reg, {
}, labels: reg,
]; },
],
};
break; break;
} }
query[Op.and].push(q);
}
}
private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) {
if (!query[Op.and]) {
query[Op.and] = [];
}
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (filterKeys[key]) {
q[key] = filterKeys[key];
}
query[Op.and].push(q);
}
}
}
private formatViewSort(order: string[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]);
}
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
size: string;
sorter: string;
filters: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const viewQuery = JSON.parse(params?.queryString || '{}');
const filterQuery = JSON.parse(params?.filters || '{}');
const sorterQuery = JSON.parse(params?.sorter || '{}');
let query: any = {};
let order = [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
];
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatFilterQuery(query, filterQuery);
this.formatViewSort(order, viewQuery);
if (sorterQuery) {
const { field, type } = sorterQuery;
if (field && type) {
order.unshift([field, type]);
}
}
let condition: any = {
where: query,
order: order,
};
if (page && size) {
condition.offset = (page - 1) * size;
condition.limit = size;
} }
try { try {
const result = await CrontabModel.findAll(condition); const result = await CrontabModel.findAll({
const count = await CrontabModel.count({ where: query }); where: query,
return { data: result, total: count }; order: [['createdAt', 'DESC']],
});
return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -323,21 +197,13 @@ export default class CronService {
const err = await this.killTask(doc.command); const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? ` 耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) { if (logFileExist) {
const str = err ? `\n${err}` : ''; const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format( `${str}\n## 执行结束... ${new Date()
'YYYY-MM-DD HH:mm:ss', .toLocaleString('zh', { hour12: false })
)}${diffTimeStr}${LOG_END_SYMBOL}`, .replace(' 24:', ' 00:')} `,
); );
} }
} }
@@ -362,7 +228,6 @@ export default class CronService {
const killLogs = []; const killLogs = [];
if (pids && pids.length > 0) { if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个 // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) { for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`; const c = `kill -9 ${id.slice(1)}`;
try { try {
@@ -404,17 +269,11 @@ export default class CronService {
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`; cmdStr = `task ${cmdStr}`;
} }
if ( if (cmdStr.endsWith('.js')) {
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`; cmdStr = `${cmdStr} now`;
} }
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' }); const cp = spawn(cmdStr, { shell: '/bin/bash' });
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid }, { status: CrontabStatus.running, pid: cp.pid },
@@ -422,7 +281,7 @@ export default class CronService {
); );
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
if (logFileExist) { if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`); fs.appendFileSync(`${absolutePath}`, `${data}`);
} }
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
@@ -433,10 +292,16 @@ export default class CronService {
cp.on('exit', async (code, signal) => { cp.on('exit', async (code, signal) => {
this.logger.info( this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`, `${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
); );
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
}); });
cp.on('close', async (code) => { cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.idle, pid: undefined },
{ where: { id } }, { where: { id } },
@@ -566,7 +431,7 @@ export default class CronService {
private async set_crontab(needReloadSchedule: boolean = false) { private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule!.length !== 5) { if (tab.isDisabled === 1 || _schedule!.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
-117
View File
@@ -1,117 +0,0 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import {
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
}
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
await this.checkPosition(tab.position!);
return doc;
}
public async insert(payload: CrontabView): Promise<CrontabView> {
return await CrontabViewModel.create(payload, { returning: true });
}
public async update(payload: CrontabView): Promise<CrontabView> {
const newDoc = await this.updateDb(payload);
return newDoc;
}
public async updateDb(payload: CrontabView): Promise<CrontabView> {
await CrontabViewModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[]) {
await CrontabViewModel.destroy({ where: { id: ids } });
}
public async list(): Promise<CrontabView[]> {
try {
const result = await CrontabViewModel.findAll({
where: {},
order: [['position', 'DESC']],
});
return result;
} catch (error) {
throw error;
}
}
public async getDb(query: any): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
public async disabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.list();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async move({
id,
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
id: number;
}): Promise<CrontabView> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position! * 2
: views[toIndex].position! / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition);
return newDoc;
}
}
+39 -54
View File
@@ -9,11 +9,10 @@ import {
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
} from '../data/dependence'; } from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import { concurrentRun } from '../config/util';
import dayjs from 'dayjs';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
@@ -28,7 +27,7 @@ export default class DependenceService {
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
this.installDependenceOneByOne(docs); this.installOrUninstallDependencies(docs);
return docs; return docs;
} }
@@ -48,7 +47,7 @@ export default class DependenceService {
status: DependenceStatus.installing, status: DependenceStatus.installing,
}); });
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]); this.installOrUninstallDependencies([newDoc]);
return newDoc; return newDoc;
} }
@@ -57,13 +56,13 @@ export default class DependenceService {
return await this.getDb({ id: payload.id }); return await this.getDb({ id: payload.id });
} }
public async remove(ids: number[], force = false): Promise<Dependence[]> { public async remove(ids: number[]) {
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.removing, log: [] }, { status: DependenceStatus.removing, log: [] },
{ where: { id: ids } }, { where: { id: ids } },
); );
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs, false, force); this.installOrUninstallDependencies(docs, false);
return docs; return docs;
} }
@@ -99,20 +98,6 @@ export default class DependenceService {
} }
} }
private installDependenceOneByOne(
docs: Dependence[],
isInstall: boolean = true,
force: boolean = false,
) {
concurrentRun(
docs.map(
(dep) => async () =>
await this.installOrUninstallDependencies([dep], isInstall, force),
),
1,
);
}
public async reInstall(ids: number[]): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.installing, log: [] }, { status: DependenceStatus.installing, log: [] },
@@ -120,15 +105,12 @@ export default class DependenceService {
); );
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs); await this.installOrUninstallDependencies(docs);
return docs; return docs;
} }
private async find(query: any, sort: any = []): Promise<Dependence[]> { private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ const docs = await DependenceModel.findAll({ where: { ...query } });
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs; return docs;
} }
@@ -146,16 +128,12 @@ export default class DependenceService {
public installOrUninstallDependencies( public installOrUninstallDependencies(
dependencies: Dependence[], dependencies: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false,
) { ) {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
if (dependencies.length === 0) { if (dependencies.length === 0) {
resolve(null); resolve(null);
return; return;
} }
const socketMessageType = !force
? 'installDependence'
: 'uninstallDependence';
const depNames = dependencies.map((x) => x.name).join(' '); const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = ( const depRunCommand = (
isInstall isInstall
@@ -164,23 +142,24 @@ export default class DependenceService {
)[dependencies[0].type as any]; )[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除'; const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[]; const depIds = dependencies.map((x) => x.id) as number[];
const startTime = dayjs(); const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
const startTime = Date.now();
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n`;
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: 'installDependence',
message, message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}`,
references: depIds, references: depIds,
}); });
await this.updateLog(depIds, message); await this.updateLog(
depIds,
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: 'installDependence',
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
@@ -189,7 +168,7 @@ export default class DependenceService {
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: 'installDependence',
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
@@ -198,7 +177,7 @@ export default class DependenceService {
cp.on('error', async (err) => { cp.on('error', async (err) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: 'installDependence',
message: JSON.stringify(err), message: JSON.stringify(err),
references: depIds, references: depIds,
}); });
@@ -207,19 +186,23 @@ export default class DependenceService {
}); });
cp.on('close', async (code) => { cp.on('close', async (code) => {
const endTime = dayjs(); const endTime = Date.now();
const isSucceed = code === 0; const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败'; const resultText = isSucceed ? '成功' : '失败';
const message = `\n依赖${actionText}${resultText},结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({ this.sockService.sendMessage({
type: socketMessageType, type: 'installDependence',
message, message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds, references: depIds,
}); });
await this.updateLog(depIds, message); await this.updateLog(
depIds,
`依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
);
let status = null; let status = null;
if (isSucceed) { if (isSucceed) {
@@ -233,9 +216,11 @@ export default class DependenceService {
} }
await DependenceModel.update({ status }, { where: { id: depIds } }); await DependenceModel.update({ status }, { where: { id: depIds } });
// 如果删除依赖成功或者强制删除 // 如果删除依赖成功3秒后删除数据库记录
if ((isSucceed || force) && !isInstall) { if (isSucceed && !isInstall) {
this.removeDb(depIds); setTimeout(() => {
this.removeDb(depIds);
}, 5000);
} }
resolve(null); resolve(null);
+35 -62
View File
@@ -1,17 +1,10 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
Env, import _ from 'lodash';
EnvModel,
EnvStatus,
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
@Service() @Service()
@@ -20,22 +13,17 @@ export default class EnvService {
public async create(payloads: Env[]): Promise<Env[]> { public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
let position = initPosition; let position = initEnvPosition;
if ( if (envs && envs.length > 0 && envs[envs.length - 1].position) {
envs && position = envs[envs.length - 1].position as number;
envs.length > 0 &&
typeof envs[envs.length - 1].position === 'number'
) {
position = envs[envs.length - 1].position!;
} }
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
position = position - stepPosition; position = position / 2;
const tab = new Env({ ...x, position }); const tab = new Env({ ...x, position });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
await this.set_envs(); await this.set_envs();
await this.checkPosition(tabs[tabs.length - 1].position!);
return docs; return docs;
} }
@@ -79,40 +67,25 @@ export default class EnvService {
const envs = await this.envs(); const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) { if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward targetPosition = isUpward
? envs[0].position! + stepPosition ? envs[0].position * 2
: envs[toIndex].position! - stepPosition; : envs[toIndex].position / 2;
} else { } else {
targetPosition = isUpward targetPosition = isUpward
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2 ? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2; : (envs[toIndex].position + envs[toIndex + 1].position) / 2;
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
position: this.getPrecisionPosition(targetPosition), position: targetPosition,
}); });
await this.checkPosition(targetPosition);
return newDoc; return newDoc;
} }
private async checkPosition(position: number) { public async envs(
const precisionPosition = parseFloat(position.toPrecision(16)); searchText: string = '',
if (precisionPosition < minPosition || precisionPosition > maxPosition) { sort: any = { position: -1 },
const envs = await this.envs(); query: any = {},
let position = initPosition; ): Promise<Env[]> {
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURIComponent(searchText);
@@ -139,11 +112,7 @@ export default class EnvService {
}; };
} }
try { try {
const result = await this.find(condition, [ const result = await this.find(condition, [['position', 'DESC']]);
['status', 'ASC'],
['position', 'DESC'],
['createdAt', 'ASC'],
]);
return result as any; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -182,26 +151,30 @@ export default class EnvService {
} }
public async set_envs() { public async set_envs() {
const envs = await this.envs('', { const envs = await this.envs(
name: { [Op.not]: null }, '',
status: EnvStatus.normal, { position: -1 },
}); { name: { [Op.not]: null } },
const groups = groupBy(envs, 'name'); );
const groups = _.groupBy(envs, 'name');
let env_string = ''; let env_string = '';
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
let value = group let value = _(group)
.map((x) => x.value) .filter((x) => x.status !== EnvStatus.disabled)
.map('value')
.join('&') .join('&')
.replace(/(\\)[^n]/g, '\\\\') .replace(/ /g, '');
.replace(/(\\$)/, '\\\\') if (/"/.test(value)) {
.replace(/"/g, '\\"') value = `'${value}'`;
.trim(); } else {
env_string += `export ${key}="${value}"\n`; value = `"${value}"`;
}
env_string += `export ${key}=${value}\n`;
} }
} }
} }
+29 -152
View File
@@ -6,7 +6,6 @@ import got from 'got';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import crypto from 'crypto'; import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import { parseBody, parseHeaders } from '../config/util';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -18,27 +17,20 @@ export default class NotificationService {
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan], ['serverChan', this.serverChan],
['pushDeer', this.pushDeer], ['pushDeer', this.pushDeer],
['chat', this.chat],
['bark', this.bark], ['bark', this.bark],
['telegramBot', this.telegramBot], ['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot], ['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp], ['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['email', this.email], ['email', this.email],
['webhook', this.webhook],
['lark', this.lark],
]); ]);
private timeout = 10000;
private title = ''; private title = '';
private content = ''; private content = '';
private params!: Omit<NotificationInfo, 'type'>; private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 30000,
retry: 1,
};
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
@@ -81,7 +73,8 @@ export default class NotificationService {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params; const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, { .post(`${gotifyUrl}/message?token=${gotifyToken}`, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent( body: `title=${encodeURIComponent(
this.title, this.title,
)}&message=${encodeURIComponent( )}&message=${encodeURIComponent(
@@ -99,7 +92,8 @@ export default class NotificationService {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, { .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` }, json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
}) })
@@ -114,7 +108,8 @@ export default class NotificationService {
: `https://sc.ftqq.com/${serverChanKey}.send`; : `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `title=${this.title}&desp=${this.content}`, body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
@@ -123,11 +118,12 @@ export default class NotificationService {
} }
private async pushDeer() { private async pushDeer() {
const { pushDeerKey, pushDeerUrl } = this.params; const { pushDeerKey } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`; const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent( body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title, this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`, )}&desp=${encodeURIComponent(this.content)}&type=markdown`,
@@ -139,19 +135,6 @@ export default class NotificationService {
); );
} }
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
const res: any = await got
.post(url, {
...this.gotOption,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return res.success;
}
private async bark() { private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params; let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) { if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
@@ -164,7 +147,8 @@ export default class NotificationService {
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
@@ -202,7 +186,8 @@ export default class NotificationService {
} }
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`, body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent, agent,
@@ -224,7 +209,8 @@ export default class NotificationService {
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`; const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -241,7 +227,8 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`; const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -258,9 +245,10 @@ export default class NotificationService {
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] = const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(','); weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const tokenRes: any = await got const { access_token } = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
corpid, corpid,
corpsecret, corpsecret,
@@ -308,9 +296,10 @@ export default class NotificationService {
const res: any = await got const res: any = await got
.post( .post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`, `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
{ {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
touser, touser,
agentid, agentid,
@@ -324,53 +313,13 @@ export default class NotificationService {
return res.errcode === 0; return res.errcode === 0;
} }
private async aibotk() {
const { aibotkKey, aibotkType, aibotkName } = this.params;
let url = '';
let json = {};
switch (aibotkType) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${aibotkKey}`,
roomName: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${aibotkKey}`,
name: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(url, {
...this.gotOption,
json: {
...json,
},
})
.json();
return res.code === 0;
}
private async iGot() { private async iGot() {
const { iGotPushKey } = this.params; const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `title=${this.title}&content=${this.content}`, body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
@@ -384,7 +333,8 @@ export default class NotificationService {
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
token: `${pushPlusToken}`, token: `${pushPlusToken}`,
title: `${this.title}`, title: `${this.title}`,
@@ -397,21 +347,6 @@ export default class NotificationService {
return res.code === 200; return res.code === 200;
} }
private async lark() {
const { larkKey } = this.params;
const res: any = await got
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
...this.gotOption,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
})
.json();
return res.StatusCode === 0;
}
private async email() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
@@ -433,62 +368,4 @@ export default class NotificationService {
return !!info.messageId; return !!info.messageId;
} }
private async webhook() {
const {
webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
const options = {
method: webhookMethod,
headers,
...this.gotOption,
allowGetBody: true,
...bodyParam,
};
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
}
private formatBody(contentType: string, body: any): object {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
private formatNotifyContent(url: string, body: string) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
.replaceAll('$title', encodeURIComponent(this.title))
.replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
.replaceAll('$title', this.title)
.replaceAll('$content', this.content),
};
}
} }
+2 -27
View File
@@ -90,9 +90,7 @@ export default class OpenService {
} }
try { try {
const result = await this.find(condition); const result = await this.find(condition);
return result return result.map((x) => ({ ...x, tokens: [] }));
.filter((x) => x.name !== 'system')
.map((x) => ({ ...x, tokens: [] }));
} catch (error) { } catch (error) {
throw error; throw error;
} }
@@ -119,7 +117,7 @@ export default class OpenService {
(x) => x.expiration >= timestamp, (x) => x.expiration >= timestamp,
); );
let tokens = invalidTokens; let tokens = invalidTokens;
if (invalidTokens.length >= 5) { if (invalidTokens.length > 5) {
tokens = [ tokens = [
...invalidTokens.slice(0, 4), ...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration }, { ...invalidTokens[4], expiration },
@@ -144,27 +142,4 @@ export default class OpenService {
return { code: 400, message: 'client_id或client_seret有误' }; return { code: 400, message: 'client_id或client_seret有误' };
} }
} }
public async generateSystemToken(): Promise<{
value: string;
expiration: number;
}> {
let systemApp = (await AppModel.findOne({
where: { name: 'system' },
})) as App;
if (!systemApp) {
systemApp = await this.create({
name: 'system',
scopes: ['crons', 'system'],
} as App);
}
const { data } = await this.authToken({
client_id: systemApp.client_id,
client_secret: systemApp.client_secret,
});
return {
...data,
value: data.token,
};
}
} }
+74 -126
View File
@@ -1,36 +1,14 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process'; import { Crontab } from '../data/cron';
import { exec } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, SimpleIntervalJob,
AsyncTask, Task,
SimpleIntervalSchedule, SimpleIntervalSchedule,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs';
interface ScheduleTaskType {
id: number;
command: string;
name?: string;
schedule?: string;
}
export interface TaskCallbacks {
onBefore?: (startTime: dayjs.Dayjs) => Promise<void>;
onStart?: (
cp: ChildProcessWithoutNullStreams,
startTime: dayjs.Dayjs,
) => Promise<void>;
onEnd?: (
cp: ChildProcessWithoutNullStreams,
endTime: dayjs.Dayjs,
diff: number,
) => Promise<void>;
onLog?: (message: string) => Promise<void>;
onError?: (message: string) => Promise<void>;
}
@Service() @Service()
export default class ScheduleService { export default class ScheduleService {
@@ -42,74 +20,7 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask(command: string, callbacks: TaskCallbacks = {}) { async createCronTask({ id = 0, command, name, schedule = '' }: Crontab) {
return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
await callbacks.onBefore?.(startTime);
const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime);
cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString());
});
cp.stderr.on('data', async (data) => {
this.logger.error(
'执行任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
);
await callbacks.onError?.(data.toString());
});
cp.on('error', async (err) => {
this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
await callbacks.onError?.(JSON.stringify(error));
resolve(null);
}
});
}
async createCronTask(
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
callbacks?: TaskCallbacks,
runImmediately = false,
) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建cron任务],任务ID: %scron: %s,任务名: %s,执行命令: %s', '[创建cron任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
@@ -121,27 +32,53 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(id + '', schedule, async () => {
await this.runTask(command, callbacks); try {
exec(
command,
{ maxBuffer: this.maxBuffer },
async (error, stdout, stderr) => {
if (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
}
if (stderr) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
stderr,
);
}
},
);
} catch (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
} finally {
}
}), }),
); );
if (runImmediately) {
await this.runTask(command, callbacks);
}
} }
async cancelCronTask({ id = 0, name }: ScheduleTaskType) { async cancelCronTask({ id = 0, name }: Crontab) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消定时任务],任务名:%s', name); this.logger.info('[取消定时任务],任务名:%s', name);
this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel(); this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
} }
async createIntervalTask( async createIntervalTask(
{ id = 0, command, name = '' }: ScheduleTaskType, { id = 0, command, name = '' }: Crontab,
schedule: SimpleIntervalSchedule, schedule: SimpleIntervalSchedule,
runImmediately = true,
callbacks?: TaskCallbacks,
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
@@ -150,37 +87,48 @@ export default class ScheduleService {
name, name,
command, command,
); );
const task = new AsyncTask( const task = new Task(name, async () => {
name, try {
async () => { exec(
return new Promise(async (resolve, reject) => { command,
await this.runTask(command, callbacks); { maxBuffer: this.maxBuffer },
}); async (error, stdout, stderr) => {
}, if (error) {
(err) => { await this.logger.info(
this.logger.error( '执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
}
if (stderr) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
stderr,
);
}
},
);
} catch (error) {
await this.logger.info(
'执行任务%s失败,时间:%s, 错误信息:%j', '执行任务%s失败,时间:%s, 错误信息:%j',
command, command,
new Date().toLocaleString(), new Date().toLocaleString(),
err, error,
); );
}, } finally {
); }
});
const job = new LongIntervalJob( const job = new SimpleIntervalJob({ ...schedule }, task, _id);
{ runImmediately: false, ...schedule },
task,
_id,
);
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
if (runImmediately) {
await this.runTask(command, callbacks);
}
} }
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) { async cancelIntervalTask({ id = 0, name }: Crontab) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name); this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
this.intervalSchedule.removeById(_id); this.intervalSchedule.removeById(_id);
+33 -36
View File
@@ -1,12 +1,8 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import fs from 'fs'; import { spawn } from 'child_process';
import path from 'path';
import SockService from './sock'; import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -14,49 +10,50 @@ export default class ScriptService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private sockService: SockService, private sockService: SockService,
private cronService: CronService, private cronService: CronService,
private scheduleService: ScheduleService,
) {} ) {}
private taskCallbacks(filePath: string): TaskCallbacks { public async runScript(path: string) {
return { const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
onEnd: async (cp, endTime, diff) => {
try {
fs.unlinkSync(filePath);
} catch (error) {}
},
onError: async (message: string) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message,
});
},
onLog: async (message: string) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message,
});
},
};
}
public async runScript(filePath: string) { cp.stdout.on('data', (data) => {
const relativePath = path.relative(config.scriptPath, filePath); this.sockService.sendMessage({
const command = `task -l ${relativePath} now`; type: 'manuallyRunScript',
this.scheduleService.runTask(command, this.taskCallbacks(filePath)); message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: JSON.stringify(err),
});
});
cp.on('close', (err) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `执行结束`,
});
});
return { code: 200 }; return { code: 200 };
} }
public async stopScript(filePath: string) { public async stopScript(path: string) {
const relativePath = path.relative(config.scriptPath, filePath); const err = await this.cronService.killTask(`task -l ${path} now`);
const err = await this.cronService.killTask(`task -l ${relativePath} now`);
const str = err ? `\n${err}` : ''; const str = err ? `\n${err}` : '';
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'manuallyRunScript', type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date() message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false }) .toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`, .replace(' 24:', ' 00:')} `,
}); });
return { code: 200 }; return { code: 200 };
-97
View File
@@ -1,97 +0,0 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import os from 'os';
import path from 'path';
@Service()
export default class SshKeyService {
private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
constructor(@Inject('logger') private logger: winston.Logger) {}
private generatePrivateKeyFile(alias: string, key: string): void {
try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
encoding: 'utf8',
mode: '400',
});
} catch (error) {
this.logger.error('生成私钥文件失败', error);
}
}
private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void {
try {
fs.unlinkSync(`${this.sshPath}/${alias}`);
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(
alias: string,
host: string,
proxy?: string,
): string {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
}
private generateSshConfig(configs: string[]) {
try {
for (const config of configs) {
fs.appendFileSync(this.sshConfigFilePath, config, {
encoding: 'utf8',
});
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
}
private removeSshConfig(alias: string) {
try {
const configRegx = this.getConfigRegx(alias);
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(configRegx, '')
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
}
public addSSHKey(
key: string,
alias: string,
host: string,
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
}
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config);
}
}
-474
View File
@@ -1,474 +0,0 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import {
Subscription,
SubscriptionModel,
SubscriptionStatus,
} from '../data/subscription';
import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import {
getFileContentByName,
concurrentRun,
fileExist,
createFile,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
import SshKeyService from './sshKey';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class SubscriptionService {
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
private sshKeyService: SshKeyService,
) {}
public async list(searchText?: string): Promise<Subscription[]> {
let query = {};
if (searchText) {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
],
};
query = {
[Op.or]: [
{
name: reg,
},
{
url: reg,
},
],
};
}
try {
const result = await SubscriptionModel.findAll({
where: query,
order: [
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
});
return result as any;
} catch (error) {
throw error;
}
}
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
} = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask(
doc: Subscription,
needCreate = true,
needAddKey = true,
runImmediately = false,
) {
const { url, host } = this.formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
doc.proxy,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
}
}
doc.command = this.formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any);
needCreate &&
(await this.scheduleService.createCronTask(
doc as any,
this.taskCallbacks(doc),
runImmediately,
));
} else {
this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule as any;
needCreate &&
(await this.scheduleService.createIntervalTask(
doc as any,
{ [type]: value } as SimpleIntervalSchedule,
runImmediately,
this.taskCallbacks(doc),
));
}
}
private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
private async handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
return {
onBefore: async (startTime) => {
const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');
const logPath = `${doc.alias}/${logTime}.log`;
await SubscriptionModel.update(
{
status: SubscriptionStatus.running,
log_path: logPath,
},
{ where: { id: doc.id } },
);
const absolutePath = await this.handleLogPath(
logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
);
// 执行sub_before
let beforeStr = '';
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before);
}
} catch (error: any) {
beforeStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (beforeStr) {
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
}
},
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
await SubscriptionModel.update(
{
pid: cp.pid,
},
{ where: { id: doc.id } },
);
},
onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
// 执行 sub_after
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after);
}
} catch (error: any) {
afterStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (afterStr) {
fs.appendFileSync(absolutePath, `${afterStr}\n`);
}
fs.appendFileSync(
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}${LOG_END_SYMBOL}`,
);
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: sub.id } },
);
this.sockService.sendMessage({
type: 'runSubscriptionEnd',
message: '订阅执行完成',
references: [doc.id as number],
});
},
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
};
}
public async create(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload);
const doc = await this.insert(tab);
await this.handleTask(doc);
return doc;
}
public async insert(payload: Subscription): Promise<Subscription> {
return await SubscriptionModel.create(payload, { returning: true });
}
public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload);
await this.handleTask(newDoc, !newDoc.is_disabled);
return newDoc;
}
public async updateDb(payload: Subscription): Promise<Subscription> {
await SubscriptionModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async status({
ids,
status,
pid,
log_path,
last_running_time = 0,
last_execution_time = 0,
}: {
ids: number[];
status: SubscriptionStatus;
pid: number;
log_path: string;
last_running_time: number;
last_execution_time: number;
}) {
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return await SubscriptionModel.update(
{ ...options },
{ where: { id: ids } },
);
}
public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false, false);
}
await SubscriptionModel.destroy({ where: { id: ids } });
}
public async getDb(query: any): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription);
}
public async run(ids: number[]) {
await SubscriptionModel.update(
{ status: SubscriptionStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
}
public async stop(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
);
}
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: ids } },
);
}
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) {
return;
}
const command = this.formatCommand(subscription);
await this.scheduleService.runTask(
command,
this.taskCallbacks(subscription),
);
}
public async disabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false);
}
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc);
}
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
}
public async log(id: number) {
const doc = await this.getDb({ id });
if (!doc || !doc.log_path) {
return '';
}
const absolutePath = await this.handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
}
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
}
}
+15 -13
View File
@@ -2,6 +2,7 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@@ -51,14 +52,12 @@ export default class SystemService {
}); });
return { code: 200, data: { ...result, code } }; return { code: 200, data: { ...result, code } };
} else { } else {
return { code: 400, message: '通知发送失败,请检查参数' }; return { code: 400, data: '通知发送失败,请检查参数' };
} }
} }
public async updateLogRemoveFrequency(frequency: number) { public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getLogRemoveFrequency();
const result = await this.updateAuthDb({ const result = await this.updateAuthDb({
...oDoc,
type: AuthDataType.removeLogFrequency, type: AuthDataType.removeLogFrequency,
info: { frequency }, info: { frequency },
}); });
@@ -69,8 +68,9 @@ export default class SystemService {
}; };
await this.scheduleService.cancelIntervalTask(cron); await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) { if (frequency > 0) {
this.scheduleService.createIntervalTask(cron, { await this.scheduleService.createIntervalTask(cron, {
days: frequency, days: frequency,
runImmediately: true,
}); });
} }
return { code: 200, data: { ...cron } }; return { code: 200, data: { ...cron } };
@@ -87,12 +87,10 @@ export default class SystemService {
let lastVersion = ''; let lastVersion = '';
let lastLog = ''; let lastLog = '';
try { try {
const result = await got.get( const result = await got.get(config.lastVersionFile, {
`${config.lastVersionFile}?t=${Date.now()}`, timeout: 6000,
{ retry: 0,
timeout: 30000, });
},
);
const lastVersionFileContent = result.body; const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1]; lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx) lastLog = lastVersionFileContent.match(logRegx)
@@ -111,7 +109,7 @@ export default class SystemService {
} catch (error: any) { } catch (error: any) {
return { return {
code: 400, code: 400,
message: error.message, data: error.message,
}; };
} }
} }
@@ -138,6 +136,10 @@ export default class SystemService {
public async updateSystem() { public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' }); const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => { cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateSystemVersion', type: 'updateSystemVersion',
@@ -165,9 +167,9 @@ export default class SystemService {
public async notify({ title, content }: { title: string; content: string }) { public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content); const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) { if (isSuccess) {
return { code: 200, message: '通知发送成功' }; return { code: 200, data: '通知发送成功' };
} else { } else {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' }; return { code: 400, data: '通知发送失败,请检查参数' };
} }
} }
} }
+24 -43
View File
@@ -3,6 +3,7 @@ import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util'; import { createRandomString, getNetIp, getPlatform } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
@@ -12,7 +13,7 @@ import { Request } from 'express';
import ScheduleService from './schedule'; import ScheduleService from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import dayjs from 'dayjs'; import got from 'got';
@Service() @Service()
export default class UserService { export default class UserService {
@@ -64,37 +65,32 @@ export default class UserService {
return this.initAuthInfo(); return this.initAuthInfo();
} }
const retriesTime = Math.pow(3, retries) * 1000; if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 1000,
);
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: `失败次数过多,请${Math.round(
data: waitTime, (Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
}; )}秒后重试`,
} data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
if ( ),
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
}; };
} }
const { ip, address } = await getNetIp(req); const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) { if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20; const expiration = twoFactorActivated ? 30 : 3;
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration, expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384', algorithm: 'HS384',
@@ -115,7 +111,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${new Date(timestamp).toLocaleString()}${address} ${
req.platform req.platform
}端 登录成功,ip地址 ${ip}`, }端 登录成功,ip地址 ${ip}`,
); );
@@ -144,7 +140,7 @@ export default class UserService {
}); });
await this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${new Date(timestamp).toLocaleString()}${address} ${
req.platform req.platform
}端 登录失败,ip地址 ${ip}`, }端 登录失败,ip地址 ${ip}`,
); );
@@ -159,16 +155,7 @@ export default class UserService {
status: LoginStatus.fail, status: LoginStatus.fail,
}, },
}); });
if (retries > 2) { return { code: 400, message: config.authError };
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
} }
} else { } else {
return this.initAuthInfo(); return this.initAuthInfo();
@@ -234,12 +221,6 @@ export default class UserService {
return { code: 200, message: '更新成功' }; return { code: 200, message: '更新成功' };
} }
public async updateAvatar(avatar: string) {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' };
}
public getUserInfo(): Promise<any> { public getUserInfo(): Promise<any> {
return new Promise((resolve) => { return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => { fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
@@ -359,7 +340,7 @@ export default class UserService {
}); });
return { code: 200, data: { ...result, code } }; return { code: 200, data: { ...result, code } };
} else { } else {
return { code: 400, message: '通知发送失败,请检查参数' }; return { code: 400, data: '通知发送失败,请检查参数' };
} }
} }
} }
-34
View File
@@ -1,34 +0,0 @@
import 'reflect-metadata';
import OpenService from './services/open';
import { Container } from 'typedi';
import LoggerInstance from './loaders/logger';
import fs from 'fs';
import config from './config';
import path from 'path';
const tokenFile = path.join(config.configPath, 'token.json');
async function getToken() {
try {
Container.set('logger', LoggerInstance);
const openService = Container.get(OpenService);
const appToken = await openService.generateSystemToken();
console.log(appToken.value);
await writeFile({
value: appToken.value,
expiration: appToken.expiration,
});
} catch (error) {
console.log(error);
}
}
async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => {
resolve();
});
});
}
getToken();
+8 -10
View File
@@ -1,12 +1,11 @@
FROM python:alpine FROM node:alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=master
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
LANG=zh_CN.UTF-8 \ LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
@@ -29,19 +28,19 @@ RUN set -x \
perl \ perl \
openssl \ openssl \
nginx \ nginx \
nodejs \ python3 \
jq \ jq \
openssh \ openssh \
npm \ py3-pip \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& git config --global user.email "qinglong@@users.noreply.github.com" \ && git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \ && npm install -g pnpm \
&& pnpm add -g pm2 ts-node typescript tslib \ && pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ && git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
@@ -49,7 +48,6 @@ RUN set -x \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& pnpm install --prod \ && pnpm install --prod \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \ && rm -rf /root/.cache \
&& rm -rf /root/.npm \ && rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
+2 -7
View File
@@ -4,17 +4,12 @@ dir_shell=/ql/shell
. $dir_shell/share.sh . $dir_shell/share.sh
link_shell link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
make_dir /run/nginx make_dir /run/nginx
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
patch_version &>/dev/null
echo echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
@@ -42,13 +37,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >$dir_log/bot.log 2>&1 & nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >$dir_log/extra.log 2>&1 & nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+15 -15
View File
@@ -1,12 +1,12 @@
upstream baseApi { upstream api {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
upstream publicApi { upstream public {
server 0.0.0.0:5400; server 0.0.0.0:5400;
} }
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default keep-alive; default keep-alive;
'websocket' upgrade; 'websocket' upgrade;
} }
@@ -16,42 +16,42 @@ server {
root /ql/static/dist; root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location QL_BASE_URL/api/public/ { location /api/public {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/public/; proxy_pass http://public;
} }
location QL_BASE_URL/api/ { location /api {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/api/; proxy_pass http://api;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
} }
location QL_BASE_URL/open/ { location /open {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/; proxy_pass http://api;
} }
gzip on; gzip on;
gzip_static on; gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript; gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any; gzip_proxied any;
gzip_vary on; gzip_vary on;
gzip_comp_level 6; gzip_comp_level 6;
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.0; gzip_http_version 1.0;
location QL_BASE_URL/ { location / {
index index.html index.htm; index index.html index.htm;
try_files $uri $uri/ QL_BASE_URL/index.html; try_files $uri $uri/ /index.html;
} }
location ~ .*\.(html)$ { location ~ .*\.(html)$ {
+19 -19
View File
@@ -5,41 +5,41 @@ error_log /var/log/nginx/error.log warn;
include /etc/nginx/modules/*.conf; include /etc/nginx/modules/*.conf;
events { events {
worker_connections 1024; worker_connections 1024;
} }
http { http {
include /etc/nginx/mime.types; include /etc/nginx/mime.types;
default_type application/octet-stream; default_type application/octet-stream;
server_tokens off; server_tokens off;
client_max_body_size 20m; client_max_body_size 20m;
client_body_buffer_size 20m; client_body_buffer_size 20m;
keepalive_timeout 65; keepalive_timeout 65;
sendfile on; sendfile on;
tcp_nodelay on; tcp_nodelay on;
ssl_prefer_server_ciphers on; ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:2m; ssl_session_cache shared:SSL:2m;
gzip on; gzip on;
gzip_static on; gzip_static on;
gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript; gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript;
gzip_proxied any; gzip_proxied any;
gzip_vary on; gzip_vary on;
gzip_comp_level 6; gzip_comp_level 6;
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.0; gzip_http_version 1.0;
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" ' '$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"'; '"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main; access_log /var/log/nginx/access.log main;
include /etc/nginx/conf.d/*.conf; include /etc/nginx/conf.d/*.conf;
} }
+25 -54
View File
@@ -2,16 +2,16 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "umi dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node back/public.ts",
"build:front": "max build", "build:front": "umi build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.js", "schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js", "public": "npm run build:back && node static/build/public.js",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true", "prepare": "umi generate tmp",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
@@ -26,60 +26,34 @@
"prettier --parser=typescript --write" "prettier --parser=typescript --write"
] ]
}, },
"pnpm": {
"peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup"
],
"allowedVersions": {
"react": "18",
"react-dom": "18",
"dva-core": "2"
}
}
},
"dependencies": { "dependencies": {
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1", "@sentry/node": "^6.18.1",
"@sentry/tracing": "^7.12.1", "@sentry/tracing": "^6.18.1",
"body-parser": "^1.19.2", "body-parser": "^1.19.2",
"celebrate": "^15.0.1", "celebrate": "^15.0.1",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.2.1", "cron-parser": "^4.2.1",
"dayjs": "^1.11.2",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^3.2.1",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-queue": "7.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5", "sequelize": "^6.17.0",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6", "sqlite3": "npm:@louislam/sqlite3@^6.0.1",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -89,53 +63,50 @@
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "^4.3.1",
"@react-hook/resize-observer": "^1.2.6", "@sentry/react": "^6.18.1",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.179",
"@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12", "@types/nedb": "^1.8.12",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-fetch": "^2.6.1",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^17.0.39",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^17.0.13",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.21", "@umijs/plugin-antd": "^0.15.0",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/plugin-esbuild": "^1.4.1",
"@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.23.0", "antd": "^4.18.9",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"lint-staged": "^13.0.3", "darkreader": "4.9.44",
"lint-staged": "^12.3.4",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1", "react": "17.x",
"rc-tween-one": "^3.0.6",
"react": "18.2.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "18.2.0", "react-dom": "17.x",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"tslib": "^2.4.0", "typescript": "^4.6.2",
"typescript": "4.8.4", "umi": "^3.5.21",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.70.0", "webpack": "^5.70.0",
-13702
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

+4 -35
View File
@@ -16,14 +16,9 @@ DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py" RepoFileExtensions="js py"
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890 ## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
ProxyUrl="" ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 ## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h" CommandTimeoutTime="1h"
@@ -34,14 +29,6 @@ MaxConcurrentNum="5"
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空 ## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300" RandomDelay="300"
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true ## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true" EnableExtraShell="true"
@@ -51,11 +38,8 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot ## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl="" BotRepoUrl=""
## 安装python依赖时指定pip源 ## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
PipMirror="https://pypi.doubanio.com/simple/" PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
## 安装node依赖时指定npm源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量 ## 通知环境变量
## 1. Server酱 ## 1. Server酱
@@ -67,7 +51,7 @@ export PUSH_KEY=""
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123 ## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH="" export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上) ## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="https://qn.whyour.cn/logo.png" export BARK_ICON="http://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong" ## 下方填写推送消息分组,默认为"QingLong"
@@ -145,19 +129,4 @@ export GOTIFY_PRIORITY=0
## deer_key 填写PushDeer的key ## deer_key 填写PushDeer的key
export DEER_KEY="" export DEER_KEY=""
## 12. Chat
## chat_url 填写synology chat地址,http://IP:PORT/webapi/***token=
## chat_token 填写后面的token
export CHAT_URL=""
export CHAT_TOKEN=""
## 13. aibotk
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
## aibotk_key (必填)填写智能微秘书个人中心的apikey
export AIBOTK_KEY=""
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 添加你需要重启自动执行的任意命令,比如 ql repo ## 添加你需要重启自动执行的任意命令,比如 ql repo
## 安装node依赖使用 pnpm add -g xxx xxx ## 安装node依赖使用 pnpm install -g xxx xxx
## 安装python依赖使用 pip3 install xxx ## 安装python依赖使用 pip3 install xxx
+133 -232
View File
@@ -38,19 +38,12 @@ let SCKEY = '';
//此处填你申请的PushDeer KEY. //此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY) //(环境变量名 DEER_KEY)
let PUSHDEER_KEY = ''; let PUSHDEER_KEY = '';
let PUSHDEER_URL = '';
// =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN
//(环境变量名 CHAT_URL CHAT_TOKEN)
let CHAT_URL = '';
let CHAT_TOKEN = '';
// =======================================Bark App通知设置区域=========================================== // =======================================Bark App通知设置区域===========================================
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX) //此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = ''; let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上) //BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'https://qn.whyour.cn/logo.png'; let BARK_ICON = 'http://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
@@ -104,27 +97,6 @@ let IGOT_PUSH_KEY = '';
let PUSH_PLUS_TOKEN = ''; let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = ''; let PUSH_PLUS_USER = '';
// =======================================Cool Push设置区域=======================================
//官方文档:https://cp.xuthus.cc/docs
//QQ_SKEY: Cool Push登录授权后推送消息的调用代码Skey
//QQ_MODE: 推送模式详情请登录获取QQ_SKEY后见https://cp.xuthus.cc/feat
let QQ_SKEY = '';
let QQ_MODE = '';
// =======================================智能微秘书设置区域=======================================
//官方文档:http://wechat.aibotk.com/docs/about
//AIBOTK_KEY 填写智能微秘书个人中心的apikey
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
let AIBOTK_KEY = '';
let AIBOTK_TYPE = '';
let AIBOTK_NAME = '';
// =======================================飞书机器人设置区域=======================================
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
//FSKEY 飞书机器人的 FSKEY
let FSKEY = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@@ -152,15 +124,6 @@ if (process.env.PUSH_KEY) {
if (process.env.DEER_KEY) { if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY; PUSHDEER_KEY = process.env.DEER_KEY;
PUSHDEER_URL = process.env.DEER_URL;
}
if (process.env.CHAT_URL) {
CHAT_URL = process.env.CHAT_URL;
}
if (process.env.CHAT_TOKEN) {
CHAT_TOKEN = process.env.CHAT_TOKEN;
} }
if (process.env.QQ_SKEY) { if (process.env.QQ_SKEY) {
@@ -236,20 +199,6 @@ if (process.env.PUSH_PLUS_TOKEN) {
if (process.env.PUSH_PLUS_USER) { if (process.env.PUSH_PLUS_USER) {
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER; PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
} }
if (process.env.AIBOTK_KEY) {
AIBOTK_KEY = process.env.AIBOTK_KEY;
}
if (process.env.AIBOTK_TYPE) {
AIBOTK_TYPE = process.env.AIBOTK_TYPE;
}
if (process.env.AIBOTK_NAME) {
AIBOTK_NAME = process.env.AIBOTK_NAME;
}
if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@@ -283,10 +232,6 @@ async function sendNotify(
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp), //go-cqhttp gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
]); ]);
} }
@@ -327,7 +272,7 @@ function gotifyNotify(text, desp) {
}); });
} }
function gobotNotify(text, desp) { function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
const options = { const options = {
@@ -338,34 +283,38 @@ function gobotNotify(text, desp) {
}, },
timeout, timeout,
}; };
$.post(options, (err, resp, data) => { setTimeout(() => {
try { $.post(options, (err, resp, data) => {
if (err) { try {
console.log('发送go-cqhttp通知调用API失败!!\n'); if (err) {
console.log(err); console.log('发送go-cqhttp通知调用API失败!!\n');
} else { console.log(err);
data = JSON.parse(data);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`); data = JSON.parse(data);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
} catch (e) { });
$.logErr(e, resp); }, time);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
}); });
} }
function serverNotify(text, desp) { function serverNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (SCKEY) { if (SCKEY) {
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换 //微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
@@ -380,104 +329,152 @@ function serverNotify(text, desp) {
}, },
timeout, timeout,
}; };
$.post(options, (err, resp, data) => { setTimeout(() => {
try { $.post(options, (err, resp, data) => {
if (err) { try {
console.log('发送通知调用API失败!!\n'); if (err) {
console.log(err); console.log('发送通知调用API失败!!\n');
} else { console.log(err);
data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`); data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
} catch (e) { });
$.logErr(e, resp); }, time);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
}); });
} }
function PushDeerNotify(text, desp) { function PushDeerNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (PUSHDEER_KEY) { if (PUSHDEER_KEY) {
// PushDeer 建议对消息内容进行 urlencode // PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp); desp = encodeURI(desp);
const options = { const options = {
url: PUSHDEER_URL || `https://api2.pushdeer.com/message/push`, url: `https://api2.pushdeer.com/message/push`,
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`, body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
timeout, timeout,
}; };
$.post(options, (err, resp, data) => { setTimeout(() => {
try { $.post(options, (err, resp, data) => {
if (err) { try {
console.log('发送通知调用API失败!!\n'); if (err) {
console.log(err); console.log('发送通知调用API失败!!\n');
} else { console.log(err);
data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else { } else {
console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`); data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
console.log(
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
} catch (e) { });
$.logErr(e, resp); }, time);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
}); });
} }
function ChatNotify(text, desp) { function CoolPush(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (CHAT_URL && CHAT_TOKEN) { if (QQ_SKEY) {
// 对消息内容进行 urlencode let options = {
desp = encodeURI(desp); url: `https://push.xuthus.cc/${QQ_MODE}/${QQ_SKEY}`,
const options = {
url: `${CHAT_URL}${CHAT_TOKEN}`,
body: `payload={"text":"${text}\n${desp}"}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/json',
}, },
}; };
// 已知敏感词
text = text.replace(/京豆/g, '豆豆');
desp = desp.replace(/京豆/g, '');
desp = desp.replace(/🐶/g, '');
desp = desp.replace(/红包/g, 'H包');
switch (QQ_MODE) {
case 'email':
options.json = {
t: text,
c: desp,
};
break;
default:
options.body = `${text}\n\n${desp}`;
}
let pushMode = function (t) {
switch (t) {
case 'send':
return '个人';
case 'group':
return 'QQ群';
case 'wx':
return '微信';
case 'ww':
return '企业微信';
case 'email':
return '邮件';
default:
return '未知方式';
}
};
$.post(options, (err, resp, data) => { $.post(options, (err, resp, data) => {
try { try {
if (err) { if (err) {
console.log('发送通知调用API失败!!\n'); console.log(`发送${pushMode(QQ_MODE)}通知调用API失败!!\n`);
console.log(err); console.log(err);
} else { } else {
data = JSON.parse(data); data = JSON.parse(data);
if (data.success) { if (data.code === 200) {
console.log('Chat发送通知消息成功🎉\n'); console.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功🎉\n`);
} else if (data.code === 400) {
console.log(
`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${
data.msg
}\n`,
);
} else if (data.code === 503) {
console.log(`QQ酷推出错,${data.message}${data.data}\n`);
} else { } else {
console.log(`Chat发送通知消息异常\n${JSON.stringify(data)}`); console.log(`酷推推送异常: ${JSON.stringify(data)}`);
} }
} }
} catch (e) { } catch (e) {
$.logErr(e); $.logErr(e, resp);
} finally { } finally {
resolve(data); resolve(data);
} }
@@ -590,7 +587,7 @@ function ddBotNotify(text, desp) {
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
content: `${text}\n\n${desp}`, content: ` ${text}\n\n${desp}`,
}, },
}, },
headers: { headers: {
@@ -657,7 +654,7 @@ function qywxBotNotify(text, desp) {
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
content: `${text}\n\n${desp}`, content: ` ${text}\n\n${desp}`,
}, },
}, },
headers: { headers: {
@@ -726,9 +723,9 @@ function qywxamNotify(text, desp) {
timeout, timeout,
}; };
$.post(options_accesstoken, (err, resp, data) => { $.post(options_accesstoken, (err, resp, data) => {
let html = desp.replace(/\n/g, '<br/>'); html = desp.replace(/\n/g, '<br/>');
let json = JSON.parse(data); var json = JSON.parse(data);
let accesstoken = json.access_token; accesstoken = json.access_token;
let options; let options;
switch (QYWX_AM_AY[4]) { switch (QYWX_AM_AY[4]) {
@@ -924,102 +921,6 @@ function pushPlusNotify(text, desp) {
}); });
} }
function aibotkNotify(text, desp) {
return new Promise((resolve) => {
if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {
let json = {};
let url = '';
switch (AIBOTK_TYPE) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${AIBOTK_KEY}`,
roomName: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${AIBOTK_KEY}`,
name: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
}
const options = {
url: url,
json,
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('智能微秘书发送通知消息失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 0) {
console.log('智能微秘书发送通知消息成功🎉。\n');
} else {
console.log(`${data.error}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}
});
}
function fsBotNotify(text, desp) {
return new Promise((resolve) => {
if (FSKEY) {
const options = {
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.StatusCode === 0) {
console.log('飞书发送通知消息成功🎉\n');
} else {
console.log(`${data.msg}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,
+1 -68
View File
@@ -61,11 +61,7 @@ push_config = {
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -82,10 +78,6 @@ push_config = {
'TG_PROXY_AUTH': '', # tg 代理认证参数 'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST 'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT 'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -281,36 +273,14 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")} data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push' url = 'https://api2.pushdeer.com/message/push'
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0: if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!") print("PushDeer 推送成功!")
else: else:
print("PushDeer 推送失败!错误信息:", response) print("PushDeer 推送失败!错误信息:", response)
def chat(title: str, content: str) -> None:
"""
通过Chat 推送消息
"""
if not push_config.get("CHAT_URL") or not push_config.get("CHAT_TOKEN"):
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
if response.status_code == 200:
print("Chat 推送成功!")
else:
print("Chat 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -529,39 +499,6 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 推送失败!") print("tg 推送失败!")
def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息。
"""
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room':
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
print(response)
if response["code"] == 0:
print("智能微秘书 推送成功!")
else:
print(f'智能微秘书 推送失败!{response["error"]}')
def one() -> str: def one() -> str:
""" """
获取一条一言。 获取一条一言。
@@ -590,8 +527,6 @@ if push_config.get("PUSH_KEY"):
notify_function.append(serverJ) notify_function.append(serverJ)
if push_config.get("DEER_KEY"): if push_config.get("DEER_KEY"):
notify_function.append(pushdeer) notify_function.append(pushdeer)
if push_config.get("CHAT_URL") and push_config.get("CHAT_TOKEN"):
notify_function.append(chat)
if push_config.get("PUSH_PLUS_TOKEN"): if push_config.get("PUSH_PLUS_TOKEN"):
notify_function.append(pushplus_bot) notify_function.append(pushplus_bot)
if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"): if push_config.get("QMSG_KEY") and push_config.get("QMSG_TYPE"):
@@ -602,8 +537,6 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot) notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk)
def send(title: str, content: str) -> None: def send(title: str, content: str) -> None:
+1 -1
View File
@@ -17,7 +17,7 @@ const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config); const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra( const putExtra = new qiniu.form_up.PutExtra(
'', '',
{}, '',
'text/plain; charset=utf-8', 'text/plain; charset=utf-8',
); );
// 文件上传 // 文件上传
+178 -160
View File
@@ -1,187 +1,205 @@
#!/usr/bin/env bash #!/usr/bin/env bash
get_token() { get_token() {
token=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_user | jq -r .token)
} }
add_cron_api() { add_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功" echo -e "$name -> 添加成功"
else else
echo -e "$name -> 添加失败(${message})" echo -e "$name -> 添加失败(${message})"
fi fi
} }
update_cron_api() { update_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}') local id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
local id=$4 local id=$4
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功" echo -e "$name -> 更新成功"
else else
echo -e "$name -> 更新失败(${message})" echo -e "$name -> 更新失败(${message})"
fi fi
} }
update_cron_command_api() { update_cron_command_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
local command=$1 local command=$1
local id=$2 local id=$2
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功" echo -e "$command -> 更新成功"
else else
echo -e "$command -> 更新失败(${message})" echo -e "$command -> 更新失败(${message})"
fi fi
} }
del_cron_api() { del_cron_api() {
local ids=$1 local ids=$1
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons?t=$currentTimeStamp" \
-X 'DELETE' \ -X 'DELETE' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "成功" echo -e "成功"
else else
echo -e "失败(${message})" echo -e "失败(${message})"
fi fi
}
get_user_info() {
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/user?t=$currentTimeStamp" \
-H 'Accept: */*' \
-H "Authorization: Bearer $token" \
-H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36' \
-H 'Referer: http://0.0.0.0:5700/crontab' \
-H 'Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7' \
--compressed
)
code=$(echo $api | jq -r .code)
if [[ $code != 200 ]]; then
echo -e "请先登录!"
exit 0
fi
} }
update_cron() { update_cron() {
local ids="$1" local ids="$1"
local status="$2" local status="$2"
local pid="${3:-''}" local pid="${3:-''}"
local logPath="$4" local logPath="$4"
local lastExecutingTime="${5:-0}" local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}" local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/crons/status?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n" >> $log_path
fi fi
} }
notify_api() { notify_api() {
local title=$1 local title=$1
local content=$2 local content=$2
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/api/system/notify?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \ --data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功"
else else
echo -e "通知失败(${message})" echo -e "通知失败(${message})"
fi fi
} }
get_token get_token
+5 -4
View File
@@ -35,10 +35,11 @@ fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data" cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data cd $dir_data
cat requirements.txt | while read LREAD; do cat requirements.txt | while read LREAD
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then do
pip3 --default-timeout=100 install ${LREAD} if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
fi pip3 --default-timeout=100 install ${LREAD}
fi
done done
echo -e "\npython3依赖安装成功...\n" echo -e "\npython3依赖安装成功...\n"
+11 -11
View File
@@ -31,10 +31,19 @@ copy_dep() {
echo -e "---> 2. 复制nginx配置文件\n" echo -e "---> 2. 复制nginx配置文件\n"
cp -fv $nginx_conf /etc/nginx/nginx.conf cp -fv $nginx_conf /etc/nginx/nginx.conf
cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
echo -e "---> 配置文件复制完成\n" echo -e "---> 配置文件复制完成\n"
} }
reload_pm2() {
pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
}
pm2_log() { pm2_log() {
echo -e "---> pm2日志" echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log" local panelOut="/root/.pm2/logs/panel-out.log"
@@ -87,18 +96,9 @@ check_pm2() {
fi fi
} }
start_public() {
echo -e "=====> 启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm pnpm install -g pm2
pnpm add -g pm2
patch_version
start_public
copy_dep copy_dep
check_ql check_ql
check_nginx check_nginx
-273
View File
@@ -1,273 +0,0 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
check_server() {
local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
}
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
cat $single_log_path
[[ -f $single_log_path ]] && rm -f $single_log_path
done
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 运行其他命令
run_else() {
local file_param="$1"
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
$timeoutCmd $which_program $file_param "$@"
}
## 命令检测
main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
handle_task_before "$@"
main "$@"
handle_task_after "$@"
+1 -1
View File
@@ -29,4 +29,4 @@ git tag -a "$version" -m "release $version"
echo -e "提交tag" echo -e "提交tag"
git push --tags git push --tags
echo -e "完成发布" echo -e "完成发布"
+23 -23
View File
@@ -8,35 +8,35 @@ days=$1
## 删除运行js脚本的旧日志 ## 删除运行js脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
for log in $log_full_path_list; do for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠 local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s))) diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log [[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi fi
done done
} }
## 删除空文件夹 ## 删除空文件夹
remove_empty_dir() { remove_empty_dir() {
cd $dir_log cd $dir_log
for dir in $(ls); do for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir rm -rf $dir
fi fi
done done
} }
## 运行 ## 运行
if [[ ${days} ]]; then if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n" echo -e "查找旧日志文件中...\n"
remove_js_log remove_js_log
remove_empty_dir remove_empty_dir
echo -e "删除旧日志执行完毕\n" echo -e "删除旧日志执行完毕\n"
fi fi
+222 -341
View File
@@ -14,6 +14,7 @@ dir_log=$dir_data/log
dir_db=$dir_data/db dir_db=$dir_data/db
dir_dep=$dir_data/deps dir_dep=$dir_data/deps
dir_list_tmp=$dir_log/.tmp dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code
dir_update_log=$dir_log/update dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static ql_static_repo=$dir_repo/static
@@ -24,7 +25,6 @@ file_sharecode=$dir_config/sharecode.sh
file_config_user=$dir_config/config.sh file_config_user=$dir_config/config.sh
file_auth_sample=$dir_sample/auth.sample.json file_auth_sample=$dir_sample/auth.sample.json
file_auth_user=$dir_config/auth.json file_auth_user=$dir_config/auth.json
file_auth_token=$dir_config/token.json
file_extra_shell=$dir_config/extra.sh file_extra_shell=$dir_config/extra.sh
file_task_before=$dir_config/task_before.sh file_task_before=$dir_config/task_before.sh
file_task_after=$dir_config/task_after.sh file_task_after=$dir_config/task_after.sh
@@ -50,437 +50,318 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系 ## 软连接及其原始文件对应关系
link_name=( link_name=(
task task
ql ql
notify
) )
original_name=( original_name=(
task.sh task.sh
update.sh update.sh
notify.sh
) )
init_env() { init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
} }
import_config() { import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""} command_timeout_time=${CommandTimeoutTime:-"1h"}
command_timeout_time=${CommandTimeoutTime:-"1h"} proxy_url=${ProxyUrl:-""}
proxy_url=${ProxyUrl:-""} file_extensions=${RepoFileExtensions:-"js py"}
file_extensions=${RepoFileExtensions:-"js py"} current_branch=${QL_BRANCH}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}" default_cron="${DefaultCronRule}"
else else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn:-80}
mem_warn=${MemoryWarn:-80}
disk_warn=${DiskWarn:-90}
} }
set_proxy() { set_proxy() {
local proxy="$1" if [[ $proxy_url ]]; then
if [[ $proxy ]]; then export http_proxy="${proxy_url}"
proxy_url="$proxy" export https_proxy="${proxy_url}"
fi fi
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
} }
unset_proxy() { unset_proxy() {
unset http_proxy unset http_proxy
unset https_proxy unset https_proxy
} }
make_dir() { make_dir() {
local dir=$1 local dir=$1
if [[ ! -d $dir ]]; then if [[ ! -d $dir ]]; then
mkdir -p $dir mkdir -p $dir
fi fi
} }
detect_termux() { detect_termux() {
if [[ $PATH == *com.termux* ]]; then if [[ ${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT} ]] || [[ $PATH == *com.termux* ]]; then
is_termux=1 is_termux=1
else else
is_termux=0 is_termux=0
fi fi
} }
detect_macos() { detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0 [[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
} }
gen_random_num() { gen_random_num() {
local divi=$1 local divi=$1
echo $((${RANDOM} % $divi)) echo $((${RANDOM} % $divi))
} }
link_shell_sub() { link_shell_sub() {
local link_path="$1" local link_path="$1"
local original_path="$2" local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path ln -sf $original_path $link_path
fi fi
} }
link_shell() { link_shell() {
if [[ $is_termux -eq 1 ]]; then if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/" local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/" local path="/usr/local/bin/"
else else
local path="" local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n" echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi fi
if [[ $path ]]; then if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}" link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done done
fi fi
} }
define_cmd() { define_cmd() {
local cmd_prefix cmd_suffix local cmd_prefix cmd_suffix
if type task &>/dev/null; then if type task &>/dev/null; then
cmd_suffix="" cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="" cmd_prefix=""
else
cmd_prefix="bash "
fi
else else
cmd_prefix="bash " cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi fi
else for ((i = 0; i < ${#link_name[*]}; i++)); do
cmd_suffix=".sh" export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
if [[ -f "$dir_shell/task.sh" ]]; then done
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
} }
fix_config() { fix_config() {
make_dir $dir_static make_dir $dir_static
make_dir $dir_data make_dir $dir_data
make_dir $dir_config make_dir $dir_config
make_dir $dir_log make_dir $dir_log
make_dir $dir_db make_dir $dir_db
make_dir $dir_scripts make_dir $dir_scripts
make_dir $dir_list_tmp make_dir $dir_list_tmp
make_dir $dir_repo make_dir $dir_repo
make_dir $dir_raw make_dir $dir_raw
make_dir $dir_update_log make_dir $dir_update_log
make_dir $dir_dep make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user cp -fv $file_config_sample $file_config_user
echo echo
fi fi
if [[ ! -f $file_env ]]; then if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n" echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env touch $file_env
echo echo
fi fi
if [[ ! -f $file_task_before ]]; then if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n" echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before cp -fv $file_task_sample $file_task_before
echo echo
fi fi
if [[ ! -f $file_task_after ]]; then if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n" echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after cp -fv $file_task_sample $file_task_after
echo echo
fi fi
if [[ ! -f $file_extra_shell ]]; then if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n" echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell cp -fv $file_extra_sample $file_extra_shell
echo echo
fi fi
if [[ ! -s $file_auth_user ]]; then if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n" echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user cp -fv $file_auth_sample $file_auth_user
echo echo
fi fi
if [[ ! -s $file_notify_py ]]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
echo echo
fi fi
if [[ ! -s $file_notify_js ]]; then if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n" echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js cp -fv $file_notify_js_sample $file_notify_js
echo echo
fi fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n" echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
echo echo
fi fi
if [[ ! -s $dep_notify_js ]]; then if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n" echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js cp -fv $file_notify_js_sample $dep_notify_js
echo echo
fi fi
if [[ ! -s $dep_notify_py ]]; then if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n" echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py cp -fv $file_notify_py_sample $dep_notify_py
echo echo
fi fi
} }
npm_install_sub() { npm_install_sub() {
if [ $is_termux -eq 1 ]; then set_proxy
npm install --production --no-bin-links if [ $is_termux -eq 1 ]; then
elif ! type pnpm &>/dev/null; then npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
npm install --production elif ! type pnpm &>/dev/null; then
else npm install --production --registry=https://registry.npm.taobao.org || npm install --production
pnpm install --loglevel error --production else
fi pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
fi
unset_proxy
} }
npm_install_1() { npm_install_1() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "运行 npm install...\n" echo -e "运行 npm install...\n"
npm_install_sub npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n" [[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current cd $dir_current
} }
npm_install_2() { npm_install_2() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n" echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub npm_install_sub
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n" echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work npm_install_1 $dir_work
fi fi
cd $dir_current cd $dir_current
} }
diff_and_copy() { diff_and_copy() {
local copy_source=$1 local copy_source=$1
local copy_to=$2 local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to cp -f $copy_source $copy_to
fi fi
} }
update_depend() { update_depend() {
local dir_current=$(pwd) local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
cd $dir_current cd $dir_current
} }
git_clone_scripts() { git_clone_scripts() {
local url="$1" local url=$1
local dir="$2" local dir=$2
local branch="$3" local branch=$3
local proxy="$4" [[ $branch ]] && local part_cmd="-b $branch "
[[ $branch ]] && local part_cmd="-b $branch " echo -e "开始克隆仓库 $url$dir\n"
echo -e "开始克隆仓库 $url$dir\n"
set_proxy "$proxy" set_proxy
git clone $part_cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
unset_proxy unset_proxy
} }
git_pull_scripts() { git_pull_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work="$1" local dir_work="$1"
local branch="$2" local branch="$2"
local proxy="$3" cd $dir_work
cd $dir_work echo -e "开始更新仓库:$dir_work\n"
echo -e "开始更新仓库:$dir_work\n"
set_proxy "$proxy" set_proxy
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git pull &>/dev/null git pull &>/dev/null
unset_proxy unset_proxy
reset_branch "$branch"
cd $dir_current cd $dir_current
} }
reset_romote_url() { reset_romote_url() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
local url=$2 local url=$2
local branch="$3" local branch="$3"
cd $dir_work if [[ -d "$dir_work/.git" ]]; then
if [[ -d "$dir_work/.git" ]]; then cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null [[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null git remote set-url origin $url &>/dev/null
else
git init local part_cmd=""
git remote add origin $url &>/dev/null reset_branch "$branch"
fi cd $dir_current
reset_branch "$branch" fi
cd $dir_current
} }
reset_branch() { reset_branch() {
local branch="$1" local branch="$1"
local part_cmd="" if [[ $branch ]]; then
if [[ $branch ]]; then part_cmd="origin/${branch}"
part_cmd="origin/${branch}" git checkout -B "$branch"
git checkout -B "$branch" &>/dev/null git branch --set-upstream-to=$part_cmd $branch
git branch --set-upstream-to=$part_cmd $branch &>/dev/null fi
fi git reset --hard $part_cmd &>/dev/null
git reset --hard $part_cmd &>/dev/null
} }
random_range() { random_range() {
local beg=$1 local beg=$1
local end=$2 local end=$2
echo $((RANDOM % ($end - $beg) + $beg)) echo $((RANDOM % ($end - $beg) + $beg))
}
reload_pm2() {
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
}
diff_time() {
local format="$1"
local begin_time="$2"
local end_time="$3"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
}
format_time() {
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
}
format_log_time() {
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
}
format_timestamp() {
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
}
patch_version() {
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
} }
init_env init_env
@@ -489,4 +370,4 @@ detect_macos
define_cmd define_cmd
fix_config fix_config
import_config $1 2>$task_error_log_path import_config $1 >$task_error_log_path 2>&1
+361 -68
View File
@@ -7,81 +7,374 @@ dir_shell=$QL_DIR/shell
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local file_param=$1 local file_param=$1
if [[ $file_param == *.js ]]; then if [[ $file_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3" which_program="python3"
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" which_program="ts-node-transpile-only"
else
which_program=""
fi
}
handle_log_path() {
local file_param=$1
if [[ -z $file_param ]];then
file_param="task"
fi
local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else else
log_dir_tmp_path="${file_param}" which_program=""
fi fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
} }
format_params() { random_delay() {
time_format="%Y-%m-%d %H:%M:%S" local random_delay_max=$RandomDelay
timeoutCmd="" if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
if type timeout &>/dev/null; then local current_min=$(date "+%-M")
timeoutCmd="timeout -k 10s $command_timeout_time " if [[ $current_min -ne 0 ]] && [[ $current_min -ne 30 ]]; then
fi delay_second=$(($(gen_random_num $random_delay_max) + 1))
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g') echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
fi
} }
while getopts ":lm:" opt; do ## scripts目录下所有可运行脚本数组
case $opt in gen_array_scripts() {
l) local dir_current=$(pwd)
show_log="true" local i="-1"
;; cd $dir_scripts
m) for file in $(ls); do
max_time="$OPTARG" if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
;; let i++
esac array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
done array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ $show_log ]] && shift $(($OPTIND - 1)) [[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
if [[ $max_time ]]; then fi
shift $(($OPTIND - 1)) done
command_timeout_time="$max_time" cd $dir_current
fi }
format_params "$@" ## 使用说明
define_program "$@" usage() {
handle_log_path "$@" define_cmd
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
eval . $dir_shell/otask.sh "$params" "$cmd" ## run nohup,$1:文件名,不含路径,带后缀
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path" run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
define_program "$file_param"
if [[ $file_param == *.js ]]; then
if [[ $# -eq 1 ]]; then
random_delay
fi
fi
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
timeout -k 10s $command_timeout_time $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval cat $single_log_path $cmd
[[ -f $single_log_path ]] && rm -f $single_log_path
done
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s")
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 运行其他命令
run_else() {
local file_param="$1"
define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log"
cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
eval timeout -k 10s $command_timeout_time $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 命令检测
main() {
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
main "$@"
exit 0 exit 0
+442 -429
View File
@@ -8,532 +8,545 @@ send_mark=$dir_shell/send_mark
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径 ## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() { diff_cron() {
local list_scripts="$1" local list_scripts="$1"
local list_task="$2" local list_task="$2"
local list_add="$3" local list_add="$3"
local list_drop="$4" local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then if [[ -s $list_task ]]; then
grep -vwf $list_task $list_scripts >$list_add grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop elif [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
fi cp -f $list_scripts $list_add
fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then if [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add grep -vwf $list_scripts $list_task >$list_drop
fi else
cp -f $list_task $list_drop
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then fi
cp -f $list_task $list_drop
fi
} }
## 检测配置文件版本 ## 检测配置文件版本
detect_config_version() { detect_config_version() {
## 识别出两个文件的版本号 ## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|") ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|") [[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件 ## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark [[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容 ## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}') update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}') update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知 ## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知" local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n" local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content echo -e $notify_content
notify_api "$notify_title" "$notify_content" notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark [[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
} }
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有$type的定时任务\n"
cat $list cat $list
echo echo
fi fi
} }
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务 ## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务
## $1:失效任务清单文件路径 ## $1:失效任务清单文件路径
del_cron() { del_cron() {
local list_drop=$1 local list_drop=$1
local path=$2 local path=$2
local detail="" local detail=""
local ids="" local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n" echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
ids="$ids,\"$id\""
else
ids="\"$id\""
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then if [[ $ids ]]; then
ids="$ids,\"$id\"" result=$(del_cron_api "$ids")
else notify_api "$path 删除任务${result}" "$detail"
ids="\"$id\""
fi fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
fi
} }
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空 ## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
## $1:新任务清单文件路径 ## $1:新任务清单文件路径
add_cron() { add_cron() {
local list_add=$1 local list_add=$1
local path=$2 local path=$2
echo -e "开始尝试自动添加定时任务...\n" echo -e "开始尝试自动添加定时任务...\n"
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
local file_name=${file/${path}\//} local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/} file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then if [[ -f $file ]]; then
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file | }" $file |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$file_name" [[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
else else
detail="${result}\n" detail="${result}\n"
fi fi
fi fi
done done
notify_api "$path 新增任务" "$detail" notify_api "$path 新增任务" "$detail"
} }
## 更新仓库 ## 更新仓库
update_repo() { update_repo() {
local url="$1" local url="$1"
local path="$2" local path="$2"
local blackword="$3" local blackword="$3"
local dependence="$4" local dependence="$4"
local branch="$5" local branch="$5"
local extensions="$6" local tmp="${url%/*}"
local proxy="$7" local authorTmp1="${tmp##*/}"
local tmp="${url%/*}" local authorTmp2="${authorTmp1##*:}"
local authorTmp1="${tmp##*/}" local author="${authorTmp2##*.}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local repo_path="${dir_repo}/${uniq_path}" local repo_path="${dir_repo}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}" make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url" local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}" reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" "${proxy}" git_pull_scripts ${repo_path} "${branch}"
else else
rm -rf ${repo_path} &>/dev/null git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}" fi
fi if [[ $exit_status -eq 0 ]]; then
if [[ $exit_status -eq 0 ]]; then echo -e "\n更新${repo_path}成功...\n"
echo -e "\n更新${repo_path}成功...\n" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" else
else echo -e "\n更新${repo_path}失败,请检查网络...\n"
echo -e "\n更新${repo_path}失败,请检查网络...\n" fi
fi
} }
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local raw_url="$url" local raw_url="$url"
local suffix="${raw_url##*.}" local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}" local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url} wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy unset_proxy
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
}" $raw_file_name | }" $raw_file_name |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name" [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi
} }
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
. $file_extra_shell . $file_extra_shell
else else
echo -e "$file_extra_shell文件不存在,跳过执行...\n" echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi fi
fi
} }
## 脚本用法 ## 脚本用法
usage() { usage() {
echo -e "ql命令使用方法:" echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙" echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本" echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件" echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本" echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志" echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复" echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数" echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录" echo -e "9. $cmd_update resettfa # 禁用两步登录"
} }
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
local mirror="github" patch_version
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200)
if [ "$githubStatus" == "" ]; then
mirror="gitee"
fi
echo -e "\n使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
local all_branch=$(cd ${dir_root} && git branch -a) local no_restart="$1"
local primary_branch="master" local all_branch=$(git branch -a)
if [[ "${all_branch}" =~ "${current_branch}" ]]; then local primary_branch="master"
primary_branch="${current_branch}" if [[ "${all_branch}" =~ "${current_branch}" ]]; then
fi primary_branch="${current_branch}"
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json) fi
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch} [[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
git_pull_scripts $dir_root ${primary_branch} reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n" echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version detect_config_version
update_depend update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else
echo -e "\n更新青龙源文件失败,请检查原因...\n"
fi
local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi
else
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
fi
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
} }
update_qinglong_static() { patch_version() {
local no_restart="$1" if [[ -f "$dir_root/db/cookie.db" ]]; then
local primary_branch="$2" echo -e "检测到旧的db文件,拷贝为新db...\n"
local url="https://${mirror}.com/whyour/qinglong-static.git" mv $dir_root/db/cookie.db $dir_root/db/env.db
if [[ -d ${ql_static_repo}/.git ]]; then rm -rf $dir_root/db/cookie.db
reset_romote_url ${ql_static_repo} ${url} ${primary_branch} echo
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
rm -rf ${ql_static_repo} &>/dev/null
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi fi
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n" if ! type ts-node &>/dev/null; then
fi pnpm i -g ts-node typescript tslib
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
}
reload_pm2() {
pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
} }
## 对比脚本 ## 对比脚本
diff_scripts() { diff_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local repo_path="$1" local repo_path="$1"
local author="$2" local author="$2"
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence"
local list_add="$dir_list_tmp/${uniq_path}_add.list" local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list" local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
if [[ -s $list_drop ]]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path del_cron $list_drop $uniq_path
fi
fi fi
fi if [[ -s $list_add ]]; then
if [[ -s $list_add ]]; then output_list_add_drop $list_add "新"
output_list_add_drop $list_add "新" if [[ ${AutoAddCron} == true ]]; then
if [[ ${AutoAddCron} == true ]]; then add_cron $list_add $uniq_path
add_cron $list_add $uniq_path fi
fi fi
fi cd $dir_current
cd $dir_current
} }
## 生成脚本的路径清单文件 ## 生成脚本的路径清单文件
gen_list_repo() { gen_list_repo() {
local dir_current=$(pwd) local dir_current=$(pwd)
local repo_path="$1" local repo_path="$1"
local author="$2" local author="$2"
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
cd ${repo_path}
local cmd="find ."
local index=0
if [[ $6 ]]; then
file_extensions="$6"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path} cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do local cmd="find ."
file_path=$(dirname $_file) local index=0
make_dir "${dir_scripts}/${uniq_path}/${file_path}" for extension in $file_extensions; do
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}" if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done done
fi files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
if [[ -d $dir_dep ]]; then files=$(echo "$files" | egrep $path)
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi fi
done if [[ $blackword ]]; then
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list" files=$(echo "$files" | egrep -v $blackword)
cd $dir_current fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done
fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi
done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current
} }
get_uniq_path() { get_uniq_path() {
local url="$1" local url="$1"
local branch="$2" local branch="$2"
local urlTmp="${url%*/}" local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}" local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}" local repo="${repoTmp%.*}"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}" local author="${authorTmp2##*.}"
uniq_path="${author}_${repo}" uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}" [[ $branch ]] && uniq_path="${uniq_path}_${branch}"
} }
main() { main() {
## for ql update ## for ql update
show_log="false" show_log="false"
while getopts ":l" opt; do while getopts ":l" opt
case $opt in do
l) case $opt in
show_log="true" l)
;; show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1
local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in
update)
cmd=">> $log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
eval echo -e "## 开始执行... $begin_time\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
eval update_qinglong "$2" $cmd
;;
extra)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >>$log_path
;;
repo)
get_user_info
get_uniq_path "$p2" "$p6"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path
else
echo -e "命令输入错误...\n"
usage
fi
;;
raw)
get_user_info
get_uniq_path "$p2"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then
update_raw "$p2" >>$log_path
else
echo -e "命令输入错误...\n"
usage
fi
;;
rmlog)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >>$log_path
;;
bot)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >>$log_path
;;
check)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/check.sh >>$log_path
;;
resetlet)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
resettfa)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
*)
echo -e "命令输入错误...\n"
usage
;;
esac esac
done local end_time=$(date '+%Y-%m-%d %H:%M:%S')
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1)) local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path
local p1=$1 cat $log_path
local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_dir="${p1}"
make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="${log_dir}/${log_time}.log"
local file_path="$dir_log/$log_path"
cmd=">> $file_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time")
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
fi
case $p1 in
update)
eval update_qinglong "$2" $cmd
;;
extra)
eval run_extra_shell $cmd
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi
;;
rmlog)
eval . $dir_shell/rmlog.sh "$p2" $cmd
;;
bot)
eval . $dir_shell/bot.sh $cmd
;;
check)
eval . $dir_shell/check.sh $cmd
;;
resetlet)
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "重置登录错误次数成功" $cmd
;;
resettfa)
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "禁用两步验证成功" $cmd
;;
*)
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
;;
esac
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
eval echo -e "\\\n          " $cmd
fi
if [[ -f $file_path ]]; then
cat $file_path
fi
} }
main "$@" main "$@"
-7
View File
@@ -1,7 +0,0 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_z0d9rbri1ci.js'],
});
export default IconFont;
-111
View File
@@ -1,111 +0,0 @@
import { Tag, Input } from 'antd';
import { TweenOneGroup } from 'rc-tween-one';
import { PlusOutlined } from '@ant-design/icons';
import { useEffect, useRef, useState } from 'react';
const EditableTagGroup = ({
value,
onChange,
}: {
value?: string[];
onChange?: (tags: string[]) => void;
}) => {
const [inputValue, setInputValue] = useState('');
const [inputVisible, setInputVisible] = useState(false);
const [tags, setTags] = useState<string[]>([]);
const saveInputRef = useRef<any>();
const handleClose = (removedTag: string) => {
const _tags = tags.filter((tag) => tag !== removedTag);
setTags(_tags);
onChange?.(_tags);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !tags.includes(inputValue)) {
setTags([...tags, inputValue]);
onChange?.([...tags, inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const tagChild = tags.map((tag) => {
const tagElem = (
<Tag
closable
onClose={(e) => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem}
</span>
);
});
useEffect(() => {
if (inputVisible && saveInputRef) {
saveInputRef.current.focus();
}
}, [inputVisible]);
useEffect(() => {
if (value) {
setTags(value);
}
}, [value]);
return (
<>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{tagChild}
</TweenOneGroup>
{inputVisible && (
<Input
ref={saveInputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={showInput}
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
>
<PlusOutlined />
</Tag>
)}
</>
);
};
export default EditableTagGroup;
-48
View File
@@ -1,48 +0,0 @@
import { useMemo, useState } from 'react';
export default (
treeData: any[],
searchValue: string,
{
treeNodeFilterProp,
}: {
treeNodeFilterProp: string;
},
) => {
return useMemo(() => {
const keys: string[] = [];
if (!searchValue) {
return { treeData, keys };
}
const upperStr = searchValue.toUpperCase();
function filterOptionFunc(_: string, dataNode: any[]) {
const value = dataNode[treeNodeFilterProp as any];
return String(value).toUpperCase().includes(upperStr);
}
function dig(list: any[], keepAll: boolean = false): any[] {
return list
.map((dataNode) => {
const children = dataNode.children;
const match = keepAll || filterOptionFunc!(searchValue, dataNode);
const childList = dig(children || [], match);
if (match || childList.length) {
childList.length && keys.push(dataNode.key);
return {
...dataNode,
children: childList,
};
}
return null;
})
.filter((node) => node);
}
return { treeData: dig(treeData), keys };
}, [treeData, searchValue, treeNodeFilterProp]);
};
-16
View File
@@ -1,16 +0,0 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer'
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
const [height, setHeight] = useState<number>()
useResizeObserver(target, (entry) => {
let _targe = entry.target as any
if (!_targe.classList.contains('ant-table-wrapper')) {
_targe = entry.target.querySelector('.ant-table-wrapper')
}
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement }))
})
return height
}
+12 -19
View File
@@ -9,7 +9,6 @@ import {
ControlOutlined, ControlOutlined,
ContainerOutlined, ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
export default { export default {
route: { route: {
@@ -35,51 +34,45 @@ export default {
{ {
path: '/crontab', path: '/crontab',
name: '定时任务', name: '定时任务',
icon: <IconFont type="ql-icon-crontab" />, icon: <FieldTimeOutlined />,
component: '@/pages/crontab/index', component: '@/pages/crontab/index',
}, },
{
path: '/subscription',
name: '订阅管理',
icon: <IconFont type="ql-icon-subs" />,
component: '@/pages/subscription/index',
},
{ {
path: '/env', path: '/env',
name: '环境变量', name: '环境变量',
icon: <IconFont type="ql-icon-env" />, icon: <RadiusSettingOutlined />,
component: '@/pages/env/index', component: '@/pages/env/index',
}, },
{ {
path: '/config', path: '/config',
name: '配置文件', name: '配置文件',
icon: <IconFont type="ql-icon-config" />, icon: <ControlOutlined />,
component: '@/pages/config/index', component: '@/pages/config/index',
}, },
{ {
path: '/script', path: '/script',
name: '脚本管理', name: '脚本管理',
icon: <IconFont type="ql-icon-script" />, icon: <FormOutlined />,
component: '@/pages/script/index', component: '@/pages/script/index',
}, },
{ {
path: '/dependence', path: '/dependence',
name: '依赖管理', name: '依赖管理',
icon: <IconFont type="ql-icon-dependence" />, icon: <ContainerOutlined />,
component: '@/pages/dependence/index', component: '@/pages/dependence/index',
}, },
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/diff', path: '/diff',
name: '对比工具', name: '对比工具',
icon: <IconFont type="ql-icon-diff" />, icon: <DiffOutlined />,
component: '@/pages/diff/index', component: '@/pages/diff/index',
}, },
{
path: '/log',
name: '任务日志',
icon: <FolderOutlined />,
component: '@/pages/log/index',
},
{ {
path: '/setting', path: '/setting',
name: '系统设置', name: '系统设置',
+40 -63
View File
@@ -1,4 +1,4 @@
@import '~antd/es/style/themes/default.less'; @import '~antd/lib/style/themes/default.less';
@import '~@/styles/variable.less'; @import '~@/styles/variable.less';
@import '~codemirror/lib/codemirror.css'; @import '~codemirror/lib/codemirror.css';
@@ -9,12 +9,6 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
} }
.ant-modal-body {
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
overflow-y: auto;
}
.log-modal { .log-modal {
.ant-modal { .ant-modal {
padding-bottom: 0 !important; padding-bottom: 0 !important;
@@ -55,14 +49,8 @@
.ant-pro-page-container-children-content { .ant-pro-page-container-children-content {
overflow: auto; overflow: auto;
height: 100%; height: 100%;
background-color: @component-background; background-color: #fff;
padding: 12px; padding: 16px;
}
}
.ql-container-wrapper-has-tab {
.ant-pro-grid-content.wide .ant-pro-page-container-children-content {
padding-top: 0;
} }
} }
@@ -142,7 +130,7 @@
flex: 1; flex: 1;
.ant-pro-grid-content-children { .ant-pro-grid-content-children {
height: 100%; height: calc(100% - 36px);
> div, > div,
.log-container, .log-container,
@@ -161,7 +149,7 @@
&.env-wrapper, &.env-wrapper,
&.config-wrapper { &.config-wrapper {
.CodeMirror { .CodeMirror {
width: calc(100vw - 24px); width: calc(100vw - 80px);
} }
} }
} }
@@ -178,9 +166,9 @@
} }
.Resizer { .Resizer {
background: @component-background; background: #fff;
opacity: 0.8; opacity: 0.2;
z-index: 100; z-index: 1;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
@@ -243,49 +231,43 @@
font-size: 13px; font-size: 13px;
} }
/* Change autocomplete styles in WebKit */ [data-dark='true'] {
input:-webkit-autofill, /* Change autocomplete styles in WebKit */
input:-webkit-autofill:hover, input:-webkit-autofill,
input:-webkit-autofill:focus, input:-webkit-autofill:hover,
textarea:-webkit-autofill, input:-webkit-autofill:focus,
textarea:-webkit-autofill:hover, textarea:-webkit-autofill,
textarea:-webkit-autofill:focus, textarea:-webkit-autofill:hover,
select:-webkit-autofill, textarea:-webkit-autofill:focus,
select:-webkit-autofill:hover, select:-webkit-autofill,
select:-webkit-autofill:focus { select:-webkit-autofill:hover,
box-shadow: none; select:-webkit-autofill:focus {
transition: background-color 5000s ease-in-out 0s; border: 1px solid @border-color-base;
-webkit-text-fill-color: @text-color; box-shadow: none;
caret-color: @text-color; transition: background-color 5000s ease-in-out 0s;
color: @text-color; -webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
}
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
} }
::placeholder { .ant-pro-page-container-children-content {
opacity: 0.5 !important; margin: 18px 18px 18px;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
} }
.ant-pro-basicLayout-content { .ant-pro-basicLayout-content {
margin: 12px; margin: 18px;
.ant-pro-page-container { .ant-pro-page-container {
margin: -12px; margin: -18px -18px -18px;
} }
.ant-pro-page-container-warp .ant-page-header {
border-bottom: 1px solid #eee;
}
.ant-pro-page-container-children-content {
margin: 0;
}
}
.ant-pro-global-header {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03);
} }
.ant-menu-item.ant-pro-sider-collapsed-button { .ant-menu-item.ant-pro-sider-collapsed-button {
@@ -296,7 +278,7 @@ select:-webkit-autofill:focus {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
color: @text-color; color: rgba(0, 0, 0, 0.85);
.side-menu-collapse-button:hover { .side-menu-collapse-button:hover {
color: #1890ff; color: #1890ff;
@@ -313,11 +295,11 @@ select:-webkit-autofill:focus {
text-align: left; text-align: left;
outline: none; outline: none;
padding: 4px 0; padding: 4px 0;
border: 1px solid fade(@component-background, 0.12); border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 4px; border-radius: 4px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
overflow: auto; overflow: auto;
background-color: @component-background; background-color: #fff;
} }
[data-dark='true'] .side-menu-user-drop-menu { [data-dark='true'] .side-menu-user-drop-menu {
@@ -343,8 +325,3 @@ select:-webkit-autofill:focus {
width: auto !important; width: auto !important;
} }
} }
pre {
word-break: break-all !important;
white-space: break-spaces !important;
}
+87 -142
View File
@@ -1,8 +1,13 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader'; import {
enable as enableDarkMode,
disable as disableDarkMode,
auto as followSystemColorScheme,
setFetchMethod,
} from 'darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
import { Link, history, Outlet, useLocation } from '@umijs/max'; import { Link, history } from 'umi';
import { import {
LogoutOutlined, LogoutOutlined,
MenuFoldOutlined, MenuFoldOutlined,
@@ -15,63 +20,21 @@ import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version'; import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd';
message,
Badge,
Modal,
Avatar,
Dropdown,
Menu,
Image,
Popover,
Descriptions,
Tooltip,
} from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export interface SharedContext { export default function (props: any) {
headerStyle: React.CSSProperties;
isPhone: boolean;
theme: 'vs' | 'vs-dark';
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: number;
version: string;
}
export default function () {
const location = useLocation();
const ctx = useCtx(); const ctx = useCtx();
const { theme, reloadTheme } = useTheme(); const theme = useTheme();
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>();
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<TSystemInfo>(); const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>(); const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const logout = () => { const logout = () => {
request.post(`${config.apiPrefix}user/logout`).then(() => { request.post(`${config.apiPrefix}user/logout`).then(() => {
@@ -88,15 +51,17 @@ export default function () {
setSystemInfo(data); setSystemInfo(data);
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
setLoading(false);
} else { } else {
getUser(); getUser();
} }
} else {
message.error(data);
} }
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}) });
.finally(() => setInitLoading(false));
}; };
const getUser = (needLoading = true) => { const getUser = (needLoading = true) => {
@@ -106,9 +71,11 @@ export default function () {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200 && data.username) { if (code === 200 && data.username) {
setUser(data); setUser(data);
if (location.pathname === '/') { if (props.location.pathname === '/') {
history.push('/crontab'); history.push('/crontab');
} }
} else {
message.error(data);
} }
needLoading && setLoading(false); needLoading && setLoading(false);
}) })
@@ -117,15 +84,27 @@ export default function () {
}); });
}; };
const reloadUser = (needLoading = false) => { const reloadUser = () => {
getUser(needLoading); getUser(false);
};
const setTheme = () => {
const media = window.matchMedia('(prefers-color-scheme: dark)');
const storageTheme = localStorage.getItem('qinglong_dark_theme');
const isDark =
(media.matches && storageTheme !== 'light') || storageTheme === 'dark';
if (isDark) {
document.body.setAttribute('data-dark', 'true');
} else {
document.body.setAttribute('data-dark', 'false');
}
}; };
useEffect(() => { useEffect(() => {
if (systemInfo && systemInfo.isInitialized && !user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
} }
}, [location.pathname]); }, [props.location.pathname]);
useEffect(() => { useEffect(() => {
if (!systemInfo) { if (!systemInfo) {
@@ -134,25 +113,15 @@ export default function () {
}, [systemInfo]); }, [systemInfo]);
useEffect(() => { useEffect(() => {
if (theme === 'vs-dark') { setTheme();
document.body.setAttribute('data-dark', 'true'); }, [theme.theme]);
} else {
document.body.setAttribute('data-dark', 'false');
}
}, [theme]);
useEffect(() => { useEffect(() => {
vhCheck(); vhCheck();
init(); init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return; setFetchMethod(window.fetch);
if (typeof window.matchMedia === 'undefined') return;
if (!DarkReader) {
return () => null;
}
setFetchMethod(fetch);
if (_theme === 'dark') { if (_theme === 'dark') {
enableDarkMode({}); enableDarkMode({});
} else if (_theme === 'light') { } else if (_theme === 'light') {
@@ -160,18 +129,12 @@ export default function () {
} else { } else {
followSystemColorScheme({}); followSystemColorScheme({});
} }
return () => {
disableDarkMode();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!user || !user.username) return; if (!user) return;
ws.current = new SockJS( ws.current = new SockJS(
`${window.location.origin}/api/ws?token=${localStorage.getItem( `${location.origin}/api/ws?token=${localStorage.getItem(config.authKey)}`,
config.authKey,
)}`,
); );
ws.current.onmessage = (e: any) => { ws.current.onmessage = (e: any) => {
@@ -179,9 +142,9 @@ export default function () {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'ping') { if (data.type === 'ping') {
if (data && data.message === 'hanhh') { if (data && data.message === 'hanhh') {
console.log('WS connection succeeded !!!'); console.log('websocket连接成功', e);
} else { } else {
console.log('WS connection Failed !!!', e); console.log('websocket连接失败', e);
} }
} }
setSocketMessage(data); setSocketMessage(data);
@@ -216,31 +179,30 @@ export default function () {
}; };
}, []); }, []);
if (initLoading) { if (
return <PageLoading />; ['/login', '/initialization', '/error'].includes(props.location.pathname)
} ) {
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${ document.title = `${
(config.documentTitleMap as any)[location.pathname] (config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`; } - 控制面板`;
if (systemInfo?.isInitialized && location.pathname === '/initialization') { if (
systemInfo?.isInitialized &&
props.location.pathname === '/initialization'
) {
history.push('/crontab'); history.push('/crontab');
} }
if (systemInfo || location.pathname === '/error') { if (systemInfo || props.location.pathname === '/error') {
return ( return React.Children.map(props.children, (child) => {
<Outlet return React.cloneElement(child, {
context={{ ...ctx,
...ctx, ...theme,
theme, user,
user, reloadUser,
reloadUser, reloadTheme: setTheme,
reloadTheme, ws: ws.current,
ws: ws.current, });
}} });
/>
);
} }
} }
@@ -251,20 +213,20 @@ export default function () {
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = ( const menu = (
<Menu <Menu className="side-menu-user-drop-menu">
className="side-menu-user-drop-menu" <Menu.Item key="1" icon={<LogoutOutlined />} onClick={logout}>
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]} 退
onClick={logout} </Menu.Item>
/> </Menu>
); );
return loading ? ( return loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
<ProLayout <ProLayout
selectedKeys={[location.pathname]} selectedKeys={[props.location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />} logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -276,23 +238,17 @@ export default function () {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<Tooltip <span
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'} style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
> >
<Badge size="small" dot={systemInfo?.branch === 'develop'}> v{version}
<span </span>
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{version}
</span>
</Badge>
</Tooltip>
</a> </a>
</> </>
} }
@@ -318,13 +274,8 @@ export default function () {
ctx.isPhone && ( ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}> <Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar shape="square" size="small" icon={<UserOutlined />} />
shape="square" <span style={{ marginLeft: 5 }}>admin</span>
size="small"
icon={<UserOutlined />}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
</Dropdown> </Dropdown>
) )
@@ -340,13 +291,8 @@ export default function () {
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}> <Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar shape="square" size="small" icon={<UserOutlined />} />
shape="square" <span style={{ marginLeft: 5 }}>admin</span>
size="small"
icon={<UserOutlined />}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
</Dropdown> </Dropdown>
)} )}
@@ -360,17 +306,16 @@ export default function () {
)} )}
{...defaultProps} {...defaultProps}
> >
<Outlet {React.Children.map(props.children, (child) => {
context={{ return React.cloneElement(child, {
...ctx, ...ctx,
theme, ...theme,
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme: setTheme,
socketMessage, socketMessage,
systemInfo, });
}} })}
/>
</ProLayout> </ProLayout>
); );
} }
+7 -17
View File
@@ -11,11 +11,8 @@ import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
const Config = () => { const Config = ({ headerStyle, isPhone, theme }: any) => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [title, setTitle] = useState('config.sh'); const [title, setTitle] = useState('config.sh');
@@ -25,10 +22,8 @@ const Config = () => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => { const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => { request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
if (code === 200) { setValue(data.data);
setValue(data);
}
}); });
}; };
@@ -36,10 +31,8 @@ const Config = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { setData(data.data);
setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -54,10 +47,8 @@ const Config = () => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: select }, data: { content, name: select },
}) })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { message.success(data.message);
message.success('保存成功');
}
setConfirmLoading(false); setConfirmLoading(false);
}); });
}; };
@@ -80,7 +71,6 @@ const Config = () => {
loading={loading} loading={loading}
extra={[ extra={[
<TreeSelect <TreeSelect
treeExpandAction="click"
className="config-select" className="config-select"
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+59 -287
View File
@@ -10,7 +10,6 @@ import {
List, List,
Divider, Divider,
Typography, Typography,
Tooltip,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -18,9 +17,6 @@ import {
FieldTimeOutlined, FieldTimeOutlined,
Loading3QuartersOutlined, Loading3QuartersOutlined,
FileOutlined, FileOutlined,
PlayCircleOutlined,
PauseCircleOutlined,
FullscreenOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { CrontabStatus } from './index'; import { CrontabStatus } from './index';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
@@ -28,7 +24,6 @@ import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import IconFont from '@/components/iconfont';
const { Text } = Typography; const { Text } = Typography;
@@ -42,12 +37,6 @@ const tabList = [
tab: '脚本', tab: '脚本',
}, },
]; ];
const LangMap: any = {
'.py': 'python',
'.js': 'javascript',
'.sh': 'shell',
'.ts': 'typescript',
};
interface LogItem { interface LogItem {
directory: string; directory: string;
@@ -79,7 +68,6 @@ const CronDetailModal = ({
const [scriptInfo, setScriptInfo] = useState<any>({}); const [scriptInfo, setScriptInfo] = useState<any>({});
const [logUrl, setLogUrl] = useState(''); const [logUrl, setLogUrl] = useState('');
const [validTabs, setValidTabs] = useState(tabList); const [validTabs, setValidTabs] = useState(tabList);
const [currentCron, setCurrentCron] = useState<any>({});
const contentList: any = { const contentList: any = {
log: ( log: (
@@ -94,19 +82,20 @@ const CronDetailModal = ({
)} )}
/> />
), ),
script: scriptInfo.filename && ( script: (
<Editor <Editor
language={LangMap[scriptInfo.filename.slice(-3)] || ''} language="shell"
theme={theme} theme={theme}
value={value} value={value}
options={{ options={{
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false, glyphMargin: false,
wordWrap: 'on', wordWrap: 'on',
}} }}
onMount={(editor, monaco) => { onMount={(editor) => {
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
@@ -114,19 +103,13 @@ const CronDetailModal = ({
}; };
const onClickItem = (item: LogItem) => { const onClickItem = (item: LogItem) => {
localStorage.setItem('logCron', currentCron.id); localStorage.setItem('logCron', cron.id);
setLogUrl( setLogUrl(`${config.apiPrefix}logs/${item.directory}/${item.filename}`);
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
);
request request
.get( .get(`${config.apiPrefix}logs/${item.directory}/${item.filename}`)
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`, .then((data) => {
) setLog(data.data);
.then(({ code, data }) => { setIsLogModalVisible(true);
if (code === 200) {
setLog(data);
setIsLogModalVisible(true);
}
}); });
}; };
@@ -138,9 +121,9 @@ const CronDetailModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons/${cron.id}/logs`) .get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
setLogs(data); setLogs(data.data);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -154,22 +137,16 @@ const CronDetailModal = ({
cmd[1] = cmd[1].replace('/ql/data/scripts/', ''); cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
} }
let p: string, s: string; let [p, s] = cmd[1].split('/');
let index = cmd[1].lastIndexOf('/'); if (!s) {
if (index >= 0) { s = p;
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = ''; p = '';
} }
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then(({ code, data }) => { .then((data) => {
if (code === 200) { setValue(data.data);
setValue(data);
}
}); });
} else { } else {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
@@ -201,10 +178,12 @@ const CronDetailModal = ({
content, content,
}, },
}) })
.then(({ code, data }) => { .then((_data: any) => {
if (code === 200) { if (_data.code === 200) {
setValue(content); setValue(content);
message.success(`保存成功`); message.success(`保存成功`);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -217,147 +196,8 @@ const CronDetailModal = ({
}); });
}; };
const runCron = () => {
Modal.confirm({
title: '确认运行',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => {
getLogs();
}, 1000);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const stopCron = () => {
Modal.confirm({
title: '确认停止',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const enabledOrDisabledCron = () => {
Modal.confirm({
title: `确认${currentCron.isDisabled === 1 ? '启用' : '禁用'}`,
content: (
<>
{currentCron.isDisabled === 1 ? '启用' : '禁用'}
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
currentCron.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [currentCron.id],
},
)
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({
...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const pinOrUnPinCron = () => {
Modal.confirm({
title: `确认${currentCron.isPinned === 1 ? '取消置顶' : '置顶'}`,
content: (
<>
{currentCron.isPinned === 1 ? '取消置顶' : '置顶'}
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
currentCron.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [currentCron.id],
},
)
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({
...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1,
});
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const fullscreen = () => {
const editorElement = editorRef.current._domElement as HTMLElement;
editorElement.parentElement?.requestFullscreen();
};
useEffect(() => { useEffect(() => {
if (cron && cron.id) { if (cron && cron.id) {
setCurrentCron(cron);
getLogs(); getLogs();
getScript(); getScript();
} }
@@ -366,79 +206,22 @@ const CronDetailModal = ({
return ( return (
<Modal <Modal
title={ title={
<div className="crontab-title-wrapper"> <>
<div> <span>{cron.name}</span>
<span>{currentCron.name}</span> {cron.labels?.length > 0 && cron.labels[0] !== '' && (
{currentCron.labels?.length > 0 && currentCron.labels[0] !== '' && ( <Divider type="vertical"></Divider>
<Divider type="vertical"></Divider> )}
)} {cron.labels?.length > 0 &&
{currentCron.labels?.length > 0 && cron.labels[0] !== '' &&
currentCron.labels[0] !== '' && cron.labels?.map((label: string, i: number) => (
currentCron.labels?.map((label: string, i: number) => ( <Tag color="blue" style={{ marginRight: 5 }}>
<Tag key={label} color="blue" style={{ marginRight: 5 }}> {label}
{label} </Tag>
</Tag> ))}
))} </>
</div>
<div className="operations">
<Tooltip
title={
currentCron.status === CrontabStatus.idle ? '运行' : '停止'
}
>
<Button
type="link"
icon={
currentCron.status === CrontabStatus.idle ? (
<PlayCircleOutlined />
) : (
<PauseCircleOutlined />
)
}
size="small"
onClick={
currentCron.status === CrontabStatus.idle ? runCron : stopCron
}
/>
</Tooltip>
<Tooltip title={currentCron.isDisabled === 1 ? '启用' : '禁用'}>
<Button
type="link"
icon={
<IconFont
type={
currentCron.isDisabled === 1
? 'ql-icon-enable'
: 'ql-icon-disable'
}
/>
}
size="small"
onClick={enabledOrDisabledCron}
/>
</Tooltip>
<Tooltip title={currentCron.isPinned === 1 ? '取消置顶' : '置顶'}>
<Button
type="link"
icon={
<IconFont
type={
currentCron.isPinned === 1
? 'ql-icon-untop'
: 'ql-icon-top'
}
/>
}
size="small"
onClick={pinOrUnPinCron}
/>
</Tooltip>
</div>
</div>
} }
centered centered
open={visible} visible={visible}
forceRender forceRender
footer={false} footer={false}
onCancel={() => handleCancel()} onCancel={() => handleCancel()}
@@ -449,22 +232,21 @@ const CronDetailModal = ({
<Card> <Card>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div> <div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">{currentCron.command}</div> <div className="cron-detail-info-value">{cron.command}</div>
</div> </div>
</Card> </Card>
<Card style={{ marginTop: 10 }}> <Card style={{ marginTop: 10 }}>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div> <div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value"> <div className="cron-detail-info-value">
{(!currentCron.isDisabled || {(!cron.isDisabled || cron.status !== CrontabStatus.idle) && (
currentCron.status !== CrontabStatus.idle) && (
<> <>
{currentCron.status === CrontabStatus.idle && ( {cron.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default"> <Tag icon={<ClockCircleOutlined />} color="default">
</Tag> </Tag>
)} )}
{currentCron.status === CrontabStatus.running && ( {cron.status === CrontabStatus.running && (
<Tag <Tag
icon={<Loading3QuartersOutlined spin />} icon={<Loading3QuartersOutlined spin />}
color="processing" color="processing"
@@ -472,30 +254,29 @@ const CronDetailModal = ({
</Tag> </Tag>
)} )}
{currentCron.status === CrontabStatus.queued && ( {cron.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default"> <Tag icon={<FieldTimeOutlined />} color="default">
</Tag> </Tag>
)} )}
</> </>
)} )}
{currentCron.isDisabled === 1 && {cron.isDisabled === 1 && cron.status === CrontabStatus.idle && (
currentCron.status === CrontabStatus.idle && ( <Tag icon={<CloseCircleOutlined />} color="error">
<Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
</Tag> )}
)}
</div> </div>
</div> </div>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div> <div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">{currentCron.schedule}</div> <div className="cron-detail-info-value">{cron.schedule}</div>
</div> </div>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div> <div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value"> <div className="cron-detail-info-value">
{currentCron.last_execution_time {cron.last_execution_time
? new Date(currentCron.last_execution_time * 1000) ? new Date(cron.last_execution_time * 1000)
.toLocaleString(language, { .toLocaleString(language, {
hour12: false, hour12: false,
}) })
@@ -506,16 +287,14 @@ const CronDetailModal = ({
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div> <div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value"> <div className="cron-detail-info-value">
{currentCron.last_running_time {cron.last_running_time ? diffTime(cron.last_running_time) : '-'}
? diffTime(currentCron.last_running_time)
: '-'}
</div> </div>
</div> </div>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div> <div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value"> <div className="cron-detail-info-value">
{currentCron.nextRunTime && {cron.nextRunTime &&
currentCron.nextRunTime cron.nextRunTime
.toLocaleString(language, { .toLocaleString(language, {
hour12: false, hour12: false,
}) })
@@ -532,20 +311,13 @@ const CronDetailModal = ({
}} }}
tabBarExtraContent={ tabBarExtraContent={
activeTabKey === 'script' && ( activeTabKey === 'script' && (
<> <Button
<Button type="primary"
type="primary" style={{ marginRight: 8 }}
style={{ marginRight: 8 }} onClick={saveFile}
onClick={saveFile} >
>
</Button>
</Button>
<Button
type="primary"
icon={<FullscreenOutlined />}
onClick={fullscreen}
/>
</>
) )
} }
> >
+11 -105
View File
@@ -2,12 +2,21 @@
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.cron {
&:hover {
cursor: pointer;
}
}
.crontab-detail { .crontab-detail {
.card-wrapper { .card-wrapper {
height: 80vh;
height: calc(80vh - var(--vh-offset, 0px));
.ant-card:last-child { .ant-card:last-child {
.ant-card-body { .ant-card-body {
height: calc(80vh - 367px); height: calc(80vh - 238px);
height: calc(80vh - var(--vh-offset, 0px) - 367px); height: calc(80vh - var(--vh-offset, 0px) - 238px);
overflow-y: auto; overflow-y: auto;
} }
} }
@@ -68,22 +77,6 @@
margin-top: 12px; margin-top: 12px;
} }
} }
.crontab-title-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
margin-right: 32px;
.operations {
display: flex;
align-items: center;
.ant-btn:not(:first-child) {
margin-left: 8px;
}
}
}
} }
.log-item { .log-item {
@@ -92,90 +85,3 @@
background: #fafafa; background: #fafafa;
} }
} }
.crontab-view {
.ant-tabs-nav-wrap {
flex: unset !important;
}
.ant-tabs-nav-operations {
position: absolute;
visibility: hidden;
pointer-events: none;
}
.view-more {
margin-left: 32px;
padding: 8px 0;
cursor: pointer;
.ant-tabs-ink-bar {
width: 0;
}
&:hover,
&:focus-visible {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
&.active {
color: #1890ff;
.ant-tabs-ink-bar {
width: 50px;
}
}
}
&.more-active {
.ant-tabs-nav-list {
.ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn {
color: unset;
}
.ant-tabs-ink-bar {
width: 0 !important;
}
}
}
}
.view-create-modal-filters {
display: flex;
.ant-space-item:nth-child(3) {
flex: 1;
}
}
.view-create-modal-sorts {
display: flex;
.ant-space-item:nth-child(2) {
flex: 1;
}
}
tr.drop-over-downward td {
border-bottom: 2px dashed #1890ff;
}
tr.drop-over-upward td {
border-top: 2px dashed #1890ff;
}
.view-manage-modal {
.ant-modal-body {
padding-top: 10px;
}
}
.view-filters-container.active {
.filter-item > div > .ant-form-item-control {
margin-left: 40px;
}
}
+263 -422
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import { import {
Button, Button,
message, message,
@@ -12,8 +12,6 @@ import {
Typography, Typography,
Input, Input,
Popover, Popover,
Tabs,
TablePaginationConfig,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -29,11 +27,6 @@ import {
PauseCircleOutlined, PauseCircleOutlined,
FieldTimeOutlined, FieldTimeOutlined,
PushpinOutlined, PushpinOutlined,
DownOutlined,
SettingOutlined,
PlusOutlined,
UnorderedListOutlined,
CheckOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -43,13 +36,9 @@ import CronLogModal from './logModal';
import CronDetailModal from './detail'; import CronDetailModal from './detail';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { history, useOutletContext } from '@umijs/max'; import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less'; import './index.less';
import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -81,11 +70,10 @@ enum OperationPath {
'unpin', 'unpin',
} }
const Crontab = () => { const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '名', title: '任务名',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
width: 150, width: 150,
@@ -94,11 +82,10 @@ const Crontab = () => {
<> <>
<a <a
onClick={() => { onClick={() => {
setDetailCron(record); goToScriptManager(record);
setIsDetailModalVisible(true);
}} }}
> >
{record.labels?.length > 0 && record.labels[0] !== '' && false ? ( {record.labels?.length > 0 && record.labels[0] !== '' ? (
<Popover <Popover
placement="right" placement="right"
trigger={isPhone ? 'click' : 'hover'} trigger={isPhone ? 'click' : 'hover'}
@@ -107,12 +94,10 @@ const Crontab = () => {
{record.labels?.map((label: string) => ( {record.labels?.map((label: string) => (
<Tag <Tag
color="blue" color="blue"
key={label}
style={{ cursor: 'point' }} style={{ cursor: 'point' }}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setSearchValue(`label:${label}`); setSearchValue(`label:${label}`);
setSearchText(`label:${label}`);
}} }}
> >
<a>{label}</a> <a>{label}</a>
@@ -137,14 +122,15 @@ const Crontab = () => {
</> </>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
multiple: 2,
}, },
}, },
{ {
title: '命令', title: '任务',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 300, width: 250,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
@@ -156,35 +142,29 @@ const Crontab = () => {
}} }}
ellipsis={{ tooltip: text, rows: 2 }} ellipsis={{ tooltip: text, rows: 2 }}
> >
<a {text}
onClick={() => {
goToScriptManager(record);
}}
>
{text}
</a>
</Paragraph> </Paragraph>
); );
}, },
sorter: { sorter: {
compare: (a: any, b: any) => a.command.localeCompare(b.command), compare: (a: any, b: any) => a.command.localeCompare(b.command),
multiple: 3,
}, },
}, },
{ {
title: '定时规则', title: '任务定时',
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110, width: 110,
align: 'center' as const, align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule), compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule),
multiple: 1,
}, },
}, },
{ {
title: '最后运行时间', title: '最后运行时间',
align: 'center' as const, align: 'center' as const,
dataIndex: 'last_execution_time',
key: 'last_execution_time',
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
@@ -214,17 +194,24 @@ const Crontab = () => {
title: '最后运行时长', title: '最后运行时长',
align: 'center' as const, align: 'center' as const,
width: 120, width: 120,
dataIndex: 'last_running_time',
key: 'last_running_time',
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
return a.last_running_time - b.last_running_time; return a.last_running_time - b.last_running_time;
}, },
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
return record.last_running_time const language = navigator.language || navigator.languages[0];
? diffTime(record.last_running_time) return (
: '-'; <span
style={{
display: 'block',
}}
>
{record.last_running_time
? diffTime(record.last_running_time)
: '-'}
</span>
);
}, },
}, },
{ {
@@ -238,11 +225,19 @@ const Crontab = () => {
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return record.nextRunTime return (
.toLocaleString(language, { <span
hour12: false, style={{
}) display: 'block',
.replace(' 24:', ' 00:'); }}
>
{record.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</span>
);
}, },
}, },
@@ -368,26 +363,12 @@ const Crontab = () => {
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logCron, setLogCron] = useState<any>(); const [logCron, setLogCron] = useState<any>();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [pageConf, setPageConf] = useState<{ const [currentPage, setCurrentPage] = useState(1);
page: number; const [pageSize, setPageSize] = useState(20);
size: number; const [tableScrollHeight, setTableScrollHeight] = useState<number>();
sorter: any;
filters: any;
}>({} as any);
const [viewConf, setViewConf] = useState<any>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false); const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>(); const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [total, setTotal] = useState<number>();
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState(false);
const [isViewManageModalVisible, setIsViewManageModalVisible] =
useState(false);
const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const cmd = record.command.split(' ') as string[];
@@ -396,13 +377,9 @@ const Crontab = () => {
cmd[1] = cmd[1].replace('/ql/data/scripts/', ''); cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
} }
let p: string, s: string; let [p, s] = cmd[1].split('/');
let index = cmd[1].lastIndexOf('/'); if (!s) {
if (index >= 0) { s = p;
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = ''; p = '';
} }
history.push(`/script?p=${p}&s=${s}`); history.push(`/script?p=${p}&s=${s}`);
@@ -413,32 +390,27 @@ const Crontab = () => {
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
const { page, size, sorter, filters } = pageConf;
let url = `${
config.apiPrefix
}crons?searchValue=${searchText}&page=${page}&size=${size}&filters=${JSON.stringify(
filters,
)}`;
if (sorter && sorter.field) {
url += `&sorter=${JSON.stringify({
field: sorter.field,
type: sorter.order === 'ascend' ? 'ASC' : 'DESC',
})}`;
}
if (viewConf) {
url += `&queryString=${JSON.stringify({
filters: viewConf.filters,
sorts: viewConf.sorts,
filterRelation: viewConf.filterRelation || 'and',
})}`;
}
request request
.get(url) .get(`${config.apiPrefix}crons?searchValue=${searchText}`)
.then(({ code, data: _data }) => { .then((data: any) => {
if (code === 200) { setValue(
const { data, total } = _data; data.data
setValue( .sort((a: any, b: any) => {
data.map((x) => { const sortA =
a.isPinned && a.status !== 0
? 5
: a.isDisabled && a.status !== 0
? 4
: a.status;
const sortB =
b.isPinned && b.status !== 0
? 5
: b.isDisabled && b.status !== 0
? 4
: b.status;
return CrontabSort[sortA] - CrontabSort[sortB];
})
.map((x) => {
return { return {
...x, ...x,
nextRunTime: cron_parser nextRunTime: cron_parser
@@ -447,9 +419,8 @@ const Crontab = () => {
.toDate(), .toDate(),
}; };
}), }),
); );
setTotal(total); setCurrentPage(1);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -479,15 +450,15 @@ const Crontab = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: [record.id] }) .delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { result.splice(i, 1);
result.splice(i, 1); setValue(result);
setValue(result); } else {
} message.error(data);
} }
}); });
}, },
@@ -512,17 +483,17 @@ const Crontab = () => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] }) .put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { result.splice(i, 1, {
result.splice(i, 1, { ...record,
...record, status: CrontabStatus.running,
status: CrontabStatus.running, });
}); setValue(result);
setValue(result); } else {
} message.error(data);
} }
}); });
}, },
@@ -547,18 +518,18 @@ const Crontab = () => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { result.splice(i, 1, {
result.splice(i, 1, { ...record,
...record, pid: null,
pid: null, status: CrontabStatus.idle,
status: CrontabStatus.idle, });
}); setValue(result);
setValue(result); } else {
} message.error(data);
} }
}); });
}, },
@@ -591,18 +562,18 @@ const Crontab = () => {
data: [record.id], data: [record.id],
}, },
) )
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1; const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { result.splice(i, 1, {
result.splice(i, 1, { ...record,
...record, isDisabled: newStatus,
isDisabled: newStatus, });
}); setValue(result);
setValue(result); } else {
} message.error(data);
} }
}); });
}, },
@@ -635,18 +606,18 @@ const Crontab = () => {
data: [record.id], data: [record.id],
}, },
) )
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1; const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { result.splice(i, 1, {
result.splice(i, 1, { ...record,
...record, isPinned: newStatus,
isPinned: newStatus, });
}); setValue(result);
setValue(result); } else {
} message.error(data);
} }
}); });
}, },
@@ -656,24 +627,6 @@ const Crontab = () => {
}); });
}; };
const getMenuItems = (record: any) => {
return [
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
{
label: record.isDisabled === 1 ? '启用' : '禁用',
key: 'enableOrDisable',
icon:
record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />,
},
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
{
label: record.isPinned === 1 ? '取消置顶' : '置顶',
key: 'pinOrUnPin',
icon: record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />,
},
];
};
const MoreBtn: React.FC<{ const MoreBtn: React.FC<{
record: any; record: any;
index: number; index: number;
@@ -684,12 +637,38 @@ const Crontab = () => {
trigger={['click']} trigger={['click']}
overlay={ overlay={
<Menu <Menu
items={getMenuItems(record)}
onClick={({ key, domEvent }) => { onClick={({ key, domEvent }) => {
domEvent.stopPropagation(); domEvent.stopPropagation();
action(key, record, index); action(key, record, index);
}} }}
/> >
<Menu.Item key="edit" icon={<EditOutlined />}>
</Menu.Item>
<Menu.Item
key="enableOrDisable"
icon={
record.isDisabled === 1 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)
}
>
{record.isDisabled === 1 ? '启用' : '禁用'}
</Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />}>
</Menu.Item>
<Menu.Item
key="pinOrUnPin"
icon={
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
}
>
{record.isPinned === 1 ? '取消置顶' : '置顶'}
</Menu.Item>
</Menu>
} }
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
@@ -748,33 +727,40 @@ const Crontab = () => {
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { const index = value.findIndex((x) => x.id === cron.id);
const index = value.findIndex((x) => x.id === cron.id); const result = [...value];
const result = [...value]; data.data.nextRunTime = cron_parser
data.nextRunTime = cron_parser .parseExpression(data.data.schedule)
.parseExpression(data.schedule) .next()
.next() .toDate();
.toDate(); result.splice(index, 1, {
if (index !== -1) { ...cron,
result.splice(index, 1, { ...data.data,
...cron, });
...data, setValue(result);
});
setValue(result);
}
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowKeys: selectedRowIds, selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
selections: [
Table.SELECTION_ALL,
Table.SELECTION_INVERT,
Table.SELECTION_NONE,
],
}; };
const delCrons = () => { const delCrons = () => {
@@ -784,11 +770,13 @@ const Crontab = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds }) .delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -807,9 +795,11 @@ const Crontab = () => {
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -819,19 +809,10 @@ const Crontab = () => {
}); });
}; };
const onPageChange = ( const onPageChange = (page: number, pageSize: number | undefined) => {
pagination: TablePaginationConfig, setCurrentPage(page);
filters: Record<string, FilterValue | null>, setPageSize(pageSize as number);
sorter: SorterResult<any> | SorterResult<any>[], localStorage.setItem('pageSize', pageSize + '');
) => {
const { current, pageSize } = pagination;
setPageConf({
page: current as number,
size: pageSize as number,
sorter,
filters,
});
localStorage.setItem('pageSize', String(pageSize));
}; };
const getRowClassName = (record: any, index: number) => { const getRowClassName = (record: any, index: number) => {
@@ -846,108 +827,19 @@ const Crontab = () => {
}, [logCron]); }, [logCron]);
useEffect(() => { useEffect(() => {
setPageConf({ ...pageConf, page: 1 }); getCrons();
}, [searchText]); }, [searchText]);
useEffect(() => { useEffect(() => {
if (pageConf.page && pageConf.size) { setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
getCrons(); setTimeout(() => {
} setTableScrollHeight(getTableScroll());
}, [pageConf, viewConf]);
useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(4).find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view);
}
}, [viewConf, enabledCronViews]);
useEffect(() => {
setPageConf({
page: 1,
size: parseInt(localStorage.getItem('pageSize') || '20'),
sorter: {},
filters: {},
}); });
getCronViews();
}, []); }, []);
const viewAction = (key: string) => {
switch (key) {
case 'new':
setIsCreateViewModalVisible(true);
break;
case 'manage':
setIsViewManageModalVisible(true);
break;
default:
tabClick(key);
break;
}
};
const menu = (
<Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
viewAction(key);
}}
items={[
...[...enabledCronViews].slice(4).map((x) => ({
label: (
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{x.name}</span>
{viewConf?.id === x.id && (
<CheckOutlined style={{ color: '#1890ff' }} />
)}
</Space>
),
key: x.id,
icon: <UnorderedListOutlined />,
})),
{
type: 'divider',
},
{
label: '新建视图',
key: 'new',
icon: <PlusOutlined />,
},
{
label: '视图管理',
key: 'manage',
icon: <SettingOutlined />,
},
]}
/>
);
const getCronViews = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/views`)
.then(({ code, data }) => {
if (code === 200) {
setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
}
})
.finally(() => {
setLoading(false);
});
};
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]);
setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null);
};
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper crontab-wrapper"
title="定时任务" title="定时任务"
extra={[ extra={[
<Search <Search
@@ -968,119 +860,91 @@ const Crontab = () => {
style: headerStyle, style: headerStyle,
}} }}
> >
<Tabs {selectedRowIds.length > 0 && (
defaultActiveKey="all" <div style={{ marginBottom: 16 }}>
size="small" <Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
tabPosition="top"
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`} </Button>
tabBarExtraContent={ <Button
<Dropdown type="primary"
overlay={menu} onClick={() => operateCrons(0)}
trigger={['click']} style={{ marginLeft: 8, marginBottom: 5 }}
overlayStyle={{ minWidth: 200 }}
> >
<div className={`view-more ${moreMenuActive ? 'active' : ''}`}>
<Space> </Button>
<Button
<DownOutlined /> type="primary"
</Space> onClick={() => operateCrons(1)}
<div className="ant-tabs-ink-bar ant-tabs-ink-bar-animated"></div> style={{ marginLeft: 8, marginRight: 8 }}
</div> >
</Dropdown>
} </Button>
onTabClick={tabClick} <Button
items={[ type="primary"
...[...enabledCronViews].slice(0, 4).map((x) => ({ style={{ marginRight: 8 }}
key: x.id, onClick={() => operateCrons(2)}
label: x.name, >
})),
]} </Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: currentPage,
onChange: onPageChange,
pageSize: pageSize,
showSizeChanger: true,
simple: isPhone,
defaultPageSize: 20,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [20, 100, 500, 1000] as any,
}}
onRow={(record) => {
return {
onClick: (event) => {
setDetailCron(record);
setIsDetailModalVisible(true);
},
};
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
/> />
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5 }}
onClick={delCrons}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</div>
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={() => { handleCancel={() => {
@@ -1113,29 +977,6 @@ const Crontab = () => {
theme={theme} theme={theme}
isPhone={isPhone} isPhone={isPhone}
/> />
<ViewCreateModal
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
<ViewManageModal
cronViews={cronViews}
visible={isViewManageModalVisible}
handleCancel={() => {
setIsViewManageModalVisible(false);
}}
cronViewChange={(data) => {
getCronViews();
if (data && data.id === viewConf.id) {
setViewConf({ ...viewConf, ...data });
}
}}
/>
</PageContainer> </PageContainer>
); );
}; };
+9 -11
View File
@@ -7,7 +7,6 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
enum CrontabStatus { enum CrontabStatus {
'running', 'running',
@@ -42,17 +41,14 @@ const CronLogModal = ({
} }
request request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then(({ code, data }) => { .then((data: any) => {
if ( if (localStorage.getItem('logCron') === String(cron.id)) {
code === 200 && const log = data.data as string;
localStorage.getItem('logCron') === String(cron.id)
) {
const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( setExecuting(
log && !logEnded(log) && !log.includes('重启面板'), log && !log.includes('执行结束') && !log.includes('重启面板'),
); );
if (log && !logEnded(log) && !log.includes('重启面板')) { if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
@@ -99,7 +95,7 @@ const CronLogModal = ({
<> <>
{(executing || loading) && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && !loading && <CheckCircleOutlined />} {!executing && !loading && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>{cron && cron.name}</span> <span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</> </>
); );
}; };
@@ -123,10 +119,12 @@ const CronLogModal = ({
return ( return (
<Modal <Modal
title={titleElement()} title={titleElement()}
open={visible} visible={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
overflowY: 'auto',
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
minHeight: '300px', minHeight: '300px',
}} }}
forceRender forceRender
+29 -28
View File
@@ -3,7 +3,6 @@ import { Modal, message, Input, Form, Button } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import cronParse from 'cron-parser'; import cronParse from 'cron-parser';
import EditableTagGroup from '@/components/tag';
const CronModal = ({ const CronModal = ({
cron, cron,
@@ -21,6 +20,11 @@ const CronModal = ({
setLoading(true); setLoading(true);
const method = cron ? 'put' : 'post'; const method = cron ? 'put' : 'post';
const payload = { ...values }; const payload = { ...values };
if (typeof payload.labels === 'string') {
payload.labels = values.labels.split(/,|/);
} else if (!payload.labels) {
payload.labels = [];
}
if (cron) { if (cron) {
payload.id = cron.id; payload.id = cron.id;
} }
@@ -28,12 +32,13 @@ const CronModal = ({
const { code, data } = await request[method](`${config.apiPrefix}crons`, { const { code, data } = await request[method](`${config.apiPrefix}crons`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
handleCancel(data); } else {
message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data);
} catch (error: any) { } catch (error: any) {
setLoading(false); setLoading(false);
} }
@@ -46,10 +51,8 @@ const CronModal = ({
return ( return (
<Modal <Modal
title={cron ? '编辑任务' : '新建任务'} title={cron ? '编辑任务' : '新建任务'}
open={visible} visible={visible}
forceRender forceRender
centered
maskClosable={false}
onOk={() => { onOk={() => {
form form
.validateFields() .validateFields()
@@ -90,7 +93,7 @@ const CronModal = ({
{ required: true }, { required: true },
{ {
validator: (rule, value) => { validator: (rule, value) => {
if (!value || cronParse.parseExpression(value).hasNext()) { if (cronParse.parseExpression(value).hasNext()) {
return Promise.resolve(); return Promise.resolve();
} else { } else {
return Promise.reject('Cron表达式格式有误'); return Promise.reject('Cron表达式格式有误');
@@ -102,7 +105,7 @@ const CronModal = ({
<Input placeholder="秒(可选) 分 时 天 月 周" /> <Input placeholder="秒(可选) 分 时 天 月 周" />
</Form.Item> </Form.Item>
<Form.Item name="labels" label="标签"> <Form.Item name="labels" label="标签">
<EditableTagGroup /> <Input placeholder="请输入任务标签" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
@@ -125,26 +128,26 @@ const CronLabelModal = ({
form form
.validateFields() .validateFields()
.then(async (values) => { .then(async (values) => {
if (typeof values.labels === 'string') {
values.labels = values.labels.split(/,|/);
}
setLoading(true); setLoading(true);
const payload = { ids, labels: values.labels }; const payload = { ids, labels: values.labels };
try { const { code, data } = await request[action](
const { code, data } = await request[action]( `${config.apiPrefix}crons/labels`,
`${config.apiPrefix}crons/labels`, {
{ data: payload,
data: payload, },
}, );
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
); );
} else {
if (code === 200) { message.error(data);
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
handleCancel(true);
}
setLoading(false);
} catch (error) {
setLoading(false);
} }
setLoading(false);
handleCancel(true);
}) })
.catch((info) => { .catch((info) => {
console.log('Validate Failed:', info); console.log('Validate Failed:', info);
@@ -168,17 +171,15 @@ const CronLabelModal = ({
return ( return (
<Modal <Modal
title="批量修改标签" title="批量修改标签"
open={visible} visible={visible}
footer={buttons} footer={buttons}
centered
maskClosable={false}
forceRender forceRender
onCancel={() => handleCancel(false)} onCancel={() => handleCancel(false)}
confirmLoading={loading} confirmLoading={loading}
> >
<Form form={form} layout="vertical" name="form_in_label_modal"> <Form form={form} layout="vertical" name="form_in_label_modal">
<Form.Item name="labels" label="标签"> <Form.Item name="labels" label="标签">
<EditableTagGroup /> <Input placeholder="请输入任务标签" />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
-316
View File
@@ -1,316 +0,0 @@
import React, { useEffect, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Statistic,
Button,
Space,
Select,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
const PROPERTIES = [
{ name: '命令', value: 'command' },
{ name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' },
];
const OPERATIONS = [
{ name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' },
{ name: '不属于', value: 'Nin' },
// { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' },
// { name: '不为空', value: 'NotNull' },
];
const SORTTYPES = [
{ name: '顺序', value: 'ASC' },
{ name: '倒序', value: 'DESC' },
];
const STATUS = [
{ name: '运行中', value: 0 },
{ name: '空闲中', value: 1 },
{ name: '已禁用', value: 2 },
];
enum ViewFilterRelation {
'and' = '且',
'or' = '或',
}
const ViewCreateModal = ({
view,
handleCancel,
visible,
}: {
view?: any;
visible: boolean;
handleCancel: (param?: any) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
const handleOk = async (values: any) => {
setLoading(true);
values.filterRelation = filterRelation;
const method = view ? 'put' : 'post';
try {
const { code, data } = await request[method](
`${config.apiPrefix}crons/views`,
{
data: view ? { ...values, id: view.id } : values,
},
);
if (code === 200) {
handleCancel(data);
}
setLoading(false);
} catch (error: any) {
setLoading(false);
}
};
useEffect(() => {
if (!view) {
form.resetFields();
}
form.setFieldsValue(
view || {
filters: [{ property: 'command', operation: 'Reg' }],
},
);
}, [view, visible]);
const operationElement = (
<Select style={{ width: 80 }}>
{OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const propertyElement = (props: any, style: React.CSSProperties = {}) => {
return (
<Select style={style}>
{props.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
};
const typeElement = (
<Select>
{SORTTYPES.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
const statusElement = (
<Select mode="multiple" allowClear placeholder="请选择状态">
{STATUS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</Select>
);
return (
<Modal
title={view ? '编辑视图' : '新建视图'}
open={visible}
forceRender
width={580}
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="env_modal">
<Form.Item
name="name"
label="视图名称"
rules={[{ required: true, message: '请输入视图名称' }]}
>
<Input placeholder="请输入视图名称" />
</Form.Item>
<Form.List name="filters">
{(fields, { add, remove }) => (
<div
style={{ position: 'relative' }}
className={`view-filters-container ${
fields.length > 1 ? 'active' : ''
}`}
>
{fields.length > 1 && (
<div
style={{
position: 'absolute',
width: 50,
borderRadius: 10,
border: '1px solid rgb(190, 220, 255)',
borderRight: 'none',
height: 56 * (fields.length - 1),
top: 46,
left: 15,
}}
>
<Button
type="primary"
size="small"
style={{
position: 'absolute',
top: '50%',
translate: '-50% -50%',
padding: '0 0 0 3px',
cursor: 'pointer',
}}
onClick={() => {
setFilterRelation(
filterRelation === 'and' ? 'or' : 'and',
);
}}
>
<>
<span>{ViewFilterRelation[filterRelation]}</span>
<IconFont type="ql-icon-d-caret" />
</>
</Button>
</div>
)}
<div>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
className="filter-item"
>
<Space
className="view-create-modal-filters"
align="baseline"
style={
fields.length > 1 ? { width: 'calc(100% - 40px)' } : {}
}
>
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 90 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'operation']}
rules={[{ required: true }]}
>
{operationElement}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{['In', 'Nin'].includes(
form.getFieldValue(['filters', index, 'operation']),
) ? (
statusElement
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
</Form.Item>
))}
<Form.Item>
<a
onClick={() =>
add({ property: 'command', operation: 'Reg' })
}
>
<PlusOutlined />
</a>
</Form.Item>
</div>
</div>
)}
</Form.List>
<Form.List name="sorts">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '排序方式' : ''}
key={key}
style={{ marginBottom: 0 }}
>
<Space className="view-create-modal-sorts" align="baseline">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 240 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'type']}
rules={[{ required: true }]}
>
{typeElement}
</Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} />
</Space>
</Form.Item>
))}
<Form.Item>
<a onClick={() => add({ property: 'command', type: 'ASC' })}>
<PlusOutlined />
</a>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
);
};
export default ViewCreateModal;
-278
View File
@@ -1,278 +0,0 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Space,
Table,
Tag,
Typography,
Button,
Switch,
} from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
import { DeleteOutlined, EditOutlined } from '@ant-design/icons';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import ViewCreateModal from './viewCreateModal';
const { Text } = Typography;
const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const ViewManageModal = ({
cronViews,
handleCancel,
visible,
cronViewChange,
}: {
cronViews: any[];
visible: boolean;
handleCancel: () => void;
cronViewChange: (data?: any) => void;
}) => {
const columns: any = [
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center' as const,
render: (text) => (
<div style={{ textAlign: 'left', paddingLeft: 30 }}>{text}</div>
),
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center' as const,
render: (v) => (v === 1 ? '系统' : '个人'),
},
{
title: '显示',
key: 'isDisabled',
dataIndex: 'isDisabled',
align: 'center' as const,
width: 100,
render: (text: string, record: any, index: number) => {
return (
<Switch
checked={!record.isDisabled}
onChange={(checked) => onShowChange(checked, record, index)}
/>
);
},
},
{
title: '操作',
key: 'action',
width: 100,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return record.type !== 1 ? (
<Space size="middle">
<a onClick={() => editView(record, index)}>
<EditOutlined />
</a>
<a onClick={() => deleteView(record, index)}>
<DeleteOutlined />
</a>
</Space>
) : (
'-'
);
},
},
];
const [list, setList] = useState<any[]>([]);
const [isCreateViewModalVisible, setIsCreateViewModalVisible] =
useState<boolean>(false);
const [editedView, setEditedView] = useState<any>(null);
const editView = (record: any, index: number) => {
setEditedView(record);
setIsCreateViewModalVisible(true);
};
const deleteView = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.name}
</Text>{' '}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
cronViewChange();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const onShowChange = (checked: boolean, record: any, index: number) => {
console.log(checked);
request
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id],
})
.then(({ code, data }) => {
if (code === 200) {
const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list);
cronViewChange();
}
});
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = list[dragIndex];
request
.put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
})
.then(({ code, data }) => {
if (code === 200) {
const newData = [...list];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setList(newData);
cronViewChange();
}
});
},
[list],
);
useEffect(() => {
setList(cronViews);
}, [cronViews]);
return (
<Modal
title="视图管理"
open={visible}
centered
width={620}
onCancel={() => handleCancel()}
className="view-manage-modal"
forceRender
footer={false}
maskClosable={false}
>
<Space
style={{
display: 'flex',
justifyContent: 'flex-end',
marginBottom: 10,
}}
>
<Button
key="2"
type="primary"
onClick={() => {
setEditedView(null);
setIsCreateViewModalVisible(true);
}}
>
</Button>
</Space>
<DndProvider backend={HTML5Backend}>
<Table
bordered
columns={columns}
pagination={false}
dataSource={list}
rowKey="id"
size="middle"
style={{ marginBottom: 20 }}
components={components}
onRow={(record: any, index: number) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<ViewCreateModal
view={editedView}
visible={isCreateViewModalVisible}
handleCancel={(data) => {
setIsCreateViewModalVisible(false);
cronViewChange(data);
}}
/>
</Modal>
);
};
export default ViewManageModal;
+108 -167
View File
@@ -16,7 +16,7 @@ import {
DeleteOutlined, DeleteOutlined,
SyncOutlined, SyncOutlined,
CheckCircleOutlined, CheckCircleOutlined,
DeleteFilled, StopOutlined,
BugOutlined, BugOutlined,
FileTextOutlined, FileTextOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@@ -27,11 +27,8 @@ import DependenceModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal'; import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -51,9 +48,7 @@ enum StatusColor {
'error', 'error',
} }
const Dependence = () => { const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const { headerStyle, isPhone, socketMessage } =
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: '序号', title: '序号',
@@ -125,15 +120,6 @@ const Dependence = () => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
{record.status !== Status. && {record.status !== Status. &&
record.status !== Status. && ( record.status !== Status. && (
<> <>
@@ -147,13 +133,17 @@ const Dependence = () => {
<DeleteOutlined /> <DeleteOutlined />
</a> </a>
</Tooltip> </Tooltip>
<Tooltip title={isPc ? '强制删除' : ''}>
<a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled />
</a>
</Tooltip>
</> </>
)} )}
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
</Space> </Space>
); );
}, },
@@ -165,11 +155,10 @@ const Dependence = () => {
const [editedDependence, setEditedDependence] = useState(); const [editedDependence, setEditedDependence] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [logDependence, setLogDependence] = useState<any>(); const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs'); const [type, setType] = useState('nodejs');
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const getDependencies = () => { const getDependencies = () => {
setLoading(true); setLoading(true);
@@ -177,10 +166,8 @@ const Dependence = () => {
.get( .get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`, `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
) )
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { setValue(data.data);
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -195,11 +182,7 @@ const Dependence = () => {
setIsModalVisible(true); setIsModalVisible(true);
}; };
const deleteDependence = ( const deleteDependence = (record: any, index: number) => {
record: any,
index: number,
force: boolean = false,
) => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: ( content: (
@@ -213,17 +196,12 @@ const Dependence = () => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, { .delete(`${config.apiPrefix}dependencies`, { data: [record.id] })
data: [record.id], .then((data: any) => {
}) if (data.code === 200) {
.then(({ code, data }) => { handleDependence(data.data[0]);
if (code === 200 && force) { } else {
const i = value.findIndex((x) => x.id === data.data[0].id); message.error(data);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
setValue(result);
}
} }
}); });
}, },
@@ -250,9 +228,11 @@ const Dependence = () => {
.put(`${config.apiPrefix}dependencies/reinstall`, { .put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id], data: [record.id],
}) })
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { if (data.code === 200) {
handleDependence(data[0]); handleDependence(data.data[0]);
} else {
message.error(data);
} }
}); });
}, },
@@ -273,60 +253,41 @@ const Dependence = () => {
result.push(...dependence); result.push(...dependence);
} else { } else {
const index = value.findIndex((x) => x.id === dependence.id); const index = value.findIndex((x) => x.id === dependence.id);
if (index !== -1) { result.splice(index, 1, {
result.splice(index, 1, { ...dependence,
...dependence, });
});
}
} }
setValue(result); setValue(result);
}; };
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowKeys: selectedRowIds, selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
const delDependencies = (force: boolean) => { const delDependencies = () => {
const forceUrl = force ? '/force' : '';
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: <></>, content: <></>,
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}dependencies${forceUrl}`, { .delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds })
data: selectedRowIds, .then((data: any) => {
}) if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]);
getDependencies();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handlereInstallDependencies = () => {
Modal.confirm({
title: '确认重新安装',
content: <></>,
onOk() {
request
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]); setSelectedRowIds([]);
getDependencies(); getDependencies();
} else {
message.error(data);
} }
}); });
}, },
@@ -339,18 +300,14 @@ const Dependence = () => {
const getDependenceDetail = (dependence: any) => { const getDependenceDetail = (dependence: any) => {
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then(({ code, data }) => { .then((data: any) => {
if (code === 200) { const index = value.findIndex((x) => x.id === dependence.id);
const index = value.findIndex((x) => x.id === dependence.id); const result = [...value];
const result = [...value]; result.splice(index, 1, {
if (index !== -1) { ...dependence,
result.splice(index, 1, { ...data.data,
...dependence, });
...data, setValue(result);
});
setValue(result);
}
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -363,6 +320,12 @@ const Dependence = () => {
getDependencies(); getDependencies();
}, [searchText, type]); }, [searchText, type]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
useEffect(() => { useEffect(() => {
if (logDependence) { if (logDependence) {
localStorage.setItem('logDependence', logDependence.id); localStorage.setItem('logDependence', logDependence.id);
@@ -387,12 +350,10 @@ const Dependence = () => {
const result = [...value]; const result = [...value];
for (let i = 0; i < references.length; i++) { for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]); const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) { result.splice(index, 1, {
result.splice(index, 1, { ...result[index],
...value[index], status,
status, });
});
}
} }
setValue(result); setValue(result);
@@ -401,9 +362,7 @@ const Dependence = () => {
const _result = [...value]; const _result = [...value];
for (let i = 0; i < references.length; i++) { for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]); const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) { _result.splice(index, 1);
_result.splice(index, 1);
}
} }
setValue(_result); setValue(_result);
}, 5000); }, 5000);
@@ -411,14 +370,45 @@ const Dependence = () => {
} }
}, [socketMessage]); }, [socketMessage]);
const panelContent = () => (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delDependencies}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</>
);
const onTabChange = (activeKey: string) => { const onTabChange = (activeKey: string) => {
setSelectedRowIds([]);
setType(activeKey); setType(activeKey);
}; };
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper dependence-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper dependence-wrapper"
title="依赖管理" title="依赖管理"
extra={[ extra={[
<Search <Search
@@ -441,64 +431,17 @@ const Dependence = () => {
size="small" size="small"
tabPosition="top" tabPosition="top"
onChange={onTabChange} onChange={onTabChange}
items={[ >
{ <Tabs.TabPane tab="NodeJs" key="nodejs">
key: 'nodejs', {panelContent()}
label: 'NodeJs', </Tabs.TabPane>
}, <Tabs.TabPane tab="Python3" key="python3">
{ {panelContent()}
key: 'python3', </Tabs.TabPane>
label: 'Python3', <Tabs.TabPane tab="Linux" key="linux">
}, {panelContent()}
{ </Tabs.TabPane>
key: 'linux', </Tabs>
label: 'Linux',
},
]}
/>
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</div>
<DependenceModal <DependenceModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
@@ -512,10 +455,8 @@ const Dependence = () => {
if (needRemove) { if (needRemove) {
const index = value.findIndex((x) => x.id === logDependence.id); const index = value.findIndex((x) => x.id === logDependence.id);
const result = [...value]; const result = [...value];
if (index !== -1) { result.splice(index, 1);
result.splice(index, 1); setValue(result);
setValue(result);
}
} else if ([...value].map((x) => x.id).includes(logDependence.id)) { } else if ([...value].map((x) => x.id).includes(logDependence.id)) {
getDependenceDetail(logDependence); getDependenceDetail(logDependence);
} }

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