Compare commits

..
322 Commits
Author SHA1 Message Date
whyour 99d7eaeff0 更新版本 v2.13.1 2022-05-29 01:50:46 +08:00
whyour c8f5eb7c2e 修复pnpm版本兼容 2022-05-29 01:50:05 +08:00
whyour 109e16e8fb 修复前端列表查询 2022-05-28 21:49:34 +08:00
whyour 555f6e04e8 修复依赖日志查看 2022-05-28 21:10:24 +08:00
whyour bcde0a54bf 修复依赖安装日志 2022-05-28 20:51:25 +08:00
whyour eee7f25bc3 修复订阅日志查看 2022-05-28 20:11:38 +08:00
whyour b10157b09a 修复系统启动执行订阅任务 2022-05-28 19:56:12 +08:00
whyour a09a21fa5d 依赖改为串行安装 2022-05-28 19:41:37 +08:00
whyour ec2918b427 修改订阅日志排版 2022-05-28 19:04:46 +08:00
whyour 57bfc19ecf 修复execSync卡死主进程 2022-05-28 18:55:52 +08:00
whyour aa110d4187 修复任务详情日志访问,修复新建订阅粘贴repo命令 2022-05-28 17:36:41 +08:00
whyour 9e8febfc11 修复七牛sdk参数 2022-05-28 00:11:43 +08:00
whyour 5905c2ce25 更新版本 2.13.0 2022-05-28 00:08:48 +08:00
whyour b03ef0d494 移除sentry上报信息可能出现的token 2022-05-27 23:47:17 +08:00
whyour 585f77ed49 修改workflow 2022-05-26 15:12:45 +08:00
whyour 1c580ea2f9 修复切换订阅interval 2022-05-26 14:41:22 +08:00
whyour b1af6575b9 修改workflow 2022-05-26 00:25:31 +08:00
whyour b24c5fdee1 更新readme 2022-05-26 00:23:25 +08:00
whyour a256ada847 修复定时任务标签默认值 2022-05-26 00:18:04 +08:00
whyour a8cdab629d 修复安装依赖逻辑 2022-05-25 02:04:13 +08:00
whyour 09cca0a920 修改linux依赖安装命令 2022-05-25 01:23:33 +08:00
whyour 6a0aa6aae2 修改node依赖安装命令 2022-05-25 01:20:23 +08:00
whyour 565cc06c27 修改文本 2022-05-25 01:10:04 +08:00
whyour 29caa4b770 新建订阅支持粘贴导入 2022-05-25 01:09:05 +08:00
whyour 953cf4795a 修复日志获取 2022-05-24 10:00:42 +08:00
whyour 31bc8395aa 修复获取脚本列表 2022-05-23 19:22:20 +08:00
whyour eb94b39e40 修复主题切换 2022-05-23 12:03:01 +08:00
whyour c16bbe0f9a 修改订阅before/after执行时机 2022-05-22 22:14:32 +08:00
whyour b60caa60d5 修改新建订阅文件后缀提示 2022-05-22 21:35:02 +08:00
whyour 1ea97e9206 修复错误界面user判断 2022-05-22 21:28:30 +08:00
whyour 32970bca71 修复登录跳转 2022-05-22 18:07:48 +08:00
whyour 1fc9f87eaa 修改antd样式目录 2022-05-22 00:44:13 +08:00
whyour b2f5960097 使用ssr-darkreader代替darkreader 2022-05-21 21:19:44 +08:00
whyour 1cfcc66548 修复根组件刷新异常 2022-05-21 11:51:16 +08:00
whyour 60152a313f 修复spawn监听事件 2022-05-21 00:47:05 +08:00
whyour d50ff20ff8 修改新建订阅字段提示 2022-05-21 00:12:12 +08:00
whyour 3aed5ef955 订阅同步表字段 2022-05-20 11:18:40 +08:00
whyour 61e0e8dbe2 订阅增加before/after 2022-05-20 01:15:45 +08:00
whyour 1ba5d52ee7 修复登录成功获取用户信息 2022-05-19 23:40:19 +08:00
whyour 03bc5bb882 增加初始化订阅任务 2022-05-18 23:30:59 +08:00
whyour 8011b4b793 修改生成ssh配置 2022-05-18 20:10:28 +08:00
whyour deb124ae23 修复删除私钥 2022-05-18 20:06:46 +08:00
whyour a3114e40d1 修复私钥权限 2022-05-18 19:23:32 +08:00
whyour 0b9ecfc3a5 完善拉取私有仓库 2022-05-18 01:14:10 +08:00
whyour c32bb4157b 统一api错误提示 2022-05-17 23:31:29 +08:00
whyour 7f9c9f05b8 修复订阅执行日志输出 2022-05-17 22:27:41 +08:00
whyour 906a074634 修复环境变量PATH 2022-05-17 13:24:19 +08:00
whyour 286ec0e714 修改dockerfile 2022-05-17 12:38:58 +08:00
whyour 0f81414497 action安装pnpm 2022-05-17 12:18:29 +08:00
whyour b451831db2 移除pnpm action缓存 2022-05-17 12:14:44 +08:00
whyour ae13185319 pnpm action缓存 2022-05-17 12:13:26 +08:00
whyour f2bcbaefd2 pnpm action缓存 2022-05-17 12:12:25 +08:00
whyour 96ddcf49c5 pnpm action缓存 2022-05-17 12:10:59 +08:00
whyour 322a4e2729 删除pnpm缓存 2022-05-17 12:06:29 +08:00
whyour 8e48960253 完善拉取公开仓库 2022-05-17 00:42:18 +08:00
whyour 3b419a8e96 兼容antd Menu 2022-05-16 01:47:03 +08:00
whyour bf1685bf79 重构任务执行逻辑 2022-05-15 20:40:29 +08:00
whyour 3ab84428f0 修复编译错误 2022-05-15 15:35:52 +08:00
whyour fdda732c4c 更新新建文件订阅 2022-05-15 15:25:23 +08:00
whyour f46b30498b 完善订阅接口 2022-05-14 21:38:26 +08:00
whyour d7ee2576f2 完善新建订阅页面 2022-05-14 19:21:35 +08:00
whyour d2c03bc298 增加订阅api 2022-05-10 19:54:05 +08:00
whyour 6283e23d29 修复编译错误 2022-05-10 14:42:25 +08:00
whyour dc9923ac13 增加新建订阅 2022-05-10 14:38:06 +08:00
whyour 62ac80257a 修复设置头像 2022-05-09 15:33:18 +08:00
whyour 868452f13c 修复设置头像 2022-05-09 15:32:44 +08:00
whyour 6c368f7ea7 支持更换头像 2022-05-09 15:31:41 +08:00
whyour 732d63d588 增加ssh key操作service 2022-05-08 22:41:56 +08:00
whyour ae40947801 更新readme 2022-05-08 11:33:39 +08:00
whyour 079d4dfa06 修复用户名显示 2022-05-08 10:06:56 +08:00
whyour 67c50bf622 添加订阅管理前端 2022-05-08 09:41:06 +08:00
whyour f1e494b1fc 修复生产环境安装依赖prepare失败 2022-05-03 18:44:00 +08:00
whyour f4f275490e 修复npm prepare命令 2022-05-03 18:25:52 +08:00
whyour baad96106f 修复pnpm全局安装 2022-05-03 18:07:12 +08:00
whyour 15fa7754b5 修复peerDependence 2022-05-03 15:55:39 +08:00
whyour 8f175f8447 修复pnpm依赖 2022-05-03 15:43:15 +08:00
whyour 9c45cfd7ed 修改action依赖 2022-05-03 15:06:28 +08:00
whyour f2426c3f66 修改依赖 2022-05-03 14:59:48 +08:00
whyour 36ae6e460d 测试pnpm版本 2022-05-03 14:47:59 +08:00
whyour 7ae93efe07 测试pnpm版本 2022-05-03 14:44:10 +08:00
whyour 8b97d92d0a 添加依赖rc-tween-one 2022-05-01 14:20:00 +08:00
whyour e5c06335ec 修复pnpm版本 2022-05-01 14:15:29 +08:00
whyour 2bd8e49302 修改定时任务添加标签交互 2022-05-01 13:51:33 +08:00
whyour c0d401e296 修复环境变量转义 2022-04-25 23:55:25 +08:00
whyour e07d2b6639 修复获取最新version文件 2022-04-24 18:43:09 +08:00
whyour 220a87627d 更新版本 v2.12.2 2022-04-24 17:50:03 +08:00
whyour 87bca2ac4e 任务详情增加运行、禁用、置顶操作 2022-04-24 17:45:50 +08:00
whyour c320906149 依赖管理增加直接强制删除 2022-04-23 15:02:54 +08:00
JerryWnandGitHub 27958a1a90 修复双引号转义逻辑 #1331 (#1337) 2022-04-18 22:55:37 +08:00
whyour 82c7011522 修复定时删除日志设置,主持设置大于24天 2022-04-18 22:29:14 +08:00
whyour fa6d57d809 修复shell发送通知 2022-04-09 11:13:08 +08:00
whyour 1060fc7476 修复并发日志目录 2022-04-06 12:18:16 +08:00
whyour 56bc38e9e7 更新readme 2022-04-05 23:24:27 +08:00
whyour 18be65d6fe 更新发布脚本 2022-04-05 23:17:03 +08:00
whyour 3409085195 更新发布脚本 2022-04-05 23:14:47 +08:00
whyour 17e17ac077 增加发布脚本 2022-04-05 23:11:48 +08:00
whyour c655e1da38 修复日志目录判断 2022-04-05 00:28:14 +08:00
whyour 0ea158724f 修复日志目录获取 2022-04-05 00:21:32 +08:00
whyour fb521498ff 修复task日志打印 2022-04-04 23:32:27 +08:00
whyour 399550ccad 修复日志创建目录 2022-04-04 22:40:02 +08:00
whyour c7c30c86f2 修复目录获取 2022-04-04 18:06:21 +08:00
whyour 5f6c0882d6 修改版本说明 2022-04-04 17:28:15 +08:00
whyour 6e05a4f3a5 更新版本号 v2.12.1 2022-04-04 17:24:42 +08:00
whyour ea8aa6a231 修改获取版本链接 2022-04-04 17:17:34 +08:00
whyour ae2570c677 修复shell日志路径截取 2022-04-03 22:31:11 +08:00
whyour c76d952d41 修改yarn命令 2022-04-03 21:49:20 +08:00
whyour cdf9fb24be 修复定时任务获取日志 2022-04-03 20:00:31 +08:00
whyour 2131e16781 删除console 2022-04-03 19:50:44 +08:00
whyour b0c8425639 修复日志路径拼接规则 2022-04-03 19:32:44 +08:00
whyour f0869567de 七牛云改为http访问 2022-04-01 08:29:16 +08:00
whyour be2a19949b 修复获取日志相对目录 2022-03-30 11:34:36 +08:00
whyour 8627cccb58 修改日志路径合并方式 2022-03-30 11:17:11 +08:00
whyour 4a8eaebf17 修复获取任务日志路径 2022-03-30 10:57:43 +08:00
kan3GitandGitHub e7edd0a5a4 tg发送消息改json (#1269)
发现有带&的消息都会漏掉&后面的内容,改为JSON发送
2022-03-26 17:23:54 +08:00
chen310andGitHub 9ccbea0a04 修复PushDeer推送Python版本错误 (#1270)
* 修复PushDeer推送Python版本错误
2022-03-26 17:20:02 +08:00
whyour ab715ef840 修复系统pushDeer通知 2022-03-26 17:17:13 +08:00
whyour 117822fe7c 修改任务最大内存占用200兆 2022-03-26 16:53:03 +08:00
whyour da922678a1 修复定时任务获取日志目录 2022-03-26 00:29:42 +08:00
whyour 133e9e0e1f 更新版本 v2.12.0 2022-03-24 08:47:35 +08:00
whyour 7cc6081d15 修复bot依赖安装目录 2022-03-19 16:44:00 +08:00
whyour 5a24f62a62 更新bot.sh 2022-03-17 23:45:37 +08:00
whyour be597355a7 修复任务详情展示脚本逻辑 2022-03-13 13:21:00 +08:00
whyour 51afcb6c5b 修复任务详情交互样式 2022-03-13 11:09:31 +08:00
whyour 92ebb70eaa 修复定时任务名称标签显示 2022-03-13 00:06:24 +08:00
whyour ab19f30342 修复系统更新时间弹窗 2022-03-13 00:03:58 +08:00
whyour fc9bc7bea2 修复任务列表名称标签提示 2022-03-12 23:57:38 +08:00
whyour 793da29f22 修复定时任务详情日志查看 2022-03-12 23:49:09 +08:00
whyour 5afe4da607 修复退出登录样式 2022-03-12 18:15:42 +08:00
whyour a7a56c03ce 修复退出登录和定时任务搜索 2022-03-12 16:14:18 +08:00
whyour 7639b588d8 修改退出登录样式 2022-03-10 23:58:57 +08:00
whyour 1d536cdd53 修改退出登录样式 2022-03-10 23:53:39 +08:00
whyour 9f08245b6d 修复编辑应用初始值 2022-03-10 22:37:44 +08:00
whyour ff9bf20e57 添加podman安装 2022-03-10 00:29:28 +08:00
whyour b201730100 修复任务详情样式 2022-03-09 23:40:59 +08:00
whyour 6ca098f9e2 修复任务详情手机样式 2022-03-09 22:32:50 +08:00
whyour e0eef05f5c 修复dockerfile 2022-03-09 00:24:36 +08:00
whyour 6398e5ec70 修复dockerfile 2022-03-09 00:17:33 +08:00
whyour 6f93219c44 任务详情脚本增加编辑功能 2022-03-08 00:11:37 +08:00
whyour 9b957ac019 修复任务详情样式 2022-03-07 23:56:15 +08:00
whyour e0d1b127aa 修改更新青龙日志 2022-03-07 23:47:20 +08:00
whyour 64072b3435 修改重置分支shell 2022-03-07 23:40:49 +08:00
whyour f81f7720f0 更新git pull参数 2022-03-07 23:27:16 +08:00
whyour 69fab610e6 修复编译静态资源目录 2022-03-07 23:21:18 +08:00
whyour d693e1f9ea 修改shell根目录获取,移除notify.sh 2022-03-07 23:16:36 +08:00
whyour 2a9ca67355 修复更新青龙命令 2022-03-07 22:33:15 +08:00
whyour 3fb01860d4 修复重置repo地址命令 2022-03-07 22:31:46 +08:00
whyour d92a46d09c 修复更新青龙命令 2022-03-07 22:27:04 +08:00
whyour ec605bfba4 修复shell更新命令 2022-03-06 11:27:48 +08:00
whyour d2f999a5e2 修改定时任务初始化查询条件 2022-03-06 00:07:59 +08:00
whyour 627f541f17 更新版本拷贝2.11.3以前版本数据到data目录 2022-03-06 00:07:08 +08:00
whyour c457dca555 自动更新定时任务2.11.3以前脚本路径 2022-03-06 00:01:03 +08:00
whyour ff3ec7395e 修改任务详情样式 2022-03-05 20:52:21 +08:00
whyour fdaba146ca 移除s390x支持,修改sqlite3包 2022-03-05 14:39:06 +08:00
whyour 31a6d30c23 修改sqlite3包 2022-03-05 13:42:45 +08:00
whyour fa3b185fd5 修改sqlite3包 2022-03-05 12:57:48 +08:00
whyour 101d4fb497 修改 sqlite3 依赖 2022-03-05 12:18:30 +08:00
whyour ccd308f5b0 修改update对应分支 2022-03-05 11:46:21 +08:00
whyour 999d9f8e01 任务详情添加日志列表,更新依赖 2022-03-05 01:24:52 +08:00
whyour 313db0d74e logo使用七牛云 2022-03-04 00:15:42 +08:00
whyour 634e9705a7 增加任务详情 2022-03-04 00:08:33 +08:00
NekoMioandGitHub c959cec738 增加PushDeer推送 #1161 (#1194)
* 增加PushDeer推送
2022-03-03 18:42:11 +08:00
whyour 95330e04a3 修复task修改环境变量时机 2022-03-02 21:37:45 +08:00
whyour a099d09af7 修改openapi获取token逻辑 2022-02-28 22:30:58 +08:00
whyour a0d0197ad7 修改error页面样式 2022-02-26 14:24:23 +08:00
whyour d49e711a34 修复error页面样式 2022-02-26 14:16:51 +08:00
whyour e3b8cd75b9 修改错误日志页面 2022-02-26 14:14:10 +08:00
whyour 7a240eaabb 修改默认跳转 2022-02-26 00:13:52 +08:00
whyour 01564e96d3 添加系统错误页面 2022-02-26 00:12:12 +08:00
whyour 03f4b5c219 修改public服务 2022-02-25 23:06:35 +08:00
whyour 99e1d44f7d 修复public服务 2022-02-25 22:53:24 +08:00
whyour 8c55659c18 增加2.10-2.11升级代码 2022-02-25 22:49:17 +08:00
whyour 3ea980b2f8 增加public服务,查询panel日志 2022-02-24 23:50:04 +08:00
whyour f5fc987b51 修改pane样式 2022-02-24 23:24:21 +08:00
whyour 9cfa4af2b2 更新git pull参数 2022-02-24 22:36:49 +08:00
whyour 3efdf1fa87 修复git reset 2022-02-24 22:22:33 +08:00
whyour 25fc832605 增加使用系统通知配置发送通知api 2022-02-24 22:10:03 +08:00
whyour a7e404c5d5 修改编辑器cdn 2022-02-20 21:36:01 +08:00
whyour 90b27f0564 修复sql查询 2022-02-20 10:15:17 +08:00
whyour 632440965d 修改杀死进程逻辑 2022-02-19 22:54:27 +08:00
whyour aedfac446b 版本文件改为七牛云存储 2022-02-19 22:48:06 +08:00
whyour 3372b0025a 修复定时删除日志 2022-02-19 17:36:49 +08:00
whyour 0112b92e77 调整数据目录 2022-02-19 13:08:14 +08:00
whyour 0caf53b147 修复删除环境变量无效 2022-02-17 11:14:43 +08:00
whyour 4d1354d5f4 更新版本 v2.11.3 2022-02-16 23:09:33 +08:00
whyour fc4562eca2 修改check命令,安装依赖使用代理
修改apk源地址
2022-02-16 23:06:12 +08:00
whyour 5b048a5510 修改logo地址 2022-02-16 22:56:54 +08:00
whyour d23049b2fe 移除esbuild 2022-02-11 13:52:12 +08:00
whyour caac5607b9 环境变量增加更新时间排序 2022-02-11 13:41:48 +08:00
Ghost LeeandGitHub fb2e69b12d 修复goCqHttpBotToken中存在&字符导致认证失败的问题 (#993)
修改goCqHttpBot鉴权方式,调整为使用Header模式
2022-02-08 18:45:24 +08:00
UkennandGitHub fc1838ff39 bark支持自定义推送图标 (#1146)
* bark添加自定义推送图标

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

* 修正树节点选中,父节点自动展开
2021-11-20 01:06:25 +08:00
whyour 920efb8fab 更新readme图片链接 2021-11-20 01:06:25 +08:00
whyour 92aeb2b9da 更新readme 2021-11-20 01:06:25 +08:00
whyour ced76cf343 更新readme 2021-11-20 01:06:25 +08:00
whyour 3750d6ecd5 增加初始化依赖目录及文件 2021-11-20 01:06:25 +08:00
风之凌殇andwhyour 7444f30384 移除dockerfile中不必要的环境变量 (#925)
* 移除上次pr新增的不必要的环境变量
2021-11-20 01:06:16 +08:00
whyour b0fd5ba47f 更新readme 2021-11-17 19:22:40 +08:00
whyour cc098bc2e5 修复拷贝deps已存在依赖 2021-11-17 10:23:45 +08:00
132 changed files with 7955 additions and 13398 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 = ''
+22 -19
View File
@@ -20,25 +20,24 @@ jobs:
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v1 - uses: actions/setup-node@v3
with:
node-version: '14'
- name: build front and back - name: build front and back
run: | run: |
yarn install npm i -g pnpm
yarn build pnpm install
yarn build-back pnpm build:front
pnpm 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'
@@ -55,7 +54,8 @@ jobs:
contents: read contents: read
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Set time zone - name: Set time zone
uses: szenius/set-timezone@v1.0 uses: szenius/set-timezone@v1.0
@@ -65,13 +65,13 @@ jobs:
timezoneWindows: "China Standard Time" timezoneWindows: "China Standard Time"
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v1 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -79,7 +79,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v3 uses: docker/metadata-action@v4
with: with:
images: | images: |
${{ github.repository }} ${{ github.repository }}
@@ -96,17 +96,20 @@ jobs:
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v1 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
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 }}
network: host
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 }}
-31
View File
@@ -1,31 +0,0 @@
name: PublishDockerImage
on:
workflow_dispatch:
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout base
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Docker login
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
- name: Docker buildx image and push on master branch
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
docker buildx build --build-arg SSH_PRIVATE_KEY="${SSH_PRIVATE_KEY}" --output "type=image,push=true" --platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/s390x --tag whyour/qinglong:latest docker/
+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
+11 -9
View File
@@ -4,15 +4,26 @@ 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: {},
// antd: {
// dark: true,
// },
mfsu: {},
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 +43,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
```
+197 -18
View File
@@ -1,22 +1,201 @@
MIT LICENSE Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Copyright (c) 2021-present whyour TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
Permission is hereby granted, free of charge, to any person obtaining 1. Definitions.
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be "License" shall mean the terms and conditions for use, reproduction,
included in all copies or substantial portions of the Software. and distribution as defined by Sections 1 through 9 of this document.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, "Licensor" shall mean the copyright owner or entity authorized by
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF the copyright owner that is granting the License.
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE "Legal Entity" shall mean the union of the acting entity and all
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION other entities that control, are controlled by, or are under common
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION control with that entity. For the purposes of this definition,
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. "control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2021 WHYOUR <https://github.com/whyour>.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+210
View File
@@ -0,0 +1,210 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center">
Timed task management panel with python3, javaScript, shell, typescript support
[![docker version][docker-version-image]][docker-version-url] [![docker pulls][docker-pulls-image]][docker-pulls-url] [![docker stars][docker-stars-image]][docker-stars-url] [![docker image size][docker-image-size-image]][docker-image-size-url]
[docker-pulls-image]: https://img.shields.io/docker/pulls/whyour/qinglong?style=flat
[docker-pulls-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
[简体中文](./README.md) | English
## Features
- Support for multiple scripting languages (python3, javaScript, shell, typescript)
- Support online management of scripts, environment variables, configuration files
- Support online view task log
- Support second-level task setting
- Support system level notification
- Support dark mode
- Support cell phone operation
## Deployment
### Local Deployment
```bash
# To be completed
```
### Podman Deployment
1. podman installation
```bash
https://podman.io/getting-started/installation
```
2. start the container
```bash
podman run -dit \
--network bridge \
-v $PWD/ql:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
### Docker Deployment
1. docker installation
```bash
sudo curl -sSL get.docker.com | sh
```
2. configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. start the container
```bash
docker run -dit \
-v $PWD/ql:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### Docker-compose Deployment
1. docker-compose installation
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. start the container
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# start
docker-compose up -d
# stop
docker-compose down
```
## Use
1. built-in commands
```bash
# Update and restart Green Dragon
ql update
# Run custom scripts extra.sh
ql extra
# Adding a single script file
ql raw <file_url>
# Add a specific script for a single repository
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# Delete old logs
ql rmlog <days>
# Start bot
ql bot
# Detecting the Green Dragon environment and repairing it
ql check
# Reset the number of login errors
ql resetlet
# Disable two-step login
ql resettfa
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
```
2. parameter description
* file_url: Script address
* repo_url: Repository address
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
* branch: Pull the branch of the repository
* days: Number of days of logs to be kept
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
## Links
- [nevinee](https://gitee.com/evine)
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Development
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Communication
[telegram channel](https://t.me/jiao_long)
## Name Origin
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
+183 -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 的定时任务管理面板
[![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,189 @@ 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://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](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. 配置国内镜像源
Configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. 启动容器
```bash
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> <extensions>
# 删除旧日志
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: 拉取仓库的分支
* extensions: 拉取仓库的文件后缀
* 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
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm
$ pnpm install
$ pnpm 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(),
+108 -45
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(req.query.searchValue as string);
const data = await cronService.crontabs( return res.send({ code: 200, data });
req.query.searchValue as string, } catch (e) {
); logger.error('🔥 error: %o', e);
return res.send({ code: 200, data }); return next(e);
} catch (e) { }
logger.error('🔥 error: %o', 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);
}
},
);
}; };
+29 -31
View File
@@ -6,30 +6,28 @@ 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); const data = await dependenceService.dependencies(req.query as any);
const data = await dependenceService.dependencies(req.query as any); 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(
'/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,15 +86,15 @@ 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');
try { try {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const data = await dependenceService.removeDb(req.body); const data = await dependenceService.remove(req.body, true);
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);
@@ -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');
+32 -34
View File
@@ -6,24 +6,22 @@ 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); const data = await envService.envs(req.query.searchValue as string);
const data = await envService.envs(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(
'/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);
+4
View File
@@ -7,6 +7,8 @@ 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';
import subscription from './subscription';
export default () => { export default () => {
const app = Router(); const app = Router();
@@ -18,6 +20,8 @@ export default () => {
script(app); script(app);
open(app); open(app);
dependence(app); dependence(app);
system(app);
subscription(app);
return app; return app;
}; };
+23 -55
View File
@@ -3,70 +3,38 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName } from '../config/util'; import { getFileContentByName, readDirs } from '../config/util';
import { join } from 'path';
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 result = readDirs(config.logPath, config.logPath);
const fileList = fs.readdirSync(config.logPath, 'utf-8'); res.send({
const dirs = []; code: 200,
for (let i = 0; i < fileList.length; i++) { data: result,
const stat = fs.lstatSync(config.logPath + fileList[i]); });
if (stat.isDirectory()) { } catch (e) {
const fileListTmp = fs.readdirSync( logger.error('🔥 error: %o', e);
`${config.logPath}/${fileList[i]}`, return next(e);
'utf-8', }
); });
dirs.push({
name: fileList[i],
isDir: true,
files: fileListTmp.reverse(),
});
} else {
dirs.push({
name: fileList[i],
isDir: false,
files: [],
});
}
}
res.send({ code: 200, dirs });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get( route.get(
'/logs/:dir/: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 { dir, file } = req.params; const filePath = join(
const content = getFileContentByName( config.logPath,
`${config.logPath}/${dir}/${file}`, (req.query.path || '') as string,
req.params.file,
); );
res.send({ code: 200, data: content }); const content = getFileContentByName(filePath);
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/logs/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const { file } = req.params;
const content = getFileContentByName(`${config.logPath}/${file}`);
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);
+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);
+85 -78
View File
@@ -1,6 +1,7 @@
import { import {
fileExist, fileExist,
getFileContentByName, getFileContentByName,
readDirs,
getLastModifyFilePath, getLastModifyFilePath,
} from '../config/util'; } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
@@ -9,84 +10,38 @@ 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('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const result = readDirs(config.scriptPath, config.scriptPath);
res.send({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.get( route.get(
'/scripts/files', '/: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 fileList = fs.readdirSync(config.scriptPath, 'utf-8'); const filePath = join(
config.scriptPath,
let result = []; req.query.path as string,
for (let i = 0; i < fileList.length; i++) { req.params.file,
const fileOrDir = fileList[i];
const fPath = path.join(config.scriptPath, fileOrDir);
const dirStat = fs.statSync(fPath);
if (['node_modules'].includes(fileOrDir)) {
continue;
}
if (dirStat.isDirectory()) {
const childFileList = fs.readdirSync(fPath, 'utf-8');
let children = [];
for (let j = 0; j < childFileList.length; j++) {
const childFile = childFileList[j];
const sPath = path.join(config.scriptPath, fileOrDir, childFile);
const _fileExist = await fileExist(sPath);
if (_fileExist && fs.statSync(sPath).isFile()) {
const statObj = fs.statSync(sPath);
children.push({
title: childFile,
value: childFile,
key: `${fileOrDir}-${childFile}`,
mtime: statObj.mtimeMs,
parent: fileOrDir,
});
}
}
result.push({
title: fileOrDir,
value: fileOrDir,
key: fileOrDir,
mtime: dirStat.mtimeMs,
disabled: true,
children: children.sort((a, b) => b.mtime - a.mtime),
});
} else {
result.push({
title: fileOrDir,
value: fileOrDir,
key: fileOrDir,
mtime: dirStat.mtimeMs,
});
}
}
res.send({
code: 200,
data: result,
});
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/scripts/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const path = req.query.path ? `${req.query.path}/` : '';
const content = getFileContentByName(
`${config.scriptPath}${path}${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 +51,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(''),
}), }),
@@ -126,7 +81,7 @@ export default (app: Router) => {
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
code: 430, code: 430,
data: '文件路径禁止访问', message: '文件路径禁止访问',
}); });
} }
if (!originFilename) { if (!originFilename) {
@@ -156,11 +111,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(),
}), }),
}), }),
@@ -172,7 +127,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) {
@@ -183,7 +138,7 @@ 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(),
@@ -197,7 +152,7 @@ export default (app: Router) => {
filename: string; filename: string;
path: string; path: string;
}; };
const filePath = `${config.scriptPath}${path}/${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) {
@@ -208,7 +163,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(),
@@ -237,4 +192,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);
}
},
);
}; };
+288
View File
@@ -0,0 +1,288 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import SubscriptionService from '../services/subscription';
import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser';
const route = Router();
export default (app: Router) => {
app.use('/subscriptions', route);
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.list(
req.query.searchValue as string,
);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
});
route.post(
'/',
celebrate({
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional().allow('').allow(null),
interval_schedule: Joi.object().optional().allow('').allow(null),
name: Joi.string().optional().allow('').allow(null),
url: Joi.string().required(),
whitelist: Joi.string().optional().allow('').allow(null),
blacklist: Joi.string().optional().allow('').allow(null),
branch: Joi.string().optional().allow('').allow(null),
dependences: Joi.string().optional().allow('').allow(null),
pull_type: Joi.string().optional().allow('').allow(null),
pull_option: Joi.object().optional().allow('').allow(null),
extensions: Joi.string().optional().allow('').allow(null),
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(),
alias: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.create(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/run',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.run(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/stop',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.stop(req.body);
return res.send({ code: 200, data });
} catch (e) {
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) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/:id/log',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.log(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/',
celebrate({
body: Joi.object({
type: Joi.string().required(),
schedule: Joi.string().optional().allow('').allow(null),
interval_schedule: Joi.object().optional().allow('').allow(null),
name: Joi.string().optional().allow('').allow(null),
url: Joi.string().required(),
whitelist: Joi.string().optional().allow('').allow(null),
blacklist: Joi.string().optional().allow('').allow(null),
branch: Joi.string().optional().allow('').allow(null),
dependences: Joi.string().optional().allow('').allow(null),
pull_type: Joi.string().optional().allow('').allow(null),
pull_option: Joi.object().optional().allow('').allow(null),
schedule_type: Joi.string().optional().allow('').allow(null),
extensions: Joi.string().optional().allow('').allow(null),
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(),
id: Joi.number().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (
!req.body.schedule ||
typeof req.body.schedule === 'object' ||
cron_parser.parseExpression(req.body.schedule).hasNext()
) {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.update(req.body);
return res.send({ code: 200, data });
} else {
return res.send({ code: 400, message: 'param schedule error' });
}
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.delete(
'/',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/:id',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.getDb({ id: req.params.id });
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.put(
'/status',
celebrate({
body: Joi.object({
ids: Joi.array().items(Joi.number().required()),
status: Joi.string().required(),
pid: Joi.string().optional(),
log_path: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.status({
...req.body,
status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
});
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.get(
'/:id/logs',
celebrate({
params: Joi.object({
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.logs(req.params.id);
return res.send({ code: 200, data });
} catch (e) {
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);
}
},
);
};
+49 -120
View File
@@ -1,15 +1,29 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import UserService from '../services/user'; import UserService from '../services/user';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { getFileContentByName } from '../config/util'; import multer from 'multer';
import path from 'path';
import { v4 as uuidV4 } from 'uuid';
import config from '../config';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.uploadPath);
},
filename: function (req, file, cb) {
const ext = path.parse(file.originalname).ext;
const key = uuidV4();
cb(null, key + ext);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/', route); app.use('/user', route);
route.post( route.post(
'/login', '/login',
celebrate({ celebrate({
@@ -47,7 +61,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,29 +81,27 @@ export default (app: Router) => {
}, },
); );
route.get( route.get('/', async (req: Request, res: Response, next: NextFunction) => {
'/user', const logger: Logger = Container.get('logger');
async (req: Request, res: Response, next: NextFunction) => { try {
const logger: Logger = Container.get('logger'); const userService = Container.get(UserService);
try { const authInfo = await userService.getUserInfo();
const userService = Container.get(UserService); res.send({
const authInfo = await userService.getUserInfo(); code: 200,
res.send({ data: {
code: 200, username: authInfo.username,
data: { avatar: authInfo.avatar,
username: authInfo.username, twoFactorActivated: authInfo.twoFactorActivated,
twoFactorActivated: authInfo.twoFactorActivated, },
}, });
}); } catch (e) {
} catch (e) { 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 +116,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 +136,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 +151,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 +173,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 +188,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 +203,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 +217,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 +239,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 {
@@ -273,65 +253,14 @@ 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( route.put(
'/system/log/remove', '/avatar',
celebrate({ upload.single('avatar'),
body: Joi.object({
frequency: 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');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const result = await userService.updateLogRemoveFrequency( const result = await userService.updateAvatar(req.file!.filename);
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); res.send(result);
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
+3 -5
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();
@@ -17,11 +19,7 @@ async function startServer() {
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.info(` Logger.debug(`✌️ Back server launched on port ${config.port}`);
################################################
🛡️ Server listening on port: ${config.port} 🛡️
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+25 -34
View File
@@ -4,47 +4,41 @@ 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 = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
'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 uploadPath = path.join(dataPath, 'upload/');
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 +59,7 @@ export default {
confFile, confFile,
envFile, envFile,
dbPath, dbPath,
cronDbFile, uploadPath,
envDbFile,
appDbFile,
authDbFile,
dependenceDbFile,
configPath, configPath,
scriptPath, scriptPath,
samplePath, samplePath,
@@ -83,13 +73,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,
}; };
+75 -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,77 @@ export async function fileExist(file: any) {
} }
}); });
} }
export async function createFile(file: string, data: string = '') {
return new Promise((resolve) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);
resolve(true);
});
}
export async function concurrentRun(
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;
}
export function readDirs(dir: string, baseDir: string = '') {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: any = files.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
if (stats.isDirectory()) {
return {
title: file,
value: file,
key,
type: 'directory',
disabled: true,
parent: relativePath,
children: readDirs(subPath, baseDir),
};
}
return {
title: file,
value: file,
type: 'file',
key,
parent: relativePath,
};
});
return result;
}
+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,
},
});
+43 -8
View File
@@ -1,34 +1,42 @@
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();
this.isSystem = options.isSystem || 0; this.isSystem = options.isSystem || 0;
this.pid = options.pid; this.pid = options.pid;
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,
});
+24 -8
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();
@@ -36,13 +37,28 @@ export enum DependenceTypes {
} }
export enum InstallDependenceCommandTypes { export enum InstallDependenceCommandTypes {
'npm i -g --force', 'pnpm add -g',
'pip3 install', 'pip3 install',
'apk add --no-cache -f', 'apk add',
} }
export enum unInstallDependenceCommandTypes { export enum unInstallDependenceCommandTypes {
'npm uninstall -g --force', 'pnpm remove -g',
'pip3 uninstall -y', 'pip3 uninstall -y',
'apk del -f', 'apk del',
} }
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 = 'http://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,
});
+5 -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,7 @@ export class SockMessage {
export type SockMessageType = export type SockMessageType =
| 'ping' | 'ping'
| 'installDependence' | 'installDependence'
| 'updateSystemVersion'; | 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd';
+106
View File
@@ -0,0 +1,106 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { SimpleIntervalSchedule } from 'toad-scheduler';
type SimpleIntervalScheduleUnit = keyof SimpleIntervalSchedule;
export class Subscription {
id?: number;
name?: string;
type?: 'public-repo' | 'private-repo' | 'file';
schedule_type?: 'crontab' | 'interval';
schedule?: string;
interval_schedule?: { type: SimpleIntervalScheduleUnit; value: number };
url?: string;
whitelist?: string;
blacklist?: string;
dependences?: string;
branch?: string;
status?: SubscriptionStatus;
pull_type?: 'ssh-key' | 'user-pwd';
pull_option?:
| { private_key: string }
| { username: string; password: string };
pid?: number;
is_disabled?: 1 | 0;
log_path?: string;
alias: string;
command?: string;
extensions?: string;
sub_before?: string;
sub_after?: string;
constructor(options: Subscription) {
this.id = options.id;
this.name = options.name || options.alias;
this.type = options.type;
this.schedule = options.schedule;
this.status =
options.status && SubscriptionStatus[options.status]
? options.status
: SubscriptionStatus.idle;
this.url = options.url;
this.whitelist = options.whitelist;
this.blacklist = options.blacklist;
this.dependences = options.dependences;
this.branch = options.branch;
this.pull_type = options.pull_type;
this.pull_option = options.pull_option;
this.pid = options.pid;
this.is_disabled = options.is_disabled;
this.log_path = options.log_path;
this.schedule_type = options.schedule_type;
this.alias = options.alias;
this.interval_schedule = options.interval_schedule;
this.extensions = options.extensions;
this.sub_before = options.sub_before;
this.sub_after = options.sub_after;
}
}
export enum SubscriptionStatus {
'running',
'idle',
'disabled',
'queued',
}
interface SubscriptionInstance
extends Model<Subscription, Subscription>,
Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
'Subscription',
{
name: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
url: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
schedule: {
unique: 'compositeIndex',
type: DataTypes.STRING,
},
interval_schedule: {
unique: 'compositeIndex',
type: DataTypes.JSON,
},
type: DataTypes.STRING,
whitelist: DataTypes.STRING,
blacklist: DataTypes.STRING,
status: DataTypes.NUMBER,
dependences: DataTypes.STRING,
extensions: DataTypes.STRING,
sub_before: DataTypes.STRING,
sub_after: DataTypes.STRING,
branch: DataTypes.STRING,
pull_type: DataTypes.STRING,
pull_option: DataTypes.JSON,
pid: DataTypes.NUMBER,
is_disabled: DataTypes.NUMBER,
log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' },
},
);
+10 -4
View File
@@ -1,13 +1,13 @@
import expressLoader from './express'; import expressLoader from './express';
import dependencyInjectorLoader from './dependencyInjector'; import depInjectorLoader from './depInjector';
import Logger from './logger'; import Logger from './logger';
import initData from './initData'; import initData from './initData';
import { Application } from 'express'; import { Application } from 'express';
import linkDeps from './deps';
import initTask from './initTask';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
await dependencyInjectorLoader({ await depInjectorLoader();
models: [],
});
Logger.info('✌️ Dependency Injector loaded'); Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp }); await expressLoader({ app: expressApp });
@@ -15,4 +15,10 @@ 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 loaded');
initTask();
Logger.info('✌️ init task loaded');
}; };
+95 -49
View File
@@ -1,57 +1,105 @@
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 { SubscriptionModel } from '../data/subscription';
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 SubscriptionModel.sync({ alter: true });
db.cronDb = new DataStore({ filename: config.cronDbFile, autoload: true }); await sequelize.sync();
db.dependenceDb = new DataStore({
filename: config.dependenceDbFile,
autoload: true,
});
db.envDb = new DataStore({ filename: config.envDbFile, autoload: true });
db.appDb = new DataStore({ filename: config.appDbFile, autoload: true });
db.authDb = new DataStore({ filename: config.authDbFile, autoload: true });
// compaction data file // try {
db.cronDb.persistence.compactDatafile(); // const queryInterface = sequelize.getQueryInterface();
db.envDb.persistence.compactDatafile(); // await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
db.dependenceDb.persistence.compactDatafile(); // await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
db.appDb.persistence.compactDatafile(); // await queryInterface.addIndex('Apps', ['name'], { unique: true });
db.authDb.persistence.compactDatafile(); // } 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,
});
cronDb.persistence.compactDatafile();
cronDb.find({}).exec(async (err, docs) => {
await CrontabModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
if (dependenceDbExist && dependenceCount === 0) {
const dependenceDb = new DataStore({
filename: dependenceDbFile,
autoload: true,
});
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 +107,3 @@ export default async () => {
Logger.info(error); Logger.info(error);
} }
}; };
export const dbs: Dbs = db;
@@ -1,12 +1,8 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import LoggerInstance from './logger'; import LoggerInstance from './logger';
export default ({ models }: { models: { name: string; model: any }[] }) => { export default () => {
try { try {
models.forEach((m) => {
Container.set(m.name, m.model);
});
Container.set('logger', LoggerInstance); Container.set('logger', LoggerInstance);
} catch (e) { } catch (e) {
LoggerInstance.error('🔥 Error on dependency injector loader: %o', e); LoggerInstance.error('🔥 Error on dependency injector loader: %o', e);
+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));
};
+15 -10
View File
@@ -1,28 +1,31 @@
import { Request, Response, NextFunction, Application } from 'express'; import express, { Request, Response, NextFunction, Application } from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import cors from 'cors'; import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import jwt 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';
import { errors } from 'celebrate';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
app.use(cors()); app.use(cors());
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use((req, res, next) => { app.use((req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/open')) { if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
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 +71,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 +102,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;
} }
@@ -131,6 +135,7 @@ export default ({ app }: { app: Application }) => {
next(err); next(err);
}); });
app.use(errors());
app.use(Sentry.Handlers.errorHandler()); app.use(Sentry.Handlers.errorHandler());
app.use( app.use(
+82 -59
View File
@@ -1,91 +1,114 @@
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 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 dependenceDb = dbs.dependenceDb;
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
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: {}, raw: true }).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) { await dependenceService.reInstall(depIds as number[]);
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) => { },
for (let i = 0; i < docs.length; i++) { },
const doc = docs[i]; }).then((docs) => {
if (doc) { for (let i = 0; i < docs.length; i++) {
exec(doc.command); const doc = docs[i];
if (doc) {
exec(doc.command);
}
}
});
// 更新2.11.3以前的脚本路径
CrontabModel.findAll({
where: {
command: {
[Op.or]: [
{ [Op.like]: `%\/${config.rootPath}\/scripts\/%` },
{ [Op.like]: `%\/${config.rootPath}\/config\/%` },
{ [Op.like]: `%\/${config.rootPath}\/log\/%` },
{ [Op.like]: `%\/${config.rootPath}\/db\/%` },
],
},
},
}).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 } },
);
} }
} }
}); }
});
// patch 禁用状态字段改变
cronDb
.find({
status: CrontabStatus.disabled,
})
.exec((err, docs) => {
if (docs.length > 0) {
const ids = docs.map((x) => x._id);
cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle, isDisabled: 1 } },
{ multi: true },
(err) => {
cronService.autosave_crontab();
},
);
}
});
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await envService.set_envs(); await envService.set_envs();
}; };
function randomSchedule(from: number, to: number) {
const result: any[] = [];
const arr = [...Array(from).keys()];
let count = arr.length;
for (let i = 0; i < to; i++) {
const index = ~~(Math.random() * count) + i;
if (result.includes(arr[index])) {
continue;
}
result[i] = arr[index];
arr[index] = arr[i];
count--;
}
return result;
}
+62
View File
@@ -0,0 +1,62 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
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 uploadPath = path.join(dataPath, 'upload/');
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');
const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh');
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);
const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath);
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);
}
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
}
if (!sshDirExist) {
fs.mkdirSync(sshPath);
}
dotenv.config({ path: confFile });
Logger.info('✌️ Init file down');
};
+31
View File
@@ -0,0 +1,31 @@
import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
export default async () => {
const systemService = Container.get(SystemService);
const scheduleService = Container.get(ScheduleService);
const subscriptionService = Container.get(SubscriptionService);
// 运行删除日志任务
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,
});
}
// 运行所有订阅
const subs = await subscriptionService.list();
for (const sub of subs) {
await subscriptionService.handleTask(sub, true, true, true);
}
};
+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 }),
+30
View File
@@ -0,0 +1,30 @@
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, message: (err && err.message) || 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.debug(`✌️ Back server launched on port ${config.publicPort}`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
+7 -13
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,6 +30,10 @@ const run = async () => {
} }
} }
} }
})
.catch((err) => {
Logger.error(err);
process.exit(1);
}); });
}; };
+276 -245
View File
@@ -1,30 +1,23 @@
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';
import path from 'path';
@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) {
const { schedule } = cron; const { schedule } = cron;
if (schedule.split(/ +/).length === 6) { if (schedule?.split(/ +/).length === 6) {
return true; return true;
} }
return false; return false;
@@ -32,7 +25,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 +32,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,156 +55,166 @@ 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( status,
{ _id: { $in: ids } }, pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return await CrontabModel.update({ ...options }, { where: { id: ids } });
}
public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true);
}
public async pin(ids: number[]) {
await CrontabModel.update({ isPinned: 1 }, { where: { id: ids } });
}
public async unPin(ids: number[]) {
await CrontabModel.update({ isPinned: 0 }, { where: { id: ids } });
}
public async addLabels(ids: string[], labels: string[]) {
const docs = await CrontabModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await CrontabModel.update(
{ {
$set: { labels: Array.from(new Set((doc.labels || []).concat(labels))),
status,
pid,
log_path,
last_running_time,
last_execution_time,
},
},
{ multi: true, returnUpdatedDocs: true },
(err) => {
resolve(null);
}, },
{ where: { id: doc.id } },
); );
}); }
} }
public async remove(ids: string[]) { public async removeLabels(ids: string[], labels: string[]) {
return new Promise((resolve: any) => { const docs = await CrontabModel.findAll({ where: { id: ids } });
this.cronDb.remove( for (const doc of docs) {
{ _id: { $in: ids } }, await CrontabModel.update(
{ multi: true }, {
async (err) => { labels: (doc.labels || []).filter((label) => !labels.includes(label)),
await this.set_crontab(true);
resolve();
}, },
{ where: { id: doc.id } },
); );
}); }
}
public async pin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 1 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
}
public async unPin(ids: string[]) {
return new Promise((resolve: any) => {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { isPinned: 0 } },
{ multi: true },
async (err) => {
resolve();
},
);
});
} }
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(':');
query = { switch (textArray[0]) {
$or: [ case 'name':
{ case 'command':
name: reg, case 'schedule':
}, case 'label':
{ const column = textArray[0] === 'label' ? 'labels' : textArray[0];
command: reg, query = {
}, [column]: {
{ [Op.or]: [
schedule: reg, { [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,
},
{
command: reg,
},
{
schedule: reg,
},
{
labels: reg,
},
],
};
break;
}
} }
return new Promise((resolve) => { try {
this.cronDb const result = await CrontabModel.findAll({
.find(query) where: query,
.sort({ created: -1 }) order: [['createdAt', 'DESC']],
.exec((err, docs) => {
resolve(docs);
});
});
}
public async get(_id: string): Promise<Crontab> {
return new Promise((resolve) => {
this.cronDb.find({ _id }).exec((err, docs) => {
resolve(docs[0]);
}); });
}); return result as any;
} } catch (error) {
throw error;
public async run(ids: string[]) {
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.queued } },
{ multi: true },
);
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 getDb(query: any): Promise<Crontab> {
return new Promise((resolve: any) => { const doc: any = await CrontabModel.findOne({ where: { ...query } });
this.cronDb return doc && (doc.get({ plain: true }) as Crontab);
.find({ _id: { $in: ids } })
.exec(async (err, docs: Crontab[]) => {
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const err = await this.killTask(doc.command);
if (doc.log_path) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${doc.log_path}`,
`${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
);
}
}
this.cronDb.update(
{ _id: { $in: ids } },
{ $set: { status: CrontabStatus.idle }, $unset: { pid: true } },
{ multi: true },
);
this.queue.clear();
resolve();
});
});
} }
private async killTask(name: string) { public async run(ids: number[]) {
await CrontabModel.update(
{ status: CrontabStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
}
public async stop(ids: number[]) {
const docs = await CrontabModel.findAll({ where: { id: ids } });
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
);
}
}
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id: ids } },
);
}
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,16 +224,23 @@ export default class CronService {
} else { } else {
return; return;
} }
const pids = pid.match(/\(\d+/g); let pids = pid.match(/\(\d+/g);
const killLogs = []; const killLogs = [];
for (const id of pids) { if (pids && pids.length > 0) {
const c = `kill -9 ${id.slice(1)}`; // node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
const { stdout, stderr } = await execAsync(c); for (const id of pids) {
if (stderr) { const c = `kill -9 ${id.slice(1)}`;
killLogs.push(stderr); try {
} const { stdout, stderr } = await execAsync(c);
if (stdout) { if (stderr) {
killLogs.push(stdout); killLogs.push(stderr);
}
if (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) : '';
@@ -263,18 +249,20 @@ 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;
const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath));
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,78 +274,63 @@ 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 (logFileExist) {
fs.appendFileSync(`${log_path}`, `${data}`); fs.appendFileSync(`${absolutePath}`, `${data}`);
} }
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
if (log_path) { if (logFileExist) {
fs.appendFileSync(`${log_path}`, `${JSON.stringify(err)}`); fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
} }
}); });
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( await this.set_crontab(true);
{ _id: { $in: ids } },
{ $set: { isDisabled: 1 } },
{ multi: true },
async (err) => {
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( await this.set_crontab(true);
{ _id: { $in: ids } },
{ $set: { isDisabled: 0 } },
{ multi: true },
async (err) => {
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 '';
} }
if (doc.log_path) { const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
return getFileContentByName(`${doc.log_path}`); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
} }
const [, commandStr, url] = doc.command.split(/ +/); const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr); let logPath = this.getKey(commandStr);
@@ -380,18 +353,78 @@ export default class CronService {
} }
} }
private getKey(command: string) { public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
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): string {
const start = const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0; command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end = const end =
command.lastIndexOf('.') !== -1 command.lastIndexOf('.') !== -1
? command.lastIndexOf('.') ? command.lastIndexOf('.')
: command.length; : command.length;
return command.substring(start, end);
const tmpStr = command.substring(0, start - 1);
let index = 0;
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
index = tmpStr.lastIndexOf('/');
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
index = tmpStr.lastIndexOf(':');
}
if (index) {
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} else {
return command.substring(start, end);
}
} }
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;
} }
@@ -400,7 +433,7 @@ export default class CronService {
var crontab_string = ''; var crontab_string = '';
tabs.forEach((tab) => { tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule.length !== 5) { if (tab.isDisabled === 1 || _schedule!.length !== 5) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';
@@ -421,40 +454,38 @@ 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() {
exec('crontab -l', (error, stdout, stderr) => { exec('crontab -l', (error, stdout, stderr) => {
var lines = stdout.split('\n'); const lines = stdout.split('\n');
var namePrefix = new Date().getTime(); const 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 = const 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+))/;
var command = line.replace(regex, '').trim(); const command = line.replace(regex, '').trim();
var schedule = line.replace(command, '').trim(); const schedule = line.replace(command, '').trim();
if ( if (
command && command &&
schedule && schedule &&
cron_parser.parseExpression(schedule).hasNext() cron_parser.parseExpression(schedule).hasNext()
) { ) {
var name = namePrefix + '_' + index; const 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);
}
} }
}); });
}); });
+163 -194
View File
@@ -1,23 +1,22 @@
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';
import { concurrentRun } from '../config/util';
@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,
@@ -29,78 +28,47 @@ export default class DependenceService {
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
this.installOrUninstallDependencies(docs); this.installDependenceOneByOne(docs);
return docs; return docs;
} }
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,
status: DependenceStatus.installing, status: DependenceStatus.installing,
}); });
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
this.installOrUninstallDependencies([newDoc]); this.installDependenceOneByOne([newDoc]);
return newDoc; return newDoc;
} }
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[], force = false): Promise<Dependence[]> {
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.installDependenceOneByOne(docs, false, force);
this.installOrUninstallDependencies(docs, false); return docs;
resolve(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,171 +78,172 @@ 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[]> { private installDependenceOneByOne(
return new Promise((resolve: any) => { docs: Dependence[],
this.dependenceDb.update( isInstall: boolean = true,
{ _id: { $in: ids } }, force: boolean = false,
{ $set: { status: DependenceStatus.installing, log: [] } }, ) {
{ multi: true, returnUpdatedDocs: true }, concurrentRun(
async (err, num, docs: Dependence[]) => { docs.map(
this.installOrUninstallDependencies(docs); (dep) => async () =>
resolve(docs); await this.installOrUninstallDependencies([dep], isInstall, force),
}, ),
); 1,
}); );
} }
private async find(query: any, sort: any): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
return new Promise((resolve) => { await DependenceModel.update(
this.dependenceDb { status: DependenceStatus.installing, log: [] },
.find(query) { where: { id: ids } },
.sort({ ...sort }) );
.exec((err, docs) => {
resolve(docs); const docs = await DependenceModel.findAll({ where: { id: ids } });
}); this.installDependenceOneByOne(docs);
}); return docs;
} }
public async get(_id: string): Promise<Dependence> { private async find(query: any, sort?: any): Promise<Dependence[]> {
return new Promise((resolve) => { const docs = await DependenceModel.findAll({ where: { ...query } });
this.dependenceDb.find({ _id }).exec((err, docs) => { return docs;
resolve(docs[0]);
});
});
} }
private async updateLog(ids: string[], log: string): Promise<void> { public async getDb(query: any): Promise<Dependence> {
return new Promise((resolve) => { const doc: any = await DependenceModel.findOne({ where: { ...query } });
this.dependenceDb.update( return doc && (doc.get({ plain: true }) as Dependence);
{ _id: { $in: ids } }, }
{ $push: { log } },
{ multi: true }, private async updateLog(ids: number[], log: string): Promise<void> {
(err, num, doc) => { const doc = await DependenceModel.findOne({ where: { id: ids } });
if (err) { const newLog = doc?.log ? [...doc.log, log] : [log];
this.logger.error(err); await DependenceModel.update({ log: newLog }, { where: { id: ids } });
} else {
resolve();
}
},
);
});
} }
public installOrUninstallDependencies( public installOrUninstallDependencies(
dependencies: Dependence[], dependencies: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false,
) { ) {
if (dependencies.length === 0) { return new Promise(async (resolve) => {
return; if (dependencies.length === 0) {
} resolve(null);
const depNames = dependencies.map((x) => x.name).join(' '); return;
const depRunCommand = ( }
isInstall const socketMessageType = !force
? InstallDependenceCommandTypes ? 'installDependence'
: unInstallDependenceCommandTypes : 'uninstallDependence';
)[dependencies[0].type as any]; const depNames = dependencies.map((x) => x.name).join(' ');
const actionText = isInstall ? '安装' : '删除'; const depRunCommand = (
const depIds = dependencies.map((x) => x._id) as string[]; isInstall
const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' }); ? InstallDependenceCommandTypes
const startTime = Date.now(); : unInstallDependenceCommandTypes
this.sockService.sendMessage({ )[dependencies[0].type as any];
type: 'installDependence', const actionText = isInstall ? '安装' : '删除';
message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date( const depIds = dependencies.map((x) => x.id) as number[];
startTime, const startTime = Date.now();
).toLocaleString()}`,
references: depIds,
});
this.updateLog(
depIds,
`开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n`,
);
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'installDependence', type: socketMessageType,
message: data.toString(), message: `开始${actionText}依赖 ${depNames},开始时间 ${new Date(
startTime,
).toLocaleString()}\n\n`,
references: depIds, references: depIds,
}); });
this.updateLog(depIds, data.toString()); await this.updateLog(
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'installDependence',
message: data.toString(),
references: depIds,
});
this.updateLog(depIds, data.toString());
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'installDependence',
message: JSON.stringify(err),
references: depIds,
});
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: 'installDependence',
message: `依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
this.updateLog(
depIds, depIds,
`依赖${actionText}${resultText}结束时间 ${new Date( `开始${actionText}依赖 ${depNames}开始时间 ${new Date(
endTime, startTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`, ).toLocaleString()}\n\n`,
); );
let status = null; const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
this.dependenceDb.update(
{ _id: { $in: depIds } },
{
$set: { status },
$unset: { pid: true },
},
{ multi: true },
);
// 如果删除依赖成功,3秒后删除数据库记录 cp.stdout.on('data', async (data) => {
if (isSucceed && !isInstall) { this.sockService.sendMessage({
setTimeout(() => { type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
this.sockService.sendMessage({
type: socketMessageType,
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
this.sockService.sendMessage({
type: socketMessageType,
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
});
cp.on('close', async (code) => {
const endTime = Date.now();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
this.sockService.sendMessage({
type: socketMessageType,
message: `\n依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
references: depIds,
});
await this.updateLog(
depIds,
`\n依赖${actionText}${resultText},结束时间 ${new Date(
endTime,
).toLocaleString()},耗时 ${(endTime - startTime) / 1000}`,
);
let status = null;
if (isSucceed) {
status = isInstall
? DependenceStatus.installed
: DependenceStatus.removed;
} else {
status = isInstall
? DependenceStatus.installFailed
: DependenceStatus.removeFailed;
}
await DependenceModel.update({ status }, { where: { id: depIds } });
// 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) {
this.removeDb(depIds); this.removeDb(depIds);
}, 5000); }
}
resolve(null);
});
}); });
} }
} }
+58 -109
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 }, await this.set_envs();
async (err) => {
await this.set_envs();
resolve();
},
);
});
} }
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( await this.set_envs();
{ _id: { $in: ids } },
{ $set: { status: EnvStatus.normal } },
{ multi: true },
async (err, num) => {
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( await this.set_envs();
{ _id: { $in: ids } },
{ $set: { name } },
{ multi: true },
async (err, num) => {
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,15 @@ 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(/(\\)[^\n]/g, '\\\\')
.replace(/(\\$)/, '\\\\')
.replace(/"/g, '\\"')
.trim();
env_string += `export ${key}="${value}"\n`;
} }
} }
} }
+47 -10
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,
{ retry: 0,
timeout: this.timeout, json: { message: `${this.title}\n${this.content}` },
retry: 0, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
json: { message: `${this.title}\n${this.content}` }, })
},
)
.json(); .json();
return res.retcode === 0; return res.retcode === 0;
} }
@@ -98,8 +117,26 @@ 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;
const url = `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
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 +144,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,
+89 -115
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 = [
code: 200, ...invalidTokens.slice(0, 4),
data: { { ...invalidTokens[4], expiration },
token, ];
token_type: 'Bearer', token = invalidTokens[4].value;
expiration, } else {
}, tokens = [...invalidTokens, { value: token, expiration }];
}); }
}, await AppModel.update(
); { tokens },
} else { { where: { client_id, client_secret } },
resolve({ code: 400, message: 'client_id或client_seret有误' }); );
} return {
}); code: 200,
}); data: {
token,
token_type: 'Bearer',
expiration,
},
};
} else {
return { code: 400, message: 'client_id或client_seret有误' };
}
} }
} }
+162 -34
View File
@@ -1,18 +1,116 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { Crontab } from '../data/cron'; import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { exec } from 'child_process'; import {
ToadScheduler,
LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule,
} from 'toad-scheduler';
import dayjs from 'dayjs';
interface ScheduleTaskType {
id: number;
command: string;
name?: string;
schedule?: string;
}
export interface TaskCallbacks {
onStart?: (
cp: ChildProcessWithoutNullStreams,
startTime: dayjs.Dayjs,
) => Promise<void>;
onEnd?: (
cp: ChildProcessWithoutNullStreams,
endTime: dayjs.Dayjs,
diff: number,
) => Promise<void>;
onLog?: (message: string) => Promise<void>;
onError?: (message: string) => Promise<void>;
}
@Service() @Service()
export default class ScheduleService { export default class ScheduleService {
private scheduleStacks = new Map<string, nodeSchedule.Job>(); private scheduleStacks = new Map<string, nodeSchedule.Job>();
private intervalSchedule = new ToadScheduler();
private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async generateSchedule({ _id = '', command, name, schedule }: Crontab) { async runTask(command: string, callbacks: TaskCallbacks = {}) {
return new Promise(async (resolve, reject) => {
try {
const startTime = dayjs();
const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime);
cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString());
});
cp.stderr.on('data', async (data) => {
this.logger.error(
'执行任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
data.toString(),
);
await callbacks.onError?.(data.toString());
});
cp.on('error', async (err) => {
this.logger.error(
'创建任务 %s 失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs();
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
error,
);
await callbacks.onError?.(JSON.stringify(error));
resolve(null);
}
});
}
async createCronTask(
{ id = 0, command, name, schedule = '' }: ScheduleTaskType,
callbacks?: TaskCallbacks,
runImmediately = false,
) {
const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建定时任务],任务ID: %scron: %s,任务名: %s,执行命令: %s', '[创建cron任务],任务ID: %scron: %s,任务名: %s,执行命令: %s',
_id, _id,
schedule, schedule,
name, name,
@@ -22,41 +120,71 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
try { await this.runTask(command, callbacks);
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 {
}
}), }),
); );
if (runImmediately) {
await this.runTask(command, callbacks);
}
} }
async cancelSchedule({ _id = '', name }: Crontab) { async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
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 = '' }: ScheduleTaskType,
schedule: SimpleIntervalSchedule,
runImmediately = true,
callbacks?: TaskCallbacks,
) {
const _id = this.formatId(id);
this.logger.info(
'[创建interval任务],任务ID: %s,任务名: %s,执行命令: %s',
_id,
name,
command,
);
const task = new AsyncTask(
name,
async () => {
return new Promise(async (resolve, reject) => {
await this.runTask(command, callbacks);
});
},
(err) => {
this.logger.error(
'执行任务%s失败,时间:%s, 错误信息:%j',
command,
new Date().toLocaleString(),
err,
);
},
);
const job = new LongIntervalJob(
{ ...schedule, runImmediately: false },
task,
_id,
);
this.intervalSchedule.addIntervalJob(job);
if (runImmediately) {
await this.runTask(command, callbacks);
}
}
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
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 };
}
}
+76
View File
@@ -0,0 +1,76 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import os from 'os';
import path from 'path';
@Service()
export default class SshKeyService {
private homedir = os.homedir();
private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.sshPath, 'config');
constructor(@Inject('logger') private logger: winston.Logger) {}
private generatePrivateKeyFile(alias: string, key: string): void {
try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
encoding: 'utf8',
mode: '400',
});
} catch (error) {
this.logger.error('生成私钥文件失败', error);
}
}
private removePrivateKeyFile(alias: string): void {
try {
fs.unlinkSync(`${this.sshPath}/${alias}`);
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(alias: string, host: string): string {
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
}
private generateSshConfig(configs: string[]) {
try {
for (const config of configs) {
fs.appendFileSync(this.sshConfigFilePath, config, {
encoding: 'utf8',
});
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
}
private removeSshConfig(config: string) {
try {
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(config, '')
.replace(/\n\n+/, '\n\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
} catch (error) {
this.logger.error(`删除ssh配置文件${config}失败`, error);
}
}
public addSSHKey(key: string, alias: string, host: string): void {
this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host);
this.removeSshConfig(config);
this.generateSshConfig([config]);
}
public removeSSHKey(alias: string, host: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host);
this.removeSshConfig(config);
}
}
+478
View File
@@ -0,0 +1,478 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import {
Subscription,
SubscriptionModel,
SubscriptionStatus,
} from '../data/subscription';
import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import {
getFileContentByName,
concurrentRun,
fileExist,
createFile,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
import SshKeyService from './sshKey';
@Service()
export default class SubscriptionService {
constructor(
@Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService,
private sockService: SockService,
private sshKeyService: SshKeyService,
) {}
public async list(searchText?: string): Promise<Subscription[]> {
let query = {};
if (searchText) {
const textArray = searchText.split(':');
switch (textArray[0]) {
case 'name':
case 'command':
case 'schedule':
case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0];
query = {
[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,
},
{
command: reg,
},
{
schedule: reg,
},
{
labels: reg,
},
],
};
break;
}
}
try {
const result = await SubscriptionModel.findAll({
where: query,
order: [['createdAt', 'DESC']],
});
return result as any;
} catch (error) {
throw error;
}
}
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
let _url = url || this.formatUrl(doc).url;
const { type, whitelist, blacklist, dependences, branch, extensions } = doc;
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}"`;
}
return command;
}
private formatUrl(doc: Subscription) {
let url = doc.url;
let host = '';
if (doc.type === 'private-repo') {
if (doc.pull_type === 'ssh-key') {
host = doc.url!.replace(/.*\@([^\:]+)\:.*/, '$1');
url = doc.url!.replace(host, doc.alias);
} else {
host = doc.url!.replace(/.*\:\/\/([^\/]+)\/.*/, '$1');
const { username, password } = doc.pull_option as any;
url = doc.url!.replace(host, `${username}:${password}@${host}`);
}
}
return { url, host };
}
public async handleTask(
doc: Subscription,
needCreate = true,
needAddKey = true,
runImmediately = false,
) {
const { url, host } = this.formatUrl(doc);
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
if (needAddKey) {
this.sshKeyService.addSSHKey(
(doc.pull_option as any).private_key,
doc.alias,
host,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host);
}
}
doc.command = this.formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any);
needCreate &&
(await this.scheduleService.createCronTask(
doc as any,
this.taskCallbacks(doc),
runImmediately,
));
} else {
this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule as any;
needCreate &&
(await this.scheduleService.createIntervalTask(
doc as any,
{ [type]: value } as SimpleIntervalSchedule,
runImmediately,
this.taskCallbacks(doc),
));
}
}
private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
}
private async handleLogPath(
logPath: string,
data: string = '',
): Promise<string> {
const absolutePath = path.resolve(config.logPath, logPath);
const logFileExist = await fileExist(absolutePath);
if (!logFileExist) {
await createFile(absolutePath, data);
}
return absolutePath;
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
return {
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
const logTime = startTime.format('YYYY-MM-DD-HH-mm-ss');
const logPath = `${doc.alias}/${logTime}.log`;
await SubscriptionModel.update(
{
status: SubscriptionStatus.running,
log_path: logPath,
pid: cp.pid,
},
{ where: { id: doc.id } },
);
const absolutePath = await this.handleLogPath(
logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
);
// 执行sub_before
let beforeStr = '';
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before);
}
} catch (error: any) {
beforeStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (beforeStr) {
fs.appendFileSync(absolutePath, `${beforeStr}\n\n`);
}
},
onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
// 执行 sub_after
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n`);
afterStr = await this.promiseExec(sub.sub_after);
}
} catch (error: any) {
afterStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (afterStr) {
fs.appendFileSync(absolutePath, `${afterStr}\n`);
}
fs.appendFileSync(
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}`,
);
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: sub.id } },
);
this.sockService.sendMessage({
type: 'runSubscriptionEnd',
message: '订阅执行完成',
references: [doc.id as number],
});
},
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await this.handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
},
};
}
public async create(payload: Subscription): Promise<Subscription> {
const tab = new Subscription(payload);
const doc = await this.insert(tab);
await this.handleTask(doc);
return doc;
}
public async insert(payload: Subscription): Promise<Subscription> {
return await SubscriptionModel.create(payload, { returning: true });
}
public async update(payload: Subscription): Promise<Subscription> {
const newDoc = await this.updateDb(payload);
await this.handleTask(newDoc);
return newDoc;
}
public async updateDb(payload: Subscription): Promise<Subscription> {
await SubscriptionModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async status({
ids,
status,
pid,
log_path,
last_running_time = 0,
last_execution_time = 0,
}: {
ids: number[];
status: SubscriptionStatus;
pid: number;
log_path: string;
last_running_time: number;
last_execution_time: number;
}) {
const options: any = {
status,
pid,
log_path,
last_execution_time,
};
if (last_running_time > 0) {
options.last_running_time = last_running_time;
}
return await SubscriptionModel.update(
{ ...options },
{ where: { id: ids } },
);
}
public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false, false);
}
await SubscriptionModel.destroy({ where: { id: ids } });
}
public async getDb(query: any): Promise<Subscription> {
const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as Subscription);
}
public async run(ids: number[]) {
await SubscriptionModel.update(
{ status: SubscriptionStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
}
public async stop(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
if (doc.pid) {
try {
process.kill(-doc.pid);
} catch (error) {
this.logger.silly(error);
}
}
await this.handleTask(doc, false);
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
);
}
await SubscriptionModel.update(
{ status: SubscriptionStatus.idle, pid: undefined },
{ where: { id: ids } },
);
}
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) {
return;
}
const command = this.formatCommand(subscription);
await this.scheduleService.runTask(
command,
this.taskCallbacks(subscription),
);
}
public async disabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false);
}
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc);
}
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
}
public async log(id: number) {
const doc = await this.getDb({ id });
if (!doc || !doc.log_path) {
return '';
}
const absolutePath = await this.handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
}
public async logs(id: number) {
const doc = await this.getDb({ id });
if (!doc) {
return [];
}
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
}
}
+177
View File
@@ -0,0 +1,177 @@
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, message: '通知发送失败,请检查参数' };
}
}
public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getLogRemoveFrequency();
const result = await this.updateAuthDb({
...oDoc,
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,
message: 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, message: '通知发送成功' };
} else {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
}
}
}
+40 -167
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([]);
} else {
const result = docs.sort(
(a, b) => b.info.timestamp - a.info.timestamp,
);
if (result.length > 100) {
this.authDb.remove({ _id: result[result.length - 1]._id });
}
resolve(result.map((x) => x.info));
}
});
}); });
if (docs && docs.length > 0) {
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
if (result.length > 100) {
await AuthModel.destroy({
where: { id: result[result.length - 1].id },
});
}
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() {
@@ -233,6 +221,12 @@ export default class UserService {
return { code: 200, message: '更新成功' }; return { code: 200, message: '更新成功' };
} }
public async updateAvatar(avatar: string) {
const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' };
}
public getUserInfo(): Promise<any> { public getUserInfo(): Promise<any> {
return new Promise((resolve) => { return new Promise((resolve) => {
fs.readFile(config.authConfigFile, 'utf8', (err, data) => { fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
@@ -315,48 +309,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) { doc = updateResult[1][0];
resolve({} as NotificationInfo); } else {
} else { doc = await AuthModel.create(payload, { returning: true });
resolve({ ...doc.info, _id: doc._id }); }
} 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) {
@@ -373,107 +346,7 @@ export default class UserService {
}); });
return { code: 200, data: { ...result, code } }; return { code: 200, data: { ...result, code } };
} else { } else {
return { code: 400, data: '通知发送失败,请检查参数' }; return { code: 400, message: '通知发送失败,请检查参数' };
} }
} }
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
+18 -12
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:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules \
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 \
@@ -33,21 +35,25 @@ RUN set -x \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& touch ~/.bashrc \
&& git 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 setup \
&& source ~/.bashrc \
&& pnpm add -g pm2 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/.local/share/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;
}
} }
+80 -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}'",
"postinstall": "umi generate tmp 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
@@ -23,67 +26,92 @@
"prettier --parser=typescript --write" "prettier --parser=typescript --write"
] ]
}, },
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"react": "17",
"react-dom": "17"
},
"ignoreMissing": [
"monaco-editor",
"react-router"
]
}
},
"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", "dayjs": "^1.11.2",
"express": "^4.17.1", "dotenv": "^16.0.0",
"express-jwt": "^6.0.0", "express": "^4.17.3",
"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",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-fetch": "^2.6.1", "node-schedule": "^2.1.0",
"node-schedule": "^2.0.0", "nodemailer": "^6.7.2",
"nodemailer": "^6.7.0", "p-queue": "7.2.0",
"p-queue": "6.6.2",
"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@^15.0.3",
"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/body-parser": "^1.19.2",
"@types/cors": "^2.8.10", "@types/cors": "^2.8.12",
"@types/express": "^4.17.8", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.1", "@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.0", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.170", "@types/lodash": "^4.14.179",
"@types/nedb": "^1.8.11", "@types/multer": "^1.4.7",
"@types/node": "^14.11.2", "@types/nedb": "^1.8.12",
"@types/node-fetch": "^2.5.8", "@types/node": "^17.0.21",
"@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/ssr-darkreader": "^4.9.45",
"compression-webpack-plugin": "6.1.1", "@umijs/test": "^3.5.21",
"darkreader": "^4.9.27", "ansi-to-react": "^6.1.6",
"lint-staged": "^10.0.7", "antd": "^4.20.5",
"nodemon": "^2.0.4", "antd-img-crop": "^4.2.3",
"prettier": "^2.2.0", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"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",
"rc-tween-one": "^3.0.6",
"react": "17.x", "react": "17.x",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
@@ -91,13 +119,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="http://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 变量名= 声明即可
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
## 添加你需要重启自动执行的任意命令,比如 ql repo ## 添加你需要重启自动执行的任意命令,比如 ql repo
## 安装node依赖使用 pnpm install -g xxx xxx ## 安装node依赖使用 pnpm add -g xxx xxx
## 安装python依赖使用 pip3 install xxx ## 安装python依赖使用 pip3 install xxx
+80 -12
View File
@@ -34,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 = 'http://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
@@ -115,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;
} }
@@ -133,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;
} }
@@ -216,8 +230,8 @@ async function sendNotify(
qywxBotNotify(text, desp), //企业微信机器人 qywxBotNotify(text, desp), //企业微信机器人
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 gotifyNotify(text, desp), //gotify
]); ]);
} }
@@ -226,10 +240,12 @@ function gotifyNotify(text, desp) {
if (GOTIFY_URL && GOTIFY_TOKEN) { if (GOTIFY_URL && GOTIFY_TOKEN) {
const options = { const options = {
url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`, url: `${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}`,
body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(desp)}&priority=${GOTIFY_PRIORITY}`, body: `title=${encodeURIComponent(text)}&message=${encodeURIComponent(
desp,
)}&priority=${GOTIFY_PRIORITY}`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
} },
}; };
$.post(options, (err, resp, data) => { $.post(options, (err, resp, data) => {
try { try {
@@ -261,7 +277,7 @@ function gobotNotify(text, desp, time = 2100) {
if (GOBOT_URL) { if (GOBOT_URL) {
const options = { const options = {
url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`, url: `${GOBOT_URL}?access_token=${GOBOT_TOKEN}&${GOBOT_QQ}`,
json: {message:`${text}\n${desp}`}, json: { message: `${text}\n${desp}` },
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@@ -346,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) {
@@ -429,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',
}, },
@@ -465,9 +529,13 @@ function tgBotNotify(text, desp) {
if (TG_BOT_TOKEN && TG_USER_ID) { if (TG_BOT_TOKEN && TG_USER_ID) {
const options = { const options = {
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`, url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
body: `chat_id=${TG_USER_ID}&text=${text}\n\n${desp}&disable_web_page_preview=true`, json: {
chat_id: `${TG_USER_ID}`,
text: `${text}\n\n${desp}`,
disable_web_page_preview: true,
},
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/json',
}, },
timeout, timeout,
}; };
@@ -726,8 +794,8 @@ function qywxamNotify(text, desp) {
if (err) { if (err) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息失败!!\n', '企业微信应用消息发送通知消息失败!!\n',
); );
console.log(err); console.log(err);
} else { } else {
@@ -735,8 +803,8 @@ function qywxamNotify(text, desp) {
if (data.errcode === 0) { if (data.errcode === 0) {
console.log( console.log(
'成员ID:' + '成员ID:' +
ChangeUserId(desp) + ChangeUserId(desp) +
'企业微信应用消息发送通知消息成功🎉。\n', '企业微信应用消息发送通知消息成功🎉。\n',
); );
} else { } else {
console.log(`${data.errmsg}\n`); console.log(`${data.errmsg}\n`);
+25
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, # 控制台输出
@@ -59,6 +60,8 @@ push_config = {
'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+ 微信推送的群组编码
@@ -104,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(
@@ -259,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": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
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+ 推送消息。
@@ -285,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:
@@ -502,6 +525,8 @@ 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"):
+40
View File
@@ -0,0 +1,40 @@
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 key = 'version.ts';
const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`,
};
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; charset=utf-8',
);
// 文件上传
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=$2
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"
repo_path="${dir_repo}/dockerbot" if [[ -z ${BotRepoUrl} ]]; then
url="https://github.com/SuMaiKaDe/bot.git"
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,9 +32,9 @@ 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 [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
@@ -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"
+4 -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"
} }
@@ -36,22 +34,12 @@ copy_dep() {
echo -e "---> 配置文件复制完成\n" echo -e "---> 配置文件复制完成\n"
} }
reload_pm2() {
pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
}
pm2_log() { pm2_log() {
echo -e "---> pm2日志" echo -e "---> pm2日志"
local panelOut="/root/.pm2/logs/panel-out.log" local panelOut="/root/.pm2/logs/panel-out.log"
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() {
@@ -100,6 +88,7 @@ check_pm2() {
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
pnpm add -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"
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
echo -e "开始发布"
echo -e "切换master分支"
git checkout master
echo -e "合并develop代码"
git merge origin/develop
echo -e "提交master代码"
git push
echo -e "更新cdn文件"
ts-node sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*")
version="v$string"
echo -e "当前版本$version"
echo -e "删除已经存在的本地tag"
git tag -d "$version" &>/dev/null
echo -e "删除已经存在的远程tag"
git push origin :refs/tags/$version &>/dev/null
echo -e "创建新tag"
git tag -a "$version" -m "release $version"
echo -e "提交tag"
git push --tags
echo -e "完成发布"
+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
+87 -20
View File
@@ -1,16 +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_dep=$dir_root/deps 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
@@ -35,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,7 +61,8 @@ 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:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
} }
import_config() { import_config() {
@@ -65,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}"
@@ -75,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
@@ -147,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
@@ -207,19 +227,34 @@ fix_config() {
if [[ -s /etc/nginx/conf.d/default.conf ]]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n" echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null > /etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
echo echo
fi fi
if [[ ! -s $dep_notify_js ]]; then
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
npm install --production --registry=https://registry.npm.taobao.org || npm install --production npm install --production --registry=https://registry.npm.taobao.org || npm install --production
else else
pnpm install --production --registry=https://registry.npm.taobao.org || pnpm install --production pnpm install --loglevel error --production --registry=https://registry.npm.taobao.org || pnpm install --production --loglevel error
fi fi
unset_proxy
} }
npm_install_1() { npm_install_1() {
@@ -272,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
} }
@@ -296,27 +337,53 @@ 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
echo $((RANDOM % ($end - $beg) + $beg)) echo $((RANDOM % ($end - $beg) + $beg))
} }
reload_pm2() {
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
init_env init_env
detect_termux detect_termux
detect_macos detect_macos
define_cmd define_cmd
fix_config fix_config
import_config $1 > $task_error_log_path 2>&1 import_config $1 >$task_error_log_path 2>&1
+108 -65
View File
@@ -1,20 +1,20 @@
#!/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
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local first_param=$1 local file_param=$1
if [[ $first_param == *.js ]]; then if [[ $file_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $first_param == *.py ]]; then elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3" which_program="python3"
elif [[ $first_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $first_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" which_program="ts-node-transpile-only"
else else
which_program="" which_program=""
@@ -76,45 +76,53 @@ run_nohup() {
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
run_normal() { run_normal() {
local first_param=$1 local file_param=$1
define_program "$first_param" define_program "$file_param"
if [[ $first_param == *.js ]]; then if [[ $file_param == *.js ]]; then
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
random_delay random_delay
fi fi
fi fi
log_time=$(date "+%Y-%m-%d-%H-%M-%S") local time=$(date)
log_dir_tmp="${first_param##*/}" log_time=$(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp_path="${first_param%%/*}" log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}" log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}" log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path" cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir" make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date -d "$time" "+%Y-%m-%d %H:%M:%S")
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date -d "$time" "+%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 $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd eval . $file_task_before "$@" $cmd
cd $dir_scripts cd $dir_scripts
local relative_path="${first_param%/*}" local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
first_param=${first_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
eval timeout -k 10s $command_timeout_time $which_program $first_param $cmd eval timeout -k 10s $command_timeout_time $which_program $file_param $cmd
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
@@ -122,7 +130,7 @@ run_normal() {
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行 ## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() { run_concurrent() {
local first_param="$1" local file_param="$1"
local env_param="$2" local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|") local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then if [[ ! $env_param ]]; then
@@ -143,57 +151,64 @@ 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 "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S") local time=$(date)
log_dir_tmp="${first_param##*/}" log_time=$(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp_path="${first_param%%/*}" log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}" log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}" log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path" cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir $log_dir make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date -d "$time" "+%Y-%m-%d %H:%M:%S")
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date -d "$time" "+%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 $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $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="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${first_param} =~ "/" ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path} cd ${relative_path}
first_param=${first_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
for i in "${!array[@]}"; do for i in "${!array[@]}"; do
export ${env_param}=${array[i]} export ${env_param}=${array[i]}
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log" single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
timeout -k 10s $command_timeout_time $which_program $first_param &>$single_log_path & timeout -k 10s $command_timeout_time $which_program $file_param &>$single_log_path &
done done
wait wait
for i in "${!array[@]}"; do for i in "${!array[@]}"; do
single_log_path="$log_dir/${single_log_time}_$((i + 1)).log" single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval cat $single_log_path $cmd eval cat $single_log_path $cmd
[[ -f $single_log_path ]] && rm -f $single_log_path [[ -f $single_log_path ]] && rm -f $single_log_path
done done
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
@@ -209,19 +224,27 @@ run_designated() {
fi fi
define_program "$file_param" define_program "$file_param"
log_time=$(date "+%Y-%m-%d-%H-%M-%S") local time=$(date)
log_time=$(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}" log_dir_tmp="${file_param##*/}"
log_dir_tmp_path="${file_param%%/*}" if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}" log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}" [[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="$dir_log/${log_dir_tmp%%.*}" log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path" cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir $log_dir make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date -d "$time" "+%Y-%m-%d %H:%M:%S")
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date -d "$time" "+%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 +259,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,8 +268,6 @@ 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} ]] && [[ ${file_param} =~ "/" ]]; then if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
@@ -256,7 +278,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 +286,50 @@ 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%%.*}" local time=$(date)
log_time=$(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}"
log_path="$log_dir/$log_time.log" log_path="$log_dir/$log_time.log"
cmd="&>> $log_path" cmd="&>> $dir_log/$log_path"
[[ "$show_log" == "true" ]] && cmd="" [[ "$show_log" == "true" ]] && cmd=""
make_dir "$log_dir" make_dir "$dir_log/$log_dir"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date -d "$time" "+%Y-%m-%d %H:%M:%S")
local begin_timestamp=$(date "+%s" -d "$begin_time") local begin_timestamp=$(date -d "$time" "+%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
@@ -327,7 +370,7 @@ main() {
esac esac
;; ;;
esac esac
[[ -f $log_path ]] && cat $log_path [[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
elif [[ $# -eq 0 ]]; then elif [[ $# -eq 0 ]]; then
echo echo
usage usage
+85 -70
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"
} }
## 更新仓库 ## 更新仓库
@@ -141,6 +141,7 @@ update_repo() {
local blackword="$3" local blackword="$3"
local dependence="$4" local dependence="$4"
local branch="$5" local branch="$5"
local extensions="$6"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
@@ -151,11 +152,6 @@ 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}"
@@ -164,17 +160,9 @@ update_repo() {
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
else else
echo -e "\n更新${repo_path}失败,重新下载全新仓库...\n" echo -e "\n更新${repo_path}失败,请检查网络...\n"
rm -rf ${repo_path}
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
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"
fi
fi fi
} }
@@ -183,15 +171,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 +204,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
@@ -242,15 +229,15 @@ run_extra_shell() {
## 脚本用法 ## 脚本用法
usage() { usage() {
echo -e "本脚本用法:" echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙" echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本" echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件" echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> # 更新单个仓库的脚本" echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志" echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复" echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数" echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录" echo -e "9. $cmd_update resettfa # 禁用两步登录"
} }
## 更新qinglong ## 更新qinglong
@@ -258,12 +245,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 +263,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 +287,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
@@ -312,17 +304,39 @@ patch_version() {
pnpm i -g ts-node typescript tslib pnpm i -g ts-node typescript tslib
fi fi
# 兼容pnpm@7
npm i -g pnpm
pnpm setup
source ~/.bashrc
git config --global pull.rebase false git config --global pull.rebase false
}
reload_pm2() { cp -f $dir_root/.env.example $dir_root/.env
pm2 l &>/dev/null
pm2 delete panel --source-map-support --time &>/dev/null if [[ -d "$dir_root/db" ]]; then
pm2 start $dir_root/build/app.js -n panel --source-map-support --time &>/dev/null 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
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_root/build/schedule.js -n schedule --source-map-support --time &>/dev/null
} }
## 对比脚本 ## 对比脚本
@@ -333,8 +347,9 @@ diff_scripts() {
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
local list_add="$dir_list_tmp/${uniq_path}_add.list" local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list" local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
@@ -370,6 +385,9 @@ gen_list_repo() {
local cmd="find ." local cmd="find ."
local index=0 local index=0
if [[ $6 ]]; then
file_extensions="$6"
fi
for extension in $file_extensions; do for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\"" cmd="${cmd} -name \"*.${extension}\""
@@ -400,7 +418,7 @@ gen_list_repo() {
fi fi
if [[ -d $dir_dep ]]; then if [[ -d $dir_dep ]]; then
cp $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi fi
for file in ${files}; do for file in ${files}; do
@@ -450,6 +468,7 @@ main() {
local p4=$4 local p4=$4
local p5=$5 local p5=$5
local p6=$6 local p6=$6
local p7=$7
local log_time=$(date "+%Y-%m-%d-%H-%M-%S") local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log" local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S') local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
@@ -470,11 +489,8 @@ main() {
repo) repo)
get_user_info get_user_info
get_uniq_path "$p2" "$p6" get_uniq_path "$p2" "$p6"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" >>$log_path update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
@@ -483,11 +499,8 @@ main() {
raw) raw)
get_user_info get_user_info
get_uniq_path "$p2" get_uniq_path "$p2"
log_path="$dir_log/update/${log_time}_${uniq_path}.log"
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
if [[ -n $p2 ]]; then if [[ -n $p2 ]]; then
update_raw "$p2" >>$log_path update_raw "$p2"
else else
echo -e "命令输入错误...\n" echo -e "命令输入错误...\n"
usage usage
@@ -527,8 +540,10 @@ main() {
esac esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S') local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) local diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
cat $log_path echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path
cat $log_path
fi
} }
main "$@" main "$@"
+7
View File
@@ -0,0 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
});
export default IconFont;
+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);
}
+110
View File
@@ -0,0 +1,110 @@
import { Tag, Input } from 'antd';
import { TweenOneGroup } from 'rc-tween-one';
import { PlusOutlined } from '@ant-design/icons';
import { useEffect, useRef, useState } from 'react';
const EditableTagGroup = ({
value,
onChange,
}: {
value?: string[];
onChange?: (tags: string[]) => void;
}) => {
const [inputValue, setInputValue] = useState('');
const [inputVisible, setInputVisible] = useState(false);
const [tags, setTags] = useState<string[]>([]);
const saveInputRef = useRef<any>();
const handleClose = (removedTag: string) => {
const _tags = tags.filter((tag) => tag !== removedTag);
setTags(_tags);
onChange?.(_tags);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = (e) => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && !tags.includes(inputValue)) {
setTags([...tags, inputValue]);
onChange?.([...tags, inputValue]);
}
setInputVisible(false);
setInputValue('');
};
const tagChild = tags.map((tag) => {
const tagElem = (
<Tag
closable
onClose={(e) => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: 'inline-block', marginBottom: 8 }}>
{tagElem}
</span>
);
});
useEffect(() => {
if (inputVisible && saveInputRef) {
saveInputRef.current.focus();
}
}, [inputVisible]);
useEffect(() => {
if (value) {
setTags(value);
}
}, [value]);
return (
<>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: 'from',
duration: 100,
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{tagChild}
</TweenOneGroup>
{inputVisible && (
<Input
ref={saveInputRef}
type="text"
size="small"
style={{ width: 78 }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)}
{!inputVisible && (
<Tag
onClick={showInput}
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
>
<PlusOutlined />
</Tag>
)}
</>
);
};
export default EditableTagGroup;
+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;
+20 -8
View File
@@ -9,6 +9,7 @@ import {
ControlOutlined, ControlOutlined,
ContainerOutlined, ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
export default { export default {
route: { route: {
@@ -25,46 +26,58 @@ 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: '定时任务',
icon: <FieldTimeOutlined />, icon: <IconFont type="ql-icon-crontab" />,
component: '@/pages/crontab/index', component: '@/pages/crontab/index',
}, },
{
path: '/subscription',
name: '订阅管理',
icon: <IconFont type="ql-icon-subs" />,
component: '@/pages/subscription/index',
},
{ {
path: '/env', path: '/env',
name: '环境变量', name: '环境变量',
icon: <RadiusSettingOutlined />, icon: <IconFont type="ql-icon-env" />,
component: '@/pages/env/index', component: '@/pages/env/index',
}, },
{ {
path: '/config', path: '/config',
name: '配置文件', name: '配置文件',
icon: <ControlOutlined />, icon: <IconFont type="ql-icon-config" />,
component: '@/pages/config/index', component: '@/pages/config/index',
}, },
{ {
path: '/script', path: '/script',
name: '脚本管理', name: '脚本管理',
icon: <FormOutlined />, icon: <IconFont type="ql-icon-script" />,
component: '@/pages/script/index', component: '@/pages/script/index',
}, },
{ {
path: '/dependence', path: '/dependence',
name: '依赖管理', name: '依赖管理',
icon: <ContainerOutlined />, icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index', component: '@/pages/dependence/index',
}, },
{ {
path: '/diff', path: '/diff',
name: '对比工具', name: '对比工具',
icon: <DiffOutlined />, icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index', component: '@/pages/diff/index',
}, },
{ {
path: '/log', path: '/log',
name: '任务日志', name: '任务日志',
icon: <FolderOutlined />, icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index', component: '@/pages/log/index',
}, },
{ {
@@ -79,6 +92,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;
+53 -14
View File
@@ -1,4 +1,4 @@
@import '~antd/lib/style/themes/default.less'; @import '~antd/es/style/themes/default.less';
@import '~@/styles/variable.less'; @import '~@/styles/variable.less';
@import '~codemirror/lib/codemirror.css'; @import '~codemirror/lib/codemirror.css';
@@ -9,6 +9,12 @@
url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype'); url('../assets/fonts/SourceCodePro-Regular.ttf') format('truetype');
} }
.ant-modal-body {
max-height: calc(85vh - 110px);
max-height: calc(85vh - var(--vh-offset, 110px));
overflow-y: auto;
}
.log-modal { .log-modal {
.ant-modal { .ant-modal {
padding-bottom: 0 !important; padding-bottom: 0 !important;
@@ -26,6 +32,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 +40,7 @@
.rename-box { .rename-box {
height: 0; height: 0;
.rename-input { .rename-input {
height: 0; height: 0;
padding: 0 !important; padding: 0 !important;
@@ -43,10 +51,11 @@
.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%;
background-color: #fff; background-color: @component-background;
padding: 16px; padding: 16px;
} }
} }
@@ -57,6 +66,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 +82,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 +92,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 +131,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,12 +167,12 @@
.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 {
background: #fff; background: @component-background;
opacity: 0.2; opacity: 0.2;
z-index: 1; z-index: 1;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
@@ -197,6 +212,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 +225,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 +252,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 +270,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;
} }
@@ -265,7 +284,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
color: rgba(0, 0, 0, 0.85); color: @text-color;
.side-menu-collapse-button:hover { .side-menu-collapse-button:hover {
color: #1890ff; color: #1890ff;
@@ -279,16 +298,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 fade(@component-background, 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: @component-background;
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;
} }
} }
+72 -61
View File
@@ -1,11 +1,6 @@
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import { import * as DarkReader from '@umijs/ssr-darkreader';
enable as enableDarkMode,
disable as disableDarkMode,
auto as followSystemColorScheme,
setFetchMethod,
} from 'darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
import { Link, history } from 'umi'; import { Link, history } from 'umi';
import { import {
@@ -20,31 +15,31 @@ 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();
const theme = useTheme(); const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>(); const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>(); const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
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');
}); });
@@ -91,26 +86,10 @@ export default function (props: any) {
}); });
}; };
const reloadUser = () => { const reloadUser = (needLoading = false) => {
getUser(false); getUser(needLoading);
}; };
const setTheme = () => {
const media = window.matchMedia('(prefers-color-scheme: dark)');
const storageTheme = localStorage.getItem('qinglong_dark_theme');
const isDark =
(media.matches && storageTheme !== 'light') || storageTheme === 'dark';
if (isDark) {
document.body.setAttribute('data-dark', 'true');
} else {
document.body.setAttribute('data-dark', 'false');
}
};
useEffect(() => {
vhCheck();
}, []);
useEffect(() => { useEffect(() => {
if (systemInfo && systemInfo.isInitialized && !user) { if (systemInfo && systemInfo.isInitialized && !user) {
getUser(); getUser();
@@ -124,12 +103,25 @@ export default function (props: any) {
}, [systemInfo]); }, [systemInfo]);
useEffect(() => { useEffect(() => {
setTheme(); if (theme === 'vs-dark') {
}, [theme.theme]); document.body.setAttribute('data-dark', 'true');
} else {
document.body.setAttribute('data-dark', 'false');
}
}, [theme]);
useEffect(() => { useEffect(() => {
vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
setFetchMethod(window.fetch); if (typeof window === 'undefined') return;
if (typeof window.matchMedia === 'undefined') return;
if (!DarkReader) {
return () => null;
}
setFetchMethod(fetch);
if (_theme === 'dark') { if (_theme === 'dark') {
enableDarkMode({}); enableDarkMode({});
} else if (_theme === 'light') { } else if (_theme === 'light') {
@@ -137,6 +129,10 @@ export default function (props: any) {
} else { } else {
followSystemColorScheme({}); followSystemColorScheme({});
} }
return () => {
disableDarkMode();
};
}, []); }, []);
useEffect(() => { useEffect(() => {
@@ -187,7 +183,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,14 +196,14 @@ 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,
...theme, theme,
user, user,
reloadUser, reloadUser,
reloadTheme: setTheme, reloadTheme,
ws: ws.current, ws: ws.current,
}); });
}); });
@@ -219,11 +217,11 @@ export default function (props: any) {
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = ( const menu = (
<Menu className="side-menu-user-drop-menu"> <Menu
<Menu.Item key="1" icon={<LogoutOutlined />} onClick={logout}> className="side-menu-user-drop-menu"
退 items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]}
</Menu.Item> onClick={logout}
</Menu> />
); );
return loading ? ( return loading ? (
<PageLoading /> <PageLoading />
@@ -232,6 +230,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="http://qn.whyour.cn/logo.png" />}
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
@@ -275,14 +274,21 @@ export default function (props: any) {
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => ( rightContentRender={() =>
<Dropdown overlay={menu} trigger={['click']}> ctx.isPhone && (
<span className="side-menu-user-wrapper"> <Dropdown overlay={menu} placement="bottomRight" trigger={['click']}>
<Avatar shape="square" size="small" icon={<UserOutlined />} /> <span className="side-menu-user-wrapper">
<span style={{ marginLeft: 5 }}>admin</span> <Avatar
</span> shape="square"
</Dropdown> size="small"
)} icon={<UserOutlined />}
src={`/api/static/${user.avatar}`}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
</Dropdown>
)
}
collapsedButtonRender={(collapsed) => ( collapsedButtonRender={(collapsed) => (
<span <span
className="side-menu-container" className="side-menu-container"
@@ -292,10 +298,15 @@ 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
<span style={{ marginLeft: 5 }}>admin</span> shape="square"
size="small"
icon={<UserOutlined />}
src={`/api/static/${user.avatar}`}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
</Dropdown> </Dropdown>
)} )}
@@ -312,10 +323,10 @@ export default function (props: any) {
{React.Children.map(props.children, (child) => { {React.Children.map(props.children, (child) => {
return React.cloneElement(child, { return React.cloneElement(child, {
...ctx, ...ctx,
...theme, theme,
user, user,
reloadUser, reloadUser,
reloadTheme: setTheme, reloadTheme,
socketMessage, socketMessage,
}); });
})} })}
+558
View File
@@ -0,0 +1,558 @@
import React, { useEffect, useRef, useState } from 'react';
import {
Modal,
message,
Input,
Form,
Button,
Card,
Tag,
List,
Divider,
Typography,
Tooltip,
} from 'antd';
import {
ClockCircleOutlined,
CloseCircleOutlined,
FieldTimeOutlined,
Loading3QuartersOutlined,
FileOutlined,
PlayCircleOutlined,
PauseCircleOutlined,
} 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';
import IconFont from '@/components/iconfont';
const { Text } = Typography;
const tabList = [
{
key: 'log',
tab: '日志',
},
{
key: 'script',
tab: '脚本',
},
];
const LangMap: any = {
'.py': 'python',
'.js': 'javascript',
'.sh': 'shell',
'.ts': 'typescript',
};
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 [currentCron, setCurrentCron] = useState<any>({});
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: scriptInfo.filename && (
<Editor
language={LangMap[scriptInfo.filename.slice(-3)] || ''}
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', currentCron.id);
setLogUrl(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
);
request
.get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
)
.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');
},
});
};
const runCron = () => {
Modal.confirm({
title: '确认运行',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then((data: any) => {
if (data.code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => {
getLogs();
}, 1000);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const stopCron = () => {
Modal.confirm({
title: '确认停止',
content: (
<>
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then((data: any) => {
if (data.code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const enabledOrDisabledCron = () => {
Modal.confirm({
title: `确认${currentCron.isDisabled === 1 ? '启用' : '禁用'}`,
content: (
<>
{currentCron.isDisabled === 1 ? '启用' : '禁用'}
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
currentCron.isDisabled === 1 ? 'enable' : 'disable'
}`,
{
data: [currentCron.id],
},
)
.then((data: any) => {
if (data.code === 200) {
setCurrentCron({
...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
});
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const pinOrUnPinCron = () => {
Modal.confirm({
title: `确认${currentCron.isPinned === 1 ? '取消置顶' : '置顶'}`,
content: (
<>
{currentCron.isPinned === 1 ? '取消置顶' : '置顶'}
{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{currentCron.name}
</Text>{' '}
</>
),
onOk() {
request
.put(
`${config.apiPrefix}crons/${
currentCron.isPinned === 1 ? 'unpin' : 'pin'
}`,
{
data: [currentCron.id],
},
)
.then((data: any) => {
if (data.code === 200) {
setCurrentCron({
...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1,
});
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
useEffect(() => {
if (cron && cron.id) {
setCurrentCron(cron);
getLogs();
getScript();
}
}, [cron]);
return (
<Modal
title={
<div className="crontab-title-wrapper">
<div>
<span>{currentCron.name}</span>
{currentCron.labels?.length > 0 && currentCron.labels[0] !== '' && (
<Divider type="vertical"></Divider>
)}
{currentCron.labels?.length > 0 &&
currentCron.labels[0] !== '' &&
currentCron.labels?.map((label: string, i: number) => (
<Tag color="blue" style={{ marginRight: 5 }}>
{label}
</Tag>
))}
</div>
<div className="operations">
<Tooltip
title={
currentCron.status === CrontabStatus.idle ? '运行' : '停止'
}
>
<Button
type="link"
icon={
currentCron.status === CrontabStatus.idle ? (
<PlayCircleOutlined />
) : (
<PauseCircleOutlined />
)
}
size="small"
onClick={
currentCron.status === CrontabStatus.idle ? runCron : stopCron
}
/>
</Tooltip>
<Tooltip title={currentCron.isDisabled === 1 ? '启用' : '禁用'}>
<Button
type="link"
icon={
<IconFont
type={
currentCron.isDisabled === 1
? 'ql-icon-enable'
: 'ql-icon-disable'
}
/>
}
size="small"
onClick={enabledOrDisabledCron}
/>
</Tooltip>
<Tooltip title={currentCron.isPinned === 1 ? '取消置顶' : '置顶'}>
<Button
type="link"
icon={
<IconFont
type={
currentCron.isPinned === 1
? 'ql-icon-untop'
: 'ql-icon-top'
}
/>
}
size="small"
onClick={pinOrUnPinCron}
/>
</Tooltip>
</div>
</div>
}
centered
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">{currentCron.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">
{(!currentCron.isDisabled ||
currentCron.status !== CrontabStatus.idle) && (
<>
{currentCron.status === CrontabStatus.idle && (
<Tag icon={<ClockCircleOutlined />} color="default">
</Tag>
)}
{currentCron.status === CrontabStatus.running && (
<Tag
icon={<Loading3QuartersOutlined spin />}
color="processing"
>
</Tag>
)}
{currentCron.status === CrontabStatus.queued && (
<Tag icon={<FieldTimeOutlined />} color="default">
</Tag>
)}
</>
)}
{currentCron.isDisabled === 1 &&
currentCron.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">{currentCron.schedule}</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{currentCron.last_execution_time
? new Date(currentCron.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">
{currentCron.last_running_time
? diffTime(currentCron.last_running_time)
: '-'}
</div>
</div>
<div className="cron-detail-info-item">
<div className="cron-detail-info-title"></div>
<div className="cron-detail-info-value">
{currentCron.nextRunTime &&
currentCron.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;
+100
View File
@@ -1,3 +1,103 @@
.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;
}
}
.crontab-title-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
margin-right: 32px;
.operations {
display: flex;
align-items: center;
.ant-btn:not(:first-child) {
margin-left: 8px;
}
}
}
}
.log-item {
cursor: pointer;
&:hover {
background: #fafafa;
}
}
+229 -134
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,25 +70,56 @@ enum OperationPath {
'unpin', 'unpin',
} }
const Crontab = ({ headerStyle, isPhone }: any) => { const Crontab = ({ headerStyle, isPhone, theme }: any) => {
const columns: any = [ const columns: any = [
{ {
title: '任务名', title: '名',
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
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
{record.isPinned ? ( onClick={() => {
<span> goToScriptManager(record);
<PushpinOutlined /> }}
</span> >
) : ( {record.labels?.length > 0 && record.labels[0] !== '' ? (
'' <Popover
)} placement="right"
</span> 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 ? (
<span>
<PushpinOutlined />
</span>
) : (
''
)}
</a>
</>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a.name.localeCompare(b.name), compare: (a: any, b: any) => a.name.localeCompare(b.name),
@@ -93,23 +127,23 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}, },
}, },
{ {
title: '任务', title: '命令',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 250, width: 250,
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: {
@@ -118,7 +152,7 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}, },
}, },
{ {
title: '任务定时', title: '定时规则',
dataIndex: 'schedule', dataIndex: 'schedule',
key: 'schedule', key: 'schedule',
width: 110, width: 110,
@@ -166,18 +200,9 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}, },
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; return record.last_running_time
return ( ? diffTime(record.last_running_time)
<span : '-';
style={{
display: 'block',
}}
>
{record.last_running_time
? diffTime(record.last_running_time)
: '-'}
</span>
);
}, },
}, },
{ {
@@ -191,19 +216,11 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return ( return record.nextRunTime
<span .toLocaleString(language, {
style={{ hour12: false,
display: 'block', })
}} .replace(' 24:', ' 00:');
>
{record.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</span>
);
}, },
}, },
@@ -282,7 +299,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 +311,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 +322,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 +340,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 +349,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 +379,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
return CrontabSort[sortA] - CrontabSort[sortB]; : a.status;
} const sortB =
return b.isPinned - a.isPinned; b.isPinned && b.status !== 0
? 5
: b.isDisabled && b.status !== 0
? 4
: b.status;
return CrontabSort[sortA] - CrontabSort[sortB];
}) })
.map((x) => { .map((x) => {
return { return {
@@ -385,14 +432,16 @@ 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); if (i !== -1) {
setValue(result); result.splice(i, 1);
setValue(result);
}
} else { } else {
message.error(data); message.error(data);
} }
@@ -418,16 +467,18 @@ 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, { if (i !== -1) {
...record, result.splice(i, 1, {
status: CrontabStatus.running, ...record,
}); status: CrontabStatus.running,
setValue(result); });
setValue(result);
}
} else { } else {
message.error(data); message.error(data);
} }
@@ -453,17 +504,19 @@ 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, { if (i !== -1) {
...record, result.splice(i, 1, {
pid: null, ...record,
status: CrontabStatus.idle, pid: null,
}); status: CrontabStatus.idle,
setValue(result); });
setValue(result);
}
} else { } else {
message.error(data); message.error(data);
} }
@@ -495,19 +548,21 @@ 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, { if (i !== -1) {
...record, result.splice(i, 1, {
isDisabled: newStatus, ...record,
}); isDisabled: newStatus,
setValue(result); });
setValue(result);
}
} else { } else {
message.error(data); message.error(data);
} }
@@ -539,19 +594,21 @@ 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, { if (i !== -1) {
...record, result.splice(i, 1, {
isPinned: newStatus, ...record,
}); isPinned: newStatus,
setValue(result); });
setValue(result);
}
} else { } else {
message.error(data); message.error(data);
} }
@@ -563,45 +620,43 @@ const Crontab = ({ headerStyle, isPhone }: any) => {
}); });
}; };
const getMenuItems = (record: any) => {
return [
{ label: '编辑', key: 'edit', icon: <EditOutlined /> },
{
label: record.isDisabled === 1 ? '启用' : '禁用',
key: 'enableOrDisable',
icon:
record.isDisabled === 1 ? <CheckCircleOutlined /> : <StopOutlined />,
},
{ label: '删除', key: 'delete', icon: <DeleteOutlined /> },
{
label: record.isPinned === 1 ? '取消置顶' : '置顶',
key: 'pinOrUnPin',
icon: record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />,
},
];
};
const MoreBtn: React.FC<{ const MoreBtn: React.FC<{
record: any; record: any;
index: number; index: number;
}> = ({ 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
<Menu.Item key="edit" icon={<EditOutlined />}> items={getMenuItems(record)}
onClick={({ key, domEvent }) => {
</Menu.Item> domEvent.stopPropagation();
<Menu.Item action(key, record, index);
key="enableOrDisable" }}
icon={ />
record.isDisabled === 1 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)
}
>
{record.isDisabled === 1 ? '启用' : '禁用'}
</Menu.Item>
<Menu.Item key="delete" icon={<DeleteOutlined />}>
</Menu.Item>
<Menu.Item
key="pinOrUnPin"
icon={
record.isPinned === 1 ? <StopOutlined /> : <PushpinOutlined />
}
>
{record.isPinned === 1 ? '取消置顶' : '置顶'}
</Menu.Item>
</Menu>
} }
> >
<a> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
</a> </a>
</Dropdown> </Dropdown>
@@ -638,7 +693,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,19 +711,21 @@ 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)
.next() .next()
.toDate(); .toDate();
result.splice(index, 1, { if (index !== -1) {
...cron, result.splice(index, 1, {
...data.data, ...cron,
}); ...data.data,
setValue(result); });
setValue(result);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -746,12 +803,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 +833,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 +890,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 +914,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 +945,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>
); );
}; };
+18 -10
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);
@@ -113,8 +123,6 @@ const CronLogModal = ({
centered centered
className="log-modal" className="log-modal"
bodyStyle={{ bodyStyle={{
overflowY: 'auto',
maxHeight: 'calc(80vh - var(--vh-offset, 0px))',
minHeight: '300px', minHeight: '300px',
}} }}
forceRender forceRender
+97 -14
View File
@@ -1,8 +1,9 @@
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';
import EditableTagGroup from '@/components/tag';
const CronModal = ({ const CronModal = ({
cron, cron,
@@ -21,18 +22,22 @@ const CronModal = ({
const method = cron ? 'put' : 'post'; const method = cron ? 'put' : 'post';
const payload = { ...values }; const payload = { ...values };
if (cron) { if (cron) {
payload._id = cron._id; payload.id = cron.id;
} }
const { code, data } = await request[method](`${config.apiPrefix}crons`, { try {
data: payload, const { code, data } = await request[method](`${config.apiPrefix}crons`, {
}); data: payload,
if (code === 200) { });
message.success(cron ? '更新Cron成功' : '添加Cron成功'); if (code === 200) {
} else { message.success(cron ? '更新Cron成功' : '新建Cron成功');
message.error(data); } else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
} }
setLoading(false);
handleCancel(data);
}; };
useEffect(() => { useEffect(() => {
@@ -41,9 +46,11 @@ const CronModal = ({
return ( return (
<Modal <Modal
title={cron ? '编辑定时' : '新建定时'} title={cron ? '编辑任务' : '新建任务'}
visible={visible} visible={visible}
forceRender forceRender
centered
maskClosable={false}
onOk={() => { onOk={() => {
form form
.validateFields() .validateFields()
@@ -84,7 +91,7 @@ const CronModal = ({
{ required: true }, { required: true },
{ {
validator: (rule, value) => { validator: (rule, value) => {
if (cronParse.parseExpression(value).hasNext()) { if (!value || cronParse.parseExpression(value).hasNext()) {
return Promise.resolve(); return Promise.resolve();
} else { } else {
return Promise.reject('Cron表达式格式有误'); return Promise.reject('Cron表达式格式有误');
@@ -95,9 +102,85 @@ const CronModal = ({
> >
<Input placeholder="秒(可选) 分 时 天 月 周" /> <Input placeholder="秒(可选) 分 时 天 月 周" />
</Form.Item> </Form.Item>
<Form.Item name="labels" label="标签">
<EditableTagGroup />
</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) => {
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}
centered
maskClosable={false}
forceRender
onCancel={() => handleCancel(false)}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="form_in_label_modal">
<Form.Item name="labels" label="标签">
<EditableTagGroup />
</Form.Item>
</Form>
</Modal>
);
};
export { CronModal as default, CronLabelModal };
+104 -45
View File
@@ -16,7 +16,7 @@ import {
DeleteOutlined, DeleteOutlined,
SyncOutlined, SyncOutlined,
CheckCircleOutlined, CheckCircleOutlined,
StopOutlined, DeleteFilled,
BugOutlined, BugOutlined,
FileTextOutlined, FileTextOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@@ -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>
);
}, },
}, },
{ {
@@ -99,6 +120,15 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
{record.status !== Status. && {record.status !== Status. &&
record.status !== Status. && ( record.status !== Status. && (
<> <>
@@ -112,17 +142,13 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
<DeleteOutlined /> <DeleteOutlined />
</a> </a>
</Tooltip> </Tooltip>
<Tooltip title={isPc ? '强制删除' : ''}>
<a onClick={() => deleteDependence(record, index, true)}>
<DeleteFilled />
</a>
</Tooltip>
</> </>
)} )}
<Tooltip title={isPc ? '日志' : ''}>
<a
onClick={() => {
setLogDependence({ ...record, timestamp: Date.now() });
}}
>
<FileTextOutlined />
</a>
</Tooltip>
</Space> </Space>
); );
}, },
@@ -161,7 +187,11 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
setIsModalVisible(true); setIsModalVisible(true);
}; };
const deleteDependence = (record: any, index: number) => { const deleteDependence = (
record: any,
index: number,
force: boolean = false,
) => {
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: ( content: (
@@ -175,10 +205,19 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
), ),
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}dependencies`, { data: [record._id] }) .delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
data: [record.id],
})
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
handleDependence(data.data[0]); if (force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
setValue(result);
}
}
} else { } else {
message.error(data); message.error(data);
} }
@@ -205,7 +244,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,10 +270,12 @@ 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, { if (index !== -1) {
...dependence, result.splice(index, 1, {
}); ...dependence,
});
}
} }
setValue(result); setValue(result);
}; };
@@ -254,13 +295,16 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
onChange: onSelectChange, onChange: onSelectChange,
}; };
const delDependencies = () => { const delDependencies = (force: boolean) => {
const forceUrl = force ? '/force' : '';
Modal.confirm({ Modal.confirm({
title: '确认删除', title: '确认删除',
content: <></>, content: <></>,
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}dependencies`, { data: selectedRowIds }) .delete(`${config.apiPrefix}dependencies${forceUrl}`, {
data: selectedRowIds,
})
.then((data: any) => { .then((data: any) => {
if (data.code === 200) { if (data.code === 200) {
setSelectedRowIds([]); setSelectedRowIds([]);
@@ -278,15 +322,17 @@ 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, { if (index !== -1) {
...dependence, result.splice(index, 1, {
...data.data, ...dependence,
}); ...data.data,
setValue(result); });
setValue(result);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -307,7 +353,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,11 +374,13 @@ 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, { if (index !== -1) {
...result[index], result.splice(index, 1, {
status, ...value[index],
}); status,
});
}
} }
setValue(result); setValue(result);
@@ -340,8 +388,10 @@ 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); if (index !== -1) {
_result.splice(index, 1);
}
} }
setValue(_result); setValue(_result);
}, 5000); }, 5000);
@@ -356,10 +406,17 @@ const Dependence = ({ headerStyle, isPhone, socketMessage }: any) => {
<Button <Button
type="primary" type="primary"
style={{ marginBottom: 5, marginLeft: 8 }} style={{ marginBottom: 5, marginLeft: 8 }}
onClick={delDependencies} onClick={() => delDependencies(false)}
> >
</Button> </Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a> <a>{selectedRowIds?.length}</a>
@@ -372,7 +429,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 +455,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 +489,13 @@ 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); if (index !== -1) {
setValue(result); result.splice(index, 1);
} else if ([...value].map((x) => x._id).includes(logDependence._id)) { setValue(result);
}
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
getDependenceDetail(logDependence); getDependenceDetail(logDependence);
} }
}} }}
+14 -11
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import Ansi from 'ansi-to-react';
const DependenceLogModal = ({ const DependenceLogModal = ({
dependence, dependence,
@@ -46,10 +47,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 +65,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);
@@ -89,17 +90,19 @@ const DependenceLogModal = ({
}, [dependence]); }, [dependence]);
useEffect(() => { useEffect(() => {
if (!socketMessage) return; if (!socketMessage || !dependence) return;
const { type, message, references } = socketMessage; const { type, message, references } = socketMessage;
if ( if (
type === 'installDependence' && type === 'installDependence' &&
message.includes('结束时间') && references.length > 0 &&
references.length > 0 references.includes(dependence.id)
) { ) {
setExecuting(false); if (message.includes('结束时间')) {
setIsRemoveFailed(message.includes('删除失败')); setExecuting(false);
setIsRemoveFailed(message.includes('删除失败'));
}
setValue(`${value}${message}`);
} }
setValue(`${value} \n ${message}`);
}, [socketMessage]); }, [socketMessage]);
useEffect(() => { useEffect(() => {
@@ -140,7 +143,7 @@ const DependenceLogModal = ({
: {} : {}
} }
> >
{value} <Ansi>{value}</Ansi>
</pre> </pre>
)} )}
</Modal> </Modal>
+6 -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](
@@ -70,6 +71,8 @@ const DependenceModal = ({
title={dependence ? '编辑依赖' : '新建依赖'} title={dependence ? '编辑依赖' : '新建依赖'}
visible={visible} visible={visible}
forceRender forceRender
centered
maskClosable={false}
onOk={() => { onOk={() => {
form form
.validateFields() .validateFields()
+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;
} }
+2
View File
@@ -41,6 +41,8 @@ const EditNameModal = ({
title="修改环境变量名称" title="修改环境变量名称"
visible={visible} visible={visible}
forceRender forceRender
centered
maskClosable={false}
onOk={() => { onOk={() => {
form form
.validateFields() .validateFields()
+27 -19
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>;
}, },
@@ -119,18 +119,18 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
key: 'value', key: 'value',
align: 'center' as const, align: 'center' as const,
width: '35%', width: '35%',
ellipsis: {
showTitle: false,
},
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Tooltip <Paragraph
placement="topLeft" style={{
title={text} wordBreak: 'break-all',
trigger={['hover', 'click']} marginBottom: 0,
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
> >
<span>{text}</span> {text}
</Tooltip> </Paragraph>
); );
}, },
}, },
@@ -149,9 +149,17 @@ const Env = ({ headerStyle, isPhone, theme }: any) => {
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,
}) })
@@ -197,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;
@@ -270,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) => {
@@ -321,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('删除成功');
@@ -351,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);
@@ -376,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) => {
@@ -485,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={{
@@ -534,7 +542,7 @@ 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: 1000, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
+17 -11
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 };
} }
const { code, data } = await request[method](`${config.apiPrefix}envs`, { try {
data: payload, const { code, data } = await request[method](`${config.apiPrefix}envs`, {
}); data: payload,
if (code === 200) { });
message.success(env ? '更新变量成功' : '添加变量成功'); if (code === 200) {
} else { message.success(env ? '更新变量成功' : '新建变量成功');
message.error(data); } else {
message.error(data);
}
setLoading(false);
handleCancel(data);
} catch (error: any) {
setLoading(false);
} }
setLoading(false);
handleCancel(data);
}; };
useEffect(() => { useEffect(() => {
@@ -57,6 +61,8 @@ const EnvModal = ({
title={env ? '编辑变量' : '新建变量'} title={env ? '编辑变量' : '新建变量'}
visible={visible} visible={visible}
forceRender forceRender
centered
maskClosable={false}
onOk={() => { onOk={() => {
form form
.validateFields() .validateFields()
@@ -77,7 +83,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: '只能输入字母数字下划线,且不能以数字开头',
}, },
]} ]}

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