Compare commits

..
217 Commits
Author SHA1 Message Date
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
110 changed files with 4388 additions and 12880 deletions
+7 -4
View File
@@ -1,9 +1,12 @@
MONGODB_URI='mongodb://' PUBLIC_PORT=5400
YIYAN_MONGODB_URI=''
CRON_PORT=5500 CRON_PORT=5500
PORT=5600 BACK_PORT=5600
PORT=5700
LOG_LEVEL='debug' LOG_LEVEL='debug'
SECRET='whyour' SECRET='whyour'
QINIU_AK = ''
QINIU_SK = ''
QINIU_SCOPE = ''
+12 -7
View File
@@ -28,17 +28,17 @@ jobs:
- name: build front and back - name: build front and back
run: | run: |
yarn install yarn install
yarn build yarn build:front
yarn build-back yarn build:back
- name: copy to static repo - name: copy to static repo
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 }}
run: | run: |
mkdir -p static mkdir -p tmp
cd ./static cd ./tmp
cp -rf ../dist ./ && cp -rf ../build ./ cp -rf ../static/* ./
git init -b ${GITHUB_BRANCH} && git add . git init -b ${GITHUB_BRANCH} && git add .
git config --local user.name 'github-actions[bot]' git config --local user.name 'github-actions[bot]'
git config --local user.email 'github-actions[bot]@users.noreply.github.com' git config --local user.email 'github-actions[bot]@users.noreply.github.com'
@@ -56,6 +56,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: '14'
- name: Set time zone - name: Set time zone
uses: szenius/set-timezone@v1.0 uses: szenius/set-timezone@v1.0
@@ -105,8 +108,10 @@ jobs:
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
build-args: MAINTAINER=${{ github.repository_owner }}, QL_BRANCH=${{ github.ref_name }} build-args: |
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
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 }}
+3 -2
View File
@@ -4,12 +4,13 @@
/node_modules /node_modules
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock
/pnpm-lock.yaml /pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
/dist /static
/build /data
# misc # misc
.DS_Store .DS_Store
+1
View File
@@ -0,0 +1 @@
sentrycli_cdnurl=https://cdn.npm.taobao.org/dist/sentry-cli
+7 -9
View File
@@ -4,15 +4,22 @@ const CompressionPlugin = require('compression-webpack-plugin');
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
layout: false, layout: false,
outputPath: 'static/dist',
nodeModulesTransform: { nodeModulesTransform: {
type: 'none', type: 'none',
}, },
fastRefresh: {}, fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: { dynamicImport: {
loading: '@/components/pageLoading', loading: '@/components/pageLoading',
}, },
favicon: '/images/g5.ico', favicon: '/images/g5.ico',
proxy: { proxy: {
'/api/public': {
target: 'http://127.0.0.1:5400/',
changeOrigin: true,
},
'/api': { '/api': {
target: 'http://127.0.0.1:5600/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
@@ -32,18 +39,9 @@ export default defineConfig({
externals: { externals: {
react: 'window.React', react: 'window.React',
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
darkreader: 'window.DarkReader',
codemirror: 'window.CodeMirror',
'sockjs-client': 'window.SockJS',
}, },
scripts: [ scripts: [
'https://gw.alipayobjects.com/os/lib/react/16.13.1/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/16.13.1/umd/react-dom.production.min.js', 'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
'https://cdn.jsdelivr.net/npm/darkreader@4/darkreader.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/lib/codemirror.min.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/shell/shell.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/python/python.js',
'https://cdn.jsdelivr.net/npm/codemirror@5/mode/javascript/javascript.js',
'https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js',
], ],
}); });
-92
View File
@@ -1,92 +0,0 @@
# 安装说明
## 前置要求
```bash
1.已安装docker-ce
2.选装docker-compose
```
## 安装方式1
```bash
1. 新建一个文件夹,用于存放相关数据
2. 下载本仓库中的`docker-compose.yml`至本地,或是复制文件内容后在本地自行建立并粘贴内容
3. 使用docker-compose启动
4. 浏览器输入ip:5700即可进入面板
# 新建数据文件夹
mkdir qinglong
cd qinglong
# 下载docker-compose.yml文件
wget https://raw.githubusercontent.com/whyour/qinglong/develop/docker-compose.yml
# 启动
docker-compose up -d
```
## 安装方式2
```bash
# 复制下列命令在ssh执行(先安装docker)
# 注:$PWD请修改为实际你想安装的路径
docker run -dit \
-v $PWD/ql/config:/ql/config \
-v $PWD/ql/log:/ql/log \
-v $PWD/ql/db:/ql/db \
-v $PWD/ql/repo:/ql/repo \
-v $PWD/ql/raw:/ql/raw \
-v $PWD/ql/scripts:/ql/scripts \
-v $PWD/ql/jbot:/ql/jbot \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
## 登录
```bash
打开浏览器访问宿主机ip的5700端口即可
例如http://192.168.100.123:5700即ip:5700
首次登录
账号:admin 密码:admin
会生成`auth.json`
在ssh输入
1.docker exec -it qinglong bash
2.cat /ql/config/auth.json
cat查看之后返回的结果类似如下字段
{"username":"admin","password":"Xb-ZYP526wmg4_h6q1WqIO"}
# admin即为登录名;Xb-ZYP526wmg4_h6q1WqIO为登录密码
```
输入此处记录的`username``password`,即可成功登录qinglong面板,登录后即可正常使用
## 拉取脚本
```bash
示例
ql repo https://github.com/xxx.git #拉取仓库
ql raw https://raw.githubusercontent.com/xxx #拉取单个脚本
```
## 备份
所有数据都将保存在`docker-compose.yml`所在的同级目录的`data`文件夹中,如需要备份,请直接备份`docker-compose.yml``data`文件夹即可
```bash
root@debian:/opt/qinglong# ls -lah
总用量 8.0K
drwxr-xr-x 3 root root 4.0K 8月 30 01:29 .
drwxr-xr-x 4 root root 4.0K 8月 30 00:51 ..
drwxr-xr-x 8 root root 4.0K 8月 30 01:30 data
-rw-r--r-- 1 root root 386 8月 30 01:29 docker-compose.yml
```
## 更新
在面板执行"更新面板"任务即可
或者
```bash
cd qinglong
docker-compose down
docker pull whyour/qinglong:latest
docker-compose up -d
```
+160 -27
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="/public/images/qinglong.png"> <img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a> </a>
</p> </p>
@@ -8,12 +8,10 @@
<div align="center"> <div align="center">
A timed task management panel that supports typescript, javaScript, python3, and shell.(支持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] [![donate][donate-image]][donate-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]
[donate-image]: https://img.shields.io/badge/donate-wechat-green?style=flat
[donate-url]: https://qinglong.whyour.cn/nice.png
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat [docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
@@ -24,31 +22,166 @@ A timed task management panel that supports typescript, javaScript, python3, and
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
<p align="center"> [![](https://z3.ax1x.com/2021/11/18/I7KrTg.jpg)](https://whyour.cn)
<img width="49%" src="/public/images/login.png">
<img width="49%" src="/public/images/home.png"> 简体中文 | [English](./README-en.md)
</p>
## 功能
- 支持多种脚本语言(python3、javaScript、shell、typescript
- 支持在线管理脚本、环境变量、配置文件
- 支持在线查看任务日志
- 支持秒级任务设置
- 支持系统级通知
- 支持暗黑模式
- 支持手机端操作
## 部署
### 本机部署
```bash
# 待完善
```
### podman 部署
1. podman 安装
```bash
https://podman.io/getting-started/installation
```
2. 启动容器
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 启动容器
```bash
docker run -dit \
-v $PWD/ql:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose 部署
1. docker-compose 安装
```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. 启动容器
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
## 使用
1. 内置命令
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
```
2. 参数说明
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
## 链接
- [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/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
$ yarn install
$ yarn start
```
打开你的浏览器,访问 http://127.0.0.1:5700
## 交流
[telegram频道](https://t.me/jiao_long)
## 名称来源
青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。 青龙,又名苍龙,在中国传统文化中是四象之一、[天之四灵](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。 《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。 在中国[二十八宿](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF)中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
## 如何安装
请查看[INSTALL.md](INSTALL.md)
## 多谢
* [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/)
* [Umijs3.0](https://umijs.org)
* [darkreader](https://github.com/darkreader/darkreader)
+4 -4
View File
@@ -8,10 +8,10 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/configs', route);
route.get( route.get(
'/configs/files', '/files',
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 {
@@ -32,7 +32,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/configs/: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 {
@@ -58,7 +58,7 @@ export default (app: Router) => {
); );
route.post( route.post(
'/configs/save', '/save',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
+100 -37
View File
@@ -7,31 +7,28 @@ import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/crons', route);
route.get(
'/crons', route.get('/', 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 {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.crontabs( const data = await cronService.crontabs(req.query.searchValue as string);
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);
return next(e); return next(e);
} }
}, });
);
route.post( route.post(
'/crons', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
command: Joi.string().required(), command: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional(), name: Joi.string().optional(),
labels: Joi.array().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -52,9 +49,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/run', '/run',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -70,9 +67,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/stop', '/stop',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -87,10 +84,55 @@ export default (app: Router) => {
}, },
); );
route.put( route.delete(
'/crons/disable', '/labels',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
labels: Joi.array().items(Joi.string().required()),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.removeLabels(
req.body.ids,
req.body.labels,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post(
'/labels',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
labels: Joi.array().items(Joi.string().required()),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.addLabels(req.body.ids, req.body.labels);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -106,9 +148,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/enable', '/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -124,13 +166,13 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/:id/log', '/:id/log',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, 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);
@@ -144,13 +186,14 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
labels: Joi.array().optional().allow(null),
command: Joi.string().optional(), command: Joi.string().optional(),
schedule: Joi.string().optional(), schedule: Joi.string().optional(),
name: Joi.string().optional(), name: Joi.string().optional(),
_id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -174,9 +217,9 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/crons', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -192,9 +235,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/pin', '/pin',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -210,9 +253,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/unpin', '/unpin',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -228,7 +271,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/import', '/import',
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 {
@@ -243,17 +286,17 @@ export default (app: Router) => {
); );
route.get( route.get(
'/crons/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, 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.get(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); logger.error('🔥 error: %o', e);
@@ -263,10 +306,10 @@ export default (app: Router) => {
); );
route.put( route.put(
'/crons/status', '/status',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.number().required()),
status: Joi.string().required(), status: Joi.string().required(),
pid: Joi.string().optional(), pid: Joi.string().optional(),
log_path: Joi.string().optional(), log_path: Joi.string().optional(),
@@ -290,4 +333,24 @@ export default (app: Router) => {
} }
}, },
); );
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 cronService = Container.get(CronService);
const data = await cronService.logs(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+19 -21
View File
@@ -6,10 +6,9 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/dependencies', route);
route.get(
'/dependencies', route.get('/', 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 {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
@@ -19,17 +18,16 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.post( route.post(
'/dependencies', '/',
celebrate({ celebrate({
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
type: Joi.number().required(), type: Joi.number().required(),
remark: Joi.number().optional().allow(''), remark: Joi.string().optional().allow(''),
}), }),
), ),
}), }),
@@ -47,13 +45,13 @@ export default (app: Router) => {
); );
route.put( route.put(
'/dependencies', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
name: Joi.string().required(), name: Joi.string().required(),
_id: Joi.string().required(), id: Joi.number().required(),
type: Joi.number().required(), type: Joi.number().required(),
remark: Joi.number().optional().allow(''), remark: Joi.string().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -70,9 +68,9 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/dependencies', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -88,9 +86,9 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/dependencies/force', '/force',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -106,17 +104,17 @@ export default (app: Router) => {
); );
route.get( route.get(
'/dependencies/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
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.get(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); logger.error('🔥 error: %o', e);
@@ -126,9 +124,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/dependencies/reinstall', '/reinstall',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
+23 -25
View File
@@ -6,10 +6,9 @@ import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/envs', route);
route.get(
'/envs', route.get('/', 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 {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
@@ -19,11 +18,10 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.post( route.post(
'/envs', '/',
celebrate({ celebrate({
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
@@ -47,13 +45,13 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string().required(), name: Joi.string().required(),
remarks: Joi.string().optional().allow(''), remarks: Joi.string().optional().allow('').allow(null),
_id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -70,9 +68,9 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/envs', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -88,17 +86,17 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/:id/move', '/:id/move',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.number().required(),
}), }),
body: Joi.object({ body: Joi.object({
fromIndex: Joi.number().required(), fromIndex: Joi.number().required(),
toIndex: Joi.number().required(), toIndex: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
@@ -112,9 +110,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/disable', '/disable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -130,9 +128,9 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/enable', '/enable',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -148,10 +146,10 @@ export default (app: Router) => {
); );
route.put( route.put(
'/envs/name', '/name',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
ids: Joi.array().items(Joi.string().required()), ids: Joi.array().items(Joi.number().required()),
name: Joi.string().required(), name: Joi.string().required(),
}), }),
}), }),
@@ -169,17 +167,17 @@ export default (app: Router) => {
); );
route.get( route.get(
'/envs/:id', '/:id',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
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.get(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) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
+2
View File
@@ -7,6 +7,7 @@ import cron from './cron';
import script from './script'; import script from './script';
import open from './open'; import open from './open';
import dependence from './dependence'; import dependence from './dependence';
import system from './system';
export default () => { export default () => {
const app = Router(); const app = Router();
@@ -18,6 +19,7 @@ export default () => {
script(app); script(app);
open(app); open(app);
dependence(app); dependence(app);
system(app);
return app; return app;
}; };
+6 -8
View File
@@ -7,10 +7,9 @@ import { getFileContentByName } from '../config/util';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/logs', route);
route.get(
'/logs', route.get('/', 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 {
const fileList = fs.readdirSync(config.logPath, 'utf-8'); const fileList = fs.readdirSync(config.logPath, 'utf-8');
@@ -40,11 +39,10 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.get( route.get(
'/logs/:dir/:file', '/:dir/: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 {
@@ -61,7 +59,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/logs/: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 {
+4 -4
View File
@@ -49,7 +49,7 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
name: Joi.string().optional().allow(''), name: Joi.string().optional().allow(''),
scopes: Joi.array().items(Joi.string()), scopes: Joi.array().items(Joi.string()),
_id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -68,7 +68,7 @@ export default (app: Router) => {
route.delete( route.delete(
'/apps', '/apps',
celebrate({ celebrate({
body: Joi.array().items(Joi.string().required()), body: Joi.array().items(Joi.number().required()),
}), }),
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');
@@ -87,10 +87,10 @@ export default (app: Router) => {
'/apps/:id/reset-secret', '/apps/:id/reset-secret',
celebrate({ celebrate({
params: Joi.object({ params: Joi.object({
id: Joi.string().required(), id: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const openService = Container.get(OpenService); const openService = Container.get(OpenService);
+77 -17
View File
@@ -9,14 +9,15 @@ 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 from 'path'; import path, { join } from 'path';
import ScriptService from '../services/script';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/scripts', route);
route.get( route.get(
'/scripts/files', '/files',
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 {
@@ -43,7 +44,7 @@ export default (app: Router) => {
children.push({ children.push({
title: childFile, title: childFile,
value: childFile, value: childFile,
key: childFile, key: `${fileOrDir}/${childFile}`,
mtime: statObj.mtimeMs, mtime: statObj.mtimeMs,
parent: fileOrDir, parent: fileOrDir,
}); });
@@ -79,14 +80,16 @@ export default (app: Router) => {
); );
route.get( route.get(
'/scripts/: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 {
const path = req.query.path ? `${req.query.path}/` : ''; const filePath = join(
const content = getFileContentByName( config.scriptPath,
`${config.scriptPath}${path}${req.params.file}`, 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); logger.error('🔥 error: %o', e);
@@ -96,11 +99,11 @@ export default (app: Router) => {
); );
route.post( route.post(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().allow(''), content: Joi.string().allow(''),
originFilename: Joi.string().allow(''), originFilename: Joi.string().allow(''),
}), }),
@@ -120,6 +123,9 @@ export default (app: Router) => {
if (!path.endsWith('/')) { if (!path.endsWith('/')) {
path += '/'; path += '/';
} }
if (!path.startsWith('/')) {
path = `${config.scriptPath}${path}`;
}
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,
@@ -153,11 +159,11 @@ export default (app: Router) => {
); );
route.put( route.put(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().optional().allow(''),
content: Joi.string().required(), content: Joi.string().required(),
}), }),
}), }),
@@ -169,7 +175,7 @@ export default (app: Router) => {
content: string; content: string;
path: string; path: string;
}; };
const filePath = `${config.scriptPath}${path}/${filename}`; const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -180,19 +186,21 @@ export default (app: Router) => {
); );
route.delete( route.delete(
'/scripts', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: 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 } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string;
}; };
const filePath = `${config.scriptPath}${filename}`; const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
@@ -203,7 +211,7 @@ export default (app: Router) => {
); );
route.post( route.post(
'/scripts/download', '/download',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
@@ -232,4 +240,56 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/run',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().optional().allow(''),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
filename: string;
path: string;
};
const filePath = join(path, filename);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+135
View File
@@ -0,0 +1,135 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
const route = Router();
export default (app: Router) => {
app.use('/system', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
version,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get(
'/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/notify',
celebrate({
body: Joi.object({
title: Joi.string().required(),
content: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.notify(req.body);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
};
+14 -115
View File
@@ -9,7 +9,8 @@ import { getFileContentByName } from '../config/util';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/user', route);
route.post( route.post(
'/login', '/login',
celebrate({ celebrate({
@@ -47,7 +48,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user', '/',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
@@ -67,9 +68,7 @@ export default (app: Router) => {
}, },
); );
route.get( route.get('/', async (req: Request, res: Response, next: NextFunction) => {
'/user',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
@@ -85,11 +84,10 @@ export default (app: Router) => {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
return next(e); return next(e);
} }
}, });
);
route.get( route.get(
'/user/two-factor/init', '/two-factor/init',
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 {
@@ -104,7 +102,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/two-factor/active', '/two-factor/active',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
code: Joi.string().required(), code: Joi.string().required(),
@@ -124,7 +122,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/two-factor/deactive', '/two-factor/deactive',
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 {
@@ -139,7 +137,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/two-factor/login', '/two-factor/login',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
code: Joi.string().required(), code: Joi.string().required(),
@@ -161,7 +159,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/user/login-log', '/login-log',
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 {
@@ -176,7 +174,7 @@ export default (app: Router) => {
); );
route.get( route.get(
'/user/notification', '/notification',
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 {
@@ -191,7 +189,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/user/notification', '/notification',
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 {
@@ -205,40 +203,8 @@ export default (app: Router) => {
}, },
); );
route.get(
'/system',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envDbContent.length === 0
) {
isInitialized = false;
}
res.send({
code: 200,
data: {
isInitialized,
},
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
// 初始化api
route.put( route.put(
'/init/user', '/init',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
@@ -259,7 +225,7 @@ export default (app: Router) => {
); );
route.put( route.put(
'/init/notification', '/notification/init',
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 {
@@ -272,71 +238,4 @@ export default (app: Router) => {
} }
}, },
); );
route.get(
'/system/log/remove',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const data = await userService.getLogRemoveFrequency();
res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/log/remove',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update-check',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.checkUpdate();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/system/update',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const userService = Container.get(UserService);
const result = await userService.updateSystem();
res.send(result);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
}; };
+2
View File
@@ -9,6 +9,8 @@ import Logger from './loaders/logger';
async function startServer() { async function startServer() {
const app = express(); const app = express();
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/db').default();
+23 -34
View File
@@ -4,47 +4,40 @@ import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
const lastVersionFile = const lastVersionFile = 'https://qn.whyour.cn/version.ts';
'https://ghproxy.com/https://raw.githubusercontent.com/whyour/qinglong/master/src/version.ts';
const envFound = dotenv.config(); const envFound = dotenv.config();
const rootPath = process.cwd(); const rootPath = process.cwd();
const envFile = path.join(rootPath, 'config/env.sh'); const dataPath = path.join(rootPath, 'data/');
const confFile = path.join(rootPath, 'config/config.sh');
const sampleFile = path.join(rootPath, 'sample/config.sample.sh');
const crontabFile = path.join(rootPath, 'config/crontab.list');
const confBakDir = path.join(rootPath, 'config/bak/');
const authConfigFile = path.join(rootPath, 'config/auth.json');
const extraFile = path.join(rootPath, 'config/extra.sh');
const configPath = path.join(rootPath, 'config/');
const scriptPath = path.join(rootPath, 'scripts/');
const bakPath = path.join(rootPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const logPath = path.join(rootPath, 'log/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh');
const crontabFile = path.join(configPath, 'crontab.list');
const authConfigFile = path.join(configPath, 'auth.json');
const extraFile = path.join(configPath, 'extra.sh');
const confBakDir = path.join(dataPath, 'config/bak/');
const sampleFile = path.join(samplePath, 'config.sample.sh');
const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const dbPath = path.join(rootPath, 'db/');
const cronDbFile = path.join(rootPath, 'db/crontab.db');
const envDbFile = path.join(rootPath, 'db/env.db');
const appDbFile = path.join(rootPath, 'db/app.db');
const authDbFile = path.join(rootPath, 'db/auth.db');
const dependenceDbFile = path.join(rootPath, 'db/dependence.db');
const versionFile = path.join(rootPath, 'src/version.ts'); const versionFile = path.join(rootPath, 'src/version.ts');
const configFound = dotenv.config({ path: confFile });
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
} }
if (configFound.error) {
throw new Error("⚠️ Couldn't find config.sh file ⚠️");
}
export default { export default {
port: parseInt(process.env.PORT as string, 10), port: parseInt(process.env.BACK_PORT as string, 10),
cronPort: parseInt(process.env.CRON_PORT as string, 10), cronPort: parseInt(process.env.CRON_PORT as string, 10),
publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
secret: process.env.SECRET || createRandomString(16, 32), secret: process.env.SECRET || createRandomString(16, 32),
logs: { logs: {
level: process.env.LOG_LEVEL || 'silly', level: process.env.LOG_LEVEL || 'silly',
@@ -65,11 +58,6 @@ export default {
confFile, confFile,
envFile, envFile,
dbPath, dbPath,
cronDbFile,
envDbFile,
appDbFile,
authDbFile,
dependenceDbFile,
configPath, configPath,
scriptPath, scriptPath,
samplePath, samplePath,
@@ -83,13 +71,14 @@ export default {
writePathList: [configPath, scriptPath], writePathList: [configPath, scriptPath],
bakPath, bakPath,
apiWhiteList: [ apiWhiteList: [
'/api/login', '/api/user/login',
'/open/auth/token', '/open/auth/token',
'/api/user/two-factor/login', '/api/user/two-factor/login',
'/api/system', '/api/system',
'/api/init/user', '/api/user/init',
'/api/init/notification', '/api/user/notification/init',
], ],
versionFile, versionFile,
lastVersionFile, lastVersionFile,
sqliteFile,
}; };
+38 -1
View File
@@ -107,7 +107,7 @@ export function createRandomString(min: number, max: number): string {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]); newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
} }
function getOne(arr) { function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)]; return arr[Math.floor(Math.random() * arr.length)];
} }
@@ -231,3 +231,40 @@ export async function fileExist(file: any) {
} }
}); });
} }
export async function concurrentRun(
fnList: Array<() => Promise<any>> = [],
max = 5,
) {
if (!fnList.length) return;
const replyList: any[] = []; // 收集任务执行结果
const startTime = new Date().getTime(); // 记录任务执行开始时间
// 任务执行程序
const schedule = async (index: number) => {
return new Promise(async (resolve) => {
const fn = fnList[index];
if (!fn) return resolve(null);
// 执行当前异步任务
const reply = await fn();
replyList[index] = reply;
// 执行完当前任务后,继续执行任务池的剩余任务
await schedule(index + max);
resolve(null);
});
};
// 任务池执行程序
const scheduleList = new Array(max)
.fill(0)
.map((_, index) => schedule(index));
// 使用 Promise.all 批量执行
const r = await Promise.all(scheduleList);
const cost = (new Date().getTime() - startTime) / 1000;
return replyList;
}
+15 -2
View File
@@ -1,14 +1,17 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class AuthInfo { export class AuthInfo {
ip?: string; ip?: string;
type: AuthDataType; type: AuthDataType;
info?: any; info?: any;
_id?: string; id?: number;
constructor(options: AuthInfo) { constructor(options: AuthInfo) {
this.ip = options.ip; this.ip = options.ip;
this.info = options.info; this.info = options.info;
this.type = options.type; this.type = options.type;
this._id = options._id; this.id = options.id;
} }
} }
@@ -23,3 +26,13 @@ export enum AuthDataType {
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency', 'removeLogFrequency' = 'removeLogFrequency',
} }
interface AuthInstance extends Model<AuthInfo, AuthInfo>, AuthInfo {}
export const AuthModel = sequelize.define<AuthInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
info: {
type: DataTypes.JSON,
allowNull: true,
},
});
+41 -6
View File
@@ -1,26 +1,31 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Crontab { export class Crontab {
name?: string; name?: string;
command: string; command: string;
schedule: string; schedule?: string;
timestamp?: string; timestamp?: string;
created?: number;
saved?: boolean; saved?: boolean;
_id?: string; id?: number;
status?: CrontabStatus; status?: CrontabStatus;
isSystem?: 1 | 0; isSystem?: 1 | 0;
pid?: number; pid?: number;
isDisabled?: 1 | 0; isDisabled?: 1 | 0;
log_path?: string; log_path?: string;
isPinned?: 1 | 0; isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
this.command = options.command; this.command = options.command;
this.schedule = options.schedule; this.schedule = options.schedule;
this.saved = options.saved; this.saved = options.saved;
this._id = options._id; this.id = options.id;
this.created = options.created; this.status =
this.status = CrontabStatus[options.status] options.status && CrontabStatus[options.status]
? options.status ? options.status
: CrontabStatus.idle; : CrontabStatus.idle;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
@@ -29,6 +34,9 @@ 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.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0;
} }
} }
@@ -38,3 +46,30 @@ export enum CrontabStatus {
'disabled', 'disabled',
'queued', 'queued',
} }
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
command: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
schedule: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
timestamp: DataTypes.STRING,
saved: DataTypes.BOOLEAN,
status: DataTypes.NUMBER,
isSystem: DataTypes.NUMBER,
pid: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
isPinned: DataTypes.NUMBER,
log_path: DataTypes.STRING,
labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER,
});
+20 -4
View File
@@ -1,7 +1,9 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence { export class Dependence {
timestamp?: string; timestamp?: string;
created?: number; id?: number;
_id?: string;
status?: DependenceStatus; status?: DependenceStatus;
type?: DependenceTypes; type?: DependenceTypes;
name?: number; name?: number;
@@ -9,8 +11,7 @@ export class Dependence {
remark?: string; remark?: string;
constructor(options: Dependence) { constructor(options: Dependence) {
this._id = options._id; this.id = options.id;
this.created = options.created || new Date().valueOf();
this.status = options.status || DependenceStatus.installing; this.status = options.status || DependenceStatus.installing;
this.type = options.type || DependenceTypes.nodejs; this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
@@ -46,3 +47,18 @@ export enum unInstallDependenceCommandTypes {
'pip3 uninstall -y', 'pip3 uninstall -y',
'apk del -f', 'apk del -f',
} }
interface DependenceInstance
extends Model<Dependence, Dependence>,
Dependence {}
export const DependenceModel = sequelize.define<DependenceInstance>(
'Dependence',
{
name: DataTypes.STRING,
type: DataTypes.NUMBER,
timestamp: DataTypes.STRING,
status: DataTypes.NUMBER,
log: DataTypes.JSON,
remark: DataTypes.STRING,
},
);
+17 -6
View File
@@ -1,17 +1,18 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Env { export class Env {
value?: string; value?: string;
timestamp?: string; timestamp?: string;
created?: number; id?: number;
_id?: string;
status?: EnvStatus; status?: EnvStatus;
position?: number; position?: number;
name?: number; name?: string;
remarks?: number; remarks?: string;
constructor(options: Env) { constructor(options: Env) {
this.value = options.value; this.value = options.value;
this._id = options._id; this.id = options.id;
this.created = options.created || new Date().valueOf();
this.status = options.status || EnvStatus.normal; this.status = options.status || EnvStatus.normal;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.position = options.position; this.position = options.position;
@@ -26,3 +27,13 @@ export enum EnvStatus {
} }
export const initEnvPosition = 9999999999; export const initEnvPosition = 9999999999;
interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING,
status: DataTypes.NUMBER,
position: DataTypes.NUMBER,
name: { type: DataTypes.STRING, unique: 'compositeIndex' },
remarks: DataTypes.STRING,
});
+15
View File
@@ -0,0 +1,15 @@
import { Sequelize } from 'sequelize';
import config from '../config/index';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`,
logging: false,
pool: {
max: 6,
min: 0,
idle: 30000,
},
});
export type ResponseType<T> = { code: number; data?: T; message?: string };
+15
View File
@@ -1,6 +1,8 @@
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan', 'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer',
'bark' = 'bark', 'bark' = 'bark',
'telegramBot' = 'telegramBot', 'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
@@ -15,6 +17,12 @@ abstract class NotificationBaseInfo {
public type!: NotificationMode; public type!: NotificationMode;
} }
export class GotifyNotification extends NotificationBaseInfo {
public gotifyUrl = '';
public gotifyToken = '';
public gotifyPriority = 0;
}
export class GoCqHttpBotNotification extends NotificationBaseInfo { export class GoCqHttpBotNotification extends NotificationBaseInfo {
public goCqHttpBotUrl = ''; public goCqHttpBotUrl = '';
public goCqHttpBotToken = ''; public goCqHttpBotToken = '';
@@ -25,8 +33,13 @@ export class ServerChanNotification extends NotificationBaseInfo {
public serverChanKey = ''; public serverChanKey = '';
} }
export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = '';
}
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
} }
@@ -70,7 +83,9 @@ export class EmailNotification extends NotificationBaseInfo {
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
+15 -3
View File
@@ -1,23 +1,26 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class App { export class App {
name: string; name: string;
scopes: AppScope[]; scopes: AppScope[];
client_id: string; client_id: string;
client_secret: string; client_secret: string;
tokens?: AppToken[]; tokens?: AppToken[];
_id?: string; id?: number;
constructor(options: App) { constructor(options: App) {
this.name = options.name; this.name = options.name;
this.scopes = options.scopes; this.scopes = options.scopes;
this.client_id = options.client_id; this.client_id = options.client_id;
this.client_secret = options.client_secret; this.client_secret = options.client_secret;
this._id = options._id; this.id = options.id;
} }
} }
export interface AppToken { export interface AppToken {
value: string; value: string;
type: 'Bearer'; type?: 'Bearer';
expiration: number; expiration: number;
} }
@@ -29,3 +32,12 @@ export enum CrontabStatus {
'disabled', 'disabled',
'queued', 'queued',
} }
interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON,
client_id: DataTypes.STRING,
client_secret: DataTypes.STRING,
tokens: DataTypes.JSON,
});
+3 -2
View File
@@ -1,7 +1,7 @@
export class SockMessage { export class SockMessage {
message?: string; message?: string;
type?: SockMessageType; type?: SockMessageType;
references?: string[]; references?: number[];
constructor(options: SockMessage) { constructor(options: SockMessage) {
this.type = options.type; this.type = options.type;
@@ -13,4 +13,5 @@ export class SockMessage {
export type SockMessageType = export type SockMessageType =
| 'ping' | 'ping'
| 'installDependence' | 'installDependence'
| 'updateSystemVersion'; | 'updateSystemVersion'
| 'manuallyRunScript';
+4
View File
@@ -3,6 +3,7 @@ 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';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
await dependencyInjectorLoader({ await dependencyInjectorLoader({
@@ -15,4 +16,7 @@ export default async ({ expressApp }: { expressApp: Application }) => {
await initData(); await initData();
Logger.info('✌️ init data loaded'); Logger.info('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps');
}; };
+90 -47
View File
@@ -1,57 +1,102 @@
import DataStore from 'nedb';
import config from '../config';
import Logger from './logger'; import Logger from './logger';
import fs from 'fs'; import path from 'path';
import DataStore from 'nedb';
import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth';
import { sequelize } from '../data';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
import config from '../config';
interface Dbs {
cronDb: DataStore;
dependenceDb: DataStore;
envDb: DataStore;
appDb: DataStore;
authDb: DataStore;
}
const db: Dbs = {} as any;
async function truncateDb() {
return new Promise(async (resolve) => {
const files = [
config.cronDbFile,
config.dependenceDbFile,
config.envDbFile,
config.appDbFile,
config.authDbFile,
];
for (const file of files) {
const _fileExist = await fileExist(file);
if (_fileExist && fs.statSync(file).size >= 1024 * 1024 * 500) {
fs.truncateSync(file, 1024 * 1024 * 500);
}
}
resolve(null);
});
}
export default async () => { export default async () => {
try { try {
await truncateDb(); await CrontabModel.sync();
await DependenceModel.sync();
await AppModel.sync();
await AuthModel.sync();
await EnvModel.sync();
await sequelize.sync();
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true }); // try {
db.dependenceDb = new DataStore({ // const queryInterface = sequelize.getQueryInterface();
filename: config.dependenceDbFile, // await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
// } catch (error) { }
// 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
const envDbFile = path.join(config.rootPath, 'db/env.db');
const appDbFile = path.join(config.rootPath, 'db/app.db');
const authDbFile = path.join(config.rootPath, 'db/auth.db');
const dependenceDbFile = path.join(config.rootPath, 'db/dependence.db');
const crondbExist = await fileExist(cronDbFile);
const dependenceDbExist = await fileExist(dependenceDbFile);
const envDbExist = await fileExist(envDbFile);
const appDbExist = await fileExist(appDbFile);
const authDbExist = await fileExist(authDbFile);
const cronCount = await CrontabModel.count();
const dependenceCount = await DependenceModel.count();
const envCount = await EnvModel.count();
const appCount = await AppModel.count();
const authCount = await AuthModel.count();
if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({
filename: cronDbFile,
autoload: true, autoload: true,
}); });
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true }); cronDb.persistence.compactDatafile();
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true }); cronDb.find({}).exec(async (err, docs) => {
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true }); await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
// compaction data file if (dependenceDbExist && dependenceCount === 0) {
db.cronDb.persistence.compactDatafile(); const dependenceDb = new DataStore({
db.envDb.persistence.compactDatafile(); filename: dependenceDbFile,
db.dependenceDb.persistence.compactDatafile(); autoload: true,
db.appDb.persistence.compactDatafile(); });
db.authDb.persistence.compactDatafile(); dependenceDb.persistence.compactDatafile();
dependenceDb.find({}).exec(async (err, docs) => {
await DependenceModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (envDbExist && envCount === 0) {
const envDb = new DataStore({
filename: envDbFile,
autoload: true,
});
envDb.persistence.compactDatafile();
envDb.find({}).exec(async (err, docs) => {
await EnvModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (appDbExist && appCount === 0) {
const appDb = new DataStore({
filename: appDbFile,
autoload: true,
});
appDb.persistence.compactDatafile();
appDb.find({}).exec(async (err, docs) => {
await AppModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (authDbExist && authCount === 0) {
const authDb = new DataStore({
filename: authDbFile,
autoload: true,
});
authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => {
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
} catch (error) { } catch (error) {
@@ -59,5 +104,3 @@ export default async () => {
Logger.info(error); Logger.info(error);
} }
}; };
export const dbs: Dbs = db;
+31
View File
@@ -0,0 +1,31 @@
import path from 'path';
import fs from 'fs';
import chokidar from 'chokidar';
import config from '../config/index';
function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
}
});
}
export default async (src: string = 'deps') => {
linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true,
});
watcher
.on('add', (path) => linkToNodeModule(src))
.on('change', (path) => linkToNodeModule(src));
};
+11 -9
View File
@@ -5,13 +5,14 @@ import routes from '../api';
import config from '../config'; import config from '../config';
import jwt from 'express-jwt'; import jwt from 'express-jwt';
import fs from 'fs'; import fs from 'fs';
import { getFileContentByName, getPlatform, getToken } from '../config/util'; import { getPlatform, getToken } from '../config/util';
import Container from 'typedi'; import Container from 'typedi';
import OpenService from '../services/open'; import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user'; 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';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
@@ -22,7 +23,7 @@ export default ({ app }: { app: Application }) => {
next(); next();
} else { } else {
return handler(req, res, { return handler(req, res, {
public: 'dist', public: 'static/dist',
rewrites: [{ source: '**', destination: '/index.html' }], rewrites: [{ source: '**', destination: '/index.html' }],
}); });
} }
@@ -68,18 +69,19 @@ export default ({ app }: { app: Application }) => {
} }
} }
const originPath = `${req.baseUrl}${req.path === '/' ? '' : req.path}`;
if ( if (
!headerToken && !headerToken &&
req.path && originPath &&
config.apiWhiteList.includes(req.path) && config.apiWhiteList.includes(originPath) &&
req.path !== '/api/crons/status' originPath !== '/api/crons/status'
) { ) {
return next(); return next();
} }
const remoteAddress = req.socket.remoteAddress; const remoteAddress = req.socket.remoteAddress;
if ( if (
remoteAddress === '::ffff:127.0.0.1' && remoteAddress === '::ffff:127.0.0.1' &&
req.path === '/api/crons/status' originPath === '/api/crons/status'
) { ) {
return next(); return next();
} }
@@ -98,19 +100,19 @@ export default ({ app }: { app: Application }) => {
}); });
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
if (!['/api/init/user', '/api/init/notification'].includes(req.path)) { if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next(); return next();
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envDbContent = getFileContentByName(config.envDbFile); const envCount = await EnvModel.count();
let isInitialized = true; let isInitialized = true;
if ( if (
Object.keys(authInfo).length === 2 && Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' && authInfo.username === 'admin' &&
authInfo.password === 'admin' && authInfo.password === 'admin' &&
envDbContent.length === 0 envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
+92 -31
View File
@@ -1,47 +1,54 @@
import DependenceService from '../services/dependence'; import DependenceService from '../services/dependence';
import { exec } from 'child_process'; import { exec } from 'child_process';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Crontab, 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 _ from 'lodash'; import _ from 'lodash';
import { dbs } from '../loaders/db'; import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import config from '../config';
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 cronDb = dbs.cronDb; const systemService = Container.get(SystemService);
const dependenceDb = dbs.dependenceDb; const scheduleService = Container.get(ScheduleService);
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
cronDb.update( await CrontabModel.update(
{ status: { $in: [CrontabStatus.running, CrontabStatus.queued] } }, { status: CrontabStatus.idle },
{ $set: { status: CrontabStatus.idle } }, { where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
{ multi: true },
); );
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
dependenceDb.find({ status: { $in: [0, 1, 2] } }).exec((err, docs) => { DependenceModel.findAll({ where: {} }).then(async (docs) => {
const groups = _.groupBy(docs, 'type'); const groups = _.groupBy(docs, 'type');
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];
const depIds = group.map((x) => x._id); const depIds = group.map((x) => x.id);
for (const dep of depIds) { for (const dep of depIds) {
dependenceService.reInstall([dep]); if (dep) {
await dependenceService.reInstall([dep]);
}
} }
} }
} }
}); });
// 初始化时执行一次所有的ql repo 任务 // 初始化时执行一次所有的ql repo 任务
cronDb CrontabModel.findAll({
.find({ where: {
command: /ql (repo|raw)/, isDisabled: { [Op.ne]: 1 },
isDisabled: { $ne: 1 }, command: {
}) [Op.or]: [{ [Op.like]: `%ql repo%` }, { [Op.like]: `%ql raw%` }],
.exec((err, docs) => { },
},
}).then((docs) => {
for (let i = 0; i < docs.length; i++) { for (let i = 0; i < docs.length; i++) {
const doc = docs[i]; const doc = docs[i];
if (doc) { if (doc) {
@@ -50,28 +57,82 @@ export default async () => {
} }
}); });
// patch 禁用状态字段改变 // 更新2.11.3以前的脚本路径
cronDb CrontabModel.findAll({
.find({ where: {
status: CrontabStatus.disabled, command: {
}) [Op.or]: [
.exec((err, docs) => { { [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
if (docs.length > 0) { { [Op.like]: `%\/${config.rootPath}\/config\/%` },
const ids = docs.map((x) => x._id); { [Op.like]: `%\/${config.rootPath}\/log\/%` },
cronDb.update( { [Op.like]: `%\/${config.rootPath}\/db\/%` },
{ _id: { $in: ids } }, ],
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
{ multi: true },
(err) => {
cronService.autosave_crontab();
}, },
},
}).then(async (docs) => {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
if (doc) {
if (doc.command.includes(`${config.rootPath}/scripts/`)) {
await CrontabModel.update(
{ command: doc.command.replace(`${config.rootPath}/scripts/`, '') },
{ where: { id: doc.id } },
); );
} }
if (doc.command.includes(`${config.rootPath}/log/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/log/${doc.command.replace(
`${config.rootPath}/log/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/config/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/config/${doc.command.replace(
`${config.rootPath}/config/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
if (doc.command.includes(`${config.rootPath}/db/`)) {
await CrontabModel.update(
{
command: `${config.rootPath}/data/db/${doc.command.replace(
`${config.rootPath}/db/`,
'',
)}`,
},
{ where: { id: doc.id } },
);
}
}
}
}); });
// 初始化保存一次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) { function randomSchedule(from: number, to: number) {
+48
View File
@@ -0,0 +1,48 @@
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
const rootPath = process.cwd();
const dataPath = path.join(rootPath, 'data/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
const logPath = path.join(dataPath, 'log/');
const samplePath = path.join(rootPath, 'sample/');
const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
const confFileExist = await fileExist(confFile);
const scriptDirExist = await fileExist(scriptPath);
const logDirExist = await fileExist(logPath);
const configDirExist = await fileExist(configPath);
if (!configDirExist) {
fs.mkdirSync(configPath);
}
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
};
+1 -1
View File
@@ -5,7 +5,7 @@ import Logger from './logger';
export default ({ expressApp }: { expressApp: Application }) => { export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({ Sentry.init({
dsn: 'https://e14681bce55f4849b11024a7d424b711@o1051273.ingest.sentry.io/6047906', 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 }),
+34
View File
@@ -0,0 +1,34 @@
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import config from './config';
const app = express();
app.get('/api/public/panel/log', (req, res) => {
exec(
'pm2 logs panel --lines 500 --nostream --timestamp',
(err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, data: err || stderr });
}
return res.send({ code: 200, data: stdout });
},
);
});
app
.listen(config.publicPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
Logger.info(`
################################################
🛡️ Public listening on port: ${config.publicPort} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+12 -15
View File
@@ -2,24 +2,14 @@ import schedule from 'node-schedule';
import express from 'express'; import express from 'express';
import { exec } from 'child_process'; import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { CrontabStatus } from './data/cron'; import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config'; import config from './config';
import { dbs } from './loaders/db';
const app = express(); const app = express();
const run = async () => { const run = async () => {
const cronDb = dbs.cronDb; CrontabModel.findAll({ where: {} })
.then((docs) => {
cronDb
.find({})
.sort({ created: 1 })
.exec((err, docs) => {
if (err) {
Logger.error(err);
process.exit(1);
}
if (docs && docs.length > 0) { if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) { for (let i = 0; i < docs.length; i++) {
const task = docs[i]; const task = docs[i];
@@ -40,12 +30,19 @@ const run = async () => {
} }
} }
} }
})
.catch((err) => {
Logger.error(err);
process.exit(1);
}); });
}; };
app app
.listen(config.cronPort, () => { .listen(config.cronPort, async () => {
run(); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(` Logger.info(`
################################################ ################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️ 🛡️ Schedule listening on port: ${config.cronPort} 🛡️
+176 -178
View File
@@ -1,25 +1,17 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import DataStore from 'nedb';
import config from '../config'; import config from '../config';
import { Crontab, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process'; import { exec, execSync, spawn } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName } from '../config/util'; import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
import PQueue from 'p-queue';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { promisify } from 'util';
import { dbs } from '../loaders/db'; import { Op } from 'sequelize';
@Service() @Service()
export default class CronService { export default class CronService {
private cronDb = dbs.cronDb;
private queue = new PQueue({
concurrency: parseInt(process.env.MaxConcurrentNum as string) || 5,
});
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
@@ -32,7 +24,6 @@ export default class CronService {
public async create(payload: Crontab): Promise<Crontab> { public async create(payload: Crontab): Promise<Crontab> {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.created = new Date().valueOf();
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc)); await this.set_crontab(this.isSixCron(doc));
@@ -40,42 +31,19 @@ export default class CronService {
} }
public async insert(payload: Crontab): Promise<Crontab> { public async insert(payload: Crontab): Promise<Crontab> {
return new Promise((resolve) => { return await CrontabModel.create(payload, { returning: true });
this.cronDb.insert(payload, (err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
const { _id, ...other } = payload; payload.saved = false;
const doc = await this.get(_id); const newDoc = await this.updateDb(payload);
const tab = new Crontab({ ...doc, ...other });
tab.saved = false;
const newDoc = await this.updateDb(tab);
await this.set_crontab(this.isSixCron(newDoc)); await this.set_crontab(this.isSixCron(newDoc));
return newDoc; return newDoc;
} }
public async updateDb(payload: Crontab): Promise<Crontab> { public async updateDb(payload: Crontab): Promise<Crontab> {
return new Promise((resolve) => { await CrontabModel.update(payload, { where: { id: payload.id } });
this.cronDb.update( return await this.getDb({ id: payload.id });
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, docs: any) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
},
);
});
} }
public async status({ public async status({
@@ -86,78 +54,91 @@ export default class CronService {
last_running_time = 0, last_running_time = 0,
last_execution_time = 0, last_execution_time = 0,
}: { }: {
ids: string[]; ids: number[];
status: CrontabStatus; status: CrontabStatus;
pid: number; pid: number;
log_path: string; log_path: string;
last_running_time: number; last_running_time: number;
last_execution_time: number; last_execution_time: number;
}) { }) {
return new Promise((resolve) => { const options: any = {
this.cronDb.update(
{ _id: { $in: ids } },
{
$set: {
status, status,
pid, pid,
log_path, log_path,
last_running_time,
last_execution_time, last_execution_time,
}, };
}, if (last_running_time > 0) {
{ multi: true, returnUpdatedDocs: true }, options.last_running_time = last_running_time;
(err) => {
resolve(null);
},
);
});
} }
public async remove(ids: string[]) { return await CrontabModel.update({ ...options }, { where: { id: ids } });
return new Promise((resolve: any) => { }
this.cronDb.remove(
{ _id: { $in: ids } }, public async remove(ids: number[]) {
{ multi: true }, await CrontabModel.destroy({ where: { id: ids } });
async (err) => {
await this.set_crontab(true); await this.set_crontab(true);
resolve();
},
);
});
} }
public async pin(ids: string[]) { public async pin(ids: number[]) {
return new Promise((resolve: any) => { await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 1 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
} }
public async unPin(ids: string[]) { public async unPin(ids: number[]) {
return new Promise((resolve: any) => { await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });
this.cronDb.update( }
{ _id: { $in: ids } },
{ $set: { isPinned: 0 } }, public async addLabels(ids: string[], labels: string[]) {
{ multi: true }, const docs = await CrontabModel.findAll({ where: { id: ids } });
async (err) => { for (const doc of docs) {
resolve(); await CrontabModel.update(
{
labels: Array.from(new Set((doc.labels || []).concat(labels))),
}, },
{ where: { id: doc.id } },
); );
}); }
}
public async removeLabels(ids: string[], labels: string[]) {
const docs = await CrontabModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await CrontabModel.update(
{
labels: (doc.labels || []).filter((label) => !labels.includes(label)),
},
{ where: { id: doc.id } },
);
}
} }
public async crontabs(searchText?: string): Promise<Crontab[]> { public async crontabs(searchText?: string): Promise<Crontab[]> {
let query = {}; let query = {};
if (searchText) { if (searchText) {
const reg = new RegExp(searchText, 'i'); const textArray = searchText.split(':');
switch (textArray[0]) {
case 'name':
case 'command':
case 'schedule':
case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
query = { query = {
$or: [ [column]: {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
],
},
};
break;
default:
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
],
};
query = {
[Op.or]: [
{ {
name: reg, name: reg,
}, },
@@ -167,44 +148,43 @@ export default class CronService {
{ {
schedule: reg, schedule: reg,
}, },
{
labels: reg,
},
], ],
}; };
break;
} }
return new Promise((resolve) => { }
this.cronDb try {
.find(query) const result = await CrontabModel.findAll({
.sort({ created: -1 }) where: query,
.exec((err, docs) => { order: [['createdAt', 'DESC']],
resolve(docs);
});
}); });
return result as any;
} catch (error) {
throw error;
}
} }
public async get(_id: string): Promise<Crontab> { public async getDb(query: any): Promise<Crontab> {
return new Promise((resolve) => { const doc: any = await CrontabModel.findOne({ where: { ...query } });
this.cronDb.find({ _id }).exec((err, docs) => { return doc && (doc.get({ plain: true }) as Crontab);
resolve(docs[0]);
});
});
} }
public async run(ids: string[]) { public async run(ids: number[]) {
this.cronDb.update( await CrontabModel.update(
{ _id: { $in: ids } }, { status: CrontabStatus.queued },
{ $set: { status: CrontabStatus.queued } }, { where: { id: ids } },
{ multi: true }, );
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
); );
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
this.queue.add(() => this.runSingle(id));
}
} }
public async stop(ids: string[]) { public async stop(ids: number[]) {
return new Promise((resolve: any) => { const docs = await CrontabModel.findAll({ where: { id: ids } });
this.cronDb
.find({ _id: { $in: ids } })
.exec(async (err, docs: Crontab[]) => {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
@@ -214,7 +194,8 @@ export default class CronService {
} }
} }
const err = await this.killTask(doc.command); const err = await this.killTask(doc.command);
if (doc.log_path) { const logFileExist = await fileExist(doc.log_path);
if (doc.log_path && logFileExist) {
const str = err ? `\n${err}` : ''; const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${doc.log_path}`, `${doc.log_path}`,
@@ -224,18 +205,14 @@ export default class CronService {
); );
} }
} }
this.cronDb.update(
{ _id: { $in: ids } }, await CrontabModel.update(
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, { status: CrontabStatus.idle, pid: undefined },
{ multi: true }, { where: { id: ids } },
); );
this.queue.clear();
resolve();
});
});
} }
private async killTask(name: string) { public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`; let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec); const execAsync = promisify(exec);
try { try {
@@ -245,10 +222,13 @@ export default class CronService {
} else { } else {
return; return;
} }
const pids = pid.match(/\(\d+/g); let pids = pid.match(/\(\d+/g);
const killLogs = []; const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
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 {
const { stdout, stderr } = await execAsync(c); const { stdout, stderr } = await execAsync(c);
if (stderr) { if (stderr) {
killLogs.push(stderr); killLogs.push(stderr);
@@ -256,6 +236,10 @@ export default class CronService {
if (stdout) { if (stdout) {
killLogs.push(stdout); killLogs.push(stdout);
} }
} catch (error: any) {
killLogs.push(error.message);
}
}
} }
return killLogs.length > 0 ? JSON.stringify(killLogs) : ''; return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) { } catch (e) {
@@ -263,18 +247,18 @@ export default class CronService {
} }
} }
private async runSingle(id: string): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.get(id); const cron = await this.getDb({ id: cronId });
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {
resolve(); resolve();
return; return;
} }
let { _id, command, log_path } = cron; let { id, command, log_path } = cron;
this.logger.silly('Running job'); this.logger.silly('Running job');
this.logger.silly('ID: ' + _id); this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command); this.logger.silly('Original command: ' + command);
let cmdStr = command; let cmdStr = command;
@@ -286,9 +270,10 @@ export default class CronService {
} }
const cp = spawn(cmdStr, { shell: '/bin/bash' }); const cp = spawn(cmdStr, { shell: '/bin/bash' });
this.cronDb.update(
{ _id }, await CrontabModel.update(
{ $set: { status: CrontabStatus.running, pid: cp.pid } }, { status: CrontabStatus.running, pid: cp.pid },
{ where: { id } },
); );
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
if (log_path) { if (log_path) {
@@ -301,57 +286,39 @@ export default class CronService {
} }
}); });
cp.on('exit', (code, signal) => { cp.on('exit', async (code, signal) => {
this.logger.info( this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`, `${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
); );
this.cronDb.update( await CrontabModel.update(
{ _id }, { status: CrontabStatus.idle, pid: undefined },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, { where: { id } },
); );
resolve(); resolve();
}); });
cp.on('close', (code) => { cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`); this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
this.cronDb.update( await CrontabModel.update(
{ _id }, { status: CrontabStatus.idle, pid: undefined },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } }, { where: { id } },
); );
resolve(); resolve();
}); });
}); });
} }
public async disabled(ids: string[]) { public async disabled(ids: number[]) {
return new Promise((resolve: any) => { await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isDisabled: 1 } },
{ multi: true },
async (err) => {
await this.set_crontab(true); await this.set_crontab(true);
resolve();
},
);
});
} }
public async enabled(ids: string[]) { public async enabled(ids: number[]) {
return new Promise((resolve: any) => { await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isDisabled: 0 } },
{ multi: true },
async (err) => {
await this.set_crontab(true); await this.set_crontab(true);
resolve();
},
);
});
} }
public async log(_id: string) { public async log(id: number) {
const doc = await this.get(_id); const doc = await this.getDb({ id });
if (!doc) { if (!doc) {
return ''; return '';
} }
@@ -380,6 +347,39 @@ export default class CronService {
} }
} }
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files
.map((x) => ({
filename: x,
directory: logPath,
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
} else {
return [];
}
}
private getKey(command: string) { private getKey(command: string) {
const start = const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0; command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
@@ -391,7 +391,7 @@ export default class CronService {
} }
private make_command(tab: Crontab) { private make_command(tab: Crontab) {
const crontab_job_string = `ID=${tab._id} ${tab.command}`; const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string; return crontab_job_string;
} }
@@ -421,7 +421,7 @@ export default class CronService {
if (needReloadSchedule) { if (needReloadSchedule) {
exec(`pm2 reload schedule`); exec(`pm2 reload schedule`);
} }
this.cronDb.update({}, { $set: { saved: true } }, { multi: true }); await CrontabModel.update({ saved: true }, { where: {} });
} }
public import_crontab() { public import_crontab() {
@@ -429,7 +429,7 @@ export default class CronService {
var lines = stdout.split('\n'); var lines = stdout.split('\n');
var namePrefix = new Date().getTime(); var namePrefix = new Date().getTime();
lines.reverse().forEach((line, index) => { lines.reverse().forEach(async (line, index) => {
line = line.replace(/\t+/g, ' '); line = line.replace(/\t+/g, ' ');
var regex = var regex =
/^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/; /^((\@[a-zA-Z]+\s+)|(([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+))/;
@@ -443,18 +443,16 @@ export default class CronService {
) { ) {
var name = namePrefix + '_' + index; var name = namePrefix + '_' + index;
this.cronDb.findOne({ command, schedule }, (err, doc) => { const _crontab = await CrontabModel.findOne({
if (err) { where: { command, schedule },
throw err;
}
if (!doc) {
this.create({ name, command, schedule });
} else {
doc.command = command;
doc.schedule = schedule;
this.update(doc);
}
}); });
if (!_crontab) {
await this.create({ name, command, schedule });
} else {
_crontab.command = command;
_crontab.schedule = schedule;
await this.update(_crontab);
}
} }
}); });
}); });
+69 -119
View File
@@ -1,23 +1,21 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import DataStore from 'nedb';
import { import {
Dependence, Dependence,
InstallDependenceCommandTypes, InstallDependenceCommandTypes,
DependenceStatus, DependenceStatus,
DependenceTypes, DependenceTypes,
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel,
} from '../data/dependence'; } from '../data/dependence';
import _ from 'lodash'; import _ from 'lodash';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import { dbs } from '../loaders/db'; import { Op } from 'sequelize';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
private dependenceDb = dbs.dependenceDb;
constructor( constructor(
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private sockService: SockService, private sockService: SockService,
@@ -34,22 +32,15 @@ export default class DependenceService {
} }
public async insert(payloads: Dependence[]): Promise<Dependence[]> { public async insert(payloads: Dependence[]): Promise<Dependence[]> {
return new Promise((resolve) => { const docs = await DependenceModel.bulkCreate(payloads);
this.dependenceDb.insert(payloads, (err, docs) => { return docs;
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
} }
public async update( public async update(
payload: Dependence & { _id: string }, payload: Dependence & { id: string },
): Promise<Dependence> { ): Promise<Dependence> {
const { _id, ...other } = payload; const { id, ...other } = payload;
const doc = await this.get(_id); const doc = await this.getDb({ id });
const tab = new Dependence({ const tab = new Dependence({
...doc, ...doc,
...other, ...other,
@@ -61,46 +52,22 @@ export default class DependenceService {
} }
private async updateDb(payload: Dependence): Promise<Dependence> { private async updateDb(payload: Dependence): Promise<Dependence> {
return new Promise((resolve) => { await DependenceModel.update(payload, { where: { id: payload.id } });
this.dependenceDb.update( return await this.getDb({ id: payload.id });
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Dependence);
}
},
);
});
} }
public async remove(ids: string[]) { public async remove(ids: number[]) {
return new Promise((resolve: any) => { await DependenceModel.update(
this.dependenceDb.update( { status: DependenceStatus.removing, log: [] },
{ _id: { $in: ids } }, { where: { id: ids } },
{ $set: { status: DependenceStatus.removing, log: [] } }, );
{ multi: true, returnUpdatedDocs: true }, const docs = await DependenceModel.findAll({ where: { id: ids } });
async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs, false); this.installOrUninstallDependencies(docs, false);
resolve(docs); return docs;
},
);
});
} }
public async removeDb(ids: string[]) { public async removeDb(ids: number[]) {
return new Promise((resolve: any) => { await DependenceModel.destroy({ where: { id: ids } });
this.dependenceDb.remove(
{ _id: { $in: ids } },
{ multi: true },
async (err) => {
resolve();
},
);
});
} }
public async dependencies( public async dependencies(
@@ -110,75 +77,61 @@ export default class DependenceService {
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] }; let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) { if (searchValue) {
const reg = new RegExp(searchValue); const encodeText = encodeURIComponent(searchValue);
condition = { const reg = {
...condition, [Op.or]: [
$or: [ { [Op.like]: `%${searchValue}%` },
{ { [Op.like]: `%${encodeText}%` },
name: reg,
},
], ],
}; };
condition = {
...condition,
name: reg,
};
}
try {
const result = await this.find(condition);
return result as any;
} catch (error) {
throw error;
} }
const newDocs = await this.find(condition, sort);
return newDocs;
} }
public async reInstall(ids: string[]): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
return new Promise((resolve: any) => { await DependenceModel.update(
this.dependenceDb.update( { status: DependenceStatus.installing, log: [] },
{ _id: { $in: ids } }, { where: { id: ids } },
{ $set: { status: DependenceStatus.installing, log: [] } },
{ multi: true, returnUpdatedDocs: true },
async (err, num, docs: Dependence[]) => {
this.installOrUninstallDependencies(docs);
resolve(docs);
},
); );
});
const docs = await DependenceModel.findAll({ where: { id: ids } });
await this.installOrUninstallDependencies(docs);
return docs;
} }
private async find(query: any, sort: any): Promise<Dependence[]> { private async find(query: any, sort?: any): Promise<Dependence[]> {
return new Promise((resolve) => { const docs = await DependenceModel.findAll({ where: { ...query } });
this.dependenceDb return docs;
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
} }
public async get(_id: string): Promise<Dependence> { public async getDb(query: any): Promise<Dependence> {
return new Promise((resolve) => { const doc: any = await DependenceModel.findOne({ where: { ...query } });
this.dependenceDb.find({ _id }).exec((err, docs) => { return doc && (doc.get({ plain: true }) as Dependence);
resolve(docs[0]);
});
});
} }
private async updateLog(ids: string[], log: string): Promise<void> { private async updateLog(ids: number[], log: string): Promise<void> {
return new Promise((resolve) => { const doc = await DependenceModel.findOne({ where: { id: ids } });
this.dependenceDb.update( const newLog = doc?.log ? [...doc.log, log] : [log];
{ _id: { $in: ids } }, await DependenceModel.update({ log: newLog }, { where: { id: ids } });
{ $push: { log } },
{ multi: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve();
}
},
);
});
} }
public installOrUninstallDependencies( public installOrUninstallDependencies(
dependencies: Dependence[], dependencies: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
) { ) {
return new Promise(async (resolve) => {
if (dependencies.length === 0) { if (dependencies.length === 0) {
resolve(null);
return; return;
} }
const depNames = dependencies.map((x) => x.name).join(' '); const depNames = dependencies.map((x) => x.name).join(' ');
@@ -188,7 +141,7 @@ export default class DependenceService {
: unInstallDependenceCommandTypes : unInstallDependenceCommandTypes
)[dependencies[0].type as any]; )[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除'; const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x._id) as string[]; const depIds = dependencies.map((x) => x.id) as number[];
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
const startTime = Date.now(); const startTime = Date.now();
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -198,40 +151,41 @@ export default class DependenceService {
).toLocaleString()}`, ).toLocaleString()}`,
references: depIds, references: depIds,
}); });
this.updateLog( await this.updateLog(
depIds, depIds,
`开始${actionText}依赖 ${depNames},开始时间 ${new Date( `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime, startTime,
).toLocaleString()}\n`, ).toLocaleString()}\n`,
); );
cp.stdout.on('data', (data) => { cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
this.updateLog(depIds, data.toString()); await this.updateLog(depIds, data.toString());
}); });
cp.stderr.on('data', (data) => { cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
this.updateLog(depIds, data.toString()); await this.updateLog(depIds, data.toString());
}); });
cp.on('error', (err) => { cp.on('error', async (err) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: 'installDependence',
message: JSON.stringify(err), message: JSON.stringify(err),
references: depIds, references: depIds,
}); });
this.updateLog(depIds, JSON.stringify(err)); await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
}); });
cp.on('close', (code) => { cp.on('close', async (code) => {
const endTime = Date.now(); const endTime = Date.now();
const isSucceed = code === 0; const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败'; const resultText = isSucceed ? '成功' : '失败';
@@ -243,7 +197,7 @@ export default class DependenceService {
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`, ).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds, references: depIds,
}); });
this.updateLog( await this.updateLog(
depIds, depIds,
`依赖${actionText}${resultText},结束时间 ${new Date( `依赖${actionText}${resultText},结束时间 ${new Date(
endTime, endTime,
@@ -260,14 +214,7 @@ export default class DependenceService {
? DependenceStatus.installFailed ? DependenceStatus.installFailed
: DependenceStatus.removeFailed; : DependenceStatus.removeFailed;
} }
this.dependenceDb.update( await DependenceModel.update({ status }, { where: { id: depIds } });
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录 // 如果删除依赖成功,3秒后删除数据库记录
if (isSucceed && !isInstall) { if (isSucceed && !isInstall) {
@@ -275,6 +222,9 @@ export default class DependenceService {
this.removeDb(depIds); this.removeDb(depIds);
}, 5000); }, 5000);
} }
resolve(null);
});
}); });
} }
} }
+55 -104
View File
@@ -3,15 +3,12 @@ import winston from 'winston';
import { getFileContentByName } from '../config/util'; import { getFileContentByName } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import DataStore from 'nedb'; import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
import { Env, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash'; import _ from 'lodash';
import { dbs } from '../loaders/db'; import { Op } from 'sequelize';
@Service() @Service()
export default class EnvService { export default class EnvService {
private envDb = dbs.envDb;
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payloads: Env[]): Promise<Env[]> { public async create(payloads: Env[]): Promise<Env[]> {
@@ -31,54 +28,32 @@ export default class EnvService {
} }
public async insert(payloads: Env[]): Promise<Env[]> { public async insert(payloads: Env[]): Promise<Env[]> {
return new Promise((resolve) => { const result = [];
this.envDb.insert(payloads, (err, docs) => { for (const env of payloads) {
if (err) { const doc = await EnvModel.create(env, { returning: true });
this.logger.error(err); result.push(doc);
} else {
resolve(docs);
} }
}); return result;
});
} }
public async update(payload: Env): Promise<Env> { public async update(payload: Env): Promise<Env> {
const { _id, ...other } = payload; const newDoc = await this.updateDb(payload);
const doc = await this.get(_id);
const tab = new Env({ ...doc, ...other });
const newDoc = await this.updateDb(tab);
await this.set_envs(); await this.set_envs();
return newDoc; return newDoc;
} }
private async updateDb(payload: Env): Promise<Env> { private async updateDb(payload: Env): Promise<Env> {
return new Promise((resolve) => { await EnvModel.update({ ...payload }, { where: { id: payload.id } });
this.envDb.update( return await this.getDb({ id: payload.id });
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc as Env);
}
},
);
});
} }
public async remove(ids: string[]) { public async remove(ids: string[]) {
return new Promise((resolve: any) => { await EnvModel.destroy({ where: { id: ids } });
this.envDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => {
await this.set_envs(); await this.set_envs();
resolve();
});
});
} }
public async move( public async move(
_id: string, id: number,
{ {
fromIndex, fromIndex,
toIndex, toIndex,
@@ -100,7 +75,7 @@ export default class EnvService {
: (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: targetPosition, position: targetPosition,
}); });
return newDoc; return newDoc;
@@ -113,103 +88,73 @@ export default class EnvService {
): Promise<Env[]> { ): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeText}%` },
],
};
condition = { condition = {
$or: [ ...condition,
{ [Op.or]: [
value: reg,
},
{ {
name: reg, name: reg,
}, },
{
value: reg,
},
{ {
remarks: reg, remarks: reg,
}, },
], ],
}; };
} }
const newDocs = await this.find(condition, sort); try {
return newDocs; const result = await this.find(condition, [['position', 'DESC']]);
return result as any;
} catch (error) {
throw error;
}
} }
private async find(query: any, sort: any): Promise<Env[]> { private async find(query: any, sort: any = []): Promise<Env[]> {
return new Promise((resolve) => { const docs = await EnvModel.findAll({
this.envDb where: { ...query },
.find(query) order: [...sort],
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
}); });
return docs;
} }
public async get(_id: string): Promise<Env> { public async getDb(query: any): Promise<Env> {
return new Promise((resolve) => { const doc: any = await EnvModel.findOne({ where: { ...query } });
this.envDb.find({ _id }).exec((err, docs) => { return doc && (doc.get({ plain: true }) as Env);
resolve(docs[0]);
});
});
}
public async getBySort(sort: any): Promise<Env> {
return new Promise((resolve) => {
this.envDb
.find({})
.sort({ ...sort })
.limit(1)
.exec((err, docs) => {
resolve(docs[0]);
});
});
} }
public async disabled(ids: string[]) { public async disabled(ids: string[]) {
return new Promise((resolve: any) => { await EnvModel.update(
this.envDb.update( { status: EnvStatus.disabled },
{ _id: { $in: ids } }, { where: { id: ids } },
{ $set: { status: EnvStatus.disabled } },
{ multi: true },
async (err) => {
await this.set_envs();
resolve();
},
); );
}); await this.set_envs();
} }
public async enabled(ids: string[]) { public async enabled(ids: string[]) {
return new Promise((resolve: any) => { await EnvModel.update({ status: EnvStatus.normal }, { where: { id: ids } });
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { status: EnvStatus.normal } },
{ multi: true },
async (err, num) => {
await this.set_envs(); await this.set_envs();
resolve();
},
);
});
} }
public async updateNames({ ids, name }: { ids: string[]; name: string }) { public async updateNames({ ids, name }: { ids: string[]; name: string }) {
return new Promise((resolve: any) => { await EnvModel.update({ name }, { where: { id: ids } });
this.envDb.update(
{ _id: { $in: ids } },
{ $set: { name } },
{ multi: true },
async (err, num) => {
await this.set_envs(); await this.set_envs();
resolve();
},
);
});
} }
public async set_envs() { public async set_envs() {
const envs = await this.envs( const envs = await this.envs(
'', '',
{ position: -1 }, { position: -1 },
{ name: { $exists: true } }, { name: { [Op.not]: null } },
); );
const groups = _.groupBy(envs, 'name'); const groups = _.groupBy(envs, 'name');
let env_string = ''; let env_string = '';
@@ -219,11 +164,17 @@ export default class EnvService {
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
env_string += `export ${key}="${_(group) let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled) .filter((x) => x.status !== EnvStatus.disabled)
.map('value') .map('value')
.join('&') .join('&')
.replace(/ /g, '')}"\n`; .replace(/ /g, '');
if (/"/.test(value)) {
value = `'${value}'`;
} else {
value = `"${value}"`;
}
env_string += `export ${key}=${value}\n`;
} }
} }
} }
+45 -7
View File
@@ -13,8 +13,10 @@ export default class NotificationService {
private userService!: UserService; private userService!: UserService;
private modeMap = new Map([ private modeMap = new Map([
['gotify', this.gotify],
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan], ['serverChan', this.serverChan],
['pushDeer', this.pushDeer],
['bark', this.bark], ['bark', this.bark],
['telegramBot', this.telegramBot], ['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
@@ -67,17 +69,34 @@ export default class NotificationService {
return true; return true;
} }
private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
this.content,
)}&priority=${gotifyPriority}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
})
.json();
return typeof res.id === 'number';
}
private async goCqHttpBot() { private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got const res: any = await got
.post( .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
`${goCqHttpBotUrl}?access_token=${goCqHttpBotToken}&${goCqHttpBotQq}`,
{
timeout: this.timeout, timeout: this.timeout,
retry: 0, retry: 0,
json: { message: `${this.title}\n${this.content}` }, json: { message: `${this.title}\n${this.content}` },
}, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
) })
.json(); .json();
return res.retcode === 0; return res.retcode === 0;
} }
@@ -98,8 +117,27 @@ export default class NotificationService {
return res.errno === 0 || res.data.errno === 0; return res.errno === 0 || res.data.errno === 0;
} }
private async pushDeer() {
const { pushDeerKey } = this.params;
// https://api2.pushdeer.com/message/push?pushkey=<key>&text=标题&desp=<markdown>&type=markdown
const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${
this.title
}&desp=${encodeURIComponent(this.content)}&type=markdown`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
return (
res.content.result.length !== undefined && res.content.result.length > 0
);
}
private async bark() { private async bark() {
let { barkPush, 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')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
@@ -107,7 +145,7 @@ export default class NotificationService {
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
timeout: this.timeout, timeout: this.timeout,
+81 -107
View File
@@ -2,90 +2,68 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { createRandomString } from '../config/util'; import { createRandomString } from '../config/util';
import config from '../config'; import config from '../config';
import DataStore from 'nedb'; import { App, AppModel } from '../data/open';
import { App } from '../data/open';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import { dbs } from '../loaders/db'; import sequelize, { Op } from 'sequelize';
@Service() @Service()
export default class OpenService { export default class OpenService {
private appDb = dbs.appDb;
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async findTokenByValue(token: string): Promise<App> { public async findTokenByValue(token: string): Promise<App | null> {
return new Promise((resolve) => { const docs = await this.find({});
this.appDb.find( const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token));
{ tokens: { $elemMatch: { value: token } } }, return doc[0];
(err, docs) => {
if (err) {
this.logger.error(err);
} else {
resolve(docs[0]);
}
},
);
});
} }
public async create(payload: App): Promise<App> { public async create(payload: App): Promise<App> {
const tab = new App({ ...payload }); const tab = { ...payload };
tab.client_id = createRandomString(12, 12); tab.client_id = createRandomString(12, 12);
tab.client_secret = createRandomString(24, 24); tab.client_secret = createRandomString(24, 24);
const docs = await this.insert([tab]); const doc = await this.insert(tab);
return { ...docs[0], tokens: [] }; return { ...doc, tokens: [] };
} }
public async insert(payloads: App[]): Promise<App[]> { public async insert(payload: App): Promise<App> {
return new Promise((resolve) => { const doc = await AppModel.create(payload, { returning: true });
this.appDb.insert(payloads, (err, docs) => { return doc.get({ plain: true }) as App;
if (err) {
this.logger.error(err);
} else {
resolve(docs);
}
});
});
} }
public async update(payload: App): Promise<App> { public async update(payload: App): Promise<App> {
const { _id, client_id, client_secret, tokens, ...other } = payload; const newDoc = await this.updateDb({
const doc = await this.get(_id); name: payload.name,
const tab = new App({ ...doc, ...other }); scopes: payload.scopes,
const newDoc = await this.updateDb(tab); id: payload.id,
} as any);
return { ...newDoc, tokens: [] }; return { ...newDoc, tokens: [] };
} }
private async updateDb(payload: App): Promise<App> { private async updateDb(payload: App): Promise<App> {
return new Promise((resolve) => { await AppModel.update(payload, { where: { id: payload.id } });
this.appDb.update( return await this.getDb({ id: payload.id });
{ _id: payload._id },
payload,
{ returnUpdatedDocs: true },
(err, num, doc, up) => {
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
},
);
});
} }
public async remove(ids: string[]) { public async getDb(query: any): Promise<App> {
return new Promise((resolve: any) => { const doc: any = await AppModel.findOne({ where: query });
this.appDb.remove({ _id: { $in: ids } }, { multi: true }, async (err) => { return doc && (doc.get({ plain: true }) as App);
resolve();
});
});
} }
public async resetSecret(_id: string): Promise<App> { public async remove(ids: number[]) {
const doc = await this.get(_id); await AppModel.destroy({ where: { id: ids } });
const tab = new App({ ...doc }); }
tab.client_secret = createRandomString(24, 24);
tab.tokens = []; public async resetSecret(id: number): Promise<App> {
const tab: any = {
client_secret: createRandomString(24, 24),
tokens: [],
id,
};
// const doc = await this.get(id);
// const tab = new App({ ...doc });
// tab.client_secret = createRandomString(24, 24);
// tab.tokens = [];
// const newDoc = await this.updateDb(tab);
// return newDoc;
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
return newDoc; return newDoc;
} }
@@ -97,42 +75,30 @@ export default class OpenService {
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const reg = new RegExp(searchText); const encodeText = encodeURIComponent(searchText);
condition = { const reg = {
$or: [ [Op.or]: [
{ { [Op.like]: `%${searchText}%` },
value: reg, { [Op.like]: `%${encodeText}%` },
},
{
name: reg,
},
{
remarks: reg,
},
], ],
}; };
condition = {
...condition,
name: reg,
};
}
try {
const result = await this.find(condition);
return result.map((x) => ({ ...x, tokens: [] }));
} catch (error) {
throw error;
} }
const newDocs = await this.find(condition, sort);
return newDocs.map((x) => ({ ...x, tokens: [] }));
} }
private async find(query: any, sort: any): Promise<App[]> { private async find(query: any, sort?: any): Promise<App[]> {
return new Promise((resolve) => { const docs = await AppModel.findAll({ where: { ...query } });
this.appDb return docs.map((x) => x.get({ plain: true }));
.find(query)
.sort({ ...sort })
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<App> {
return new Promise((resolve) => {
this.appDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
});
});
} }
public async authToken({ public async authToken({
@@ -142,30 +108,38 @@ export default class OpenService {
client_id: string; client_id: string;
client_secret: string; client_secret: string;
}): Promise<any> { }): Promise<any> {
const token = uuidV4(); let token = uuidV4();
const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天 const expiration = Math.round(Date.now() / 1000) + 2592000; // 2592000 30天
return new Promise((resolve) => { const doc = await AppModel.findOne({ where: { client_id, client_secret } });
this.appDb.find({ client_id, client_secret }).exec((err, docs) => { if (doc) {
if (docs && docs[0]) { const timestamp = Math.round(Date.now() / 1000);
this.appDb.update( const invalidTokens = (doc.tokens || []).filter(
{ client_id, client_secret }, (x) => x.expiration >= timestamp,
{ $push: { tokens: { value: token, expiration } } }, );
{}, let tokens = invalidTokens;
(err, num, doc) => { if (invalidTokens.length > 5) {
resolve({ tokens = [
...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration },
];
token = invalidTokens[4].value;
} else {
tokens = [...invalidTokens, { value: token, expiration }];
}
await AppModel.update(
{ tokens },
{ where: { client_id, client_secret } },
);
return {
code: 200, code: 200,
data: { data: {
token, token,
token_type: 'Bearer', token_type: 'Bearer',
expiration, expiration,
}, },
}); };
},
);
} else { } else {
resolve({ code: 400, message: 'client_id或client_seret有误' }); return { code: 400, message: 'client_id或client_seret有误' };
} }
});
});
} }
} }
+72 -4
View File
@@ -3,16 +3,25 @@ import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { Crontab } from '../data/cron'; import { Crontab } from '../data/cron';
import { exec } from 'child_process'; import { exec } from 'child_process';
import {
ToadScheduler,
SimpleIntervalJob,
Task,
SimpleIntervalSchedule,
} from 'toad-scheduler';
@Service() @Service()
export default class ScheduleService { export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>(); private scheduleStacks = new Map<string, nodeSchedule.Job>();
private intervalSchedule = new ToadScheduler();
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async generateSchedule({ _id = '', command, name, schedule }: Crontab) { async createCronTask({ id = 0, command, name, schedule = '' }: Crontab) {
const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建定时任务],任务ID: %scron: %s,任务名: %s,执行命令: %s', '[创建cron任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
_id, _id,
schedule, schedule,
name, name,
@@ -21,7 +30,7 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(id + '', schedule, async () => {
try { try {
exec(command, async (error, stdout, stderr) => { exec(command, async (error, stdout, stderr) => {
if (error) { if (error) {
@@ -55,8 +64,67 @@ export default class ScheduleService {
); );
} }
async cancelSchedule({ _id = '', name }: Crontab) { async cancelCronTask({ id = 0, name }: Crontab) {
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(
{ id = 0, command, name = '' }: Crontab,
schedule: SimpleIntervalSchedule,
) {
const _id = this.formatId(id);
this.logger.info(
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
_id,
name,
command,
);
const task = new Task(name, async () => {
try {
exec(command, 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 {
}
});
const job = new SimpleIntervalJob({ ...schedule }, task, _id);
this.intervalSchedule.addIntervalJob(job);
}
async cancelIntervalTask({ id = 0, name }: Crontab) {
const _id = this.formatId(id);
this.logger.info('[取消interval任务],任务ID: %s,任务名:%s', _id, name);
this.intervalSchedule.removeById(_id);
}
private formatId(id: number): string {
return String(id);
}
} }
+61
View File
@@ -0,0 +1,61 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { spawn } from 'child_process';
import SockService from './sock';
import CronService from './cron';
@Service()
export default class ScriptService {
constructor(
@Inject('logger') private logger: winston.Logger,
private sockService: SockService,
private cronService: CronService,
) {}
public async runScript(path: string) {
const cp = spawn(`task -l ${path} now`, { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'manuallyRunScript',
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 };
}
public async stopScript(path: string) {
const err = await this.cronService.killTask(`task -l ${path} now`);
const str = err ? `\n${err}` : '';
this.sockService.sendMessage({
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
return { code: 200 };
}
}
+175
View File
@@ -0,0 +1,175 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import got from 'got';
@Service()
export default class SystemService {
@Inject((type) => NotificationService)
private notificationService!: NotificationService;
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
) {}
public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return doc || {};
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
await AuthModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type });
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
const code = Math.random().toString().slice(-6);
const isSuccess = await this.notificationService.testNotify(
notificationInfo,
'青龙',
`【蛟龙】测试通知 https://t.me/jiao_long`,
);
if (isSuccess) {
const result = await this.updateAuthDb({
type: AuthDataType.notification,
info: { ...notificationInfo },
});
return { code: 200, data: { ...result, code } };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
id: result.id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
};
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, {
days: frequency,
runImmediately: true,
});
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try {
const result = await got.get(config.lastVersionFile, {
timeout: 6000,
retry: 0,
});
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {}
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) {
return { code: 200, data: '通知发送成功' };
} else {
return { code: 400, data: '通知发送失败,请检查参数' };
}
}
}
+31 -164
View File
@@ -6,8 +6,7 @@ import * as fs from 'fs';
import _ from 'lodash'; import _ from 'lodash';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import DataStore from 'nedb'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { AuthDataType, AuthInfo, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
import { Request } from 'express'; import { Request } from 'express';
@@ -15,13 +14,11 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; import got from 'got';
import { dbs } from '../loaders/db';
@Service() @Service()
export default class UserService { export default class UserService {
@Inject((type) => NotificationService) @Inject((type) => NotificationService)
private notificationService!: NotificationService; private notificationService!: NotificationService;
private authDb = dbs.authDb;
constructor( constructor(
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
@@ -174,33 +171,24 @@ export default class UserService {
} }
public async getLoginLog(): Promise<AuthInfo[]> { public async getLoginLog(): Promise<AuthInfo[]> {
return new Promise((resolve) => { const docs = await AuthModel.findAll({
this.authDb.find({ type: AuthDataType.loginLog }).exec((err, docs) => { where: { type: AuthDataType.loginLog },
if (err || docs.length === 0) { });
resolve([]); if (docs && docs.length > 0) {
} else { const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
const result = docs.sort(
(a, b) => b.info.timestamp - a.info.timestamp,
);
if (result.length > 100) { if (result.length > 100) {
this.authDb.remove({ _id: result[result.length - 1]._id }); await AuthModel.destroy({
} where: { id: result[result.length - 1].id },
resolve(result.map((x) => x.info));
}
});
}); });
} }
return result.map((x) => x.info);
}
return [];
}
private async insertDb(payload: AuthInfo): Promise<AuthInfo> { private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
return new Promise((resolve) => { const doc = await AuthModel.create({ ...payload }, { returning: true });
this.authDb.insert(payload, (err, doc) => { return doc;
if (err) {
this.logger.error(err);
} else {
resolve(doc);
}
});
});
} }
private initAuthInfo() { private initAuthInfo() {
@@ -315,48 +303,27 @@ export default class UserService {
} }
public async getNotificationMode(): Promise<NotificationInfo> { public async getNotificationMode(): Promise<NotificationInfo> {
return new Promise((resolve) => { const doc = await this.getDb({ type: AuthDataType.notification });
this.authDb return (doc && doc.info) || {};
.find({ type: AuthDataType.notification })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({} as NotificationInfo);
} else {
resolve(docs[0].info);
}
});
});
}
public async getLogRemoveFrequency() {
return new Promise((resolve) => {
this.authDb
.find({ type: AuthDataType.removeLogFrequency })
.exec((err, docs) => {
if (err || docs.length === 0) {
resolve({});
} else {
resolve(docs[0].info);
}
});
});
} }
private async updateAuthDb(payload: AuthInfo): Promise<any> { private async updateAuthDb(payload: AuthInfo): Promise<any> {
return new Promise((resolve) => { let doc = await AuthModel.findOne({ type: payload.type });
this.authDb.update( if (doc) {
{ type: payload.type }, const updateResult = await AuthModel.update(payload, {
{ ...payload }, where: { id: doc.id },
{ upsert: true, returnUpdatedDocs: true }, returning: true,
(err, num, doc: any) => {
if (err) {
resolve({} as NotificationInfo);
} else {
resolve({ ...doc.info, _id: doc._id });
}
},
);
}); });
doc = updateResult[1][0];
} else {
doc = await AuthModel.create(payload, { returning: true });
}
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
} }
public async updateNotificationMode(notificationInfo: NotificationInfo) { public async updateNotificationMode(notificationInfo: NotificationInfo) {
@@ -376,104 +343,4 @@ export default class UserService {
return { code: 400, data: '通知发送失败,请检查参数' }; return { code: 400, data: '通知发送失败,请检查参数' };
} }
} }
public async updateLogRemoveFrequency(frequency: number) {
const result = await this.updateAuthDb({
type: AuthDataType.removeLogFrequency,
info: { frequency },
});
const cron = {
_id: result._id,
name: '删除日志',
command: `ql rmlog ${frequency}`,
schedule: `5 23 */${frequency} * *`,
};
await this.scheduleService.cancelSchedule(cron);
if (frequency > 0) {
await this.scheduleService.generateSchedule(cron);
}
return { code: 200, data: { ...cron } };
}
public async checkUpdate() {
try {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
const lastVersionFileContent = await (
await got.get(config.lastVersionFile)
).body;
const lastVersion = lastVersionFileContent.match(versionRegx)![1];
const lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
return {
code: 200,
data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
lastVersion,
lastLog,
},
};
} catch (error: any) {
return {
code: 400,
data: error.message,
};
}
}
private checkHasNewVersion(curVersion: string, lastVersion: string) {
const curArr = curVersion.split('.').map((x) => parseInt(x, 10));
const lastArr = lastVersion.split('.').map((x) => parseInt(x, 10));
if (curArr[0] < lastArr[0]) {
return true;
}
if (curArr[0] === lastArr[0] && curArr[1] < lastArr[1]) {
return true;
}
if (
curArr[0] === lastArr[0] &&
curArr[1] === lastArr[1] &&
curArr[2] < lastArr[2]
) {
return true;
}
return false;
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
} }
+13
View File
@@ -0,0 +1,13 @@
import { CommandModule } from 'yargs';
export const updateCommand: CommandModule = {
command: 'update',
describe: 'Update and restart qinglong',
builder: (yargs) => {
return yargs.option('repositority', {
type: 'string',
alias: 'r',
describe: `Specify the release warehouse address of the package`,
});
},
handler: async (argv) => {},
};
+13
View File
@@ -0,0 +1,13 @@
import * as yargs from 'yargs';
import { green, red } from 'chalk';
import { updateCommand } from './commands/update';
yargs
.usage('Usage: ql [command] <options>')
.command(updateCommand)
.fail((err) => {
console.error(`${red(err)}`);
})
.alias('h', 'help')
.showHelp()
.recommendCommands().argv;
-16
View File
@@ -1,16 +0,0 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data/config:/ql/config
- ./data/log:/ql/log
- ./data/db:/ql/db
- ./data/scripts:/ql/scripts
- ./data/repo:/ql/repo
ports:
- "0.0.0.0:5700:5700"
environment:
- ENABLE_HANGUP=true
- ENABLE_WEB_PANEL=true
restart: always
+15 -10
View File
@@ -1,19 +1,21 @@
FROM node:lts-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=master ARG QL_BRANCH=master
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
LANG=zh_CN.UTF-8 \ LANG=zh_CN.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
QL_DIR=/ql \ QL_DIR=/ql \
QL_MAINTAINER=${QL_MAINTAINER} \
QL_URL=${QL_URL} \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR} WORKDIR ${QL_DIR}
RUN set -x \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \ && sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \ && apk update -f \
&& apk upgrade \ && apk upgrade \
&& apk --no-cache add -f bash \ && apk --no-cache add -f bash \
@@ -34,20 +36,23 @@ RUN set -x \
&& 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 \ && touch ~/.bashrc \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& 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" \
&& npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& 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 \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& npm install -g pnpm \
&& pnpm install -g pm2 \
&& pnpm install -g ts-node typescript tslib \
&& rm -rf /root/.npm \
&& pnpm install --prod \ && pnpm install --prod \
&& rm -rf /root/.pnpm-store \ && rm -rf /root/.pnpm-store \
&& rm -rf /root/.cache \
&& 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 \
&& cp -rf /static/* ${QL_DIR} \ && mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+9
View File
@@ -0,0 +1,9 @@
version: '2'
services:
web:
image: whyour/qinglong:latest
volumes:
- ./data:/ql/data
ports:
- "0.0.0.0:5700:5700"
restart: unless-stopped
+12 -6
View File
@@ -6,6 +6,7 @@ link_shell
echo -e "======================1. 检测配置文件========================\n" echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d make_dir /etc/nginx/conf.d
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
pm2 l &>/dev/null pm2 l &>/dev/null
@@ -19,24 +20,29 @@ echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "nginx启动成功...\n" echo -e "nginx启动成功...\n"
echo -e "======================4. 启动控制面板========================\n" echo -e "======================4. 启动面板监控========================\n"
pm2 delete public &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time
echo -e "监控服务启动成功...\n"
echo -e "======================5. 启动控制面板========================\n"
pm2 delete panel &>/dev/null pm2 delete panel &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time pm2 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n" echo -e "控制面板启动成功...\n"
echo -e "======================5. 启动定时任务========================\n" echo -e "======================6. 启动定时任务========================\n"
pm2 delete schedule &>/dev/null pm2 delete schedule &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n" echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================6. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.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 "======================7. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 & nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+16 -1
View File
@@ -2,6 +2,10 @@ upstream api {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
upstream public {
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;
@@ -9,9 +13,16 @@ map $http_upgrade $connection_upgrade {
server { server {
listen 5700; listen 5700;
root /ql/dist; root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location /api/public {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://public;
}
location /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;
@@ -42,4 +53,8 @@ server {
index index.html index.htm; index index.html index.htm;
try_files $uri $uri/ /index.html; try_files $uri $uri/ /index.html;
} }
location ~ .*\.(html)$ {
add_header Cache-Control no-cache;
}
} }
+64 -52
View File
@@ -1,14 +1,17 @@
{ {
"private": true, "private": true,
"scripts": { "scripts": {
"start": "umi dev", "start": "concurrently -n w: npm:start:*",
"build": "umi build", "start:front": "umi dev",
"build-back": "tsc -p tsconfig.back.json", "start:back": "nodemon",
"start-back": "nodemon", "start:public": "ts-node back/public.ts",
"panel": "npm run build-back && node build/app.js", "build:front": "umi build",
"schedule": "npm run build-back && node build/schedule.js", "build:back": "tsc -p tsconfig.back.json",
"prepare": "umi generate tmp", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule.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}'",
"prepare": "umi generate tmp",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
@@ -25,15 +28,16 @@
}, },
"dependencies": { "dependencies": {
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^6.14.0", "@sentry/node": "^6.18.1",
"@sentry/tracing": "^6.14.0", "@sentry/tracing": "^6.18.1",
"body-parser": "^1.19.0", "body-parser": "^1.19.2",
"celebrate": "^13.0.3", "celebrate": "^15.0.1",
"chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^3.5.0", "cron-parser": "^4.2.1",
"dotenv": "^8.2.0", "dotenv": "^16.0.0",
"express": "^4.17.1", "express": "^4.17.3",
"express-jwt": "^6.0.0", "express-jwt": "^6.1.1",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
@@ -41,48 +45,56 @@
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-fetch": "^3.2.1",
"node-schedule": "^2.0.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.0", "nodemailer": "^6.7.2",
"p-queue": "6.6.2", "p-queue": "7.2.0",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.17.0",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.21", "sockjs": "^0.3.24",
"typedi": "^0.8.0", "sqlite3": "npm:@louislam/sqlite3@^6.0.1",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.3.3" "winston": "^3.6.0",
"yargs": "^17.3.1"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.26.0", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "^4.3.1", "@monaco-editor/react": "^4.3.1",
"@sentry/react": "^6.14.0", "@sentry/react": "^6.18.1",
"@sentry/webpack-plugin": "^1.18.3", "@types/cors": "^2.8.12",
"@types/cors": "^2.8.10", "@types/express": "^4.17.13",
"@types/express": "^4.17.8", "@types/express-jwt": "^6.0.4",
"@types/express-jwt": "^6.0.1", "@types/jsonwebtoken": "^8.5.8",
"@types/jsonwebtoken": "^8.5.0", "@types/lodash": "^4.14.179",
"@types/lodash": "^4.14.170", "@types/nedb": "^1.8.12",
"@types/nedb": "^1.8.11", "@types/node": "^17.0.21",
"@types/node": "^14.11.2", "@types/node-fetch": "^2.6.1",
"@types/node-fetch": "^2.5.8",
"@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.1", "@types/qrcode.react": "^1.0.2",
"@types/react": "^17.0.0", "@types/react": "^17.0.39",
"@types/react-dom": "^17.0.0", "@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",
"@umijs/plugin-antd": "^0.11.0", "@types/uuid": "^8.3.4",
"@umijs/test": "^3.3.9", "@umijs/plugin-antd": "^0.15.0",
"antd": "^4.17.0-alpha.6", "@umijs/plugin-esbuild": "^1.4.1",
"codemirror": "^5.62.2", "@umijs/test": "^3.5.21",
"compression-webpack-plugin": "6.1.1", "ansi-to-react": "^6.1.6",
"darkreader": "^4.9.27", "antd": "^4.18.9",
"lint-staged": "^10.0.7", "codemirror": "^5.65.2",
"nodemon": "^2.0.4", "compression-webpack-plugin": "9.2.0",
"prettier": "^2.2.0", "concurrently": "^7.0.0",
"darkreader": "4.9.44",
"lint-staged": "^12.3.4",
"nodemon": "^2.0.15",
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"react": "17.x", "react": "17.x",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
@@ -91,13 +103,13 @@
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "17.x", "react-dom": "17.x",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.5.2", "sockjs-client": "^1.6.0",
"ts-node": "^9.0.0", "ts-node": "^10.6.0",
"typescript": "^4.1.2", "typescript": "^4.6.2",
"umi": "^3.5.0", "umi": "^3.5.21",
"umi-request": "^1.3.5", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"webpack": "^5.28.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

+19 -2
View File
@@ -16,8 +16,8 @@ DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py" RepoFileExtensions="js py"
## 由于github仓库拉取较慢,所以会默认添加代理前缀,如不需要请移除 ## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
GithubProxyUrl="https://ghproxy.com/" ProxyUrl=""
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 ## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h" CommandTimeoutTime="1h"
@@ -35,6 +35,9 @@ EnableExtraShell="true"
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot ## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot="" AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空 ## 安装bot依赖时指定pip源,默认使用清华源,如不需要源,设置此参数为空
PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple" PipMirror="https://pypi.tuna.tsinghua.edu.cn/simple"
@@ -47,6 +50,8 @@ export PUSH_KEY=""
## 2. BARK ## 2. BARK
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123 ## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH="" export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong" ## 下方填写推送消息分组,默认为"QingLong"
@@ -112,4 +117,16 @@ export GOBOT_URL=""
export GOBOT_TOKEN="" export GOBOT_TOKEN=""
export GOBOT_QQ="" export GOBOT_QQ=""
## 10. gotify
## gotify_url 填写gotify地址,如https://push.example.de:8080
## gotify_token 填写gotify的消息应用token
## gotify_priority 填写推送消息优先级,默认为0
export GOTIFY_URL=""
export GOTIFY_TOKEN=""
export GOTIFY_PRIORITY=0
## 11. PushDeer
## deer_key 填写PushDeer的key
export DEER_KEY=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+118 -1
View File
@@ -13,6 +13,13 @@
const querystring = require('querystring'); const querystring = require('querystring');
const $ = new Env(); const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒) const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
//gotify_url 填写gotify地址,如https://push.example.de:8080
//gotify_token 填写gotify的消息应用token
//gotify_priority 填写推送消息优先级,默认为0
let GOTIFY_URL = '';
let GOTIFY_TOKEN = '';
let GOTIFY_PRIORITY = 0;
// =======================================go-cqhttp通知设置区域=========================================== // =======================================go-cqhttp通知设置区域===========================================
//gobot_url 填写请求地址http://127.0.0.1/send_private_msg //gobot_url 填写请求地址http://127.0.0.1/send_private_msg
//gobot_token 填写在go-cqhttp文件设置的访问密钥 //gobot_token 填写在go-cqhttp文件设置的访问密钥
@@ -27,9 +34,16 @@ let GOBOT_QQ = ''; // 如果GOBOT_URL设置 /send_private_msg 则需要填入 us
//(环境变量名 PUSH_KEY) //(环境变量名 PUSH_KEY)
let SCKEY = ''; let SCKEY = '';
// =======================================PushDeer通知设置区域===========================================
//此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY)
let PUSHDEER_KEY = '';
// =======================================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或以上)
let BARK_ICON = 'https://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
@@ -84,6 +98,16 @@ let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = ''; let PUSH_PLUS_USER = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
}
if (process.env.GOTIFY_TOKEN) {
GOTIFY_TOKEN = process.env.GOTIFY_TOKEN;
}
if (process.env.GOTIFY_PRIORITY) {
GOTIFY_PRIORITY = process.env.GOTIFY_PRIORITY;
}
if (process.env.GOBOT_URL) { if (process.env.GOBOT_URL) {
GOBOT_URL = process.env.GOBOT_URL; GOBOT_URL = process.env.GOBOT_URL;
} }
@@ -98,6 +122,10 @@ if (process.env.PUSH_KEY) {
SCKEY = process.env.PUSH_KEY; SCKEY = process.env.PUSH_KEY;
} }
if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY;
}
if (process.env.QQ_SKEY) { if (process.env.QQ_SKEY) {
QQ_SKEY = process.env.QQ_SKEY; QQ_SKEY = process.env.QQ_SKEY;
} }
@@ -116,6 +144,9 @@ if (process.env.BARK_PUSH) {
} else { } else {
BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`; BARK_PUSH = `https://api.day.app/${process.env.BARK_PUSH}`;
} }
if (process.env.BARK_ICON) {
BARK_ICON = process.env.BARK_ICON;
}
if (process.env.BARK_SOUND) { if (process.env.BARK_SOUND) {
BARK_SOUND = process.env.BARK_SOUND; BARK_SOUND = process.env.BARK_SOUND;
} }
@@ -200,9 +231,47 @@ async function sendNotify(
qywxamNotify(text, desp), //企业微信应用消息推送 qywxamNotify(text, desp), //企业微信应用消息推送
iGotNotify(text, desp, params), //iGot iGotNotify(text, desp, params), //iGot
gobotNotify(text, desp), //go-cqhttp gobotNotify(text, desp), //go-cqhttp
gotifyNotify(text, desp), //gotify
]); ]);
} }
function gotifyNotify(text, desp) {
return new Promise((resolve) => {
if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
desp,
)}&priority=${GOTIFY_PRIORITY}`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('gotify发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.id) {
console.log('gotify发送通知消息成功🎉\n');
} else {
console.log(`${data.message}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve();
}
});
} else {
resolve();
}
});
}
function gobotNotify(text, desp, time = 2100) { function gobotNotify(text, desp, time = 2100) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
@@ -293,6 +362,52 @@ function serverNotify(text, desp, time = 2100) {
}); });
} }
function PushDeerNotify(text, desp, time = 2100) {
return new Promise((resolve) => {
if (PUSHDEER_KEY) {
// PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp);
const options = {
url: `https://api2.pushdeer.com/message/push`,
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
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);
}
});
}, time);
} else {
resolve();
}
});
}
function CoolPush(text, desp) { function CoolPush(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (QQ_SKEY) { if (QQ_SKEY) {
@@ -376,7 +491,9 @@ function BarkNotify(text, desp, params = {}) {
const options = { const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent( url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp, desp,
)}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(params)}`, )}?icon=${BARK_ICON}?sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
params,
)}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
+50
View File
@@ -35,6 +35,7 @@ push_config = {
'BARK_ARCHIVE': '', # bark 推送是否存档 'BARK_ARCHIVE': '', # bark 推送是否存档
'BARK_GROUP': '', # bark 推送分组 'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音 'BARK_SOUND': '', # bark 推送声音
'BARK_ICON': '', # bark 推送图标
'CONSOLE': True, # 控制台输出 'CONSOLE': True, # 控制台输出
@@ -51,10 +52,16 @@ push_config = {
# /send_group_msg 时填入 group_id=QQ群 # /send_group_msg 时填入 group_id=QQ群
'GOBOT_TOKEN': '', # go-cqhttp 的 access_token 'GOBOT_TOKEN': '', # go-cqhttp 的 access_token
'GOTIFY_URL': '', # gotify地址,如https://push.example.de:8080
'GOTIFY_TOKEN': '', # gotify的消息应用token
'GOTIFY_PRIORITY': 0, # 推送消息优先级,默认为0
'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY 'IGOT_PUSH_KEY': '', # iGot 聚合推送的 IGOT_PUSH_KEY
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌 'PUSH_PLUS_TOKEN': '', # push+ 微信推送的用户令牌
'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码 'PUSH_PLUS_USER': '', # push+ 微信推送的群组编码
@@ -100,6 +107,7 @@ def bark(title: str, content: str) -> None:
"BARK_ARCHIVE": "isArchive", "BARK_ARCHIVE": "isArchive",
"BARK_GROUP": "group", "BARK_GROUP": "group",
"BARK_SOUND": "sound", "BARK_SOUND": "sound",
"BARK_ICON": "icon",
} }
params = "" params = ""
for pair in filter( for pair in filter(
@@ -194,6 +202,25 @@ def go_cqhttp(title: str, content: str) -> None:
print("go-cqhttp 推送失败!") print("go-cqhttp 推送失败!")
def gotify(title:str,content:str) -> None:
"""
使用 gotify 推送消息。
"""
if not push_config.get("GOTIFY_URL") or not push_config.get("GOTIFY_TOKEN"):
print("gotify 服务的 GOTIFY_URL 或 GOTIFY_TOKEN 未设置!!\n取消推送")
return
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title,"message": content,"priority": push_config.get("GOTIFY_PRIORITY")}
response = requests.post(url,data=data).json()
if response.get("id"):
print("gotify 推送成功!")
else:
print("gotify 推送失败!")
def iGot(title: str, content: str) -> None: def iGot(title: str, content: str) -> None:
""" """
使用 iGot 推送消息。 使用 iGot 推送消息。
@@ -236,6 +263,24 @@ def serverJ(title: str, content: str) -> None:
print(f'serverJ 推送失败!错误码:{response["message"]}') print(f'serverJ 推送失败!错误码:{response["message"]}')
def pushdeer(title: str, content: str) -> None:
"""
通过PushDeer 推送消息
"""
if not push_config.get("DEER_KEY"):
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return
print("PushDeer 服务启动")
data = {"text": title, "desp": urllib.parse.urlencode({"text": content})}
url = 'https://api2.pushdeer.com/message/push'
response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0:
print("PushDeer 推送成功!")
else:
print("PushDeer 推送失败!错误信息:", response)
def pushplus_bot(title: str, content: str) -> None: def pushplus_bot(title: str, content: str) -> None:
""" """
通过 push+ 推送消息。 通过 push+ 推送消息。
@@ -262,6 +307,7 @@ def pushplus_bot(title: str, content: str) -> None:
else: else:
url_old = "http://pushplus.hxtrip.com/send" url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(url=url_old, data=body, headers=headers).json() response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200: if response["code"] == 200:
@@ -473,10 +519,14 @@ if push_config.get("FSKEY"):
notify_function.append(feishu_bot) notify_function.append(feishu_bot)
if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"): if push_config.get("GOBOT_URL") and push_config.get("GOBOT_QQ"):
notify_function.append(go_cqhttp) notify_function.append(go_cqhttp)
if push_config.get("GOTIFY_URL") and push_config.get("GOTIFY_TOKEN"):
notify_function.append(gotify)
if push_config.get("IGOT_PUSH_KEY"): if push_config.get("IGOT_PUSH_KEY"):
notify_function.append(iGot) notify_function.append(iGot)
if push_config.get("PUSH_KEY"): if push_config.get("PUSH_KEY"):
notify_function.append(serverJ) notify_function.append(serverJ)
if push_config.get("DEER_KEY"):
notify_function.append(pushdeer)
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"):
+36
View File
@@ -0,0 +1,36 @@
import * as qiniu from 'qiniu';
import dotenv from 'dotenv';
const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const options = {
scope: process.env.QINIU_SCOPE,
};
const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra('', '', 'text/plain');
const key = 'version.ts';
// 文件上传
formUploader.putFile(
uploadToken,
key,
localFile,
putExtra,
function (respErr, respBody, respInfo) {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
console.log(respBody);
} else {
console.log(respInfo.statusCode);
console.log(respBody);
}
},
);
+28 -2
View File
@@ -61,7 +61,7 @@ update_cron_api() {
-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)
@@ -93,7 +93,7 @@ update_cron_command_api() {
-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)
@@ -176,4 +176,30 @@ update_cron() {
fi fi
} }
notify_api() {
local title=$1
local content=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/api/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-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 "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-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" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
}
get_token get_token
+15 -9
View File
@@ -1,10 +1,16 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 ## 导入通用变量与函数
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
url="${github_proxy_url}https://github.com/SuMaiKaDe/bot.git"
if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git"
repo_path="${dir_repo}/dockerbot" repo_path="${dir_repo}/dockerbot"
else
url=${BotRepoUrl}
repo_path="${dir_repo}/diybot"
fi
echo -e "\n1、安装bot依赖...\n" echo -e "\n1、安装bot依赖...\n"
apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev apk --no-cache add -f zlib-dev gcc jpeg-dev python3-dev musl-dev freetype-dev
@@ -16,9 +22,9 @@ if [[ ! -d ${repo_path}/.git ]]; then
git_clone_scripts ${url} ${repo_path} "main" git_clone_scripts ${url} ${repo_path} "main"
fi fi
cp -rf "$repo_path/jbot" $dir_root cp -rf "$repo_path/jbot" $dir_data
if [[ ! -f "$dir_root/config/bot.json" ]]; then if [[ ! -f "$dir_config/bot.json" ]]; then
cp -f "$repo_path/config/bot.json" "$dir_root/config" cp -f "$repo_path/config/bot.json" "$dir_config"
fi fi
echo -e "\nbot文件下载成功...\n" echo -e "\nbot文件下载成功...\n"
@@ -26,12 +32,12 @@ echo -e "3、安装python3依赖...\n"
if [[ $PipMirror ]]; then if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror pip3 config set global.index-url $PipMirror
fi fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_root" cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_root cd $dir_data
cat requirements.txt | while read LREAD cat requirements.txt | while read LREAD
do do
if test ! -z "$(pip3 show "${LREAD%%=*}" 1>/dev/null)"; then if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD} pip3 --default-timeout=100 install ${LREAD}
fi fi
done done
@@ -40,7 +46,7 @@ echo -e "\npython3依赖安装成功...\n"
echo -e "4、启动bot程序...\n" echo -e "4、启动bot程序...\n"
make_dir $dir_log/bot make_dir $dir_log/bot
cd $dir_root cd $dir_data
ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null ps -ef | grep "python3 -m jbot" | grep -v grep | awk '{print $1}' | xargs kill -9 2>/dev/null
nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 & nohup python3 -m jbot >$dir_log/bot/nohup.log 2>&1 &
echo -e "bot启动成功...\n" echo -e "bot启动成功...\n"
+8 -15
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
@@ -10,13 +10,11 @@ reset_env() {
echo -e "---> 配置文件检测完成\n" echo -e "---> 配置文件检测完成\n"
echo -e "---> 2. 开始安装青龙依赖\n" echo -e "---> 2. 开始安装青龙依赖\n"
rm -rf $dir_root/node_modules
npm_install_2 $dir_root npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n" echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n" echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
rm -rf $dir_scripts/node_modules
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n" echo -e "---> 脚本依赖安装完成\n"
} }
@@ -39,25 +37,19 @@ copy_dep() {
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
if test -z "$(pm2 info panel 1>/dev/null)"; then pm2 delete panel --source-map-support --time &>/dev/null
pm2 reload panel --source-map-support --time &>/dev/null pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
fi
if test -z "$(pm2 info schedule 1>/dev/null)"; then pm2 delete schedule --source-map-support --time &>/dev/null
pm2 reload schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
fi
} }
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"
local panelError="/root/.pm2/logs/panel-error.log" local panelError="/root/.pm2/logs/panel-error.log"
tail -n 20 "$panelOut" tail -n 100 "$panelOut"
tail -n 20 "$panelError" tail -n 100 "$panelError"
} }
check_nginx() { check_nginx() {
@@ -106,6 +98,7 @@ check_pm2() {
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
pnpm install -g pm2
copy_dep copy_dep
check_ql check_ql
check_nginx check_nginx
-5
View File
@@ -1,5 +0,0 @@
const notify = require('/ql/scripts/sendNotify.js');
const title = process.argv[2];
const content = process.argv[3];
notify.sendNotify(`${title}`, `${content}`);
-8
View File
@@ -1,8 +0,0 @@
#!/bin/bash
#author:spark thanks to: https://github.com/sparkssssssss/scripts
. /ql/config/config.sh
title=$(echo $1|sed 's/-/_/g')
msg=$(echo -e $2)
node /ql/shell/notify.js "$title" "$msg"
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 ## 导入通用变量与函数
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
days=$1 days=$1
+68 -15
View File
@@ -1,15 +1,18 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 目录 ## 目录
dir_root=/ql dir_root=$QL_DIR
dir_data=$dir_root/data
dir_shell=$dir_root/shell dir_shell=$dir_root/shell
dir_sample=$dir_root/sample dir_sample=$dir_root/sample
dir_config=$dir_root/config dir_static=$dir_root/static
dir_scripts=$dir_root/scripts dir_config=$dir_data/config
dir_repo=$dir_root/repo dir_scripts=$dir_data/scripts
dir_raw=$dir_root/raw dir_repo=$dir_data/repo
dir_log=$dir_root/log dir_raw=$dir_data/raw
dir_db=$dir_root/db dir_log=$dir_data/log
dir_db=$dir_data/db
dir_dep=$dir_data/deps
dir_list_tmp=$dir_log/.tmp dir_list_tmp=$dir_log/.tmp
dir_code=$dir_log/code dir_code=$dir_log/code
dir_update_log=$dir_log/update dir_update_log=$dir_log/update
@@ -34,6 +37,8 @@ file_notify_js=$dir_scripts/sendNotify.js
task_error_log_path=$dir_log/task_error.log task_error_log_path=$dir_log/task_error.log
nginx_app_conf=$dir_root/docker/front.conf nginx_app_conf=$dir_root/docker/front.conf
nginx_conf=$dir_root/docker/nginx.conf nginx_conf=$dir_root/docker/nginx.conf
dep_notify_py=$dir_dep/notify.py
dep_notify_js=$dir_dep/sendNotify.js
## 清单文件 ## 清单文件
list_crontab_user=$dir_config/crontab.list list_crontab_user=$dir_config/crontab.list
@@ -57,6 +62,7 @@ original_name=(
init_env() { init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules
export PYTHONUNBUFFERED=1
} }
import_config() { import_config() {
@@ -64,8 +70,9 @@ import_config() {
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-"1h"}
github_proxy_url=${GithubProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}" default_cron="${DefaultCronRule}"
@@ -74,6 +81,18 @@ import_config() {
fi fi
} }
set_proxy() {
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
}
unset_proxy() {
unset http_proxy
unset https_proxy
}
make_dir() { make_dir() {
local dir=$1 local dir=$1
if [[ ! -d $dir ]]; then if [[ ! -d $dir ]]; then
@@ -146,6 +165,8 @@ define_cmd() {
} }
fix_config() { fix_config() {
make_dir $dir_static
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
@@ -154,6 +175,7 @@ fix_config() {
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
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"
@@ -208,9 +230,23 @@ fix_config() {
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
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
} }
npm_install_sub() { npm_install_sub() {
set_proxy
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links npm install --production --no-bin-links --registry=https://registry.npm.taobao.org || npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then elif ! type pnpm &>/dev/null; then
@@ -218,6 +254,7 @@ npm_install_sub() {
else else
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production
fi fi
unset_proxy
} }
npm_install_1() { npm_install_1() {
@@ -270,21 +307,27 @@ git_clone_scripts() {
local branch=$3 local branch=$3
[[ $branch ]] && local part_cmd="-b $branch " [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n" echo -e "开始克隆仓库 $url$dir\n"
set_proxy
git clone $part_cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
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"
[[ $branch ]] && local part_cmd="origin/${branch}"
cd $dir_work cd $dir_work
echo -e "开始更新仓库:$dir_work\n" echo -e "开始更新仓库:$dir_work\n"
set_proxy
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git reset --hard $part_cmd git pull &>/dev/null
git pull unset_proxy
reset_branch "$branch"
cd $dir_current cd $dir_current
} }
@@ -294,17 +337,27 @@ reset_romote_url() {
local url=$2 local url=$2
local branch="$3" local branch="$3"
[[ $branch ]] && local part_cmd="origin/${branch}"
if [[ -d "$dir_work/.git" ]]; then if [[ -d "$dir_work/.git" ]]; then
cd $dir_work 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
git reset --hard $part_cmd >/dev/null
local part_cmd=""
reset_branch "$branch"
cd $dir_current cd $dir_current
fi fi
} }
reset_branch() {
local branch="$1"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch"
git branch --set-upstream-to=$part_cmd $branch
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() { random_range() {
local beg=$1 local beg=$1
local end=$2 local end=$2
+35 -24
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 导入通用变量与函数 ## 导入通用变量与函数
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
@@ -10,7 +10,7 @@ define_program() {
local first_param=$1 local first_param=$1
if [[ $first_param == *.js ]]; then if [[ $first_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $first_param == *.py ]]; then elif [[ $first_param == *.py ]] || [[ $first_param == *.pyc ]]; then
which_program="python3" which_program="python3"
elif [[ $first_param == *.sh ]]; then elif [[ $first_param == *.sh ]]; then
which_program="bash" which_program="bash"
@@ -96,7 +96,7 @@ run_normal() {
make_dir "$log_dir" make_dir "$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
@@ -106,7 +106,7 @@ run_normal() {
cd $dir_scripts cd $dir_scripts
local relative_path="${first_param%/*}" local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]]; then if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
first_param=${first_param/$relative_path\//} first_param=${first_param/$relative_path\//}
fi fi
@@ -114,7 +114,7 @@ run_normal() {
eval . $file_task_after "$@" $cmd eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
@@ -143,6 +143,7 @@ run_concurrent() {
done done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
define_program "$first_param" define_program "$first_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S") log_time=$(date "+%Y-%m-%d-%H-%M-%S")
@@ -157,7 +158,7 @@ run_concurrent() {
make_dir $log_dir make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
@@ -166,15 +167,13 @@ run_concurrent() {
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N") single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts cd $dir_scripts
local relative_path="${first_param%/*}" local relative_path="${first_param%/*}"
if [[ ! -z ${relative_path} ]]; then if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
first_param=${first_param/$relative_path\//} first_param=${first_param/$relative_path\//}
fi fi
@@ -193,7 +192,7 @@ run_concurrent() {
eval . $file_task_after "$@" $cmd eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
@@ -221,7 +220,7 @@ run_designated() {
make_dir $log_dir make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date "+%s")
local envs=$(eval echo "\$${env_param}") local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g')) local array=($(echo $envs | sed 's/&/ /g'))
@@ -236,6 +235,7 @@ run_designated() {
done done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
@@ -244,11 +244,9 @@ run_designated() {
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
@@ -256,7 +254,7 @@ run_designated() {
eval . $file_task_after "$@" $cmd eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
@@ -264,29 +262,42 @@ run_designated() {
## 运行其他命令 ## 运行其他命令
run_else() { run_else() {
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local file_param="$1"
local log_dir_tmp="${1##*/}" define_program "$file_param"
local log_dir="$dir_log/${log_dir_tmp%%.*}" log_time=$(date "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path" cmd="&>> $log_path"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir" make_dir $log_dir
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date "+%s")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd [[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $first_param" | perl -pe "s|.*ID=(.*) $cmd_task $first_param\.*|\1|" | head -1 | awk -F " " '{print $1}') 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" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
eval timeout -k 10s $command_timeout_time "$@" $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 . $file_task_after "$@" $cmd 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_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s" -d "$end_time") local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp )) local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
+71 -59
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
dir_shell=/ql/shell dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
@@ -43,7 +43,7 @@ detect_config_version() {
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 "$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 fi
else else
@@ -91,7 +91,7 @@ del_cron() {
done done
if [[ $ids ]]; then if [[ $ids ]]; then
result=$(del_cron_api "$ids") result=$(del_cron_api "$ids")
notify "$path 删除任务${result}" "$detail" notify_api "$path 删除任务${result}" "$detail"
fi fi
} }
@@ -131,7 +131,7 @@ add_cron() {
fi fi
fi fi
done done
notify "$path 新增任务" "$detail" notify_api "$path 新增任务" "$detail"
} }
## 更新仓库 ## 更新仓库
@@ -151,31 +151,18 @@ update_repo() {
make_dir "${dir_scripts}/${uniq_path}" make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url" local formatUrl="$url"
if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && formatUrl="${github_proxy_url}${url}"
fi
fi
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}" git_pull_scripts ${repo_path} "${branch}"
else else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence"
else
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n"
rm -rf ${repo_path}
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
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"
else else
echo -e "\n更新${repo_path}失败,请检查网络...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi fi
fi
} }
## 更新所有 raw 文件 ## 更新所有 raw 文件
@@ -183,15 +170,14 @@ update_raw() {
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local raw_url="$url" local raw_url="$url"
if [[ $github_proxy_url ]]; then
if [[ $url =~ "github.com" ]] || [[ $url =~ "githubusercontent.com" ]]; then
[[ ! $url =~ "https://ghproxy.com" ]] && raw_url="${github_proxy_url}${url}"
fi
fi
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
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
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"
@@ -217,7 +203,7 @@ update_raw() {
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 "新增任务通知" "\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 fi
else else
@@ -258,12 +244,17 @@ update_qinglong() {
patch_version patch_version
local no_restart="$1" local no_restart="$1"
local all_branch=$(git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json) [[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "${github_proxy_url}https://github.com/whyour/qinglong.git" "master" reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root "master" git_pull_scripts $dir_root ${primary_branch}
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$dir_root成功...\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
@@ -271,23 +262,23 @@ update_qinglong() {
[[ -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 else
echo -e "\n更新$dir_root失败,请检查原因...\n" echo -e "\n更新青龙源文件失败,请检查原因...\n"
fi fi
local url="${github_proxy_url}https://github.com/whyour/qinglong-static.git" local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} "master" reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} "master" git_pull_scripts ${ql_static_repo} ${primary_branch}
else else
git_clone_scripts ${url} ${ql_static_repo} git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新$ql_static_repo成功...\n" echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat /ql/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1) local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n" echo -e "\n当前版本 $static_version...\n"
cd $dir_root
rm -rf $dir_root/build && rm -rf $dir_root/dist rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_root cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..." echo -e "重启面板中..."
@@ -295,16 +286,16 @@ update_qinglong() {
reload_pm2 reload_pm2
fi fi
else else
echo -e "\n更新$dir_root失败,请检查原因...\n" echo -e "\n更新青龙静态资源失败,请检查原因...\n"
fi fi
} }
patch_version() { patch_version() {
if [[ -f "/ql/db/cookie.db" ]]; then if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n" echo -e "检测到旧的db文件,拷贝为新db...\n"
mv /ql/db/cookie.db /ql/db/env.db mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf /ql/db/cookie.db rm -rf $dir_root/db/cookie.db
echo echo
fi fi
@@ -313,22 +304,43 @@ patch_version() {
fi fi
git config --global pull.rebase false 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() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
if [[ $(pm2 info panel 2>/dev/null) ]]; then pm2 delete panel --source-map-support --time &>/dev/null
pm2 reload panel --source-map-support --time &>/dev/null pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
fi
if [[ $(pm2 info schedule 2>/dev/null) ]]; then pm2 delete schedule --source-map-support --time &>/dev/null
pm2 reload schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
else
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
fi
} }
## 对比脚本 ## 对比脚本
@@ -391,16 +403,11 @@ gen_list_repo() {
if [[ $blackword ]]; then if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword) files=$(echo "$files" | egrep -v $blackword)
fi fi
if [[ $dependence ]]; then
cd ${dir_scripts}
depInScripts=$(eval $cmd | sed 's/^..//' | egrep -v $uniq_path | egrep $dependence)
for dep in ${depInScripts}; do
file_path=$(dirname $dep)
[[ ! $file_path =~ "/" ]] && file_path=""
make_dir "${dir_scripts}/${uniq_path}/${file_path#*/}"
cp -f $dep "${dir_scripts}/${uniq_path}/${file_path#*/}"
done
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) results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do for _file in ${results}; do
@@ -409,6 +416,11 @@ gen_list_repo() {
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}" cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done done
fi fi
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do for file in ${files}; do
filename=$(basename $file) filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}" cp -f $file "$dir_scripts/${uniq_path}/${filename}"
+77
View File
@@ -0,0 +1,77 @@
.react-terminal-wrapper {
width: 100%;
background: #252a33;
color: #eee;
font-size: 18px;
font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier,
monospace;
border-radius: 4px;
padding: 75px 45px 35px;
position: relative;
-webkit-box-sizing: border-box;
box-sizing: border-box;
}
.react-terminal {
overflow: auto;
display: flex;
flex-direction: column;
}
.react-terminal-wrapper.react-terminal-light {
background: #ddd;
color: #1a1e24;
}
.react-terminal-wrapper:before {
content: '';
position: absolute;
top: 15px;
left: 15px;
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
background: #d9515d;
-webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930;
}
.react-terminal-wrapper:after {
content: attr(data-terminal-name);
position: absolute;
color: #a2a2a2;
top: 5px;
left: 0;
width: 100%;
text-align: center;
}
.react-terminal-wrapper.react-terminal-light:after {
color: #d76d77;
}
.react-terminal-line {
display: block;
line-height: 1.5;
}
.react-terminal-line:before {
content: '';
display: inline-block;
vertical-align: middle;
color: #a2a2a2;
}
.react-terminal-light .react-terminal-line:before {
color: #d76d77;
}
.react-terminal-input:before {
margin-right: 0.75em;
content: '$';
}
.react-terminal-input[data-terminal-prompt]:before {
content: attr(data-terminal-prompt);
}
+80
View File
@@ -0,0 +1,80 @@
import React, { useEffect, useRef } from 'react';
import './index.less';
export enum LineType {
Input,
Output,
}
export enum ColorMode {
Light,
Dark,
}
export interface Props {
name?: string;
prompt?: string;
colorMode?: ColorMode;
lineData: Array<{ type: LineType; value: string | React.ReactNode }>;
startingInputValue?: string;
}
const Terminal = ({
name,
prompt,
colorMode,
lineData,
startingInputValue = '',
}: Props) => {
const lastLineRef = useRef<null | HTMLElement>(null);
// An effect that handles scrolling into view the last line of terminal input or output
const performScrolldown = useRef(false);
useEffect(() => {
if (performScrolldown.current) {
// skip scrolldown when the component first loads
setTimeout(
() =>
lastLineRef?.current?.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
}),
500,
);
}
performScrolldown.current = true;
}, [lineData.length]);
const renderedLineData = lineData.map((ld, i) => {
const classes = ['react-terminal-line'];
if (ld.type === LineType.Input) {
classes.push('react-terminal-input');
}
// `lastLineRef` is used to ensure the terminal scrolls into view to the last line; make sure to add the ref to the last
if (lineData.length === i + 1) {
return (
<span className={classes.join(' ')} key={i} ref={lastLineRef}>
{ld.value}
</span>
);
} else {
return (
<span className={classes.join(' ')} key={i}>
{ld.value}
</span>
);
}
});
const classes = ['react-terminal-wrapper'];
if (colorMode === ColorMode.Light) {
classes.push('react-terminal-light');
}
return (
<div className={classes.join(' ')} data-terminal-name={name}>
<div className="react-terminal">{renderedLineData}</div>
</div>
);
};
export default Terminal;
+6 -1
View File
@@ -25,6 +25,12 @@ export default {
hideInMenu: true, hideInMenu: true,
component: '@/pages/initialization/index', component: '@/pages/initialization/index',
}, },
{
name: '错误',
path: '/error',
hideInMenu: true,
component: '@/pages/error/index',
},
{ {
path: '/crontab', path: '/crontab',
name: '定时任务', name: '定时任务',
@@ -79,6 +85,5 @@ export default {
fixSiderbar: true, fixSiderbar: true,
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
logo: '/images/qinglong.png',
siderWidth: 180, siderWidth: 180,
} as any; } as any;
+43 -10
View File
@@ -26,6 +26,7 @@
.monaco-editor:not(.rename-box) { .monaco-editor:not(.rename-box) {
height: calc(100vh - 128px) !important; height: calc(100vh - 128px) !important;
height: calc(100vh - var(--vh-offset, 0px) - 128px) !important; height: calc(100vh - var(--vh-offset, 0px) - 128px) !important;
.view-overlays .current-line { .view-overlays .current-line {
border-width: 0; border-width: 0;
} }
@@ -33,6 +34,7 @@
.rename-box { .rename-box {
height: 0; height: 0;
.rename-input { .rename-input {
height: 0; height: 0;
padding: 0 !important; padding: 0 !important;
@@ -43,6 +45,7 @@
.ant-pro-grid-content.wide { .ant-pro-grid-content.wide {
max-width: unset !important; max-width: unset !important;
overflow: auto; overflow: auto;
.ant-pro-page-container-children-content { .ant-pro-page-container-children-content {
overflow: auto; overflow: auto;
height: 100%; height: 100%;
@@ -57,6 +60,7 @@
.ant-tooltip { .ant-tooltip {
max-width: 500px !important; max-width: 500px !important;
.ant-tooltip-inner { .ant-tooltip-inner {
word-break: break-all !important; word-break: break-all !important;
} }
@@ -72,6 +76,7 @@
.log-select { .log-select {
width: 250px; width: 250px;
} }
.ant-page-header-heading-left { .ant-page-header-heading-left {
min-width: 100px; min-width: 100px;
} }
@@ -81,6 +86,7 @@
.config-select { .config-select {
width: 250px; width: 250px;
} }
.ant-page-header-heading-left { .ant-page-header-heading-left {
min-width: 100px; min-width: 100px;
} }
@@ -119,10 +125,13 @@
flex-direction: column; flex-direction: column;
height: calc(100vh - 48px); height: calc(100vh - 48px);
height: calc(100vh - var(--vh-offset, 0px) - 48px); height: calc(100vh - var(--vh-offset, 0px) - 48px);
.ant-pro-grid-content.wide { .ant-pro-grid-content.wide {
flex: 1; flex: 1;
.ant-pro-grid-content-children { .ant-pro-grid-content-children {
height: calc(100% - 36px); height: calc(100% - 36px);
> div, > div,
.log-container, .log-container,
.react-codemirror2, .react-codemirror2,
@@ -152,8 +161,8 @@
.ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header { .ant-layout-content.ant-pro-basicLayout-content.ant-pro-basicLayout-has-header {
margin-bottom: 0 !important; margin-bottom: 0 !important;
min-height: calc(100vh - 66px); min-height: calc(100vh - 72px);
min-height: calc(100vh - var(--vh-offset, 0px) - 66px); min-height: calc(100vh - var(--vh-offset, 0px) - 72px);
} }
.Resizer { .Resizer {
@@ -197,6 +206,7 @@
.Resizer.disabled { .Resizer.disabled {
cursor: not-allowed; cursor: not-allowed;
} }
.Resizer.disabled:hover { .Resizer.disabled:hover {
border-color: transparent; border-color: transparent;
} }
@@ -209,6 +219,7 @@
.inline-countdown.ant-statistic { .inline-countdown.ant-statistic {
display: inline-block; display: inline-block;
.ant-statistic-content { .ant-statistic-content {
font-size: 14px; font-size: 14px;
padding: 0 3px; padding: 0 3px;
@@ -235,6 +246,7 @@
box-shadow: none; box-shadow: none;
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color; -webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
} }
::placeholder { ::placeholder {
@@ -252,6 +264,7 @@
.ant-pro-basicLayout-content { .ant-pro-basicLayout-content {
margin: 18px; margin: 18px;
.ant-pro-page-container { .ant-pro-page-container {
margin: -18px -18px -18px; margin: -18px -18px -18px;
} }
@@ -279,16 +292,36 @@
.side-menu-user-drop-menu { .side-menu-user-drop-menu {
position: relative; position: relative;
margin: 0;
padding: 4px 0;
text-align: left; text-align: left;
background-clip: padding-box;
outline: none; outline: none;
background-color: #fff; padding: 4px 0;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 4px; border-radius: 4px;
box-shadow: 0 6px 16px -8px rgb(0 0 0 / 8%), 0 9px 28px 0 rgb(0 0 0 / 5%), box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
0 12px 48px 16px rgb(0 0 0 / 3%); overflow: auto;
.ant-dropdown-menu-item:hover { background-color: #fff;
color: #1890ff; }
[data-dark='true'] .side-menu-user-drop-menu {
background-color: #373739;
}
.ant-pro-sider-logo {
padding: 16px 8px !important;
h1 {
margin-left: 5px !important;
}
img {
width: 32px !important;
border-radius: 52% !important;
}
}
.ant-pro-global-header-logo {
a img {
// 移动端logo被拉伸
width: auto !important;
} }
} }
+17 -20
View File
@@ -20,18 +20,11 @@ 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 { message, Badge, Modal, Avatar, Dropdown, Menu, Popover } from 'antd'; import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } 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 { Integrations } from '@sentry/tracing'; import { init } from '../utils/init';
Sentry.init({
dsn: 'https://ea2fede373244db99c536210b910d9da@o1051273.ingest.sentry.io/6047851',
integrations: [new Integrations.BrowserTracing()],
release: version,
tracesSampleRate: 1.0,
});
export default function (props: any) { export default function (props: any) {
const ctx = useCtx(); const ctx = useCtx();
@@ -44,7 +37,7 @@ export default function (props: any) {
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const logout = () => { const logout = () => {
request.post(`${config.apiPrefix}logout`).then(() => { request.post(`${config.apiPrefix}user/logout`).then(() => {
localStorage.removeItem(config.authKey); localStorage.removeItem(config.authKey);
history.push('/login'); history.push('/login');
}); });
@@ -107,10 +100,6 @@ export default function (props: any) {
} }
}; };
useEffect(() => {
vhCheck();
}, []);
useEffect(() => { useEffect(() => {
if (systemInfo && systemInfo.isInitialized && !user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
@@ -128,6 +117,9 @@ export default function (props: any) {
}, [theme.theme]); }, [theme.theme]);
useEffect(() => { useEffect(() => {
vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
setFetchMethod(window.fetch); setFetchMethod(window.fetch);
if (_theme === 'dark') { if (_theme === 'dark') {
@@ -187,7 +179,9 @@ export default function (props: any) {
}; };
}, []); }, []);
if (['/login', '/initialization'].includes(props.location.pathname)) { if (
['/login', '/initialization', '/error'].includes(props.location.pathname)
) {
document.title = `${ document.title = `${
(config.documentTitleMap as any)[props.location.pathname] (config.documentTitleMap as any)[props.location.pathname]
} - 控制面板`; } - 控制面板`;
@@ -198,7 +192,7 @@ export default function (props: any) {
history.push('/crontab'); history.push('/crontab');
} }
if (systemInfo) { if (systemInfo || props.location.pathname === '/error') {
return React.Children.map(props.children, (child) => { return React.Children.map(props.children, (child) => {
return React.cloneElement(child, { return React.cloneElement(child, {
...ctx, ...ctx,
@@ -232,6 +226,7 @@ export default function (props: any) {
selectedKeys={[props.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" />}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -275,14 +270,16 @@ export default function (props: any) {
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => ( rightContentRender={() =>
<Dropdown overlay={menu} trigger={['click']}> ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span> <span style={{ marginLeft: 5 }}>admin</span>
</span> </span>
</Dropdown> </Dropdown>
)} )
}
collapsedButtonRender={(collapsed) => ( collapsedButtonRender={(collapsed) => (
<span <span
className="side-menu-container" className="side-menu-container"
@@ -292,7 +289,7 @@ export default function (props: any) {
}} }}
> >
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} trigger={['hover']}> <Dropdown overlay={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <Avatar shape="square" size="small" icon={<UserOutlined />} />
<span style={{ marginLeft: 5 }}>admin</span> <span style={{ marginLeft: 5 }}>admin</span>
+340
View File
@@ -0,0 +1,340 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Button,
Card,
Tag,
List,
Divider,
Typography,
} from 'antd';
import {
ClockCircleOutlined,
CloseCircleOutlined,
FieldTimeOutlined,
Loading3QuartersOutlined,
FileOutlined,
} from '@ant-design/icons';
import { CrontabStatus } from './index';
import { diffTime } from '@/utils/date';
import { request } from '@/utils/http';
import config from '@/utils/config';
import CronLogModal from './logModal';
import Editor from '@monaco-editor/react';
const { Text } = Typography;
const tabList = [
{
key: 'log',
tab: '日志',
},
{
key: 'script',
tab: '脚本',
},
];
interface LogItem {
directory: string;
filename: string;
}
const language = navigator.language || navigator.languages[0];
const CronDetailModal = ({
cron = {},
handleCancel,
visible,
theme,
isPhone,
}: {
cron?: any;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
theme: string;
isPhone: boolean;
}) => {
const [activeTabKey, setActiveTabKey] = useState('log');
const [loading, setLoading] = useState(true);
const [logs, setLogs] = useState<LogItem[]>([]);
const [log, setLog] = useState('');
const [value, setValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const editorRef = useRef<any>(null);
const [scriptInfo, setScriptInfo] = useState<any>({});
const [logUrl, setLogUrl] = useState('');
const [validTabs, setValidTabs] = useState(tabList);
const contentList: any = {
log: (
<List
dataSource={logs}
loading={loading}
renderItem={(item) => (
<List.Item className="log-item" onClick={() => onClickItem(item)}>
<FileOutlined style={{ marginRight: 10 }} />
{item.directory}/{item.filename}
</List.Item>
)}
/>
),
script: (
<Editor
language="shell"
theme={theme}
value={value}
options={{
fontSize: 12,
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
onMount={(editor) => {
editorRef.current = editor;
}}
/>
),
};
const onClickItem = (item: LogItem) => {
localStorage.setItem('logCron', cron.id);
setLogUrl(`${config.apiPrefix}logs/${item.directory}/${item.filename}`);
request
.get(`${config.apiPrefix}logs/${item.directory}/${item.filename}`)
.then((data) => {
setLog(data.data);
setIsLogModalVisible(true);
});
};
const onTabChange = (key: string) => {
setActiveTabKey(key);
};
const getLogs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => {
if (data.code === 200) {
setLogs(data.data);
}
})
.finally(() => setLoading(false));
};
const getScript = () => {
const cmd = cron.command.split(' ') as string[];
if (cmd[0] === 'task') {
setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
setScriptInfo({ parent: p, filename: s });
request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => {
setValue(data.data);
});
} else {
setValidTabs([validTabs[0]]);
}
};
const saveFile = () => {
Modal.confirm({
title: `确认保存`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{scriptInfo.filename}
</Text>{' '}
</>
),
onOk() {
const content = editorRef.current
? editorRef.current.getValue().replace(/\r\n/g, '\n')
: value;
return new Promise((resolve, reject) => {
request
.put(`${config.apiPrefix}scripts`, {
data: {
filename: scriptInfo.filename,
path: scriptInfo.parent || '',
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
setValue(content);
message.success(`保存成功`);
} else {
message.error(_data);
}
resolve(null);
})
.catch((e) => reject(e));
});
},
onCancel() {
console.log('Cancel');
},
});
};
useEffect(() => {
if (cron && cron.id) {
getLogs();
getScript();
}
}, [cron]);
return (
<Modal
title={
<>
<span>{cron.name}</span>
{cron.labels?.length > 0 && cron.labels[0] !== '' && (
<Divider type="vertical"></Divider>
)}
{cron.labels?.length > 0 &&
cron.labels[0] !== '' &&
cron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}>
{label}
</Tag>
))}
</>
}
centered
visible={visible}
forceRender
footer={false}
onCancel={() => handleCancel()}
wrapClassName="crontab-detail"
width={!isPhone ? '80vw' : ''}
>
<div className="card-wrapper">
<Card>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">{cron.command}</div>
</div>
</Card>
<Card style={{ marginTop: 10 }}>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{(!cron.isDisabled || cron.status !== CrontabStatus.idle) && (
<>
{cron.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
</Tag>
)}
{cron.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
</Tag>
)}
{cron.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
</Tag>
)}
</>
)}
{cron.isDisabled === 1 && cron.status === CrontabStatus.idle && (
<Tag icon={<CloseCircleOutlined />} color="error">
</Tag>
)}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">{cron.schedule}</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{cron.last_execution_time
? new Date(cron.last_execution_time * 1000)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')
: '-'}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{cron.last_running_time ? diffTime(cron.last_running_time) : '-'}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{cron.nextRunTime &&
cron.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</div>
</div>
</Card>
<Card
style={{ marginTop: 10 }}
tabList={validTabs}
activeTabKey={activeTabKey}
onTabChange={(key) => {
onTabChange(key);
}}
tabBarExtraContent={
activeTabKey === 'script' && (
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={saveFile}
>
</Button>
)
}
>
{contentList[activeTabKey]}
</Card>
</div>
<CronLogModal
visible={isLogModalVisible}
handleCancel={() => {
setIsLogModalVisible(false);
}}
cron={cron}
data={log}
logUrl={logUrl}
/>
</Modal>
);
};
export default CronDetailModal;
+84
View File
@@ -1,3 +1,87 @@
.ant-table-pagination.ant-pagination { .ant-table-pagination.ant-pagination {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
.cron {
&:hover {
cursor: pointer;
}
}
.crontab-detail {
.card-wrapper {
height: 80vh;
height: calc(80vh - var(--vh-offset, 0px));
.ant-card:last-child {
.ant-card-body {
height: calc(80vh - 238px);
height: calc(80vh - var(--vh-offset, 0px) - 238px);
overflow-y: auto;
}
}
}
.ant-modal-body {
background: #eee;
padding: 12px;
}
.ant-card-body {
padding: 18px;
}
.ant-card-head {
padding: 0 18px;
}
.ant-card:first-child {
max-height: 66px;
overflow: auto;
.ant-card-body {
min-width: 600px;
}
.cron-detail-info-item {
display: flex;
.cron-detail-info-title {
width: 50px;
}
.cron-detail-info-value {
flex: 1;
margin-top: 0;
}
}
}
.ant-card:nth-child(2) {
overflow-x: auto;
.ant-card-body {
display: flex;
justify-content: space-between;
min-width: 600px;
}
}
.cron-detail-info-item {
flex: auto;
.cron-detail-info-title {
color: #888;
}
.cron-detail-info-value {
margin-top: 12px;
}
}
}
.log-item {
cursor: pointer;
&:hover {
background: #fafafa;
}
}
+151 -43
View File
@@ -11,6 +11,7 @@ import {
Menu, Menu,
Typography, Typography,
Input, Input,
Popover,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -30,24 +31,26 @@ import {
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import CronModal from './modal'; import CronModal, { CronLabelModal } from './modal';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
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 { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
import { history } from 'umi';
import './index.less'; import './index.less';
const { Text } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
enum CrontabStatus { export enum CrontabStatus {
'running', 'running',
'idle', 'idle',
'disabled', 'disabled',
'queued', 'queued',
} }
const CrontabSort: any = { 0: 0, 3: 1, 1: 2, 4: 3 }; const CrontabSort: any = { 0: 0, 5: 1, 3: 2, 1: 3, 4: 4 };
enum OperationName { enum OperationName {
'启用', '启用',
@@ -67,7 +70,7 @@ enum OperationPath {
'unpin', 'unpin',
} }
const Crontab = ({ headerStyle, isPhone }: any) => { const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const columns: any = [ const columns: any = [
{ {
title: '任务名', title: '任务名',
@@ -76,8 +79,38 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
width: 150, width: 150,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<span> <>
{record.name || record._id}{' '} <a
onClick={() => {
goToScriptManager(record);
}}
>
{record.labels?.length > 0 && record.labels[0] !== '' ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
content={
<div>
{record.labels?.map((label: string) => (
<Tag
color="blue"
style={{ cursor: 'point' }}
onClick={(e) => {
e.stopPropagation();
setSearchValue(`label:${label}`);
}}
>
<a>{label}</a>
</Tag>
))}
</div>
}
>
{record.name || '-'}
</Popover>
) : (
record.name || '-'
)}
{record.isPinned ? ( {record.isPinned ? (
<span> <span>
<PushpinOutlined /> <PushpinOutlined />
@@ -85,7 +118,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
) : ( ) : (
'' ''
)} )}
</span> </a>
</>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
@@ -100,16 +134,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<span <Paragraph
style={{ style={{
textAlign: 'left',
width: '100%',
display: 'inline-block',
wordBreak: 'break-all', wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}} }}
ellipsis={{ tooltip: text, rows: 2 }}
> >
{text} {text}
</span> </Paragraph>
); );
}, },
sorter: { sorter: {
@@ -282,7 +316,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
{record.status === CrontabStatus.idle && ( {record.status === CrontabStatus.idle && (
<Tooltip title={isPc ? '运行' : ''}> <Tooltip title={isPc ? '运行' : ''}>
<a <a
onClick={() => { onClick={(e) => {
e.stopPropagation();
runCron(record, index); runCron(record, index);
}} }}
> >
@@ -293,7 +328,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
{record.status !== CrontabStatus.idle && ( {record.status !== CrontabStatus.idle && (
<Tooltip title={isPc ? '停止' : ''}> <Tooltip title={isPc ? '停止' : ''}>
<a <a
onClick={() => { onClick={(e) => {
e.stopPropagation();
stopCron(record, index); stopCron(record, index);
}} }}
> >
@@ -303,7 +339,8 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
)} )}
<Tooltip title={isPc ? '日志' : ''}> <Tooltip title={isPc ? '日志' : ''}>
<a <a
onClick={() => { onClick={(e) => {
e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() }); setLogCron({ ...record, timestamp: Date.now() });
}} }}
> >
@@ -320,6 +357,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [value, setValue] = useState<any[]>([]); const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [isLabelModalVisible, setIsLabelModalVisible] = useState(false);
const [editedCron, setEditedCron] = useState(); const [editedCron, setEditedCron] = useState();
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
@@ -328,6 +366,27 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20); const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
if (cmd[0] === 'task') {
if (cmd[1].startsWith('/ql/data/scripts')) {
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let [p, s] = cmd[1].split('/');
if (!s) {
s = p;
p = '';
}
history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') {
location.href = cmd[2];
}
};
const getCrons = () => { const getCrons = () => {
setLoading(true); setLoading(true);
@@ -337,14 +396,19 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
setValue( setValue(
data.data data.data
.sort((a: any, b: any) => { .sort((a: any, b: any) => {
const sortA = a.isDisabled ? 4 : a.status; const sortA =
const sortB = b.isDisabled ? 4 : b.status; a.isPinned && a.status !== 0
a.isPinned = a.isPinned ? a.isPinned : 0; ? 5
b.isPinned = b.isPinned ? b.isPinned : 0; : a.isDisabled && a.status !== 0
if (a.isPinned === b.isPinned) { ? 4
: a.status;
const sortB =
b.isPinned && b.status !== 0
? 5
: b.isDisabled && b.status !== 0
? 4
: b.status;
return CrontabSort[sortA] - CrontabSort[sortB]; return CrontabSort[sortA] - CrontabSort[sortB];
}
return b.isPinned - a.isPinned;
}) })
.map((x) => { .map((x) => {
return { return {
@@ -385,12 +449,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: [record._id] }) .delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => { .then((data: any) => {
if (data.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);
result.splice(i, 1); result.splice(i, 1);
setValue(result); setValue(result);
} else { } else {
@@ -418,11 +482,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
), ),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [record._id] }) .put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => { .then((data: any) => {
if (data.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);
result.splice(i, 1, { result.splice(i, 1, {
...record, ...record,
status: CrontabStatus.running, status: CrontabStatus.running,
@@ -453,11 +517,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
), ),
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [record._id] }) .put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => { .then((data: any) => {
if (data.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);
result.splice(i, 1, { result.splice(i, 1, {
...record, ...record,
pid: null, pid: null,
@@ -495,14 +559,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
record.isDisabled === 1 ? 'enable' : 'disable' record.isDisabled === 1 ? 'enable' : 'disable'
}`, }`,
{ {
data: [record._id], data: [record.id],
}, },
) )
.then((data: any) => { .then((data: any) => {
if (data.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);
result.splice(i, 1, { result.splice(i, 1, {
...record, ...record,
isDisabled: newStatus, isDisabled: newStatus,
@@ -539,14 +603,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
record.isPinned === 1 ? 'unpin' : 'pin' record.isPinned === 1 ? 'unpin' : 'pin'
}`, }`,
{ {
data: [record._id], data: [record.id],
}, },
) )
.then((data: any) => { .then((data: any) => {
if (data.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);
result.splice(i, 1, { result.splice(i, 1, {
...record, ...record,
isPinned: newStatus, isPinned: newStatus,
@@ -568,10 +632,16 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
index: number; index: number;
}> = ({ record, index }) => ( }> = ({ record, index }) => (
<Dropdown <Dropdown
arrow arrow={{ pointAtCenter: true }}
placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ overlay={
<Menu onClick={({ key }) => action(key, record, index)}> <Menu
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
action(key, record, index);
}}
>
<Menu.Item key="edit" icon={<EditOutlined />}> <Menu.Item key="edit" icon={<EditOutlined />}>
</Menu.Item> </Menu.Item>
@@ -601,7 +671,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
</Menu> </Menu>
} }
> >
<a> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
</a> </a>
</Dropdown> </Dropdown>
@@ -638,7 +708,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}; };
const handleCrons = (cron: any) => { const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x._id === cron._id); const index = value.findIndex((x) => x.id === cron.id);
const result = [...value]; const result = [...value];
cron.nextRunTime = cron_parser cron.nextRunTime = cron_parser
.parseExpression(cron.schedule) .parseExpression(cron.schedule)
@@ -656,9 +726,9 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron._id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => { .then((data: any) => {
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.data.nextRunTime = cron_parser
.parseExpression(data.data.schedule) .parseExpression(data.data.schedule)
@@ -746,12 +816,12 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}; };
const getRowClassName = (record: any, index: number) => { const getRowClassName = (record: any, index: number) => {
return record.isPinned ? 'pinned-cron' : ''; return record.isPinned ? 'pinned-cron cron' : 'cron';
}; };
useEffect(() => { useEffect(() => {
if (logCron) { if (logCron) {
localStorage.setItem('logCron', logCron._id); localStorage.setItem('logCron', logCron.id);
setIsLogModalVisible(true); setIsLogModalVisible(true);
} }
}, [logCron]); }, [logCron]);
@@ -776,11 +846,14 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
placeholder="请输入名称或者关键词" placeholder="请输入名称或者关键词"
style={{ width: 'auto' }} style={{ width: 'auto' }}
enterButton enterButton
allowClear
loading={loading} loading={loading}
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addCron()}> <Button key="2" type="primary" onClick={() => addCron()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -830,6 +903,13 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
> >
</Button> </Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a> <a>{selectedRowIds?.length}</a>
@@ -847,9 +927,18 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
defaultPageSize: 20, defaultPageSize: 20,
showTotal: (total: number, range: number[]) => showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`, `${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [20, 100, 500, 1000] as any,
}}
onRow={(record) => {
return {
onClick: (event) => {
setDetailCron(record);
setIsDetailModalVisible(true);
},
};
}} }}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading} loading={loading}
@@ -869,6 +958,25 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
handleCancel={handleCancel} handleCancel={handleCancel}
cron={editedCron} cron={editedCron}
/> />
<CronLabelModal
visible={isLabelModalVisible}
handleCancel={(needUpdate?: boolean) => {
setIsLabelModalVisible(false);
if (needUpdate) {
getCrons();
}
}}
ids={selectedRowIds}
/>
<CronDetailModal
visible={isDetailModalVisible}
handleCancel={() => {
setIsDetailModalVisible(false);
}}
cron={detailCron}
theme={theme}
isPhone={isPhone}
/>
</PageContainer> </PageContainer>
); );
}; };
+17 -7
View File
@@ -20,10 +20,14 @@ const CronLogModal = ({
cron, cron,
handleCancel, handleCancel,
visible, visible,
data,
logUrl,
}: { }: {
cron?: any; cron?: any;
visible: boolean; visible: boolean;
handleCancel: () => void; handleCancel: () => void;
data?: string;
logUrl?: string;
}) => { }) => {
const [value, setValue] = useState<string>('启动中...'); const [value, setValue] = useState<string>('启动中...');
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
@@ -36,9 +40,9 @@ const CronLogModal = ({
setLoading(true); setLoading(true);
} }
request request
.get(`${config.apiPrefix}crons/${cron._id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => { .then((data: any) => {
if (localStorage.getItem('logCron') === cron._id) { if (localStorage.getItem('logCron') === String(cron.id)) {
const log = data.data as string; const log = data.data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( setExecuting(
@@ -61,7 +65,7 @@ const CronLogModal = ({
<Countdown <Countdown
className="inline-countdown" className="inline-countdown"
format="ss" format="ss"
value={Date.now() + 1000 * 10} value={Date.now() + 1000 * 30}
/> />
</span> </span>
@@ -70,7 +74,7 @@ const CronLogModal = ({
}); });
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 10000); }, 30000);
} }
} }
}) })
@@ -90,17 +94,23 @@ const CronLogModal = ({
return ( return (
<> <>
{(executing || loading) && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && <CheckCircleOutlined />} {!executing && !loading && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '} <span style={{ marginLeft: 5 }}>-{cron && cron.name}</span>{' '}
</> </>
); );
}; };
useEffect(() => { useEffect(() => {
if (cron) { if (cron && cron.id && visible) {
getCronLog(true); getCronLog(true);
} }
}, [cron]); }, [cron, visible]);
useEffect(() => {
if (data) {
setValue(data);
}
}, [data]);
useEffect(() => { useEffect(() => {
setIsPhone(document.body.clientWidth < 768); setIsPhone(document.body.clientWidth < 768);
+92 -6
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd'; 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';
@@ -20,19 +20,28 @@ const CronModal = ({
setLoading(true); setLoading(true);
const method = cron ? 'put' : 'post'; const method = cron ? 'put' : 'post';
const payload = { ...values }; const payload = { ...values };
if (cron) { if (typeof payload.labels === 'string') {
payload._id = cron._id; payload.labels = values.labels.split(/,|/);
} else if (!payload.labels) {
payload.labels = [];
} }
if (cron) {
payload.id = cron.id;
}
try {
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成功');
} else { } else {
message.error(data); message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data); handleCancel(data);
} catch (error: any) {
setLoading(false);
}
}; };
useEffect(() => { useEffect(() => {
@@ -41,7 +50,7 @@ const CronModal = ({
return ( return (
<Modal <Modal
title={cron ? '编辑定时' : '新建定时'} title={cron ? '编辑任务' : '新建任务'}
visible={visible} visible={visible}
forceRender forceRender
onOk={() => { onOk={() => {
@@ -95,9 +104,86 @@ const CronModal = ({
> >
<Input placeholder="秒(可选) 分 时 天 月 周" /> <Input placeholder="秒(可选) 分 时 天 月 周" />
</Form.Item> </Form.Item>
<Form.Item name="labels" label="标签">
<Input placeholder="请输入任务标签" />
</Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
}; };
export default CronModal; const CronLabelModal = ({
ids,
handleCancel,
visible,
}: {
ids: Array<string>;
visible: boolean;
handleCancel: (needUpdate?: boolean) => void;
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const update = async (action: 'delete' | 'post') => {
form
.validateFields()
.then(async (values) => {
if (typeof values.labels === 'string') {
values.labels = values.labels.split(/,|/);
}
setLoading(true);
const payload = { ids, labels: values.labels };
const { code, data } = await request[action](
`${config.apiPrefix}crons/labels`,
{
data: payload,
},
);
if (code === 200) {
message.success(
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
} else {
message.error(data);
}
setLoading(false);
handleCancel(true);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
};
useEffect(() => {
form.resetFields();
}, [ids, visible]);
const buttons = [
<Button onClick={() => handleCancel(false)}></Button>,
<Button type="primary" danger onClick={() => update('delete')}>
</Button>,
<Button type="primary" onClick={() => update('post')}>
</Button>,
];
return (
<Modal
title="批量修改标签"
visible={visible}
footer={buttons}
forceRender
onCancel={() => handleCancel(false)}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="form_in_label_modal">
<Form.Item name="labels" label="标签">
<Input placeholder="请输入任务标签" />
</Form.Item>
</Form>
</Modal>
);
};
export { CronModal as default, CronLabelModal };
+36 -15
View File
@@ -82,13 +82,34 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
); );
}, },
}, },
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
align: 'center' as const,
},
{ {
title: '创建时间', title: '创建时间',
key: 'created', key: 'timestamp',
dataIndex: 'created', dataIndex: 'timestamp',
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return <span>{new Date(record.created).toLocaleString()}</span>; const language = navigator.language || navigator.languages[0];
const time = record.createdAt || record.timestamp;
const date = new Date(time)
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
return (
<Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span>
</Tooltip>
);
}, },
}, },
{ {
@@ -175,7 +196,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] }) .delete(`${config.apiPrefix}dependencies`, { data: [record.id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
handleDependence(data.data[0]); handleDependence(data.data[0]);
@@ -205,7 +226,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}dependencies/reinstall`, { .put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record._id], data: [record.id],
}) })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
@@ -231,7 +252,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
if (Array.isArray(dependence)) { if (Array.isArray(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);
result.splice(index, 1, { result.splice(index, 1, {
...dependence, ...dependence,
}); });
@@ -278,9 +299,9 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const getDependenceDetail = (dependence: any) => { const getDependenceDetail = (dependence: any) => {
request request
.get(`${config.apiPrefix}dependencies/${dependence._id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then((data: any) => {
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, { result.splice(index, 1, {
...dependence, ...dependence,
@@ -307,7 +328,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
useEffect(() => { useEffect(() => {
if (logDependence) { if (logDependence) {
localStorage.setItem('logDependence', logDependence._id); localStorage.setItem('logDependence', logDependence.id);
setIsLogModalVisible(true); setIsLogModalVisible(true);
} }
}, [logDependence]); }, [logDependence]);
@@ -328,7 +349,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
} }
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]);
result.splice(index, 1, { result.splice(index, 1, {
...result[index], ...result[index],
status, status,
@@ -340,7 +361,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
setTimeout(() => { setTimeout(() => {
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]);
_result.splice(index, 1); _result.splice(index, 1);
} }
setValue(_result); setValue(_result);
@@ -372,7 +393,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 768, y: tableScrollHeight }} scroll={{ x: 768, y: tableScrollHeight }}
loading={loading} loading={loading}
@@ -398,7 +419,7 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addDependence()}> <Button key="2" type="primary" onClick={() => addDependence()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -432,11 +453,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
handleCancel={(needRemove?: boolean) => { handleCancel={(needRemove?: boolean) => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
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];
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);
} }
}} }}
+5 -5
View File
@@ -46,10 +46,10 @@ const DependenceLogModal = ({
const getDependenceLog = () => { const getDependenceLog = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}dependencies/${dependence._id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then((data: any) => {
if (localStorage.getItem('logDependence') === dependence._id) { if (localStorage.getItem('logDependence') === String(dependence.id)) {
const log = (data.data.log || []).join('\n') as string; const log = (data.data.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('结束时间')); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败')); setIsRemoveFailed(log.includes('删除失败'));
@@ -64,7 +64,7 @@ const DependenceLogModal = ({
setRemoveLoading(true); setRemoveLoading(true);
request request
.delete(`${config.apiPrefix}dependencies/force`, { .delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence._id], data: [dependence.id],
}) })
.then((data: any) => { .then((data: any) => {
cancel(true); cancel(true);
@@ -99,7 +99,7 @@ const DependenceLogModal = ({
setExecuting(false); setExecuting(false);
setIsRemoveFailed(message.includes('删除失败')); setIsRemoveFailed(message.includes('删除失败'));
} }
setValue(`${value} \n ${message}`); setValue(`${value}${message}`);
}, [socketMessage]); }, [socketMessage]);
useEffect(() => { useEffect(() => {
+4 -3
View File
@@ -26,7 +26,7 @@ const DependenceModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const { name, split, type } = values; const { name, split, type, remark } = values;
const method = dependence ? 'put' : 'post'; const method = dependence ? 'put' : 'post';
let payload; let payload;
if (!dependence) { if (!dependence) {
@@ -36,13 +36,14 @@ const DependenceModal = ({
return { return {
name: x, name: x,
type, type,
remark,
}; };
}); });
} else { } else {
payload = [{ name, type }]; payload = [{ name, type, remark }];
} }
} else { } else {
payload = { ...values, _id: dependence._id }; payload = { ...values, id: dependence.id };
} }
try { try {
const { code, data } = await request[method]( const { code, data } = await request[method](
+1
View File
@@ -13,6 +13,7 @@
} }
.diff-switch-file { .diff-switch-file {
min-width: 768px;
.ant-form-item { .ant-form-item {
margin-bottom: 8px; margin-bottom: 8px;
} }
+37 -21
View File
@@ -27,7 +27,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index'; import { getTableScroll } from '@/utils/index';
const { Text } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status { enum Status {
@@ -101,7 +101,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
{ {
title: '序号', title: '序号',
align: 'center' as const, align: 'center' as const,
width: 50, width: 60,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
}, },
@@ -118,15 +118,19 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataIndex: 'value', dataIndex: 'value',
key: 'value', key: 'value',
align: 'center' as const, align: 'center' as const,
width: '44%', width: '35%',
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tooltip placement="topLeft" title={text}> <Paragraph
<span>{text}</span> style={{
</Tooltip> wordBreak: 'break-all',
marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
{text}
</Paragraph>
); );
}, },
}, },
@@ -141,19 +145,31 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
dataIndex: 'timestamp', dataIndex: 'timestamp',
key: 'timestamp', key: 'timestamp',
align: 'center' as const, align: 'center' as const,
width: 164, width: 165,
ellipsis: { ellipsis: {
showTitle: false, showTitle: false,
}, },
sorter: {
compare: (a: any, b: any) => {
const updatedAtA = new Date(a.updatedAt || a.timestamp).getTime();
const updatedAtB = new Date(b.updatedAt || b.timestamp).getTime();
return updatedAtA - updatedAtB;
},
},
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
const date = new Date(text) const time = record.updatedAt || record.timestamp;
const date = new Date(time)
.toLocaleString(language, { .toLocaleString(language, {
hour12: false, hour12: false,
}) })
.replace(' 24:', ' 00:') .replace(' 24:', ' 00:');
return ( return (
<Tooltip placement="topLeft" title={date}> <Tooltip
placement="topLeft"
title={date}
trigger={['hover', 'click']}
>
<span>{date}</span> <span>{date}</span>
</Tooltip> </Tooltip>
); );
@@ -189,7 +205,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 100, width: 120,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
@@ -262,7 +278,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
record.status === Status. ? 'enable' : 'disable' record.status === Status. ? 'enable' : 'disable'
}`, }`,
{ {
data: [record._id], data: [record.id],
}, },
) )
.then((data: any) => { .then((data: any) => {
@@ -313,7 +329,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: [record._id] }) .delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
message.success('删除成功'); message.success('删除成功');
@@ -343,7 +359,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
const handleEnv = (env: any) => { const handleEnv = (env: any) => {
const result = [...value]; const result = [...value];
const index = value.findIndex((x) => x._id === env._id); const index = value.findIndex((x) => x.id === env.id);
if (index === -1) { if (index === -1) {
env = Array.isArray(env) ? env : [env]; env = Array.isArray(env) ? env : [env];
result.push(...env); result.push(...env);
@@ -368,7 +384,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
} }
const dragRow = value[dragIndex]; const dragRow = value[dragIndex];
request request
.put(`${config.apiPrefix}envs/${dragRow._id}/move`, { .put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex }, data: { fromIndex: dragIndex, toIndex: hoverIndex },
}) })
.then((data: any) => { .then((data: any) => {
@@ -477,7 +493,7 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Button key="2" type="primary" onClick={() => addEnv()}> <Button key="2" type="primary" onClick={() => addEnv()}>
</Button>, </Button>,
]} ]}
header={{ header={{
@@ -526,9 +542,9 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
dataSource={value} dataSource={value}
rowKey="_id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 768, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record: any, index: number) => { onRow={(record: any, index: number) => {
+7 -3
View File
@@ -34,18 +34,22 @@ const EnvModal = ({
payload = [{ value, name, remarks }]; payload = [{ value, name, remarks }];
} }
} else { } else {
payload = { ...values, _id: env._id }; payload = { ...values, id: env.id };
} }
try {
const { code, data } = await request[method](`${config.apiPrefix}envs`, { const { code, data } = await request[method](`${config.apiPrefix}envs`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(env ? '更新变量成功' : '添加变量成功'); message.success(env ? '更新变量成功' : '新建变量成功');
} else { } else {
message.error(data); message.error(data);
} }
setLoading(false); setLoading(false);
handleCancel(data); handleCancel(data);
} catch (error: any) {
setLoading(false);
}
}; };
useEffect(() => { useEffect(() => {
@@ -77,7 +81,7 @@ const EnvModal = ({
rules={[ rules={[
{ required: true, message: '请输入环境变量名称', whitespace: true }, { required: true, message: '请输入环境变量名称', whitespace: true },
{ {
pattern: /^[a-zA-Z_][0-9a-zA-Z_]+$/, pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
message: '只能输入字母数字下划线,且不能以数字开头', message: '只能输入字母数字下划线,且不能以数字开头',
}, },
]} ]}
+12
View File
@@ -0,0 +1,12 @@
.error-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
.react-terminal-wrapper {
max-width: 90%;
height: calc(100vh - 80px);
overflow-y: auto;
}
}
+59
View File
@@ -0,0 +1,59 @@
import React, { useState, useEffect } from 'react';
import config from '@/utils/config';
import { request } from '@/utils/http';
import Terminal, { ColorMode, LineType } from '../../components/terminal';
import { PageLoading } from '@ant-design/pro-layout';
import { history } from 'umi';
import Ansi from 'ansi-to-react';
import './index.less';
const Error = ({ user, theme }: any) => {
const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志');
const getLog = () => {
setLoading(true);
request
.get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => {
setData(data.data);
})
.finally(() => setLoading(false));
};
useEffect(() => {
if (user) {
history.push('/crontab');
}
}, [user]);
useEffect(() => {
getLog();
}, []);
return (
<div className="error-wrapper">
{loading ? (
<PageLoading />
) : (
<Terminal
name="服务错误"
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
lineData={[
{ type: LineType.Input, value: 'pm2 logs panel' },
{
type: LineType.Output,
value: (
<pre>
<Ansi>{data}</Ansi>
</pre>
),
},
]}
/>
)}
</div>
);
};
export default Error;
+12 -3
View File
@@ -35,7 +35,7 @@ const Initialization = () => {
const submitAccountSetting = (values: any) => { const submitAccountSetting = (values: any) => {
setLoading(true); setLoading(true);
request request
.put(`${config.apiPrefix}init/user`, { .put(`${config.apiPrefix}user/init`, {
data: { data: {
username: values.username, username: values.username,
password: values.password, password: values.password,
@@ -52,8 +52,9 @@ const Initialization = () => {
}; };
const submitNotification = (values: any) => { const submitNotification = (values: any) => {
setLoading(true);
request request
.put(`${config.apiPrefix}init/notification`, { .put(`${config.apiPrefix}user/notification/init`, {
data: { data: {
...values, ...values,
}, },
@@ -73,6 +74,10 @@ const Initialization = () => {
setFields(_fields || []); setFields(_fields || []);
}; };
useEffect(() => {
localStorage.removeItem(config.authKey);
}, []);
const steps = [ const steps = [
{ {
title: '欢迎使用', title: '欢迎使用',
@@ -225,7 +230,11 @@ const Initialization = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.top}> <div className={styles.top}>
<div className={styles.header}> <div className={styles.header}>
<img alt="logo" className={styles.logo} src="/images/qinglong.png" /> <img
alt="logo"
className={styles.logo}
src="https://qn.whyour.cn/logo.png"
/>
<span className={styles.title}></span> <span className={styles.title}></span>
</div> </div>
</div> </div>
+7 -1
View File
@@ -13,7 +13,6 @@
&-scroller { &-scroller {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-right: 1px dashed #f0f0f0;
padding-top: 6px; padding-top: 6px;
} }
} }
@@ -22,3 +21,10 @@
display: flex; display: flex;
position: relative; position: relative;
} }
:global {
.Pane.vertical.Pane1 {
padding-right: 5px;
border-right: 1px dashed #f0f0f0;
}
}
+29 -7
View File
@@ -1,12 +1,11 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react'; import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input } from 'antd'; import { TreeSelect, Tree, Input, Empty } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2'; import { Controlled as CodeMirror } from 'react-codemirror2';
import { useCtx, useTheme } from '@/utils/hooks';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
@@ -16,7 +15,6 @@ function getFilterData(keyword: string, data: any) {
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
expandedKeys.push(...item.children.map((x: any) => x.key));
} else { } else {
const children: any[] = []; const children: any[] = [];
(item.children || []).forEach((subItem: any) => { (item.children || []).forEach((subItem: any) => {
@@ -29,7 +27,7 @@ function getFilterData(keyword: string, data: any) {
...item, ...item,
children, children,
}); });
expandedKeys.push(...children.map((x) => x.key)); expandedKeys.push(item.key);
} }
} }
}); });
@@ -47,6 +45,7 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getLogs = () => { const getLogs = () => {
setLoading(true); setLoading(true);
@@ -100,8 +99,12 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data); const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree); setFilterData(tree);
setExpandedKeys(expandedKeys);
}, },
[data, setFilterData], [data, setFilterData],
); );
@@ -125,9 +128,8 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data} treeData={data}
placeholder="请选择日志文件" placeholder="请选择日志"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
] ]
@@ -140,9 +142,13 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
{!isPhone && ( {!isPhone && (
<SplitPane split="vertical" size={200} maxSize={-100}> <SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
{data.length > 0 ? (
<>
<Input.Search <Input.Search
className={styles['left-tree-search']} className={styles['left-tree-search']}
onChange={onSearch} onChange={onSearch}
placeholder="请输入日志名"
allowClear
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
@@ -155,6 +161,22 @@ const Log = ({ headerStyle, isPhone, theme }: any) => {
onSelect={onTreeSelect} onSelect={onTreeSelect}
></Tree> ></Tree>
</div> </div>
</>
) : (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Empty
description="暂无日志"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
)}
</div> </div>
<Editor <Editor
language="shell" language="shell"
+1
View File
@@ -32,6 +32,7 @@
.logo { .logo {
width: 48px; width: 48px;
height: 48px;
display: block; display: block;
margin-bottom: 24px; margin-bottom: 24px;
} }
+20 -5
View File
@@ -31,7 +31,7 @@ const Login = () => {
setTwoFactor(false); setTwoFactor(false);
setWaitTime(null); setWaitTime(null);
request request
.post(`${config.apiPrefix}login`, { .post(`${config.apiPrefix}user/login`, {
data: { data: {
username: values.username, username: values.username,
password: values.password, password: values.password,
@@ -113,6 +113,14 @@ const Login = () => {
} }
}; };
const codeInputChange = (e: React.ChangeEvent) => {
const { value } = e.target as any;
const regx = /^[0-9]{6}$/;
if (regx.test(value)) {
completeTowFactor({ code: value });
}
};
useEffect(() => { useEffect(() => {
const isAuth = localStorage.getItem(config.authKey); const isAuth = localStorage.getItem(config.authKey);
if (isAuth) { if (isAuth) {
@@ -124,7 +132,11 @@ const Login = () => {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.top}> <div className={styles.top}>
<div className={styles.header}> <div className={styles.header}>
<img alt="logo" className={styles.logo} src="/images/qinglong.png" /> <img
alt="logo"
className={styles.logo}
src="https://qn.whyour.cn/logo.png"
/>
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
</span> </span>
@@ -140,12 +152,15 @@ const Login = () => {
{ {
pattern: /^[0-9]{6}$/, pattern: /^[0-9]{6}$/,
message: '验证码为6位数字', message: '验证码为6位数字',
validateTrigger: 'onBlur',
}, },
]} ]}
hasFeedback
> >
<Input placeholder="6位数字" autoFocus autoComplete="off" /> <Input
placeholder="6位数字"
onChange={codeInputChange}
autoFocus
autoComplete="off"
/>
</FormItem> </FormItem>
<Button <Button
type="primary" type="primary"
+101 -22
View File
@@ -24,72 +24,134 @@ const prefixMap: any = {
const EditModal = ({ const EditModal = ({
treeData, treeData,
currentFile, currentNode,
content, content,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
treeData?: any; treeData?: any;
currentFile?: string;
content?: string; content?: string;
visible: boolean; visible: boolean;
socketMessage: any;
currentNode: any;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const [language, setLanguage] = useState<string>('javascript'); const [language, setLanguage] = useState<string>('javascript');
const [fileName, setFileName] = useState<string>(''); const [cNode, setCNode] = useState<any>();
const [selectedKey, setSelectedKey] = useState<string>('');
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [log, setLog] = useState<string>(''); const [log, setLog] = useState<string>('');
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === selectedKey || !value) {
return;
}
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setFileName(value); setCNode(node);
setLanguage(newMode); setLanguage(newMode);
getDetail(node); getDetail(node);
setSelectedKey(node.key);
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request.get(`${config.apiPrefix}scripts/${node.value}`).then((data) => { request
.get(`${config.apiPrefix}scripts/${node.value}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data); setValue(data.data);
}); });
}; };
const run = () => {}; const run = () => {
setLog('');
request
.put(`${config.apiPrefix}scripts/run`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(true);
});
};
const stop = () => {
if (!cNode || !cNode.value) {
return;
}
request
.put(`${config.apiPrefix}scripts/stop`, {
data: {
filename: cNode.value,
path: cNode.parent || '',
},
})
.then((data) => {
setIsRunning(false);
});
};
useEffect(() => { useEffect(() => {
if (currentFile) { if (!socketMessage) {
setFileName(currentFile); return;
setValue(content as string);
} }
}, [currentFile, content]);
let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (_message === '执行结束') {
_message = '';
setTimeout(() => {
setIsRunning(false);
}, 300);
}
if (log) {
_message = `\n${_message}`;
}
setLog(`${log}${_message}`);
}, [socketMessage]);
useEffect(() => {
if (currentNode) {
setCNode(currentNode);
setValue(content as string);
setSelectedKey(currentNode.key);
}
}, [content, currentNode]);
return ( return (
<Drawer <Drawer
className="edit-modal" className="edit-modal"
closable={false}
title={ title={
<> <>
<span style={{ marginRight: 8 }}>{fileName}</span>
<TreeSelect <TreeSelect
style={{ marginRight: 8, width: 120 }} style={{ marginRight: 8, width: 150 }}
value={currentFile} value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={treeData} treeData={treeData}
placeholder="请选择脚本文件" placeholder="请选择脚本文件"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/> />
<Select <Select
value={language} value={language}
style={{ width: 120, marginRight: 8 }} style={{ width: 110, marginRight: 8 }}
onChange={(e) => { onChange={(e) => {
setLanguage(e); setLanguage(e);
}} }}
@@ -99,8 +161,12 @@ const EditModal = ({
<Option value="shell">shell</Option> <Option value="shell">shell</Option>
<Option value="python">python</Option> <Option value="python">python</Option>
</Select> </Select>
<Button type="primary" style={{ marginRight: 8 }} onClick={run}> <Button
type="primary"
style={{ marginRight: 8 }}
onClick={isRunning ? stop : run}
>
{isRunning ? '停止' : '运行'}
</Button> </Button>
<Button <Button
type="primary" type="primary"
@@ -129,6 +195,16 @@ const EditModal = ({
> >
</Button> </Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => {
stop();
handleCancel();
}}
>
退
</Button>
</> </>
} }
width={'100%'} width={'100%'}
@@ -136,7 +212,12 @@ const EditModal = ({
onClose={cancel} onClose={cancel}
visible={visible} visible={visible}
> >
<SplitPane split="vertical" minSize={200} defaultSize="50%"> <SplitPane
split="vertical"
minSize={200}
defaultSize="50%"
style={{ height: 'calc(100vh - 55px)' }}
>
<Editor <Editor
language={language} language={language}
value={value} value={value}
@@ -151,9 +232,7 @@ const EditModal = ({
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
<div> <pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
<pre>{log}</pre>
</div>
</SplitPane> </SplitPane>
<SaveModal <SaveModal
visible={saveModalVisible} visible={saveModalVisible}
@@ -164,7 +243,7 @@ const EditModal = ({
content: content:
editorRef.current && editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'), editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: fileName, filename: cNode?.value,
}} }}
/> />
<SettingModal <SettingModal
+32 -4
View File
@@ -1,28 +1,42 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd'; import { Modal, message, Input, Form, Select } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
const { Option } = Select;
const EditScriptNameModal = ({ const EditScriptNameModal = ({
handleCancel, handleCancel,
treeData,
visible, visible,
}: { }: {
visible: boolean; visible: boolean;
handleCancel: (file?: { filename: string }) => void; treeData: any[];
handleCancel: (file?: {
filename: string;
path: string;
key: string;
}) => void;
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [dirs, setDirs] = useState<any[]>([]);
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
values.path = values.path || '';
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: { filename: values.filename, content: '' }, data: { filename: values.filename, path: values.path, content: '' },
}) })
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel({ filename: values.filename }); handleCancel({
filename: values.filename,
path: values.path,
key: `${values.path}-${values.filename}`,
});
} else { } else {
message.error(data); message.error(data);
} }
@@ -33,6 +47,8 @@ const EditScriptNameModal = ({
useEffect(() => { useEffect(() => {
form.resetFields(); form.resetFields();
const originDirs = treeData.filter((x) => x.disabled);
setDirs([{ key: '' }, ...originDirs]);
}, [visible]); }, [visible]);
return ( return (
@@ -56,10 +72,22 @@ const EditScriptNameModal = ({
<Form form={form} layout="vertical" name="edit_name_modal"> <Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item <Form.Item
name="filename" name="filename"
label="文件名"
rules={[{ required: true, message: '请输入文件名' }]} rules={[{ required: true, message: '请输入文件名' }]}
> >
<Input placeholder="请输入文件名" /> <Input placeholder="请输入文件名" />
</Form.Item> </Form.Item>
<Form.Item
label="父目录"
name="path"
initialValue={dirs && dirs.length > 0 ? dirs[0].key : ''}
>
<Select placeholder="请选择父目录">
{dirs.map((x) => (
<Option value={x.key}>{x.key || '根'}</Option>
))}
</Select>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
+7 -1
View File
@@ -13,7 +13,6 @@
&-scroller { &-scroller {
flex: 1; flex: 1;
overflow: auto; overflow: auto;
border-right: 1px dashed #f0f0f0;
padding-top: 6px; padding-top: 6px;
} }
} }
@@ -22,3 +21,10 @@
display: flex; display: flex;
position: relative; position: relative;
} }
:global {
.Pane.vertical.Pane1 {
padding-right: 5px;
border-right: 1px dashed #f0f0f0;
}
}
+139 -22
View File
@@ -10,6 +10,7 @@ import {
Tooltip, Tooltip,
Dropdown, Dropdown,
Menu, Menu,
Empty,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -31,20 +32,37 @@ import {
UserOutlined, UserOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import EditScriptNameModal from './editNameModal'; import EditScriptNameModal from './editNameModal';
import debounce from 'lodash/debounce';
import { history } from 'umi';
const { Text } = Typography; const { Text } = Typography;
function getFilterData(keyword: string, data: any) { function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) { if (keyword) {
const tree: any = []; const tree: any = [];
data.forEach((item: any) => { data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) { if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item); tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
} }
}); });
return { tree }; if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
} }
return { tree: data }; }
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
} }
const LangMap: any = { const LangMap: any = {
@@ -54,7 +72,7 @@ const LangMap: any = {
'.ts': 'typescript', '.ts': 'typescript',
}; };
const Script = ({ headerStyle, isPhone, theme }: any) => { const Script = ({ headerStyle, isPhone, theme, socketMessage }: any) => {
const [title, setTitle] = useState('请选择脚本文件'); const [title, setTitle] = useState('请选择脚本文件');
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<any>();
@@ -70,6 +88,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false); const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [currentNode, setCurrentNode] = useState<any>(); const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => { const getScripts = () => {
setLoading(true); setLoading(true);
@@ -78,6 +97,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
.then((data) => { .then((data) => {
setData(data.data); setData(data.data);
setFilterData(data.data); setFilterData(data.data);
initGetScript();
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -90,6 +110,23 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
}); });
}; };
const initGetScript = () => {
const { p, s } = history.location.query as any;
if (s) {
const vkey = `${p}/${s}`;
const obj = {
node: {
title: s,
value: s,
key: p ? vkey : s,
parent: p,
},
};
setExpandedKeys([p]);
onTreeSelect([vkey], obj);
}
};
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
@@ -97,12 +134,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
setValue('加载中...'); setValue('加载中...');
const newMode = value ? LangMap[value.slice(-3)] : ''; const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(value); setSelect(node.key);
setTitle(node.parent || node.value); setTitle(node.parent || node.value);
setCurrentNode(node); setCurrentNode(node);
getDetail(node); getDetail(node);
}; };
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback( const onTreeSelect = useCallback(
(keys: Key[], e: any) => { (keys: Key[], e: any) => {
const content = editorRef.current const content = editorRef.current
@@ -131,13 +172,24 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
setSearchValue(keyword); debounceSearch(keyword);
const { tree } = getFilterData(keyword.toLocaleLowerCase(), data);
setFilterData(tree);
}, },
[data, setFilterData], [data, setFilterData],
); );
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300),
[data, setFilterData],
);
const editFile = () => { const editFile = () => {
setTimeout(() => { setTimeout(() => {
setIsEditing(true); setIsEditing(true);
@@ -147,7 +199,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const cancelEdit = () => { const cancelEdit = () => {
setIsEditing(false); setIsEditing(false);
setValue('加载中...'); setValue('加载中...');
getDetail({ value: select }); getDetail(currentNode);
}; };
const saveFile = () => { const saveFile = () => {
@@ -157,7 +209,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<> <>
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{select} {currentNode.value}
</Text>{' '} </Text>{' '}
</> </>
@@ -170,7 +222,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request request
.put(`${config.apiPrefix}scripts`, { .put(`${config.apiPrefix}scripts`, {
data: { data: {
filename: select, filename: currentNode.value,
path: currentNode.parent || '', path: currentNode.parent || '',
content, content,
}, },
@@ -210,15 +262,30 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request request
.delete(`${config.apiPrefix}scripts`, { .delete(`${config.apiPrefix}scripts`, {
data: { data: {
filename: select, filename: currentNode.value,
path: currentNode.parent || '',
}, },
}) })
.then((_data: any) => { .then((_data: any) => {
if (_data.code === 200) { if (_data.code === 200) {
message.success(`删除成功`); message.success(`删除成功`);
let newData = [...data]; let newData = [...data];
const index = newData.findIndex((x) => x.value === select); if (currentNode.parent) {
const parentNodeIndex = newData.findIndex(
(x) => x.key === currentNode.parent,
);
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
newData.splice(index, 1); newData.splice(index, 1);
}
setData(newData); setData(newData);
} else { } else {
message.error(_data); message.error(_data);
@@ -236,12 +303,27 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
}; };
const addFileModalClose = ( const addFileModalClose = (
{ filename }: { filename: string } = { filename: '' }, { filename, path, key }: { filename: string; path: string; key: string } = {
filename: '',
path: '',
key: '',
},
) => { ) => {
if (filename) { if (filename) {
const newData = [...data]; const newData = [...data];
const _file = { title: filename, key: filename, value: filename }; const _file = { title: filename, key, value: filename, parent: path };
if (path) {
const parentNodeIndex = newData.findIndex((x) => x.key === path);
const parentNode = newData[parentNodeIndex];
if (parentNode.children && parentNode.children.length > 0) {
parentNode.children.unshift(_file);
} else {
parentNode.children = [_file];
}
newData.splice(parentNodeIndex, 1, { ...parentNode });
} else {
newData.unshift(_file); newData.unshift(_file);
}
setData(newData); setData(newData);
onSelect(_file.value, _file); onSelect(_file.value, _file);
setIsEditing(true); setIsEditing(true);
@@ -253,7 +335,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
request request
.post(`${config.apiPrefix}scripts/download`, { .post(`${config.apiPrefix}scripts/download`, {
data: { data: {
filename: select, filename: currentNode.value,
}, },
}) })
.then((_data: any) => { .then((_data: any) => {
@@ -261,7 +343,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = select; a.download = currentNode.value;
document.documentElement.appendChild(a); document.documentElement.appendChild(a);
a.click(); a.click();
document.documentElement.removeChild(a); document.documentElement.removeChild(a);
@@ -297,12 +379,22 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
) : ( ) : (
<Menu> <Menu>
<Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}> <Menu.Item key="add" icon={<PlusOutlined />} onClick={addFile}>
</Menu.Item> </Menu.Item>
<Menu.Item key="edit" icon={<EditOutlined />} onClick={editFile}> <Menu.Item
key="edit"
icon={<EditOutlined />}
onClick={editFile}
disabled={!select}
>
</Menu.Item> </Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />} onClick={deleteFile}> <Menu.Item
key="delete"
icon={<DeleteOutlined />}
onClick={deleteFile}
disabled={!select}
>
</Menu.Item> </Menu.Item>
</Menu> </Menu>
@@ -321,9 +413,8 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data} treeData={data}
placeholder="请选择脚本文件" placeholder="请选择脚本"
showSearch showSearch
key="value"
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}> <Dropdown overlay={menu} trigger={['click']}>
@@ -349,6 +440,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
</Tooltip>, </Tooltip>,
<Tooltip title="编辑"> <Tooltip title="编辑">
<Button <Button
disabled={!select}
type="primary" type="primary"
onClick={editFile} onClick={editFile}
icon={<EditOutlined />} icon={<EditOutlined />}
@@ -357,6 +449,7 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<Tooltip title="删除"> <Tooltip title="删除">
<Button <Button
type="primary" type="primary"
disabled={!select}
onClick={deleteFile} onClick={deleteFile}
icon={<DeleteOutlined />} icon={<DeleteOutlined />}
/> />
@@ -379,9 +472,13 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
{!isPhone && ( {!isPhone && (
<SplitPane split="vertical" size={200} maxSize={-100}> <SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
{data.length > 0 ? (
<>
<Input.Search <Input.Search
className={styles['left-tree-search']} className={styles['left-tree-search']}
onChange={onSearch} onChange={onSearch}
placeholder="请输入脚本名"
allowClear
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
@@ -390,10 +487,28 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
showIcon={true} showIcon={true}
height={height} height={height}
selectedKeys={[select]} selectedKeys={[select]}
expandedKeys={expandedKeys}
onExpand={onExpand}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
></Tree> ></Tree>
</div> </div>
</>
) : (
<div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100%',
}}
>
<Empty
description="暂无脚本"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</div>
)}
</div> </div>
<Editor <Editor
language={mode} language={mode}
@@ -432,14 +547,16 @@ const Script = ({ headerStyle, isPhone, theme }: any) => {
<EditModal <EditModal
visible={isLogModalVisible} visible={isLogModalVisible}
treeData={data} treeData={data}
currentFile={select} currentNode={currentNode}
content={value} content={value}
socketMessage={socketMessage}
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
/> />
<EditScriptNameModal <EditScriptNameModal
visible={isAddFileModalVisible} visible={isAddFileModalVisible}
treeData={data}
handleCancel={addFileModalClose} handleCancel={addFileModalClose}
/> />
</div> </div>
+49
View File
@@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message } from 'antd';
import styles from './index.less';
const { Link } = Typography;
const About = () => {
return (
<div className={styles.container}>
<img
alt="logo"
style={{ width: 140, marginRight: 20 }}
src="https://qn.whyour.cn/logo.png"
/>
<div className={styles.right}>
<span className={styles.title}></span>
<span className={styles.desc}>
python3javaScriptshelltypescript A timed
task management panel that supports typescript, javaScript, python3,
and shell.
</span>
<div>
<Link
href="https://github.com/whyour/qinglong"
target="_blank"
style={{ marginRight: 15 }}
>
Github
</Link>
<Link
href="https://t.me/jiao_long"
target="_blank"
style={{ marginRight: 15 }}
>
Telegram频道
</Link>
<Link
href="https://github.com/whyour/qinglong/issues"
target="_blank"
>
BUG
</Link>
</div>
</div>
</div>
);
};
export default About;
+2 -2
View File
@@ -20,13 +20,13 @@ const AppModal = ({
const method = app ? 'put' : 'post'; const method = app ? 'put' : 'post';
const payload = { ...values }; const payload = { ...values };
if (app) { if (app) {
payload._id = app._id; payload.id = app.id;
} }
const { code, data } = await request[method](`${config.apiPrefix}apps`, { const { code, data } = await request[method](`${config.apiPrefix}apps`, {
data: payload, data: payload,
}); });
if (code === 200) { if (code === 200) {
message.success(app ? '更新应用成功' : '添加应用成功'); message.success(app ? '更新应用成功' : '新建应用成功');
} else { } else {
message.error(data); message.error(data);
} }
+31 -5
View File
@@ -24,7 +24,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
message.success('已经是最新版了!'); showForceUpdateModal();
} }
} else { } else {
message.error(data); message.error(data);
@@ -39,6 +39,32 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
}; };
const showForceUpdateModal = () => {
Modal.confirm({
width: 500,
title: '更新',
content: (
<>
<div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version}
</div>
</>
),
okText: '确认',
cancelText: '强制更新',
onCancel() {
showUpdatingModal();
request
.put(`${config.apiPrefix}system/update`)
.then((_data: any) => {})
.catch((error: any) => {
console.log(error);
});
},
});
};
const showConfirmUpdateModal = (data: any) => { const showConfirmUpdateModal = (data: any) => {
const { lastVersion, lastLog } = data; const { lastVersion, lastLog } = data;
Modal.confirm({ Modal.confirm({
@@ -113,7 +139,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
return; return;
} }
const newMessage = `${value}\n${_message}`; const newMessage = `${value}${_message}`;
modalRef.current.update({ modalRef.current.update({
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <div style={{ height: '60vh', overflowY: 'auto' }}>
@@ -146,16 +172,16 @@ const CheckUpdate = ({ socketMessage }: any) => {
<Countdown <Countdown
className="inline-countdown" className="inline-countdown"
format="ss" format="ss"
value={Date.now() + 1000 * 10} value={Date.now() + 1000 * 30}
/> />
</span> </span>
), ),
duration: 10, duration: 30,
}); });
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();
}, 10000); }, 30000);
} }
}, [socketMessage]); }, [socketMessage]);

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