Compare commits

..
641 Commits
Author SHA1 Message Date
whyour 325482cbb1 更新版本 v2.13.4 2022-07-16 12:31:50 +08:00
whyour 45e02a55c3 修改npm镜像源 2022-07-16 12:25:14 +08:00
whyour 6b8ef75b51 修复获取环境变量详情 2022-07-16 12:16:33 +08:00
whyour f2535ece76 移除内置token判断 2022-07-11 21:21:35 +08:00
whyour b2afa65cb1 任务详情编辑脚本支持全屏 2022-07-04 22:44:51 +08:00
whyour 3a62cc37ff 环境变量增加导出功能 2022-07-04 22:24:50 +08:00
whyour 93c69826ad 脚本管理按文件类型排序 2022-06-26 22:25:21 +08:00
whyour 70727d0e26 修复mac和linux兼容性 2022-06-26 18:59:39 +08:00
whyour 2578aae0ba 修复手动停止任务耗时日志 2022-06-19 14:32:09 +08:00
whyour 616e56ea95 修复系统token验证 2022-06-18 19:24:56 +08:00
whyour 96ae20de68 修复系统token权限 2022-06-18 19:18:14 +08:00
whyour 6975ff4aa1 修复repo命令 2022-06-18 19:03:50 +08:00
whyour 1d8a1e9c4a 修复repo命令 2022-06-18 19:00:22 +08:00
whyour 2719280cdc 修改结束message 2022-06-18 12:45:38 +08:00
whyour 575e41172f 修改日期打印 2022-06-18 12:43:23 +08:00
whyour bd50b1e976 Merge branch 'develop' of github.com:whyour/qinglong into develop 2022-06-18 12:30:11 +08:00
whyour dfb2572b4f 修复node kill进程逻辑 2022-06-18 12:30:02 +08:00
二毛andGitHub 7746b18023 区分原生termux和termux中安装的Linux发行版 (#1498)
检测termux时只看环境变量,以便于区分原生termux和termux中安装的Linux发行版
测试发现${ANDROID_RUNTIME_ROOT}${ANDROID_ROOT}在termux中安装的Linux发行版中仍旧存在,但是发行版中应该不用设置软连接吧
2022-06-14 22:55:49 +08:00
whyour 05c738c7c6 修改系统内部获取token方式 2022-06-14 22:43:18 +08:00
whyour e561e12356 修改更新定时任务参数验证 2022-06-12 21:12:01 +08:00
whyour 595b28ba62 移除notify软链命令 2022-06-10 21:23:43 +08:00
whyour 366caed74e 修复根目录环境变量 2022-06-09 16:44:58 +08:00
whyour 2e26a8c24e 修复shell本地开发兼容性 2022-06-09 12:10:41 +08:00
whyour c09c375066 修复无法更新脚本内容为空 2022-06-08 23:57:22 +08:00
whyour 4f93905507 修改默认pip源 2022-06-08 22:54:44 +08:00
whyour f152136f0b 更新版本 v2.13.3 2022-06-08 18:39:51 +08:00
whyour c24d3d6e2f 移除console 2022-06-08 18:39:10 +08:00
whyour 33d820acdb 修改npm peer dependence策略 2022-06-08 11:19:54 +08:00
whyour 78ef75bbfe 移除无用日志 2022-06-08 11:12:15 +08:00
whyour 31541fb87e 修复调试脚本结束条件 2022-06-06 21:25:09 +08:00
whyour 0c169e3f6c 修复调试脚本目录 2022-06-05 21:25:09 +08:00
whyour 73c15cb3bc 修复调试脚本运行需要先保存 2022-06-05 19:26:32 +08:00
whyour 4373a2df49 修复脚本管理和日志管理树字段 2022-06-05 18:44:35 +08:00
whyour cb004cfbf5 修复粘贴订阅私有仓库字段 2022-06-05 11:56:48 +08:00
whyour 7f2d6d159c 修复创建脚本选择目录 2022-06-04 01:26:01 +08:00
whyour 4bba024e4a 新建脚本支持文件上传 2022-06-04 00:12:26 +08:00
whyour acb43f3ad8 更新版本 v2.13.2 2022-06-01 00:28:35 +08:00
whyour b160f914ad 修改订阅执行逻辑 2022-06-01 00:25:32 +08:00
whyour 6116aa6d12 修复私钥配置文件匹配 2022-06-01 00:03:58 +08:00
whyour d43cee53c9 修改interval_schedule验证 2022-05-30 10:26:30 +08:00
whyour defea8ff9b 修复订阅添加定时规则 2022-05-30 10:21:15 +08:00
whyour b290b60d46 修复新建订阅定时 2022-05-30 09:58:51 +08:00
whyour 706b5e8108 修复订阅任务创建逻辑 2022-05-29 22:51:36 +08:00
whyour b55c7d5747 修复依赖安装顺序,优先linux依赖安装 2022-05-29 15:01:48 +08:00
whyour 6b8fd94f0c 系统启动安装依赖优先安装linux依赖 2022-05-29 11:35:31 +08:00
whyour e18710a30a 更新dockerfile 2022-05-29 11:13:32 +08:00
whyour c0f0da4e96 修复dockerfile 2022-05-29 10:18:08 +08:00
whyour 49ef5cd61f 修复pnpm环境变量 2022-05-29 10:02:03 +08:00
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
whyour 1cfcf8d194 更新版本 v2.10.8 2021-11-16 14:36:13 +08:00
whyour d1e9f741d0 修复脚本管理删除、新建增加选择父目录 2021-11-16 14:18:02 +08:00
kilo5hzandGitHub 0cde9d07ee 增加gotify推送 (#905)
* 增加gotify推送
2021-11-15 23:04:55 +08:00
whyour a266425348 修复拷贝deps 2021-11-13 22:05:54 +08:00
whyour 6dbe02e321 修复dep目录 2021-11-13 21:54:45 +08:00
whyour 39979879c9 更新版本 v2.10.7 2021-11-13 21:21:35 +08:00
whyour ba3f604a5c 修复bot安装依赖判断 2021-11-13 20:58:27 +08:00
whyour 9572582401 修改拷贝依赖 2021-11-13 19:05:34 +08:00
whyour 2bcef0ba75 修复脚本管理搜索 2021-11-13 17:42:06 +08:00
whyour 31cd7c4007 外部依赖改为系统直接分发通知文件js和py版 2021-11-13 17:21:27 +08:00
whyour 1c4c1799b0 修复环境变量表格样式 2021-11-13 16:44:21 +08:00
whyour 2d558df15e 修复schedule服务 2021-11-13 12:34:49 +08:00
whyour 7b769da4e2 修复脚本管理树结构 2021-11-13 11:14:43 +08:00
whyour 4e95d62933 更新版本 v2.10.6 2021-11-13 00:03:12 +08:00
whyour 83255280b9 修复检查更新 2021-11-12 23:55:57 +08:00
whyour 2546cebdea 修复定时匹配 2021-11-12 23:42:11 +08:00
whyour f14a3fe0f1 修复脚本管理数据 2021-11-12 23:18:24 +08:00
whyour c3eb845be4 修改workflow变量 2021-11-12 22:09:02 +08:00
MiniersandGitHub 1eef77dca5 增加环境变量更新时间 (#887) 2021-11-12 21:55:03 +08:00
风之凌殇andGitHub d7d9f47fe5 增加在添加语义化版本tag时自动生成static并构建多个不同tag的容器的workflow,简化版本发布流程 (#888) 2021-11-12 21:54:42 +08:00
whyour fce6a996d9 更新版本号 v2.10.5 2021-11-12 14:23:16 +08:00
whyour 48a484d7de 修复文件判断引起服务异常 2021-11-12 14:22:21 +08:00
whyour 4b81d3c612 更新版本号 2021-11-12 13:18:32 +08:00
whyour 21b6edd7eb 修复entrypoint启动失败 2021-11-12 13:16:53 +08:00
whyour 9a43e022e9 修改版本说明序号 2021-11-11 23:04:29 +08:00
whyour 62bef3f13d 更新版本号 v2.10.3 2021-11-11 23:03:28 +08:00
whyour c87f5fa004 服务重启时依赖改为单独安装,避免同时失败 2021-11-11 22:56:47 +08:00
whyour 8d99f3ffbb 修复获取日志判断 2021-11-11 22:50:28 +08:00
whyour 100ca7b54b 单个数据库文件大于500兆时,裁剪文件,避免服务无法启动 2021-11-11 22:41:12 +08:00
whyour 4a2624f12a 脚本管理改成树结构 2021-11-11 00:10:05 +08:00
whyour 21b01a85a8 修复pushplus通知设置 2021-11-10 23:33:19 +08:00
whyour e039ae436e 修复依赖文件获取 2021-11-10 23:26:21 +08:00
whyour b13a1a45f8 修复repo依赖路径 2021-11-10 10:35:53 +08:00
whyour 667fe2bbba 修复shell语法 2021-11-09 23:55:50 +08:00
whyour f083871ca0 修复repo文件比对 2021-11-09 23:23:07 +08:00
whyour 782e1ba235 修改ql repo逻辑 2021-11-09 23:07:43 +08:00
whyour b92c6edc4e 添加动态引入 2021-11-09 22:34:24 +08:00
whyour bdf4657c73 修改退出下拉样式 2021-11-08 22:56:53 +08:00
whyour 8559438611 修改数据库加载 2021-11-08 22:47:46 +08:00
whyour 04045fd410 修复centos -x兼容性 2021-11-07 21:54:31 +08:00
whyour 1c71a9d8a8 回退shell [ 2021-11-07 19:18:36 +08:00
whyour 9a9ae9782c 修改pnpm安装源 2021-11-07 12:51:10 +08:00
whyour da2569850b 修复entrypoint 2021-11-07 11:25:18 +08:00
whyour b475035415 修复 arm 没有nginx conf.d 目录 2021-11-07 11:08:21 +08:00
OreoandGitHub 7baee27585 更新 Python 通知文件 (#832)
已在 Python3.6.x(云函数平台),3.8.x,3.10.0 测试通过
特性如下
1. 支持 11 种推送方式,未来还会继续添加
2. 多线程通知
3. 支持一言(一句话)和控制台输出开关
4. 对于现有推送方式,补充了配置变量
2021-11-06 20:45:53 +08:00
whyour 2cb2f1ce0a 更新dockerfile 2021-11-06 20:19:07 +08:00
whyour 8c1407e2a9 修改dockerfile 2021-11-06 19:04:01 +08:00
whyour 7bcb113085 更新docker yml 2021-11-06 18:58:18 +08:00
whyour 42dc27addf 修复任务日志结束时间展示 2021-11-06 00:50:44 +08:00
whyour aac30398c4 修复toLocaleString 0点显示24点 2021-11-05 23:56:16 +08:00
whyour ceae888372 修改shell语法 2021-11-05 23:39:22 +08:00
whyour 61df7050db 修复移动端展开导航 2021-11-04 19:24:55 +08:00
whyour af5561cbbf 更新版本号 v2.10.2 2021-11-04 18:10:43 +08:00
whyour bce30f922d 修复基础镜像pip3依赖 2021-11-04 18:10:14 +08:00
whyour d6ed394557 修复openapi路由 2021-11-04 18:07:34 +08:00
whyour 7b512a1681 更新版本号 v2.10.1 2021-11-04 15:09:07 +08:00
whyour 7af0597cd5 添加白屏时间及错误上报 2021-11-04 15:05:34 +08:00
whyour 2f5cf6d93c 修复check命令 2021-11-03 16:35:18 +08:00
whyour d672d2d104 修改失败任务日志 2021-11-01 23:06:23 +08:00
whyour c0d1fb1ef4 shell 中 [ 改为 [[ 2021-11-01 23:04:10 +08:00
whyour 3c882ad0ce 移动退出登录位置 2021-11-01 15:23:06 +08:00
whyour 58acd2eb68 修复表格滚动高度 2021-10-31 19:45:51 +08:00
whyour 590473e7d2 修复无法覆盖安装依赖 2021-10-31 17:34:19 +08:00
21139157c8 修改错误文字 (#809)
Co-authored-by: 14K <9112790+the14k@user.noreply.gitee.com>
2021-10-29 23:26:44 +08:00
whyour 8f8296ab64 支持服务端端口直接访问前端 2021-10-28 23:35:21 +08:00
whyour 40bf07d2a4 修复置顶后编辑置顶消失 2021-10-28 22:38:34 +08:00
whyour fd2b4fb86d 更新版本号 v2.10.0 2021-10-24 17:51:20 +08:00
whyour 783e867ef6 修复python移除依赖命令 2021-10-24 17:19:14 +08:00
whyour 3873d9f978 添加依赖管理 2021-10-24 17:07:16 +08:00
whyour 7dece938c8 依赖安装日志增加安装时间 2021-10-24 11:11:23 +08:00
whyour 0842d66c79 修复表格初次获取滚动高度 2021-10-23 23:44:11 +08:00
whyour d071e50d62 由于浏览器兼容性,移除dynamicImport 2021-10-23 23:31:46 +08:00
whyour 337f01aff1 修复python3任务无法停止 2021-10-23 23:22:08 +08:00
whyour 5614e41eb0 修复service引入路径 2021-10-23 18:46:24 +08:00
whyour 4f0fdbe453 初始化安装所有依赖 2021-10-23 18:44:25 +08:00
whyour 49c068db78 修改依赖安装结束标识 2021-10-23 18:27:20 +08:00
whyour fbba44d482 添加依赖管理 2021-10-23 18:23:32 +08:00
whyour 10c07b0a09 两步验证输入框自动聚焦 2021-10-21 22:25:15 +08:00
whyour 20893698ba 更新版本号 v2.9.10 2021-10-21 21:59:24 +08:00
longjuanandGitHub 31de659ed3 更新install.md (#789) 2021-10-21 21:55:26 +08:00
whyour 6c08a16c66 修改websocket日志 2021-10-21 21:52:06 +08:00
whyour 21e7178f48 修复table滚动高度 2021-10-21 19:13:38 +08:00
whyour aefd267eb2 更新monaco 2021-10-19 22:41:59 +08:00
whyour fb1a3980d4 检查密码默认值 2021-10-19 21:46:24 +08:00
whyour cfc467f8a5 修复tg系统通知认证参数 2021-10-18 19:18:46 +08:00
whyour 48120f012d 修复检查更新日志展示 2021-10-18 17:05:38 +08:00
whyour c907a78437 修复shell变量重复 2021-10-16 19:32:23 +08:00
whyour 915c0d09a9 升级umi 2021-10-16 17:21:19 +08:00
whyour 80638862cc 添加 yarn.lock 2021-10-16 17:07:05 +08:00
whyour a7350948cd 更新版本号 v2.9.9 2021-10-16 15:19:53 +08:00
whyour 1e1be013b5 脚本管理移动端支持添加脚本、编辑脚本 2021-10-16 15:11:23 +08:00
hanhh 4062584eae 移除无用代码 2021-10-16 13:39:18 +08:00
hanhh abedddffa6 修复表格样式 2021-10-16 10:53:14 +08:00
hanhh 38858ccdb2 修改pm2任务日志 2021-10-15 22:32:32 +08:00
hanhh d56e6359d0 修复更新日志匹配正则 2021-10-15 22:14:42 +08:00
hanhh 5886991464 放开ql update删除操作 2021-10-15 22:01:34 +08:00
hanhh 838326616b 更新版本号 v2.9.8-001 2021-10-15 11:47:32 +08:00
hanhh fcf80fe946 修复设置删除日志 2021-10-15 10:53:27 +08:00
phoenixandGitHub 2e63a7056c 修复nginx.conf错误 (#776)
Fix nginx: [emerg] unknown directive.
Log
`nginx: [emerg] unknown directive "client_body_buffer_size:" in /etc/nginx/nginx.conf:18
`
2021-10-15 10:40:58 +08:00
hanhh 7c807e3ec8 更新版本 2.9.8 2021-10-14 22:07:21 +08:00
hanhh b558aa0934 修复初始化流程 2021-10-14 22:00:36 +08:00
hanhh 38757758cb 增加日志删除频率设置 2021-10-14 10:58:54 +08:00
hanhh 84dd3d8fd1 修改版本更新执行命令 2021-10-13 23:50:25 +08:00
hanhh 19423c2d30 添加ql -l update 2021-10-13 23:45:46 +08:00
hanhh 98bba7d7fa 更新nginx配置文件 2021-10-13 23:31:38 +08:00
hanhh dcaa9ce000 修复websocket连接域名 2021-10-13 23:09:48 +08:00
hanhh 3cae44a136 修复版本号匹配正则 2021-10-13 22:53:10 +08:00
hanhh df8a3d2692 修复版本号匹配 2021-10-13 22:45:24 +08:00
hanhh 56e43f1808 移除远程版本更新链接 2021-10-13 22:41:59 +08:00
hanhh 2f6b95cccf 修复获取最后版本文件 2021-10-13 22:39:59 +08:00
hanhh ad9f6b2b64 修复获取最新版本号 2021-10-13 22:05:54 +08:00
hanhh 165c3dc976 容器启动忽略config和env中的错误 2021-10-13 14:25:38 +08:00
hanhh cbe6390b41 修复版本检查 2021-10-12 23:06:28 +08:00
hanhh 026eaf6606 完善检查更新操作 2021-10-12 22:54:17 +08:00
hanhh ee184f8e62 并发命令支持指定账号 2021-10-12 21:58:41 +08:00
hanhh 5745231dc9 添加系统更新操作和设置删除日志频率 2021-10-12 00:27:42 +08:00
hanhh cbdc3a9861 添加检查更新和删除日志频率操作 2021-10-08 23:02:45 +08:00
hanhh ba5c42ad6d 修复表格滚动样式和交互 2021-10-08 22:39:17 +08:00
hanhh b056a731a1 增加新版本检测 2021-10-07 15:26:25 +08:00
hanhh b4cd94f2d9 增加环境变量名称正则校验 2021-10-06 16:38:46 +08:00
hanhh 3b8cb43f02 添加新版本提醒 2021-10-06 16:22:53 +08:00
hanhh 82cb11c3c4 更新版本号 v2.9.7 2021-10-03 22:26:16 +08:00
SowevoandGitHub eb7522a90b 增加企业微信机器人推送的python版本 (#748) 2021-10-03 22:16:07 +08:00
hanhh 3898201b1e 修复登录两步提醒样式 2021-10-03 22:05:56 +08:00
hanhh bb6ab0c17b 修改初始化判断条件 2021-10-03 22:01:44 +08:00
hanhh 652c40ab1f 增加初始化过程 2021-10-03 20:58:55 +08:00
hanhh ca0d6215a9 修复entrypoint启动nginx配置 2021-09-30 23:49:19 +08:00
hanhh 61d155257c ql update添加patch 2021-09-30 19:14:40 +08:00
hanhh 91399f1a1f 移除check中init git 2021-09-30 19:08:13 +08:00
hanhh 2e738c5977 更新版本号 v2.9.6 2021-09-29 23:16:59 +08:00
hanhh c0bd51cf03 修复类型检查 2021-09-29 23:15:09 +08:00
hanhh ca5afe04f8 修复退出登录获取token出错 2021-09-29 23:09:13 +08:00
hanhh 47db0b9f4e 对比工具支持对比config目录下的文件 2021-09-29 22:48:49 +08:00
hanhh 27f17a377a 重启服务时自动压缩db 2021-09-29 21:40:35 +08:00
hanhh ecaf731ecf 修复loading状态位置 2021-09-29 21:19:25 +08:00
hanhh 9fba2a85f6 修复清空文件操作 2021-09-27 22:04:47 +08:00
hanhh 93f75be182 task和ql命令添加基础目录检测 2021-09-27 19:38:06 +08:00
hanhh d537901f59 测试动态加载 2021-09-25 12:11:13 +08:00
hanhh 260c1ed4a4 修改定时任务列表滚动宽度 2021-09-25 11:40:55 +08:00
hanhh ac2bd52463 修复status接口参数 2021-09-25 02:00:50 +08:00
hanhh 16c6f8fee1 修复并发参数 2021-09-25 01:51:18 +08:00
hanhh 8a0db21f17 更新版本号 2021-09-25 00:43:05 +08:00
hanhh 9ab08a71c0 增加登录设备展示 2021-09-25 00:42:03 +08:00
hanhh 381c32a3b4 修复更新任务状态api 2021-09-25 00:28:18 +08:00
hanhh 350c0f10c4 修复ql resettfa命令 2021-09-25 00:15:23 +08:00
hanhh d5b982f813 ql resettfa增加兼容字段 2021-09-24 23:33:52 +08:00
hanhh 6587375ff2 移除定时任务左右列固定n 2021-09-24 23:31:50 +08:00
hanhh f5ecb81bad ql bot移除自动更新远程仓库 2021-09-24 23:22:40 +08:00
hanhh 0141cd81c4 修复服务端ts类型检查 2021-09-24 22:49:14 +08:00
hanhh ff412c7497 更新版本号 2021-09-24 22:30:09 +08:00
hanhh 623f239f91 修复定时任务操作宽度 2021-09-24 22:28:25 +08:00
hanhh c7ba304c6a 支持多端登录 2021-09-24 22:24:39 +08:00
hanhh c8393604d1 修复task运行指定账号 2021-09-24 00:24:54 +08:00
hanhh c5d47a67aa 修复定时任务列表下次运行时间 2021-09-24 00:11:14 +08:00
hanhh 3a1423cd59 task支持指定账号运行 2021-09-23 23:59:53 +08:00
hanhh d57f4cf6cd 修改定时任务表格布局 2021-09-23 22:12:19 +08:00
hanhh af420942c4 添加最后运行时间和运行时长 2021-09-23 13:42:32 +08:00
hanhh eac4e5e66f 修复主题切换全局class未变化 2021-09-21 23:29:38 +08:00
hanhh b8822a98f7 定时任务添加下次运行时间 2021-09-21 20:11:39 +08:00
hanhh 95322b42c0 修改登录通知消息 2021-09-20 23:56:20 +08:00
hanhh 33f5a06d32 修复pushplus通知参数 2021-09-20 22:23:56 +08:00
hanhh a7e5126c92 修复新建文件 2021-09-20 00:59:38 +08:00
hanhh c62f3e5b8e 添加bak目录ignore 2021-09-20 00:47:58 +08:00
hanhh 5d3f413a05 修改测试通知内容 2021-09-19 23:10:58 +08:00
hanhh 644154ca38 更新版本号 2021-09-19 23:00:53 +08:00
hanhh cccce5ff74 修复通知 2021-09-19 23:00:21 +08:00
hanhh aa1fe98188 定时任务增加置顶功能 2021-09-19 22:25:57 +08:00
hanhh b6a78550d6 添加全局主题className 2021-09-19 21:27:20 +08:00
hanhh 70b7f2c493 脚本管理支持新增文件 2021-09-19 21:23:03 +08:00
hanhh ea44e5f209 修复pageTitle 2021-09-18 23:27:50 +08:00
hanhh 41ef87d9f8 任务详情日志添加loading 2021-09-18 16:33:12 +08:00
hanhh c0b4a70aa0 添加全局loading 2021-09-18 15:08:45 +08:00
hanhh 955875cb7a 修复关闭通知 2021-09-18 02:09:06 +08:00
hanhh 827e8571aa 添加通知设置页面 2021-09-18 01:44:55 +08:00
whyour aed08b0f59 添加通知api 2021-09-17 18:06:14 +08:00
hanhh 3ddaf41e27 更新通知服务 2021-09-17 00:35:27 +08:00
whyour 38a9cce6bf 更新通知服务 2021-09-16 21:01:29 +08:00
whyour ae255ef381 添加通知服务 2021-09-16 18:01:25 +08:00
prcxuandGitHub 4316bef01e 更新安装说明 (#705) 2021-09-15 11:18:58 +08:00
JH2628andGitHub 8491c5706d 修复config sample序号 (#694) 2021-09-14 19:25:00 +08:00
hanhh 7b7fab1f32 修改系统异常提醒 2021-09-13 00:12:44 +08:00
hanhh 5a6fccdc2f 更新tg链接 2021-09-12 20:28:43 +08:00
hanhh 23668be46d 更新版本号 2021-09-11 13:53:58 +08:00
hanhh f92f2c1666 修复写入任务错误日志逻辑 2021-09-11 13:53:24 +08:00
hanhh 895740a3cb 修改获取用户数据逻辑 2021-09-11 02:03:31 +08:00
hanhh 4c76c3231f 移除自动压缩db数据 2021-09-09 13:02:49 +08:00
hanhh ca9d3f9559 更新版本号 2021-09-08 12:26:21 +08:00
hanhh 3ca3c5e75c 修复开启两步失败提醒 2021-09-08 12:26:05 +08:00
hanhh c2c0b1dfdc 添加应用名称改为选填 2021-09-08 12:15:30 +08:00
hanhh c633774487 修复两步验证逻辑 2021-09-08 12:13:19 +08:00
hanhh c2d86fc517 更新版本号 2021-09-07 23:53:01 +08:00
hanhh d7c8d150ca 修改登陆日志title 2021-09-07 23:52:40 +08:00
L1n3andGitHub 6b8a4e65f2 添加安装说明 (#667) 2021-09-07 17:46:53 +08:00
LeonandGitHub 482e43dd1f 修复 Python 和 JavaScript Server酱和企业微信推送的环境变量不一致 (#673) 2021-09-07 17:46:14 +08:00
hanhh 1437babc6c 增加登录日志 2021-09-07 01:40:41 +08:00
Zy143LandGitHub 5ed882943a 修复python和JavaScript钉钉推送的环境变量不一致 (#659) 2021-09-05 23:53:18 +08:00
hanhh e678b3fb3a config文件读取添加黑名单 2021-09-05 23:48:58 +08:00
hanhh a5770bc4e4 添加环境变量命令input改为textarea 2021-09-05 23:38:36 +08:00
hanhh 59821bb939 修改ql check依赖安装逻辑 2021-09-03 22:30:24 +08:00
hanhh b58040be44 修改两步验证逻辑 2021-09-03 19:09:09 +08:00
hanhh be3b8ad52e 修复环境变量拖拽 2021-09-02 19:16:28 +08:00
hanhh 0fdaf97461 更新两步验证逻辑 2021-09-01 23:03:27 +08:00
L1n3andGitHub f5f036372f 添加 docker-compose.yml (#629) 2021-08-31 15:52:24 +08:00
hanhh 3fceb983e6 增加上次登录状态 2021-08-31 15:29:23 +08:00
hanhh ae6a39196d 增加ql resettfa禁用两步验证 2021-08-31 11:06:20 +08:00
hanhh 2810ff2753 更新版本号 2021-08-31 10:56:37 +08:00
hanhh 205c0cf8d3 两步验证失败增加ip记录 2021-08-31 10:54:46 +08:00
hanhh d9c801a091 添加两步验证 2021-08-30 23:37:26 +08:00
hanhh 0833aa3b30 增加nedb数据自定比对,测试对db文件不断增大的影响 2021-08-29 00:52:21 +08:00
hanhh 9c90bbc146 修复移动端无法打开脚本管理、任务日志 2021-08-29 00:34:47 +08:00
hanhh 231b387076 更换ip地址查询接口 2021-08-27 14:00:11 +08:00
hanhh 3de432652f 更换ip查询接口 2021-08-27 13:27:32 +08:00
hanhh cad714493e 更新版本号 2021-08-27 11:08:08 +08:00
hanhh 6a690a6108 更新nginx配置 2021-08-27 11:04:31 +08:00
hanhh 1b85c52557 修改新建应用placeholder 2021-08-27 10:52:49 +08:00
hanhh b37be92ea9 搜索时去掉前后空格 2021-08-27 10:47:30 +08:00
HeroWongandGitHub f95979f445 fix: 只有一页时不隐藏分页栏,确保用户可以更改 pagesize (#613) 2021-08-26 21:27:22 +08:00
hanhh 09e07218ae 添加openapi模块 2021-08-26 19:01:39 +08:00
hanhh 085f5a9451 更新版本号 2021-08-25 23:55:16 +08:00
hanhh eb4e935554 修复cron分隔符,修复登录获取ip逻辑 2021-08-25 23:54:42 +08:00
hanhh 57986cf425 修复登录倒计时颜色 2021-08-25 18:58:45 +08:00
hanhh 7bf14b9acc 更新版本号 2021-08-25 18:20:29 +08:00
hanhh 3ba0be34d9 修复6位cron判断 2021-08-25 18:19:22 +08:00
hanhh 0f828a3c75 修复群辉update之后403 2021-08-25 17:58:21 +08:00
hanhh 16a768a142 更新版本号 2021-08-24 18:21:19 +08:00
hanhh f380d20279 添加重置登录错误次数命令 2021-08-24 18:20:36 +08:00
hanhh be908ab36c 添加默认定时规则配置 2021-08-23 19:10:57 +08:00
hanhh b545454537 task传递参数给task_before和task_after 2021-08-23 15:58:25 +08:00
hanhh a5ff8dd1d6 更新readme图片 2021-08-23 15:31:13 +08:00
hanhh 59f23e518b 修复readme图片 2021-08-23 15:26:30 +08:00
hanhh 858b3f0176 更新版本号 2021-08-23 13:50:44 +08:00
hanhh 89b8494ad0 修复获取ip api失败时,无法登陆
修复禁用或者运行定时任务出现重复
2021-08-23 13:44:54 +08:00
hanhh 64ecd75bd5 修复定时任务搜索后页码 2021-08-23 12:29:29 +08:00
hanhh 138eb3393d 修复error_log文件读取 2021-08-23 11:41:34 +08:00
hanhh 1ad51d2925 更新版本号 2021-08-23 10:47:11 +08:00
hanhh 317e175bca 回退update.sh 2021-08-23 10:43:26 +08:00
hanhh 0e287aa7d1 修复task_before eval 2021-08-22 22:33:07 +08:00
hanhh 28018b705b 修复未产生日志文件cat报错 2021-08-22 22:02:31 +08:00
hanhh 46ec72d6be ql命令增加-l参数 2021-08-22 21:57:10 +08:00
hanhh 82494a87af 修改task.sh任务顺序 2021-08-22 20:54:40 +08:00
hanhh 95c19b447a 修复task语法 2021-08-22 18:57:52 +08:00
hanhh 9c6eae936a 修复task.sh执行语法 2021-08-22 18:21:31 +08:00
hanhh 77605a9360 修改task.sh中参数名 2021-08-22 16:05:58 +08:00
hanhh 82880c3f06 修复最后登录ip读取 2021-08-22 15:25:16 +08:00
hanhh 11143c771b 更新版本号 2021-08-22 15:23:58 +08:00
hanhh 0968eaa5a8 修复任务执行参数出错 2021-08-22 15:23:12 +08:00
hanhh 0687f9d472 修改登陆成功提示文本 2021-08-22 14:47:07 +08:00
hanhh 25f0f5951a 更新版本号 2021-08-22 14:07:43 +08:00
hanhh eec0ce65ad 修复获取访问ip逻辑 2021-08-22 14:07:00 +08:00
hanhh be4e100168 更新版本号 2021-08-22 12:59:54 +08:00
hanhh 59e4f1063c 修复登录重试判断 2021-08-22 12:59:34 +08:00
hanhh 359e21f02b 登录增加错误延迟提醒 2021-08-22 12:28:45 +08:00
hanhh 6894bd9c0e 登录增加最后登录时间地点,登录失败增加重试间隔 2021-08-22 01:22:23 +08:00
hanhh 7040f02823 默认定时改为随机时间 2021-08-21 19:26:37 +08:00
hanhh 41408de337 修复更新环境变量备注无法删除 2021-08-21 18:34:53 +08:00
hanhh 72e243dce8 增加命令行运行日志实时打印 2021-08-21 17:26:41 +08:00
hanhh 77d4840e2a 定时任务,环境变量添加状态筛选 2021-08-20 16:31:24 +08:00
hanhh 0d5961821c 更新版本号 2021-08-20 16:17:04 +08:00
hanhh f34980b173 移除console 2021-08-20 14:26:12 +08:00
hanhh 5b21a6e529 手动运行任务添加错误捕获 2021-08-20 10:59:59 +08:00
hanhh 6dbb737d4b 修复脚本管理编辑交互,修复ico无法加载 2021-08-19 23:46:26 +08:00
hanhh 76ecfbaaa2 更新版本号 2021-08-19 14:47:07 +08:00
hanhh d9aacbcb92 定时任务支持任务名,定时,命令排序
环境变量支持名称排序
2021-08-19 14:45:42 +08:00
hanhh e088325d95 脚本管理支持编辑删除文件 2021-08-19 14:18:25 +08:00
hanhh b29dd2c580 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:31:39 +08:00
hanhh 89e82fd5b0 修复未勾选拆分环境变量参数验证失败 2021-08-17 22:26:33 +08:00
hanhh 9f0b671d42 修复手动运行任务进程 2021-08-17 22:06:04 +08:00
hanhh 7de7c724dc 更新版本号 2021-08-17 18:35:53 +08:00
hanhh 3525b1d92a 支持批量创建同名称环境变量,日志和脚本管理PC页支持宽度拖拽 2021-08-17 18:34:55 +08:00
hanhh 30cea84886 修改暗黑模式默认值 2021-08-16 18:05:52 +08:00
DerekandGitHub 0c2cc7f575 更正ql"本脚本用法"中的序号 (#552) 2021-08-16 17:17:45 +08:00
197 changed files with 6910 additions and 34611 deletions
-2
View File
@@ -1,4 +1,3 @@
UPDATE_PORT=5300
PUBLIC_PORT=5400 PUBLIC_PORT=5400
CRON_PORT=5500 CRON_PORT=5500
BACK_PORT=5600 BACK_PORT=5600
@@ -11,4 +10,3 @@ SECRET='whyour'
QINIU_AK='' QINIU_AK=''
QINIU_SK='' QINIU_SK=''
QINIU_SCOPE='' QINIU_SCOPE=''
TEMP=''
-42
View File
@@ -1,42 +0,0 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve
body:
- type: input
id: version
attributes:
label: Qinglong version
validations:
required: true
- type: textarea
id: steps-to-reproduce
attributes:
label: Steps to reproduce
description: |
What do we need to do after opening your repro in order to make the bug happen? Clear and concise reproduction instructions are important for us to be able to triage your issue in a timely manner. Note that you can use [Markdown](https://guides.github.com/features/mastering-markdown/) to format lists and code.
placeholder: Steps to reproduce
validations:
required: true
- type: textarea
id: expected
attributes:
label: What is expected?
validations:
required: true
- type: textarea
id: actually-happening
attributes:
label: What is actually happening?
validations:
required: true
- type: textarea
id: system-info
attributes:
label: System Info
description: Output of `npx envinfo --system --binaries --browsers`
render: shell
placeholder: System, Binaries, Browsers
- type: textarea
id: additional-comments
attributes:
label: Any additional comments?
description: e.g. some background/context of how you ran into this bug.
-8
View File
@@ -1,8 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Telegram Chat
url: https://t.me/jiao_long
about: Ask questions and discuss with other Qinglong users in real time.
- name: Questions & Discussions
url: https://github.com/whyour/qinglong/discussions/new?category=q-a
about: Use GitHub discussions for message-board style questions and discussions.
@@ -1,40 +0,0 @@
name: "\U0001F680 New feature proposal"
description: Suggest an idea for this project
labels: [":sparkles: feature request"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in the project and taking the time to fill out this feature report!
- type: textarea
id: feature-description
attributes:
label: Clear and concise description of the problem
description: "Explain your use case, context, and rationale behind this feature request. More importantly, what is the **end user experience** you are trying to build that led to the need for this feature?"
validations:
required: true
- type: textarea
id: suggested-solution
attributes:
label: Suggested solution
description: "In module [xy] we could provide following implementation..."
validations:
required: true
- type: textarea
id: alternative
attributes:
label: Alternative
description: Clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Any other context or screenshots about the feature request here.
- type: checkboxes
id: checkboxes
attributes:
label: Validations
description: Before submitting the issue, please make sure you do the following
options:
- label: Check that there isn't already an issue that request the same feature to avoid creating a duplicate.
required: true
-31
View File
@@ -1,31 +0,0 @@
## PR Type
What kind of change does this PR introduce?
<!-- Please check the one that applies to this PR using "x". -->
- [ ] Bugfix
- [ ] Feature
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Other... Please describe:
## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->
Issue Number: N/A
## What is the new behavior?
## Does this PR introduce a breaking change?
- [ ] Yes
- [ ] No
<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications below. -->
## Other information
-7
View File
@@ -1,7 +0,0 @@
# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: |
💖 Thanks for opening this pull request! 💖
Please be patient and we will get back to you as soon as we can.
# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
Congrats on merging your first pull request! 🎉🎉🎉
+30 -150
View File
@@ -2,56 +2,31 @@ name: Build And Push Docker Image
on: on:
push: push:
paths-ignore:
- "*.md"
branches: branches:
- "master" - 'master'
- "develop" - 'develop'
- "test-cli" # Sequence of patterns matched against refs/tags
tags: tags:
- "v*" - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule: schedule:
- cron: "00 20 * * *" # 参考 https://jasonet.co/posts/scheduled-actions/
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
# minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitlab.com:whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: git@gitee.com:whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v3 - uses: actions/setup-node@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v4
with:
cache: "pnpm"
- name: build front and back - name: build front and back
run: | run: |
pnpm install --frozen-lockfile npm i -g pnpm
pnpm install
pnpm build:front pnpm build:front
pnpm build:back pnpm build:back
@@ -59,9 +34,6 @@ jobs:
env: env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }} GITHUB_BRANCH: ${{ github.ref_name }}
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
run: | run: |
mkdir -p tmp mkdir -p tmp
cd ./tmp cd ./tmp
@@ -72,50 +44,34 @@ jobs:
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
mkdir -p ~/.ssh
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}"
git gc
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build: build:
needs: build-static needs: build-static
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用 runs-on: ubuntu-latest
runs-on: ubuntu-20.04
# runs-on: self-hosted
permissions: permissions:
packages: write packages: write
contents: read contents: read
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v3 - uses: actions/setup-node@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v4
with:
cache: "pnpm"
- name: Setup timezone - name: Set time zone
uses: szenius/set-timezone@v1.2 uses: szenius/set-timezone@v1.0
with: with:
timezoneLinux: Asia/Shanghai timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 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@v3 uses: docker/login-action@v2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -123,114 +79,38 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v4
with: with:
images: | images: |
${{ github.repository }} ${{ github.repository }}
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
# generate Docker tags based on the following events/attributes # generate Docker tags based on the following events/attributes
# nightly, master, pr-2, 1.2.3, 1.2, 1 # nightly, master, pr-2, 1.2.3, 1.2, 1
flavor: |
latest=false
tags: | tags: |
type=schedule,pattern=nightly type=schedule,pattern=nightly
type=edge type=edge
type=ref,event=branch
type=ref,event=pr type=ref,event=pr
type=ref,event=branch,enable=${{ github.ref != format('refs/heads/{0}', 'master') }}
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 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@v5 uses: docker/build-push-action@v3
with: with:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386 context: docker/
context: .
file: ./docker/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: |
echo ${{ steps.docker_build.outputs.digest }}
build310:
if: ${{ github.ref_name == 'master' }}
needs: build-static
runs-on: ubuntu-20.04
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: "8.3.1"
- uses: actions/setup-node@v4
with:
cache: "pnpm"
- name: Setup timezone
uses: szenius/set-timezone@v1.2
with:
timezoneLinux: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push python3.10
id: docker_build_310
uses: docker/build-push-action@v5
with:
build-args: |
MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: .
file: ./docker/310.Dockerfile
push: true
tags: whyour/qinglong:python3.10
cache-from: type=registry,ref=whyour/qinglong:cache-python3.10
cache-to: type=registry,ref=whyour/qinglong:cache-python3.10,mode=max
- name: Image digest
run: |
echo ${{ steps.docker_build_310.outputs.digest }}
+8 -1
View File
@@ -5,6 +5,7 @@
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock /yarn.lock
/pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
@@ -22,4 +23,10 @@
.env .env
.history .history
.version.ts .version.ts
/.tmp
/config
/log
/db
/manual_log
/scripts
/bak
+1
View File
@@ -1 +1,2 @@
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
strict-peer-dependencies=false strict-peer-dependencies=false
-17
View File
@@ -6,20 +6,3 @@ package.json
.umi .umi
.umi-production .umi-production
.umi-test .umi-test
.history
.tmp
node_modules
npm-debug.log*
yarn-error.log
yarn.lock
package-lock.json
static
data
DS_Store
src/.umi
src/.umi-production
src/.umi-test
.env.local
.env
version.ts
/.tmp
+19 -33
View File
@@ -1,39 +1,32 @@
import { defineConfig } from '@umijs/max'; import { defineConfig } from 'umi';
const CompressionPlugin = require('compression-webpack-plugin'); const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
jsMinifier: 'terser', layout: false,
antd: {},
locale: {
antd: true,
title: true,
baseNavigator: true,
},
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, nodeModulesTransform: {
favicons: [`https://qn.whyour.cn/favicon.svg`], type: 'none',
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: {
[`${baseUrl}api/update`]: {
target: 'http://127.0.0.1:5300/',
changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/update`]: '/api' },
}, },
[`${baseUrl}api/public`]: { fastRefresh: {},
esbuild: {},
webpack5: {},
dynamicImport: {
loading: '@/components/pageLoading',
},
favicon: '/images/g5.ico',
proxy: {
'/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
changeOrigin: true, changeOrigin: true,
pathRewrite: { [`^${baseUrl}api/public`]: '/api' },
}, },
[`${baseUrl}api`]: { '/api': {
target: 'http://127.0.0.1:5600/', target: 'http://127.0.0.1:5600/',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
pathRewrite: { [`^${baseUrl}api`]: '/api' },
}, },
}, },
chainWebpack: ((config: any) => { chainWebpack: (config) => {
config.plugin('compression-webpack-plugin').use( config.plugin('compression-webpack-plugin').use(
new CompressionPlugin({ new CompressionPlugin({
algorithm: 'gzip', algorithm: 'gzip',
@@ -42,20 +35,13 @@ export default defineConfig({
minRatio: 0.6, minRatio: 0.6,
}), }),
); );
}) as any, },
externals: { externals: {
react: 'window.React', react: 'window.React',
'react-dom': 'window.ReactDOM', 'react-dom': 'window.ReactDOM',
}, },
headScripts: [ scripts: [
`./api/env.js`, 'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
'https://gw.alipayobjects.com/os/lib/react/18.2.0/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/18.2.0/umd/react-dom.production.min.js',
],
copy: [
{
from: 'node_modules/monaco-editor/min/vs',
to: 'static/dist/monaco-editor/min/vs',
},
], ],
}); });
+123 -127
View File
@@ -1,29 +1,30 @@
<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"> <div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<h1 align="center">Qinglong</h1> Timed task management panel with python3, javaScript, shell, typescript support
[简体中文](./README.md) | English [![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]
Timed task management platform supporting Python3, JavaScript, Shell, Typescript
[![npm version][npm-version-image]][npm-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]
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
[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-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-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div> </div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) [![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
[简体中文](./README.md) | English
## Features ## Features
@@ -35,49 +36,85 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
- Support dark mode - Support dark mode
- Support cell phone operation - Support cell phone operation
## Version
### docker
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
```bash
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### npm
The npm version supports `debian/ubuntu/alpine` systems and requires `node/npm/python3/pip3/pnpm` to be installed.
```bash
npm i @whyour/qinglong
```
## Deployment ## Deployment
### Docker (Recommended) ### Local Deployment
```bash ```bash
# curl -sSL get.docker.com | sh # 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 \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql:/ql/data \
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
-p 5700:5700 \ -p 5700:5700 \
# Deployment paths are not required, e.g. /test.
-e QlBaseUrl="/" \
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
-e QlPort="5700" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \ --restart unless-stopped \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### Docker-compose (Recommended) ### 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 ```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -87,66 +124,9 @@ docker-compose up -d
docker-compose down docker-compose down
``` ```
### Podman (Recommended) ## Use
```bash 1. built-in commands
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
-p 5700:5700 \
# Deployment paths are not required, e.g. /test.
-e QlBaseUrl="/" \
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### Npm
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3/pnpm yourself
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
```
```bash
npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong
qinglong
# Add the environment variables QL_DIR and QL_DATA_DIR when prompted
export QL_DIR=""
export QL_DATA_DIR=""
# Run again
qinglong
```
## Built-in commands
- task
```bash
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Use -- to split, -- followed by a parameter that is passed to the script, as in the following example, the script receives the parameter -u whyour -p password
task <file_path> -- -u whyour -p password
```
- ql
```bash ```bash
# Update and restart Green Dragon # Update and restart Green Dragon
@@ -167,33 +147,31 @@ ql check
ql resetlet ql resetlet
# Disable two-step login # Disable two-step login
ql resettfa 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>
``` ```
| **Parameter** | **Description** | 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 |
| extensions | Pull the branch of the repository |
| branch | Number of days of logs to be kept |
| days | File path for task execution |
| file_path | The name of the environment variable that needs to be concurrent or specified at the time of task execution |
## Development * file_url: Script address
* repo_url: Repository address
```bash * whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
git clone https://github.com/whyour/qinglong.git * blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
cd qinglong * 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
cp .env.example .env * branch: Pull the branch of the repository
# Recommended use pnpm https://pnpm.io/zh/installation * days: Number of days of logs to be kept
npm install -g pnpm@8.3.1 * file_path: File path for task execution
pnpm install * env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
pnpm start * account_number: Specify the account number of an environment variable to be executed when the task is executed
```
Open your browser and visit <http://127.0.0.1:5700>
## Links ## Links
@@ -201,10 +179,28 @@ Open your browser and visit <http://127.0.0.1:5700>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org) - [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
## 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 ## 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". 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".
+121 -125
View File
@@ -1,31 +1,30 @@
<div align="center"> <p align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png"> <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">青龙</h1> <h1 align="center">青龙</h1>
简体中文 | [English](./README-en.md) <div align="center">
支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台 支持python3、javaScript、shell、typescript 的定时任务管理面板
Timed task management platform supporting 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]
[![npm version][npm-version-image]][npm-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]
[npm-version-image]: https://img.shields.io/npm/v/@whyour/qinglong?style=flat
[npm-version-url]: https://www.npmjs.com/package/@whyour/qinglong?activeTab=readme
[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-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-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong [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-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[Demo](http://demo.dlww.cc:4433/) / [Issues](https://github.com/whyour/qinglong/issues) / [Telegram Channel](https://t.me/jiao_long) / [Buy Me a Coffee](https://www.buymeacoffee.com/qinglong)
[演示](http://demo.dlww.cc:4433/) / [反馈](https://github.com/whyour/qinglong/issues) / [Telegram 频道](https://t.me/jiao_long) / [打赏开发者](https://user-images.githubusercontent.com/22700758/244744295-29cd0cd1-c8bb-4ea1-adf6-29bd390ad4dd.jpg)
</div> </div>
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png) [![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
简体中文 | [English](./README-en.md)
## 功能 ## 功能
@@ -37,49 +36,86 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
- 支持暗黑模式 - 支持暗黑模式
- 支持手机端操作 - 支持手机端操作
## 版本
### docker
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
```bash
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### npm
npm 版本支持 `debian/ubuntu/alpine` 系统,需要自行安装 `node/npm/python3/pip3/pnpm`
```bash
npm i @whyour/qinglong
```
## 部署 ## 部署
### docker (推荐) ### 本机部署
```bash ```bash
# curl -sSL get.docker.com | sh # 待完善
```
### 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 \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \ -p 5700:5700 \
# 部署路径非必须,比如 /test
-e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \ --restart unless-stopped \
whyour/qinglong:latest whyour/qinglong:latest
``` ```
### docker-compose (推荐) ### 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 ```bash
# curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
mkdir qinglong mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
@@ -89,64 +125,9 @@ docker-compose up -d
docker-compose down docker-compose down
``` ```
### podman (推荐) ## 使用
```bash 1. 内置命令
# https://podman.io/getting-started/installation
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \
# 部署路径非必须,比如 /test
-e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
```
### npm
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3/pnpm
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
```
```bash
npm install -g node-pre-gyp pnpm@8.3.1
npm install -g @whyour/qinglong
qinglong
# 根据提示增加环境变量 QL_DIR 和 QL_DATA_DIR
export QL_DIR=""
export QL_DATA_DIR=""
# 再次执行
qinglong
```
## 内置命令
- task
```bash
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
task <file_path> -- -u whyour -p password
```
- ql
```bash ```bash
# 更新并重启青龙 # 更新并重启青龙
@@ -167,33 +148,30 @@ ql check
ql resetlet ql resetlet
# 禁用两步登录 # 禁用两步登录
ql resettfa 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下的仓库目录,不受黑名单影响,多个竖线分割 |
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
| branch | 拉取仓库的分支 |
| days | 需要保留的日志的天数 |
| file_path | 任务执行时的文件路径 |
## 开发 * file_url: 脚本地址
* repo_url: 仓库地址
```bash * whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串
git clone https://github.com/whyour/qinglong.git * blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串
cd qinglong * dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响
cp .env.example .env * branch: 拉取仓库的分支
# 推荐使用 pnpm https://pnpm.io/zh/installation * extensions: 拉取仓库的文件后缀
npm install -g pnpm@8.3.1 * days: 需要保留的日志的天数
pnpm install * file_path: 任务执行时的文件路径
pnpm start * env_name: 任务执行时需要并发或者指定时的环境变量名称
``` * account_number: 任务执行时指定某个环境变量需要执行的账号序号
打开你的浏览器,访问 <http://127.0.0.1:5700>
## 链接 ## 链接
@@ -201,10 +179,28 @@ pnpm start
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs](https://umijs.org) - [Umijs3.0](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```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)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
+20 -39
View File
@@ -3,36 +3,19 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
import { SAMPLE_FILES } from '../config/const';
import ConfigService from '../services/config';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/configs', route); app.use('/configs', route);
route.get(
'/sample',
async (req: Request, res: Response, next: NextFunction) => {
try {
res.send({
code: 200,
data: SAMPLE_FILES,
});
} catch (e) {
return next(e);
}
},
);
route.get( route.get(
'/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 {
const fileList = await fs.readdir(config.configPath, 'utf-8'); const fileList = fs.readdirSync(config.configPath, 'utf-8');
res.send({ res.send({
code: 200, code: 200,
data: fileList data: fileList
@@ -48,11 +31,24 @@ export default (app: Router) => {
); );
route.get( route.get(
'/detail', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const configService = Container.get(ConfigService); let content = '';
await configService.getFile(req.query.path as string, res); if (config.blackFileList.includes(req.params.file)) {
res.send({ code: 403, message: '文件无法访问' });
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
`${config.samplePath}${req.params.file}`,
);
} else {
content = getFileContentByName(
`${config.configPath}${req.params.file}`,
);
}
res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
@@ -74,27 +70,12 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) { if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' }); res.send({ code: 403, message: '文件无法访问' });
} }
let path = join(config.configPath, name); const path = `${config.configPath}${name}`;
if (name.startsWith('data/scripts/')) { fs.writeFileSync(path, content);
path = join(config.rootPath, name);
}
await fs.writeFile(path, content);
res.send({ code: 200, message: '保存成功' }); res.send({ code: 200, message: '保存成功' });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.get(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
try {
const configService = Container.get(ConfigService);
await configService.getFile(req.params.file, res);
} catch (e) {
return next(e);
}
},
);
}; };
+4 -157
View File
@@ -2,7 +2,6 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import CronService from '../services/cron'; import CronService from '../services/cron';
import CronViewService from '../services/cronView';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
const route = Router(); const route = Router();
@@ -10,141 +9,11 @@ const route = Router();
export default (app: Router) => { export default (app: Router) => {
app.use('/crons', route); app.use('/crons', route);
route.get(
'/views',
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.list();
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.post(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.create(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views',
celebrate({
body: Joi.object({
name: Joi.string().required(),
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
if (req.body.type === 1) {
return res.send({ code: 400, message: '参数错误' });
} else {
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
}
} catch (e) {
return next(e);
}
},
);
route.delete(
'/views',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.remove(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/move',
celebrate({
body: Joi.object({
fromIndex: Joi.number().required(),
toIndex: Joi.number().required(),
id: Joi.number().required(),
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.move(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/disable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.disabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.put(
'/views/enable',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.enabled(req.body);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
}
},
);
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.crontabs(req.query as any); const data = await cronService.crontabs(req.query.searchValue as string);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
logger.error('🔥 error: %o', e); logger.error('🔥 error: %o', e);
@@ -152,21 +21,6 @@ export default (app: Router) => {
} }
}); });
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.find(req.query as any);
return res.send({ code: 200, data });
} catch (e) {
logger.error('🔥 error: %o', e);
return next(e);
}
},
);
route.post( route.post(
'/', '/',
celebrate({ celebrate({
@@ -175,10 +29,6 @@ export default (app: Router) => {
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional(), name: Joi.string().optional(),
labels: Joi.array().optional(), labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -335,10 +185,6 @@ export default (app: Router) => {
command: Joi.string().required(), command: Joi.string().required(),
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional().allow(null), name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
@@ -458,12 +304,13 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.status({ const data = await cronService.status({
...req.body, ...req.body,
status: req.body.status ? parseInt(req.body.status) : undefined, status: parseInt(req.body.status),
pid: req.body.pid ? parseInt(req.body.pid) : undefined, pid: parseInt(req.body.pid) || '',
}); });
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
-16
View File
@@ -134,20 +134,4 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/cancel',
celebrate({
body: Joi.array().items(Joi.number().required()),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const dependenceService = Container.get(DependenceService);
await dependenceService.cancel(req.body);
return res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+2 -53
View File
@@ -3,22 +3,8 @@ import { Container } from 'typedi';
import EnvService from '../services/env'; import EnvService from '../services/env';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
import { safeJSONParse } from '../config/util';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/envs', route); app.use('/envs', route);
@@ -40,9 +26,7 @@ export default (app: Router) => {
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string() name: Joi.string().required(),
.required()
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
remarks: Joi.string().optional().allow(''), remarks: Joi.string().optional().allow(''),
}), }),
), ),
@@ -51,9 +35,6 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
if (!req.body?.length) {
return res.send({ code: 400, message: '参数不正确' });
}
const data = await envService.create(req.body); const data = await envService.create(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
@@ -113,6 +94,7 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.move(req.params.id, req.body); const data = await envService.move(req.params.id, req.body);
@@ -195,37 +177,4 @@ export default (app: Router) => {
} }
}, },
); );
route.post(
'/upload',
upload.single('env'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = safeJSONParse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
if (data.every((x) => x.name && x.value)) {
const result = await envService.create(
data.map((x) => ({
name: x.name,
value: x.value,
remarks: x.remarks,
})),
);
return res.send({ code: 200, data: result });
} else {
return res.send({
code: 400,
message: '每条数据 name 或者 value 字段不能为空,参考导出文件格式',
});
}
} catch (e) {
return next(e);
}
},
);
}; };
+4 -49
View File
@@ -3,9 +3,8 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs, rmPath } from '../config/util'; import { getFileContentByName, readDirs } from '../config/util';
import { join } from 'path'; import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -15,7 +14,7 @@ export default (app: Router) => {
route.get('/', async (req: Request, res: Response, next: NextFunction) => { route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const result = await readDirs(config.logPath, config.logPath, blacklist); const result = readDirs(config.logPath, config.logPath, blacklist);
res.send({ res.send({
code: 200, code: 200,
data: result, data: result,
@@ -26,29 +25,10 @@ export default (app: Router) => {
} }
}); });
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
try {
if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' });
}
const filePath = join(
config.logPath,
(req.query.path || '') as string,
req.query.file as string,
);
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
}
},
);
route.get( route.get(
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
if (blacklist.includes(req.path)) { if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' }); return res.send({ code: 403, message: '暂无权限' });
@@ -58,36 +38,11 @@ export default (app: Router) => {
(req.query.path || '') as string, (req.query.path || '') as string,
req.params.file, req.params.file,
); );
const content = await getFileContentByName(filePath); const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
await rmPath(filePath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+27 -103
View File
@@ -4,13 +4,12 @@ import {
readDirs, readDirs,
getLastModifyFilePath, getLastModifyFilePath,
readDir, readDir,
rmPath,
} from '../config/util'; } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises'; import * as fs from 'fs';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path'; import path, { join, parse } from 'path';
import ScriptService from '../services/script'; import ScriptService from '../services/script';
@@ -34,33 +33,15 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let result = []; let result = [];
const blacklist = [ const blacklist = ['node_modules', '.git'];
'node_modules',
'.git',
'.pnpm',
'pnpm-lock.yaml',
'yarn.lock',
'package-lock.json',
];
if (req.query.path) { if (req.query.path) {
const targetPath = path.join( const targetPath = path.join(
config.scriptPath, config.scriptPath,
req.query.path as string, req.query.path as string,
); );
result = await readDir(targetPath, config.scriptPath, blacklist); result = readDir(targetPath, config.scriptPath, blacklist);
} else { } else {
result = await readDirs( result = readDirs(config.scriptPath, config.scriptPath, blacklist);
config.scriptPath,
config.scriptPath,
blacklist,
(a, b) => {
if (a.type === b.type) {
return a.title.localeCompare(b.title);
} else {
return a.type === 'directory' ? -1 : 1;
}
},
);
} }
res.send({ res.send({
code: 200, code: 200,
@@ -72,31 +53,17 @@ export default (app: Router) => {
} }
}); });
route.get(
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
try {
const scriptService = Container.get(ScriptService);
const content = await scriptService.getFile(
req.query.path as string,
req.query.file as string,
);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
}
},
);
route.get( route.get(
'/:file', '/:file',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const scriptService = Container.get(ScriptService); const filePath = join(
const content = await scriptService.getFile( config.scriptPath,
req.query.path as string, req.query.path as string,
req.params.file, req.params.file,
); );
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content }); res.send({ code: 200, data: content });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -110,13 +77,11 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path, content, originFilename, directory } = let { filename, path, content, originFilename } = req.body as {
req.body as {
filename: string; filename: string;
path: string; path: string;
content: string; content: string;
originFilename: string; originFilename: string;
directory: string;
}; };
if (!path) { if (!path) {
@@ -126,7 +91,7 @@ export default (app: Router) => {
path += '/'; path += '/';
} }
if (!path.startsWith('/')) { if (!path.startsWith('/')) {
path = join(config.scriptPath, path); path = `${config.scriptPath}${path}`;
} }
if (config.writePathList.every((x) => !path.startsWith(x))) { if (config.writePathList.every((x) => !path.startsWith(x))) {
return res.send({ return res.send({
@@ -136,34 +101,25 @@ export default (app: Router) => {
} }
if (req.file) { if (req.file) {
await fs.rename(req.file.path, join(path, filename)); fs.renameSync(req.file.path, join(path, req.file.filename));
return res.send({ code: 200 });
}
if (directory) {
await fs.mkdir(join(path, directory), { recursive: true });
return res.send({ code: 200 }); return res.send({ code: 200 });
} }
if (!originFilename) { if (!originFilename) {
originFilename = filename; originFilename = filename;
} }
const originFilePath = join( const originFilePath = `${path}${originFilename.replace(/\//g, '')}`;
path, const filePath = `${path}${filename.replace(/\//g, '')}`;
`${originFilename.replace(/\//g, '')}`, if (fs.existsSync(originFilePath)) {
); fs.copyFileSync(
const filePath = join(path, `${filename.replace(/\//g, '')}`);
const fileExists = await fileExist(filePath);
if (fileExists) {
await fs.copyFile(
originFilePath, originFilePath,
join(config.bakPath, originFilename.replace(/\//g, '')), `${config.bakPath}${originFilename.replace(/\//g, '')}`,
); );
if (filename !== originFilename) { if (filename !== originFilename) {
await rmPath(originFilePath); fs.unlinkSync(originFilePath);
} }
} }
await fs.writeFile(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -189,7 +145,7 @@ export default (app: Router) => {
path: string; path: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
await fs.writeFile(filePath, content); fs.writeFileSync(filePath, content);
return res.send({ code: 200 }); return res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -203,19 +159,17 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path, type } = req.body as { let { filename, path } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
await rmPath(filePath); fs.unlinkSync(filePath);
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -236,7 +190,7 @@ export default (app: Router) => {
let { filename } = req.body as { let { filename } = req.body as {
filename: string; filename: string;
}; };
const filePath = join(config.scriptPath, filename); const filePath = `${config.scriptPath}${filename}`;
// const stats = fs.statSync(filePath); // const stats = fs.statSync(filePath);
// res.set({ // res.set({
// 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件 // 'Content-Type': 'application/octet-stream', //告诉浏览器这是一个二进制文件
@@ -268,7 +222,7 @@ export default (app: Router) => {
let { filename, content, path } = req.body; let { filename, content, path } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
await fs.writeFile(filePath, content || '', { encoding: 'utf8' }); fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath); const result = await scriptService.runScript(filePath);
@@ -284,53 +238,23 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
pid: Joi.number().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, path, pid } = req.body; let { filename, content, path } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const logPath = join(config.logPath, path, `${name}.swap`);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath, pid); const result = await scriptService.stopScript(filePath);
setTimeout(() => {
rmPath(logPath);
}, 3000);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.put(
'/rename',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
newFilename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type, newFilename } = req.body as {
filename: string;
path: string;
type: string;
newFilename: string;
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
await fs.rename(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+1 -11
View File
@@ -49,9 +49,6 @@ export default (app: Router) => {
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(), schedule_type: Joi.string().required(),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -180,9 +177,6 @@ export default (app: Router) => {
sub_before: Joi.string().optional().allow('').allow(null), sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
autoAddCron: Joi.boolean().optional().allow('').allow(null),
autoDelCron: Joi.boolean().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
@@ -210,16 +204,12 @@ export default (app: Router) => {
'/', '/',
celebrate({ celebrate({
body: Joi.array().items(Joi.number().required()), body: Joi.array().items(Joi.number().required()),
query: Joi.object({
force: Joi.boolean().optional(),
t: Joi.number()
})
}), }),
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 subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.remove(req.body, req.query); const data = await subscriptionService.remove(req.body);
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
+17 -238
View File
@@ -1,30 +1,13 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs/promises'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import SystemService from '../services/system'; import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { import { EnvModel } from '../data/env';
getUniqPath,
handleLogPath,
parseVersion,
promiseExec,
} from '../config/util';
import dayjs from 'dayjs';
import multer from 'multer';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.tmpPath);
},
filename: function (req, file, cb) {
cb(null, 'data.tgz');
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/system', route); app.use('/system', route);
@@ -34,14 +17,18 @@ export default (app: Router) => {
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const { version, changeLog, changeLogLink, publishTime } = const envCount = await EnvModel.count();
await parseVersion(config.versionFile); const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const version = currentVersionFile.match(versionRegx)![1];
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' &&
envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
@@ -50,10 +37,6 @@ export default (app: Router) => {
data: { data: {
isInitialized, isInitialized,
version, version,
publishTime: dayjs(publishTime).unix(),
branch: process.env.QL_BRANCH || 'master',
changeLog,
changeLogLink,
}, },
}); });
} catch (e) { } catch (e) {
@@ -63,12 +46,12 @@ export default (app: Router) => {
}); });
route.get( route.get(
'/config', '/log/remove',
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 systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const data = await systemService.getSystemConfig(); const data = await systemService.getLogRemoveFrequency();
res.send({ code: 200, data }); res.send({ code: 200, data });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -77,16 +60,19 @@ export default (app: Router) => {
); );
route.put( route.put(
'/config/log-remove-frequency', '/log/remove',
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
logRemoveFrequency: Joi.number().allow(null), frequency: Joi.number().required(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const result = await systemService.updateLogRemoveFrequency(req.body); const result = await systemService.updateLogRemoveFrequency(
req.body.frequency,
);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -94,96 +80,6 @@ export default (app: Router) => {
}, },
); );
route.put(
'/config/cron-concurrency',
celebrate({
body: Joi.object({
cronConcurrency: Joi.number().allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateCronConcurrency(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/dependence-proxy',
celebrate({
body: Joi.object({
dependenceProxy: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updateDependenceProxy(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/node-mirror',
celebrate({
body: Joi.object({
nodeMirror: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
res.setHeader('Content-type', 'application/octet-stream');
await systemService.updateNodeMirror(req.body, res);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/python-mirror',
celebrate({
body: Joi.object({
pythonMirror: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.updatePythonMirror(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/config/linux-mirror',
celebrate({
body: Joi.object({
linuxMirror: Joi.string().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
res.setHeader('Content-type', 'application/octet-stream');
await systemService.updateLinuxMirror(req.body, res);
} catch (e) {
return next(e);
}
},
);
route.put( route.put(
'/update-check', '/update-check',
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -212,25 +108,6 @@ export default (app: Router) => {
}, },
); );
route.put(
'/reload',
celebrate({
body: Joi.object({
type: Joi.string().optional().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem(req.body.type);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put( route.put(
'/notify', '/notify',
celebrate({ celebrate({
@@ -250,102 +127,4 @@ export default (app: Router) => {
} }
}, },
); );
route.put(
'/command-run',
celebrate({
body: Joi.object({
command: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const command = req.body.command;
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
let id = await promiseExec(idStr);
const uniqPath = await getUniqPath(command, id);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logPath = `${uniqPath}/${logTime}.log`;
res.setHeader('Content-type', 'application/octet-stream');
await systemService.run(
{ ...req.body, logPath },
{
onStart: async (cp, startTime) => {
res.setHeader('QL-Task-Pid', `${cp.pid}`);
},
onEnd: async (cp, endTime, diff) => {
res.end();
},
onError: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
await fs.appendFile(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
await fs.appendFile(absolutePath, `\n${message}`);
},
},
);
} catch (e) {
return next(e);
}
},
);
route.put(
'/command-stop',
celebrate({
body: Joi.object({
command: Joi.string().optional(),
pid: Joi.number().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.stop(req.body);
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.put(
'/data/export',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.exportData(res);
} catch (e) {
return next(e);
}
},
);
route.put(
'/data/import',
upload.single('data'),
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.importData();
res.send(result);
} catch (e) {
return next(e);
}
},
);
route.get('/log', async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.getSystemLog(res);
} catch (e) {
return next(e);
}
});
}; };
-8
View File
@@ -6,7 +6,6 @@ import { celebrate, Joi } from 'celebrate';
import multer from 'multer'; import multer from 'multer';
import path from 'path'; import path from 'path';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit';
import config from '../config'; import config from '../config';
const route = Router(); const route = Router();
@@ -27,10 +26,6 @@ export default (app: Router) => {
route.post( route.post(
'/login', '/login',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}),
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
username: Joi.string().required(), username: Joi.string().required(),
@@ -74,9 +69,6 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
if (process.env.DeployEnv === 'demo') {
return res.send({ code: 450, message: '未知错误' });
}
const userService = Container.get(UserService); const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body); await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' }); res.send({ code: 200, message: '更新成功' });
+5 -5
View File
@@ -1,6 +1,9 @@
import 'reflect-metadata'; // We need this in order to use @Decorators import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config'; import config from './config';
import express from 'express'; import express from 'express';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
async function startServer() { async function startServer() {
@@ -15,14 +18,11 @@ async function startServer() {
await require('./loaders/app').default({ expressApp: app }); await require('./loaders/app').default({ expressApp: app });
const server = app const server = app
.listen(config.port, '0.0.0.0', () => { .listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`); Logger.debug(`✌️ Back server launched on port ${config.port}`);
console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
console.error(err);
process.exit(1); process.exit(1);
}); });
-25
View File
@@ -1,25 +0,0 @@
export const LOG_END_SYMBOL = '     ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
export const SAMPLE_FILES = [
{
title: 'config.sample.sh',
value: 'sample/config.sample.sh',
target: 'config.sh',
},
{
title: 'notify.js',
value: 'sample/notify.js',
target: 'data/scripts/sendNotify.js',
},
{
title: 'notify.py',
value: 'sample/notify.py',
target: 'data/scripts/notify.py',
},
];
+6 -49
View File
@@ -1,32 +1,14 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { createRandomString } from './share'; import { createRandomString } from './util';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
if (!process.env.QL_DIR) { const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`;
// 声明QL_DIR环境变量
let qlHomePath = path.join(__dirname, '../../');
// 生产环境
if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../');
}
process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
}
const lastVersionFile = `https://qn.whyour.cn/version.yaml`; const envFound = dotenv.config();
const rootPath = process.cwd();
const rootPath = process.env.QL_DIR as string; const dataPath = path.join(rootPath, 'data/');
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
let dataPath = path.join(rootPath, 'data/');
if (process.env.QL_DATA_DIR) {
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
}
const shellPath = path.join(rootPath, 'shell/');
const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
@@ -34,12 +16,8 @@ const bakPath = path.join(dataPath, 'bak/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/'); const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/');
const systemLogPath = path.join(dataPath, 'syslog/');
const envFile = path.join(configPath, 'env.sh'); const envFile = path.join(configPath, 'env.sh');
const jsEnvFile = path.join(configPath, 'env.js');
const pyEnvFile = path.join(configPath, 'env.py');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const crontabFile = path.join(configPath, 'crontab.list'); const crontabFile = path.join(configPath, 'crontab.list');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
@@ -51,10 +29,7 @@ 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 versionFile = path.join(rootPath, 'version.yaml'); const versionFile = path.join(rootPath, 'src/version.ts');
const dataTgzFile = path.join(tmpPath, 'data.tgz');
const shareShellFile = path.join(shellPath, 'share.sh');
const dependenceProxyFile = path.join(configPath, 'dependence-proxy.sh');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
@@ -64,7 +39,6 @@ export default {
port: parseInt(process.env.BACK_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), publicPort: parseInt(process.env.PUBLIC_PORT as string, 10),
updatePort: parseInt(process.env.UPDATE_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',
@@ -73,11 +47,6 @@ export default {
prefix: '/api', prefix: '/api',
}, },
rootPath, rootPath,
tmpPath,
dataPath,
dataTgzFile,
shareShellFile,
dependenceProxyFile,
configString, configString,
loginFaild, loginFaild,
authError, authError,
@@ -89,8 +58,6 @@ export default {
sampleFile, sampleFile,
confFile, confFile,
envFile, envFile,
jsEnvFile,
pyEnvFile,
dbPath, dbPath,
uploadPath, uploadPath,
configPath, configPath,
@@ -101,10 +68,7 @@ export default {
'config.sh.sample', 'config.sh.sample',
'cookie.sh', 'cookie.sh',
'crontab.list', 'crontab.list',
'dependence-proxy.sh',
'env.sh', 'env.sh',
'env.js',
'env.py',
'token.json', 'token.json',
], ],
writePathList: [configPath, scriptPath], writePathList: [configPath, scriptPath],
@@ -116,15 +80,8 @@ export default {
'/api/system', '/api/system',
'/api/user/init', '/api/user/init',
'/api/user/notification/init', '/api/user/notification/init',
'/open/user/login',
'/open/user/two-factor/login',
'/open/system',
'/open/user/init',
'/open/user/notification/init',
], ],
versionFile, versionFile,
lastVersionFile, lastVersionFile,
sqliteFile, sqliteFile,
sshdPath,
systemLogPath,
}; };
-28
View File
@@ -1,28 +0,0 @@
import { Request, Response } from 'express';
import pick from 'lodash/pick';
let pickedEnv: Record<string, string>;
function getPickedEnv() {
if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
if (picked.QlBaseUrl) {
if (!picked.QlBaseUrl.startsWith('/')) {
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
}
if (!picked.QlBaseUrl.endsWith('/')) {
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
}
}
pickedEnv = picked as Record<string, string>;
return picked;
}
export function serveEnv(_req: Request, res: Response) {
res.type('.js');
res.send(
Object.entries(getPickedEnv())
.map(([k, v]) => `window.__ENV__${k}=${JSON.stringify(v)};`)
.join('\n'),
);
}
-84
View File
@@ -1,84 +0,0 @@
export function createRandomString(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
const ENGLISH = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const special = ['-', '_'];
const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = [];
arr.push(getOne(num));
arr.push(getOne(english));
arr.push(getOne(ENGLISH));
arr.push(getOne(special));
const len = min + Math.floor(Math.random() * (max - min + 1));
for (let i = 4; i < len; i++) {
arr.push(config[Math.floor(Math.random() * config.length)]);
}
const newArr = [];
for (let j = 0; j < len; j++) {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
}
function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)];
}
return newArr.join('');
}
-46
View File
@@ -1,46 +0,0 @@
import { Subscription } from '../data/subscription';
import isNil from 'lodash/isNil';
export function 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 };
}
export function formatCommand(doc: Subscription, url?: string) {
let command = `SUB_ID=${doc.id} ql `;
let _url = url || formatUrl(doc).url;
const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
autoAddCron,
autoDelCron,
} = doc;
if (type === 'file') {
command += `raw "${_url}" "${proxy || ''}" "${
isNil(autoAddCron) ? true : Boolean(autoAddCron)
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
isNil(autoAddCron) ? true : Boolean(autoAddCron)
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
}
return command;
}
+171 -341
View File
@@ -1,36 +1,25 @@
import * as fs from 'fs/promises'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import got from 'got'; import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
import config from './index';
import { TASK_COMMAND } from './const';
import Logger from '../loaders/logger';
export * from './share'; export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
export async function getFileContentByName(fileName: string) { return fs.readFileSync(fileName, 'utf8');
const _exsit = await fileExist(fileName);
if (_exsit) {
return await fs.readFile(fileName, 'utf8');
} }
return ''; return '';
} }
export async function getLastModifyFilePath(dir: string) { export function getLastModifyFilePath(dir: string) {
let filePath = ''; let filePath = '';
const _exsit = await fileExist(dir); if (fs.existsSync(dir)) {
if (_exsit) { const arr = fs.readdirSync(dir);
const arr = await fs.readdir(dir);
arr.forEach(async (item) => { arr.forEach((item) => {
const fullpath = path.join(dir, item); const fullpath = path.join(dir, item);
const stats = await fs.lstat(fullpath); const stats = fs.statSync(fullpath);
if (stats.isFile()) { if (stats.isFile()) {
if (stats.mtimeMs >= 0) { if (stats.mtimeMs >= 0) {
filePath = fullpath; filePath = fullpath;
@@ -41,6 +30,91 @@ export async function getLastModifyFilePath(dir: string) {
return filePath; return filePath;
} }
export function createRandomString(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
const ENGLISH = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const special = ['-', '_'];
const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = [];
arr.push(getOne(num));
arr.push(getOne(english));
arr.push(getOne(ENGLISH));
arr.push(getOne(special));
const len = min + Math.floor(Math.random() * (max - min + 1));
for (let i = 4; i < len; i++) {
arr.push(config[Math.floor(Math.random() * config.length)]);
}
const newArr = [];
for (let j = 0; j < len; j++) {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
}
function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)];
}
return newArr.join('');
}
export function getToken(req: any) { export function getToken(req: any) {
const { authorization = '' } = req.headers; const { authorization = '' } = req.headers;
if (authorization && authorization.split(' ')[0] === 'Bearer') { if (authorization && authorization.split(' ')[0] === 'Bearer') {
@@ -61,8 +135,7 @@ export async function getNetIp(req: any) {
...req.ips, ...req.ips,
req.socket.remoteAddress, req.socket.remoteAddress,
]), ]),
].filter(Boolean); ];
let ip = ipArray[0]; let ip = ipArray[0];
if (ipArray.length > 1) { if (ipArray.length > 1) {
@@ -83,42 +156,34 @@ export async function getNetIp(req: any) {
break; break;
} }
} }
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length); ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) { if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = ''; ip = '';
} }
if (!ip) {
return { address: `获取失败`, ip };
}
try { try {
const csdnApi = got const baiduApi = got
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, { .get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
timeout: 10000,
retry: 0,
})
.text(); .text();
const pconlineApi = got const ipApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, { .get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 10000, timeout: 10000,
retry: 0, retry: 0,
}) })
.buffer(); .buffer();
const [csdnBody, pconlineBody] = await await Promise.all<any>([ const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
csdnApi,
pconlineApi, const ipRegx = /.*IP :(.*)\n/;
]); const addrRegx = /.*数据二 :(.*)\n/;
const csdnRes = JSON.parse(csdnBody); if (data && ipRegx.test(data) && addrRegx.test(data)) {
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK')); const ip = data.match(ipRegx)[1];
let address = ''; const addr = data.match(addrRegx)[1];
if (csdnBody && csdnRes.code == 200) { return { address: addr, ip };
address = csdnRes.data.address; } else if (ipApiBody) {
} else if (pconlineRes && pconlineRes.addr) { const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
address = pconlineRes.addr; return { address: `${addr}`, ip };
} else {
return { address: `获取失败`, ip };
} }
return { address, ip };
} catch (error) { } catch (error) {
return { address: `获取失败`, ip }; return { address: `获取失败`, ip };
} }
@@ -158,29 +223,22 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
} }
export async function fileExist(file: any) { export async function fileExist(file: any) {
return new Promise((resolve) => {
try { try {
await fs.access(file); fs.accessSync(file);
return true; resolve(true);
} catch (error) { } catch (error) {
return false; resolve(false);
} }
});
} }
export async function createFile(file: string, data: string = '') { export async function createFile(file: string, data: string = '') {
await fs.mkdir(path.dirname(file), { recursive: true }); return new Promise((resolve) => {
await fs.writeFile(file, data); fs.mkdirSync(path.dirname(file), { recursive: true });
} fs.writeFileSync(file, data);
resolve(true);
export async function 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;
} }
export async function concurrentRun( export async function concurrentRun(
@@ -225,82 +283,56 @@ enum FileType {
'file', 'file',
} }
interface IFile { export function readDirs(
title: string;
key: string;
type: 'directory' | 'file';
parent: string;
mtime: number;
size?: number;
children?: IFile[];
}
export function dirSort(a: IFile, b: IFile): number {
if (a.type === 'file' && b.type === 'file') {
return b.mtime - a.mtime;
} else if (a.type === 'directory' && b.type === 'directory') {
return a.title.localeCompare(b.title);
} else {
return a.type === 'directory' ? -1 : 1;
}
}
export async function readDirs(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
sort: (a: IFile, b: IFile) => number = dirSort,
): Promise<IFile[]> {
const relativePath = path.relative(baseDir, dir);
const files = await fs.readdir(dir);
const result: IFile[] = [];
for (const file of files) {
const subPath = path.join(dir, file);
const stats = await fs.lstat(subPath);
const key = path.join(relativePath, file);
if (blacklist.includes(file) || stats.isSymbolicLink()) {
continue;
}
if (stats.isDirectory()) {
const children = await readDirs(subPath, baseDir, blacklist, sort);
result.push({
title: file,
key,
type: 'directory',
parent: relativePath,
mtime: stats.mtime.getTime(),
children: children.sort(sort),
});
} else {
result.push({
title: file,
type: 'file',
key,
parent: relativePath,
size: stats.size,
mtime: stats.mtime.getTime(),
});
}
}
return result.sort(sort);
}
export async function readDir(
dir: string, dir: string,
baseDir: string = '', baseDir: string = '',
blacklist: string[] = [], blacklist: string[] = [],
) { ) {
const relativePath = path.relative(baseDir, dir); const relativePath = path.relative(baseDir, dir);
const files = await fs.readdir(dir); const files = fs.readdirSync(dir);
const result: any = files const result: any = files
.filter((x) => !blacklist.includes(x)) .filter((x) => !blacklist.includes(x))
.map(async (file: string) => { .map((file: string) => {
const subPath = path.join(dir, file); const subPath = path.join(dir, file);
const stats = await fs.lstat(subPath); const stats = fs.statSync(subPath);
const key = path.join(relativePath, file);
if (stats.isDirectory()) {
return {
title: file,
key,
type: 'directory',
disabled: true,
parent: relativePath,
children: readDirs(subPath, baseDir).sort(
(a: any, b: any) =>
(FileType as any)[a.type] - (FileType as any)[b.type],
),
};
}
return {
title: file,
type: 'file',
key,
parent: relativePath,
};
});
return result.sort(
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
);
}
export function readDir(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const key = path.join(relativePath, file); const key = path.join(relativePath, file);
return { return {
title: file, title: file,
@@ -312,216 +344,14 @@ export async function readDir(
return result; return result;
} }
export async function promiseExec(command: string): Promise<string> { export function promiseExec(command: string): Promise<string> {
try {
const { stderr, stdout } = await promisify(exec)(command, {
maxBuffer: 200 * 1024 * 1024,
encoding: 'utf8',
});
return stdout || stderr;
} catch (error) {
return JSON.stringify(error);
}
}
export async function promiseExecSuccess(command: string): Promise<string> {
try {
const { stdout } = await promisify(exec)(command, {
maxBuffer: 200 * 1024 * 1024,
encoding: 'utf8',
});
return stdout || '';
} catch (error) {
return '';
}
}
export function parseHeaders(headers: string) {
if (!headers) return {};
const parsed: any = {};
let key;
let val;
let i;
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
}
function parseString(
input: string,
valueFormatFn?: (v: string) => string,
): Record<string, string> {
const regex = /(\w+):\s*((?:(?!\n\w+:).)*)/g;
const matches: Record<string, string> = {};
let match;
while ((match = regex.exec(input)) !== null) {
const [, key, value] = match;
const _key = key.trim();
if (!_key || matches[_key]) {
continue;
}
let _value = value.trim();
try {
_value = valueFormatFn ? valueFormatFn(_value) : _value;
const jsonValue = JSON.parse(_value);
matches[_key] = jsonValue;
} catch (error) {
matches[_key] = _value;
}
}
return matches;
}
export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded'
| 'text/plain',
valueFormatFn?: (v: string) => string,
) {
if (contentType === 'text/plain' || !body) {
return valueFormatFn && body ? valueFormatFn(body) : body;
}
const parsed = parseString(body, valueFormatFn);
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
export function psTree(pid: number): Promise<number[]> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
psTreeFun(pid, (err: any, pids: number[]) => { exec(
if (err) { command,
reject(err); { maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
} (err, stdout, stderr) => {
resolve(pids.filter((x) => !isNaN(x))); resolve(stdout || stderr || JSON.stringify(err));
}); },
);
}); });
} }
export async function killTask(pid: number) {
const pids = await psTree(pid);
if (pids.length) {
try {
[pid, ...pids].reverse().forEach((x) => {
process.kill(x, 15);
});
} catch (error) {}
} else {
process.kill(pid, 2);
}
}
export async function getPid(cmd: string) {
const taskCommand = `ps -eo pid,command | grep "${cmd}" | grep -v grep | awk '{print $1}' | head -1 | xargs echo -n`;
const pid = await promiseExec(taskCommand);
return pid ? Number(pid) : undefined;
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
publishTime: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await fs.readFile(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
export async function getUniqPath(
command: string,
id: string,
): Promise<string> {
if (/^\d+$/.test(id)) {
id = `_${id}`;
} else {
id = '';
}
const items = command.split(/ +/);
let str = items[0];
if (items[0] === TASK_COMMAND) {
str = items[1];
}
const dotIndex = str.lastIndexOf('.');
if (dotIndex !== -1) {
str = str.slice(0, dotIndex);
}
const slashIndex = str.lastIndexOf('/');
let tempStr = '';
if (slashIndex !== -1) {
tempStr = str.slice(0, slashIndex);
const _slashIndex = tempStr.lastIndexOf('/');
if (_slashIndex !== -1) {
tempStr = tempStr.slice(_slashIndex + 1);
}
str = `${tempStr}_${str.slice(slashIndex + 1)}`;
}
return `${str}${id}`;
}
export function safeJSONParse(value?: string) {
if (!value) {
return {};
}
try {
return JSON.parse(value);
} catch (error) {
Logger.error('[JSON.parse失败]', error);
return {};
}
}
export async function rmPath(path: string) {
try {
const _exsit = await fileExist(path);
if (_exsit) {
await fs.rm(path, { force: true, recursive: true, maxRetries: 5 });
}
} catch (error) {
Logger.error('[rmPath失败]', error);
}
}
+38
View File
@@ -0,0 +1,38 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: any;
id?: number;
constructor(options: AuthInfo) {
this.ip = options.ip;
this.info = options.info;
this.type = options.type;
this.id = options.id;
}
}
export enum LoginStatus {
'success',
'fail',
}
export enum AuthDataType {
'loginLog' = 'loginLog',
'authToken' = 'authToken',
'notification' = 'notification',
'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,
},
});
+6 -18
View File
@@ -17,19 +17,15 @@ export class Crontab {
labels?: string[]; labels?: string[];
last_running_time?: number; last_running_time?: number;
last_execution_time?: number; last_execution_time?: number;
sub_id?: number;
extra_schedules?: Array<{ schedule: string }>;
task_before?: string;
task_after?: string;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
this.command = options.command.trim(); 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.status = this.status =
typeof options.status === 'number' && 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();
@@ -41,21 +37,17 @@ export class Crontab {
this.labels = options.labels || []; this.labels = options.labels || [];
this.last_running_time = options.last_running_time || 0; this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0; this.last_execution_time = options.last_execution_time || 0;
this.sub_id = options.sub_id;
this.extra_schedules = options.extra_schedules;
this.task_before = options.task_before;
this.task_after = options.task_after;
} }
} }
export enum CrontabStatus { export enum CrontabStatus {
'running' = 0, 'running',
'queued' = 0.5, 'idle',
'idle' = 1,
'disabled', 'disabled',
'queued',
} }
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { } interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export const CrontabModel = sequelize.define<CronInstance>('Crontab', { export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: { name: {
unique: 'compositeIndex', unique: 'compositeIndex',
@@ -80,8 +72,4 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
labels: DataTypes.JSON, labels: DataTypes.JSON,
last_running_time: DataTypes.NUMBER, last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER, last_execution_time: DataTypes.NUMBER,
sub_id: { type: DataTypes.NUMBER, allowNull: true },
extra_schedules: DataTypes.JSON,
task_before: DataTypes.STRING,
task_after: DataTypes.STRING,
}); });
-62
View File
@@ -1,62 +0,0 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
export enum CronViewType {
'系统' = 1,
'个人',
}
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
property: string;
operation: string;
value: string;
}
export class CrontabView {
name?: string;
id?: number;
position?: number;
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
filterRelation?: 'and' | 'or';
type?: CronViewType;
constructor(options: CrontabView) {
this.name = options.name;
this.id = options.id;
this.position = options.position;
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
this.sorts = options.sorts;
this.filterRelation = options.filterRelation;
this.type = options.type || CronViewType.;
}
}
export interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
'CrontabView',
{
name: {
unique: 'name',
type: DataTypes.STRING,
},
position: DataTypes.NUMBER,
isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON,
sorts: DataTypes.JSON,
filterRelation: {
type: DataTypes.STRING,
allowNull: true,
},
type: DataTypes.NUMBER,
},
);
+9 -26
View File
@@ -4,21 +4,18 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence { export class Dependence {
timestamp?: string; timestamp?: string;
id?: number; id?: number;
status: DependenceStatus; status?: DependenceStatus;
type: DependenceTypes; type?: DependenceTypes;
name: string; name?: number;
log?: string[]; log?: string[];
remark?: string; remark?: string;
constructor(options: Dependence) { constructor(options: Dependence) {
this.id = options.id; this.id = options.id;
this.status = this.status = options.status || DependenceStatus.installing;
typeof options.status === 'number' && DependenceStatus[options.status]
? options.status
: DependenceStatus.queued;
this.type = options.type || DependenceTypes.nodejs; this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.name = options.name.trim(); this.name = options.name;
this.log = options.log || []; this.log = options.log || [];
this.remark = options.remark || ''; this.remark = options.remark || '';
} }
@@ -31,8 +28,6 @@ export enum DependenceStatus {
'removing', 'removing',
'removed', 'removed',
'removeFailed', 'removeFailed',
'queued',
'cancelled',
} }
export enum DependenceTypes { export enum DependenceTypes {
@@ -43,29 +38,17 @@ export enum DependenceTypes {
export enum InstallDependenceCommandTypes { export enum InstallDependenceCommandTypes {
'pnpm add -g', 'pnpm add -g',
'pip3 install --disable-pip-version-check --root-user-action=ignore', 'pip3 install',
'apk add --no-check-certificate', 'apk add',
}
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 show --disable-pip-version-check',
'apk info -es',
}
export enum versionDependenceCommandTypes {
'@',
'==',
'=',
} }
export enum unInstallDependenceCommandTypes { export enum unInstallDependenceCommandTypes {
'pnpm remove -g', 'pnpm remove -g',
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y', 'pip3 uninstall -y',
'apk del', 'apk del',
} }
export interface DependenceInstance interface DependenceInstance
extends Model<Dependence, Dependence>, extends Model<Dependence, Dependence>,
Dependence {} Dependence {}
export const DependenceModel = sequelize.define<DependenceInstance>( export const DependenceModel = sequelize.define<DependenceInstance>(
+3 -9
View File
@@ -13,10 +13,7 @@ export class Env {
constructor(options: Env) { constructor(options: Env) {
this.value = options.value; this.value = options.value;
this.id = options.id; this.id = options.id;
this.status = this.status = options.status || EnvStatus.normal;
typeof options.status === 'number' && EnvStatus[options.status]
? options.status
: EnvStatus.normal;
this.timestamp = new Date().toString(); this.timestamp = new Date().toString();
this.position = options.position; this.position = options.position;
this.name = options.name; this.name = options.name;
@@ -29,12 +26,9 @@ export enum EnvStatus {
'disabled', 'disabled',
} }
export const maxPosition = 9000000000000000; export const initEnvPosition = 9999999999;
export const initPosition = 4500000000000000;
export const stepPosition = 10000000000;
export const minPosition = 100;
export interface EnvInstance extends Model<Env, Env>, Env {} interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', { export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' }, value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING, timestamp: DataTypes.STRING,
+4 -12
View File
@@ -1,23 +1,15 @@
import { Sequelize, Transaction } from 'sequelize'; import { Sequelize } from 'sequelize';
import config from '../config/index'; import config from '../config/index';
import { join } from 'path';
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: join(config.dbPath, 'database.sqlite'), storage: `${config.dbPath}database.sqlite`,
logging: false, logging: false,
retry: {
max: 10,
match: ['SQLITE_BUSY: database is locked'],
},
pool: { pool: {
max: 5, max: 6,
min: 2, min: 0,
idle: 30000, idle: 30000,
acquire: 30000,
evict: 10000,
}, },
transactionType: Transaction.TYPES.IMMEDIATE,
}); });
export type ResponseType<T> = { code: number; data?: T; message?: string }; export type ResponseType<T> = { code: number; data?: T; message?: string };
+3 -69
View File
@@ -1,25 +1,16 @@
import { IncomingHttpHeaders } from 'http';
export enum NotificationMode { export enum NotificationMode {
'gotify' = 'gotify', 'gotify' = 'gotify',
'goCqHttpBot' = 'goCqHttpBot', 'goCqHttpBot' = 'goCqHttpBot',
'serverChan' = 'serverChan', 'serverChan' = 'serverChan',
'pushDeer' = 'pushDeer', 'pushDeer' = 'pushDeer',
'bark' = 'bark', 'bark' = 'bark',
'chat' = 'chat',
'telegramBot' = 'telegramBot', 'telegramBot' = 'telegramBot',
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot', 'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp', 'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'wePlusBot' = 'wePlusBot',
'email' = 'email', 'email' = 'email',
'pushMe' = 'pushMe',
'feishu' = 'feishu',
'webhook' = 'webhook',
'chronocat' = 'Chronocat',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -44,22 +35,13 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class PushDeerNotification extends NotificationBaseInfo { export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = ''; public pushDeerKey = '';
public pushDeerUrl = '';
}
export class ChatNotification extends NotificationBaseInfo {
public chatUrl = '';
public chatToken = '';
} }
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'https://qn.whyour.cn/logo.png'; public barkIcon = 'http://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
public barkLevel = 'active';
public barkUrl = '';
public barkArchive=""
} }
export class TelegramBotNotification extends NotificationBaseInfo { export class TelegramBotNotification extends NotificationBaseInfo {
@@ -68,7 +50,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
public telegramBotProxyHost = ''; public telegramBotProxyHost = '';
public telegramBotProxyPort = ''; public telegramBotProxyPort = '';
public telegramBotProxyAuth = ''; public telegramBotProxyAuth = '';
public telegramBotApiHost = 'https://api.telegram.org'; public telegramBotApiHost = 'api.telegram.org';
} }
export class DingtalkBotNotification extends NotificationBaseInfo { export class DingtalkBotNotification extends NotificationBaseInfo {
@@ -78,18 +60,10 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
export class WeWorkBotNotification extends NotificationBaseInfo { export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = ''; public weWorkBotKey = '';
public weWorkOrigin = '';
} }
export class WeWorkAppNotification extends NotificationBaseInfo { export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = ''; public weWorkAppKey = '';
public weWorkOrigin = '';
}
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
} }
export class IGotNotification extends NotificationBaseInfo { export class IGotNotification extends NotificationBaseInfo {
@@ -101,62 +75,22 @@ export class PushPlusNotification extends NotificationBaseInfo {
public pushPlusUser = ''; public pushPlusUser = '';
} }
export class WePlusBotNotification extends NotificationBaseInfo {
public wePlusBotToken = '';
public wePlusBotReceiver = '';
public wePlusBotVersion = '';
}
export class EmailNotification extends NotificationBaseInfo { export class EmailNotification extends NotificationBaseInfo {
public emailService: string = ''; public emailService: string = '';
public emailUser: string = ''; public emailUser: string = '';
public emailPass: string = ''; public emailPass: string = '';
} }
export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: string = '';
public pushMeUrl: string = '';
}
export class ChronocatNotification extends NotificationBaseInfo {
public chronocatURL: string = '';
public chronocatQQ: string = '';
public chronocatToken: string = '';
}
export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = '';
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
}
export interface NotificationInfo export interface NotificationInfo
extends GoCqHttpBotNotification, extends GoCqHttpBotNotification,
GotifyNotification, GotifyNotification,
ServerChanNotification, ServerChanNotification,
PushDeerNotification, PushDeerNotification,
ChatNotification,
BarkNotification, BarkNotification,
TelegramBotNotification, TelegramBotNotification,
DingtalkBotNotification, DingtalkBotNotification,
WeWorkBotNotification, WeWorkBotNotification,
WeWorkAppNotification, WeWorkAppNotification,
AibotkNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
WePlusBotNotification, EmailNotification {}
EmailNotification,
PushMeNotification,
WebhookNotification,
ChronocatNotification,
LarkNotification {}
+8 -1
View File
@@ -26,7 +26,14 @@ export interface AppToken {
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs'; export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export interface AppInstance extends Model<App, App>, App {} export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', { export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' }, name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON, scopes: DataTypes.JSON,
+1 -4
View File
@@ -16,7 +16,4 @@ export type SockMessageType =
| 'uninstallDependence' | 'uninstallDependence'
| 'updateSystemVersion' | 'updateSystemVersion'
| 'manuallyRunScript' | 'manuallyRunScript'
| 'runSubscriptionEnd' | 'runSubscriptionEnd';
| 'reloadSystem'
| 'updateNodeMirror'
| 'updateLinuxMirror';
+3 -12
View File
@@ -28,17 +28,14 @@ export class Subscription {
extensions?: string; extensions?: string;
sub_before?: string; sub_before?: string;
sub_after?: string; sub_after?: string;
proxy?: string;
autoAddCron?: 1 | 0;
autoDelCron?: 1 | 0;
constructor(options: Subscription) { constructor(options: Subscription) {
this.id = options.id; this.id = options.id;
this.name = options.name || options.alias; this.name = options.name || options.alias;
this.type = options.type; this.type = options.type;
this.schedule = options.schedule; this.schedule = options.schedule;
this.status = this.status = this.status =
typeof options.status === 'number' && SubscriptionStatus[options.status] options.status && SubscriptionStatus[options.status]
? options.status ? options.status
: SubscriptionStatus.idle; : SubscriptionStatus.idle;
this.url = options.url; this.url = options.url;
@@ -57,9 +54,6 @@ export class Subscription {
this.extensions = options.extensions; this.extensions = options.extensions;
this.sub_before = options.sub_before; this.sub_before = options.sub_before;
this.sub_after = options.sub_after; this.sub_after = options.sub_after;
this.proxy = options.proxy;
this.autoAddCron = options.autoAddCron ? 1 : 0;
this.autoDelCron = options.autoDelCron ? 1 : 0;
} }
} }
@@ -70,7 +64,7 @@ export enum SubscriptionStatus {
'queued', 'queued',
} }
export interface SubscriptionInstance interface SubscriptionInstance
extends Model<Subscription, Subscription>, extends Model<Subscription, Subscription>,
Subscription {} Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>( export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
@@ -108,8 +102,5 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
log_path: DataTypes.STRING, log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING, schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' }, alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true },
autoAddCron: { type: DataTypes.NUMBER, allowNull: true },
autoDelCron: { type: DataTypes.NUMBER, allowNull: true },
}, },
); );
-61
View File
@@ -1,61 +0,0 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { NotificationInfo } from './notify';
export class SystemInfo {
ip?: string;
type: AuthDataType;
info?: SystemModelInfo;
id?: number;
constructor(options: SystemInfo) {
this.ip = options.ip;
this.info = options.info;
this.type = options.type;
this.id = options.id;
}
}
export enum LoginStatus {
'success',
'fail',
}
export enum AuthDataType {
'loginLog' = 'loginLog',
'authToken' = 'authToken',
'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig',
}
export interface SystemConfigInfo {
logRemoveFrequency?: number;
cronConcurrency?: number;
dependenceProxy?: string;
nodeMirror?: string;
pythonMirror?: string;
linuxMirror?: string;
}
export interface LoginLogInfo {
timestamp?: number;
address?: string;
ip?: string;
platform?: string;
status?: LoginStatus;
}
export type SystemModelInfo = SystemConfigInfo &
Partial<NotificationInfo> &
LoginLogInfo;
export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { }
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
info: {
type: DataTypes.JSON,
allowNull: true,
},
});
-2
View File
@@ -3,5 +3,3 @@ declare namespace Express {
platform: 'desktop' | 'mobile'; platform: 'desktop' | 'mobile';
} }
} }
declare module 'pstree.remy';
+8 -13
View File
@@ -7,23 +7,18 @@ import linkDeps from './deps';
import initTask from './initTask'; import initTask from './initTask';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
depInjectorLoader(); await depInjectorLoader();
Logger.info('✌️ Dependency loaded'); Logger.info('✌️ Dependency Injector loaded');
console.log('✌️ Dependency loaded');
await expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded');
await initData(); await initData();
Logger.info('✌️ Init data loaded'); Logger.info('✌️ init data loaded');
console.log('✌️ Init data loaded');
await linkDeps(); await linkDeps();
Logger.info('✌️ Link deps loaded'); Logger.info('✌️ link deps loaded');
console.log('✌️ Link deps loaded');
initTask(); initTask();
Logger.info('✌️ Init task loaded'); Logger.info('✌️ init task loaded');
console.log('✌️ Init task loaded');
expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded');
console.log('✌️ Express loaded');
}; };
+13 -46
View File
@@ -5,59 +5,27 @@ import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron'; import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open'; import { AppModel } from '../data/open';
import { SystemModel } from '../data/system'; 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 { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
import config from '../config'; import config from '../config';
import { sequelize } from '../data';
export default async () => { export default async () => {
try { try {
await CrontabModel.sync(); await CrontabModel.sync();
await DependenceModel.sync(); await DependenceModel.sync();
await AppModel.sync(); await AppModel.sync();
await SystemModel.sync(); await AuthModel.sync();
await EnvModel.sync(); await EnvModel.sync();
await SubscriptionModel.sync(); await SubscriptionModel.sync();
await CrontabViewModel.sync();
// 初始化新增字段 // try {
try { // const queryInterface = sequelize.getQueryInterface();
await sequelize.query( // await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
'alter table CrontabViews add column filterRelation VARCHAR(255)', // await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
); // await queryInterface.addIndex('Apps', ['name'], { unique: true });
} catch (error) {} // } catch (error) { }
try {
await sequelize.query(
'alter table Subscriptions add column proxy VARCHAR(255)',
);
} catch (error) {}
try {
await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoAddCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column autoDelCron NUMBER',
);
} catch (error) {}
try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) { }
try {
await sequelize.query('alter table Crontabs add column extra_schedules JSON');
} catch (error) { }
try {
await sequelize.query('alter table Crontabs add column task_before TEXT');
} catch (error) { }
try {
await sequelize.query('alter table Crontabs add column task_after TEXT');
} catch (error) { }
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
@@ -75,7 +43,7 @@ export default async () => {
const dependenceCount = await DependenceModel.count(); const dependenceCount = await DependenceModel.count();
const envCount = await EnvModel.count(); const envCount = await EnvModel.count();
const appCount = await AppModel.count(); const appCount = await AppModel.count();
const authCount = await SystemModel.count(); const authCount = await AuthModel.count();
if (crondbExist && cronCount === 0) { if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({ const cronDb = new DataStore({
filename: cronDbFile, filename: cronDbFile,
@@ -127,14 +95,13 @@ export default async () => {
}); });
authDb.persistence.compactDatafile(); authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => { authDb.find({}).exec(async (err, docs) => {
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true }); await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
}); });
} }
console.log('✌️ DB loaded');
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
} catch (error) { } catch (error) {
console.error('✌️ DB load failed'); Logger.info('✌️ DB load failed');
Logger.error(error); Logger.info(error);
} }
}; };
+20 -29
View File
@@ -1,58 +1,49 @@
import path from 'path'; import path from 'path';
import fs from 'fs/promises'; import fs from 'fs';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import config from '../config/index'; import config from '../config/index';
import { fileExist, promiseExec, rmPath } from '../config/util'; import { promiseExec } from '../config/util';
async function linkToNodeModule(src: string, dst?: string) { function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src); const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src); const source = path.join(config.rootPath, src);
try { fs.lstat(target, (err, stat) => {
const stats = await fs.lstat(target); if (!stat) {
if (!stats) { fs.symlink(source, target, 'dir', (err) => {
await fs.symlink(source, target, 'dir'); if (err) throw err;
});
} }
} catch (error) {} });
} }
async function linkCommand() { async function linkCommand() {
const commandPath = await promiseExec('which node'); const commandPath = await promiseExec('which node');
const commandDir = path.dirname(commandPath); const commandDir = path.dirname(commandPath);
const oldLinkShell = [ const linkShell = [
{ {
src: 'update.sh', src: 'update.sh',
dest: 'ql', dest: 'ql',
tmp: 'ql_tmp', },
{
src: 'task.sh',
dest: 'task',
}, },
]; ];
// const newLinkShell = [
// {
// src: 'task.mjs',
// dest: 'task',
// tmp: 'task_tmp',
// },
// ];
for (const link of oldLinkShell) { for (const link of linkShell) {
const source = path.join(config.rootPath, 'shell', link.src); const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest); const target = path.join(commandDir, link.dest);
const tmpTarget = path.join(commandDir, link.tmp); if (fs.existsSync(target)) {
await fs.symlink(source, tmpTarget); fs.unlinkSync(target);
await fs.rename(tmpTarget, target); }
fs.symlink(source, target, (err) => {});
} }
// for (const link of newLinkShell) {
// const source = path.join(config.rootPath, 'cli', link.src);
// const target = path.join(commandDir, link.dest);
// const tmpTarget = path.join(commandDir, link.tmp);
// await fs.symlink(source, tmpTarget);
// await fs.rename(tmpTarget, target);
// }
} }
export default async (src: string = 'deps') => { export default async (src: string = 'deps') => {
await linkCommand(); await linkCommand();
await linkToNodeModule(src); linkToNodeModule(src);
const source = path.join(config.rootPath, src); const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, { const watcher = chokidar.watch(source, {
+28 -29
View File
@@ -3,42 +3,39 @@ import bodyParser from 'body-parser';
import cors from 'cors'; import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import jwt, { UnauthorizedError } from 'express-jwt'; import jwt from 'express-jwt';
import fs from 'fs/promises'; import fs from 'fs';
import { getPlatform, getToken, safeJSONParse } 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 * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
import Logger from './logger';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback'); app.enable('trust proxy');
app.use(cors()); app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath)); app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
app.use( app.use((req, res, next) => {
'/api/public', if (req.path.startsWith('/api') || req.path.startsWith('/open')) {
createProxyMiddleware({ next();
target: `http://0.0.0.0:${config.publicPort}/api`, } else {
changeOrigin: true, return handler(req, res, {
pathRewrite: { '/api/public': '' }, public: 'static/dist',
logProvider: () => Logger, rewrites: [{ source: '**', destination: '/index.html' }],
}), });
); }
});
app.use(bodyParser.json({ limit: '50mb' })); app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true })); app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use( app.use(
jwt({ jwt({
secret: config.secret, secret: config.secret as string,
algorithms: ['HS384'], algorithms: ['HS384'],
}).unless({ }).unless({
path: [...config.apiWhiteList, /^\/open\//], path: [...config.apiWhiteList, /^\/open\//],
@@ -78,24 +75,22 @@ export default ({ app }: { app: Application }) => {
if ( if (
!headerToken && !headerToken &&
originPath && originPath &&
config.apiWhiteList.includes(originPath) config.apiWhiteList.includes(originPath) &&
originPath !== '/api/crons/status'
) { ) {
return next(); return next();
} }
const data = await fs.readFile(config.authConfigFile, 'utf8'); const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data && headerToken) { if (data) {
const { token = '', tokens = {} } = safeJSONParse(data); const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
} }
const errorCode = headerToken ? 'invalid_token' : 'credentials_required'; const err: any = new Error('UnauthorizedError');
const errorMessage = headerToken err.status = 401;
? 'jwt malformed'
: 'No authorization token was found';
const err = new UnauthorizedError(errorCode, { message: errorMessage });
next(err); next(err);
}); });
@@ -105,12 +100,14 @@ export default ({ app }: { app: Application }) => {
} }
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
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' &&
envCount === 0
) { ) {
isInitialized = false; isInitialized = false;
} }
@@ -171,6 +168,8 @@ export default ({ app }: { app: Application }) => {
}, },
); );
app.use(Sentry.Handlers.errorHandler());
app.use( app.use(
( (
err: Error & { status: number }, err: Error & { status: number },
+17 -65
View File
@@ -4,85 +4,37 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import { DependenceModel, DependenceStatus } from '../data/dependence'; import _ from 'lodash';
import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import config from '../config'; import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService);
// 初始化增加系统配置 // 初始化更新所有任务状态为空闲
await SystemModel.upsert({ type: AuthDataType.systemConfig }); await CrontabModel.update(
await SystemModel.upsert({ type: AuthDataType.notification }); { status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
);
const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖 // 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({ DependenceModel.findAll({
where: {}, where: {},
order: [ order: [['type', 'DESC']],
['type', 'DESC'],
['createdAt', 'DESC'],
],
raw: true, raw: true,
}).then(async (docs) => { }).then(async (docs) => {
await DependenceModel.update( const groups = _.groupBy(docs, 'type');
{ status: DependenceStatus.queued, log: [] }, const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
{ where: { id: docs.map((x) => x.id!) } }, for (const key of keys) {
); const group = groups[key];
setTimeout(() => { const depIds = group.map((x) => x.id);
dependenceService.installDependenceOneByOne(docs); await dependenceService.reInstall(depIds as number[]);
}, 5000);
});
};
// 初始化更新 linux/python/nodejs 镜像源配置
const systemConfig = await systemService.getSystemConfig();
if (systemConfig.info?.pythonMirror) {
systemService.updatePythonMirror({
pythonMirror: systemConfig.info?.pythonMirror,
});
}
if (systemConfig.info?.linuxMirror) {
systemService.updateLinuxMirror(
{
linuxMirror: systemConfig.info?.linuxMirror,
},
undefined,
() => installDependencies(),
);
} else {
installDependencies();
}
if (systemConfig.info?.nodeMirror) {
systemService.updateNodeMirror({
nodeMirror: systemConfig.info?.nodeMirror,
});
}
// 初始化新增默认全部任务视图
CrontabViewModel.findAll({
where: { type: CronViewType., name: '全部任务' },
raw: true,
}).then((docs) => {
if (docs.length === 0) {
CrontabViewModel.create({
name: '全部任务',
type: CronViewType.,
position: initPosition / 2,
});
} }
}); });
// 初始化更新所有任务状态为空闲
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
// 初始化时执行一次所有的ql repo 任务 // 初始化时执行一次所有的ql repo 任务
CrontabModel.findAll({ CrontabModel.findAll({
where: { where: {
@@ -125,7 +77,7 @@ export default async () => {
if (doc.command.includes(`${config.rootPath}/log/`)) { if (doc.command.includes(`${config.rootPath}/log/`)) {
await CrontabModel.update( await CrontabModel.update(
{ {
command: `${config.dataPath}/log/${doc.command.replace( command: `${config.rootPath}/data/log/${doc.command.replace(
`${config.rootPath}/log/`, `${config.rootPath}/log/`,
'', '',
)}`, )}`,
@@ -136,7 +88,7 @@ export default async () => {
if (doc.command.includes(`${config.rootPath}/config/`)) { if (doc.command.includes(`${config.rootPath}/config/`)) {
await CrontabModel.update( await CrontabModel.update(
{ {
command: `${config.dataPath}/config/${doc.command.replace( command: `${config.rootPath}/data/config/${doc.command.replace(
`${config.rootPath}/config/`, `${config.rootPath}/config/`,
'', '',
)}`, )}`,
@@ -147,7 +99,7 @@ export default async () => {
if (doc.command.includes(`${config.rootPath}/db/`)) { if (doc.command.includes(`${config.rootPath}/db/`)) {
await CrontabModel.update( await CrontabModel.update(
{ {
command: `${config.dataPath}/db/${doc.command.replace( command: `${config.rootPath}/data/db/${doc.command.replace(
`${config.rootPath}/db/`, `${config.rootPath}/db/`,
'', '',
)}`, )}`,
+29 -76
View File
@@ -1,38 +1,24 @@
import fs from 'fs/promises'; import fs from 'fs';
import path from 'path'; import path from 'path';
import os from 'os'; import os from 'os';
import dotenv from 'dotenv';
import Logger from './logger'; import Logger from './logger';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
const rootPath = process.env.QL_DIR as string; const rootPath = process.cwd();
let dataPath = path.join(rootPath, 'data/'); const dataPath = path.join(rootPath, 'data/');
if (process.env.QL_DATA_DIR) {
dataPath = process.env.QL_DATA_DIR.replace(/\/$/g, '');
}
const configPath = path.join(dataPath, 'config/'); const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/'); const scriptPath = path.join(dataPath, 'scripts/');
const logPath = path.join(dataPath, 'log/'); const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/'); const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json'); const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json'); const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
const scriptNotifyJsFile = path.join(scriptPath, 'sendNotify.js');
const scriptNotifyPyFile = path.join(scriptPath, 'notify.py');
const TaskBeforeFile = path.join(configPath, 'task_before.sh');
const TaskAfterFile = path.join(configPath, 'task_after.sh');
const homedir = os.homedir(); const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh'); const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d');
const systemLogPath = path.join(dataPath, 'syslog');
export default async () => { export default async () => {
const authFileExist = await fileExist(authConfigFile); const authFileExist = await fileExist(authConfigFile);
@@ -43,81 +29,48 @@ export default async () => {
const uploadDirExist = await fileExist(uploadPath); const uploadDirExist = await fileExist(uploadPath);
const sshDirExist = await fileExist(sshPath); const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath); const bakDirExist = await fileExist(bakPath);
const sshdDirExist = await fileExist(sshdPath);
const systemLogDirExist = await fileExist(systemLogPath);
const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
const TaskBeforeFileExist = await fileExist(TaskBeforeFile);
const TaskAfterFileExist = await fileExist(TaskAfterFile);
if (!configDirExist) { if (!configDirExist) {
await fs.mkdir(configPath); fs.mkdirSync(configPath);
} }
if (!scriptDirExist) {
await fs.mkdir(scriptPath);
}
if (!logDirExist) {
await fs.mkdir(logPath);
}
if (!tmpDirExist) {
await fs.mkdir(tmpPath);
}
if (!uploadDirExist) {
await fs.mkdir(uploadPath);
}
if (!sshDirExist) {
await fs.mkdir(sshPath);
}
if (!bakDirExist) {
await fs.mkdir(bakPath);
}
if (!sshdDirExist) {
await fs.mkdir(sshdPath);
}
if (!systemLogDirExist) {
await fs.mkdir(systemLogPath);
}
// 初始化文件
if (!authFileExist) { if (!authFileExist) {
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile)); fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
} }
if (!confFileExist) { if (!confFileExist) {
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile)); fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
} }
if (!scriptNotifyJsFileExist) { if (!scriptDirExist) {
await fs.writeFile( fs.mkdirSync(scriptPath);
scriptNotifyJsFile,
await fs.readFile(sampleNotifyJsFile),
);
} }
if (!scriptNotifyPyFileExist) { if (!logDirExist) {
await fs.writeFile( fs.mkdirSync(logPath);
scriptNotifyPyFile,
await fs.readFile(sampleNotifyPyFile),
);
} }
if (!TaskBeforeFileExist) { if (!uploadDirExist) {
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile)); fs.mkdirSync(uploadPath);
} }
if (!TaskAfterFileExist) { if (!sshDirExist) {
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile)); fs.mkdirSync(sshPath);
} }
if (!bakDirExist) {
fs.mkdirSync(bakPath);
}
dotenv.config({ path: confFile });
// 声明QL_DIR环境变量
let qlHomePath = path.join(__dirname, '../../');
// 生产环境
if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../');
}
process.env.QL_DIR = qlHomePath;
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
console.log('✌️ Init file down');
}; };
+16 -31
View File
@@ -1,51 +1,36 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system'; import SystemService from '../services/system';
import ScheduleService, { ScheduleTaskType } from '../services/schedule'; import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
import { join } from 'path';
export default async () => { export default async () => {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const scheduleService = Container.get(ScheduleService); const scheduleService = Container.get(ScheduleService);
const subscriptionService = Container.get(SubscriptionService); const subscriptionService = Container.get(SubscriptionService);
// 生成内置token
let tokenCommand = `tsx ${join(config.rootPath, 'back/token.ts')}`;
const tokenFile = join(config.rootPath, 'static/build/token.js');
if (await fileExist(tokenFile)) {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: NaN,
name: '生成token',
command: tokenCommand,
} as ScheduleTaskType;
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, {
days: 28,
});
// 运行删除日志任务 // 运行删除日志任务
const data = await systemService.getSystemConfig(); const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.logRemoveFrequency) { if (data && data.info && data.info.frequency) {
const rmlogCron = { const cron = {
id: data.id as number, id: data.id,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${data.info.logRemoveFrequency}`, command: `ql rmlog ${data.info.frequency}`,
}; };
await scheduleService.cancelIntervalTask(rmlogCron); await scheduleService.createIntervalTask(cron, {
scheduleService.createIntervalTask(rmlogCron, { days: data.info.frequency,
days: data.info.logRemoveFrequency, runImmediately: true,
}); });
} }
// 运行所有订阅 // 运行所有订阅
await subscriptionService.setSshConfig();
const subs = await subscriptionService.list(); const subs = await subscriptionService.list();
for (const sub of subs) { for (const sub of subs) {
subscriptionService.handleTask(sub, !sub.is_disabled, !sub.is_disabled); await subscriptionService.handleTask(
sub,
!sub.is_disabled,
true,
!sub.is_disabled,
);
} }
}; };
+20 -26
View File
@@ -1,38 +1,32 @@
import winston from 'winston'; import winston from 'winston';
import 'winston-daily-rotate-file';
import config from '../config'; import config from '../config';
import path from 'path';
const levelMap: Record<string, string> = { const transports = [];
info: '🔵', if (process.env.NODE_ENV !== 'development') {
warn: '🟡', transports.push(new winston.transports.Console());
error: '🔴', } else {
debug: '🔶' transports.push(
} new winston.transports.Console({
format: winston.format.combine(
const customFormat = winston.format.combine( winston.format.cli(),
winston.format.splat(), winston.format.splat(),
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), ),
winston.format.align(), }),
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
); );
}
const defaultOptions = {
format: customFormat,
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "7d",
};
const LoggerInstance = winston.createLogger({ const LoggerInstance = winston.createLogger({
level: config.logs.level, level: config.logs.level,
levels: winston.config.npm.levels, levels: winston.config.npm.levels,
transports: [ format: winston.format.combine(
new winston.transports.DailyRotateFile({ winston.format.timestamp({
filename: path.join(config.systemLogPath, '%DATE%.log'), format: 'YYYY-MM-DD HH:mm:ss',
...defaultOptions, }),
}) winston.format.errors({ stack: true }),
], winston.format.splat(),
winston.format.json(),
),
transports,
}); });
export default LoggerInstance; export default LoggerInstance;
+6 -17
View File
@@ -1,32 +1,21 @@
import { Application } from 'express'; import { Application } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config';
import fs from 'fs';
import { parseVersion } from '../config/util';
export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({ Sentry.init({
ignoreErrors: [ dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Sentry.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.8,
release: version, tracesSampleRate: 1.0,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
expressApp.use(Sentry.Handlers.tracingHandler()); expressApp.use(Sentry.Handlers.tracingHandler());
Logger.info('✌️ Sentry loaded'); Logger.info('✌️ Sentry loaded');
console.log('✌️ Sentry loaded');
}; };
+5 -5
View File
@@ -6,11 +6,11 @@ export default async ({ server }: { server: Server }) => {
await Sock({ server }); await Sock({ server });
Logger.info('✌️ Sock loaded'); Logger.info('✌️ Sock loaded');
process.on('uncaughtException', (error) => { process.on('SIGINT', () => {
Logger.error('Uncaught exception:', error); Logger.info('✌️ Server need close');
server.close(() => {
Logger.info('✌️ Server closed');
process.exit(0);
}); });
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled rejection:', reason, promise);
}); });
}; };
+6 -5
View File
@@ -1,25 +1,26 @@
import sockJs from 'sockjs'; import sockJs from 'sockjs';
import { Server } from 'http'; import { Server } from 'http';
import Logger from './logger';
import { Container } from 'typedi'; import { Container } from 'typedi';
import SockService from '../services/sock'; import SockService from '../services/sock';
import config from '../config/index'; import config from '../config/index';
import fs from 'fs/promises'; import fs from 'fs';
import { getPlatform, safeJSONParse } from '../config/util'; import { getPlatform } from '../config/util';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} }); const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const sockService = Container.get(SockService); const sockService = Container.get(SockService);
echo.on('connection', async (conn) => { echo.on('connection', (conn) => {
if (!conn.headers || !conn.url || !conn.pathname) { if (!conn.headers || !conn.url || !conn.pathname) {
conn.close('404'); conn.close('404');
} }
const data = await fs.readFile(config.authConfigFile, 'utf8'); const data = fs.readFileSync(config.authConfigFile, 'utf8');
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) { if (data) {
const { token = '', tokens = {} } = safeJSONParse(data); const { token = '', tokens = {} } = JSON.parse(data);
if (headerToken === token || tokens[platform] === headerToken) { if (headerToken === token || tokens[platform] === headerToken) {
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' })); conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
sockService.addClient(conn); sockService.addClient(conn);
-106
View File
@@ -1,106 +0,0 @@
import bodyParser from 'body-parser';
import { errors } from 'celebrate';
import cors from 'cors';
import { Application, NextFunction, Request, Response } from 'express';
import jwt from 'express-jwt';
import Container from 'typedi';
import config from '../config';
import SystemService from '../services/system';
import Logger from './logger';
export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback');
app.use(cors());
app.use(bodyParser.json({ limit: '50mb' }));
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }));
app.use(
jwt({
secret: config.secret,
algorithms: ['HS384'],
}),
);
app.put(
'/api/reload',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem();
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.put(
'/api/system',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('system');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.put(
'/api/data',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const result = await systemService.reloadSystem('data');
res.send(result);
} catch (e) {
Logger.error('🔥 error: %o', e);
return next(e);
}
},
);
app.use((req, res, next) => {
const err: any = new Error('Not Found');
err['status'] = 404;
next(err);
});
app.use(errors());
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
if (err.name === 'UnauthorizedError') {
return res
.status(err.status)
.send({ code: 401, message: err.message })
.end();
}
return next(err);
},
);
app.use(
(
err: Error & { status: number },
req: Request,
res: Response,
next: NextFunction,
) => {
res.status(err.status || 500);
res.json({
code: err.status || 500,
message: err.message,
});
},
);
};
-26
View File
@@ -1,26 +0,0 @@
syntax = "proto3";
package com.ql.cron;
service Cron {
rpc addCron(AddCronRequest) returns (AddCronResponse);
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
}
message ISchedule { string schedule = 1; }
message ICron {
string id = 1;
string schedule = 2;
string command = 3;
repeated ISchedule extra_schedules = 4;
string name = 5;
}
message AddCronRequest { repeated ICron crons = 1; }
message AddCronResponse {}
message DeleteCronRequest { repeated string ids = 1; }
message DeleteCronResponse {}
-502
View File
@@ -1,502 +0,0 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientUnaryCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "com.ql.cron";
export interface ISchedule {
schedule: string;
}
export interface ICron {
id: string;
schedule: string;
command: string;
extraSchedules: ISchedule[];
name: string;
}
export interface AddCronRequest {
crons: ICron[];
}
export interface AddCronResponse {
}
export interface DeleteCronRequest {
ids: string[];
}
export interface DeleteCronResponse {
}
function createBaseISchedule(): ISchedule {
return { schedule: "" };
}
export const ISchedule = {
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.schedule !== "") {
writer.uint32(10).string(message.schedule);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseISchedule();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.schedule = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): ISchedule {
return { schedule: isSet(object.schedule) ? String(object.schedule) : "" };
},
toJSON(message: ISchedule): unknown {
const obj: any = {};
message.schedule !== undefined && (obj.schedule = message.schedule);
return obj;
},
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
return ISchedule.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
const message = createBaseISchedule();
message.schedule = object.schedule ?? "";
return message;
},
};
function createBaseICron(): ICron {
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
}
export const ICron = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== "") {
writer.uint32(10).string(message.id);
}
if (message.schedule !== "") {
writer.uint32(18).string(message.schedule);
}
if (message.command !== "") {
writer.uint32(26).string(message.command);
}
for (const v of message.extraSchedules) {
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
}
if (message.name !== "") {
writer.uint32(42).string(message.name);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.id = reader.string();
continue;
case 2:
if (tag !== 18) {
break;
}
message.schedule = reader.string();
continue;
case 3:
if (tag !== 26) {
break;
}
message.command = reader.string();
continue;
case 4:
if (tag !== 34) {
break;
}
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
continue;
case 5:
if (tag !== 42) {
break;
}
message.name = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): ICron {
return {
id: isSet(object.id) ? String(object.id) : "",
schedule: isSet(object.schedule) ? String(object.schedule) : "",
command: isSet(object.command) ? String(object.command) : "",
extraSchedules: Array.isArray(object?.extraSchedules)
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
: [],
name: isSet(object.name) ? String(object.name) : "",
};
},
toJSON(message: ICron): unknown {
const obj: any = {};
message.id !== undefined && (obj.id = message.id);
message.schedule !== undefined && (obj.schedule = message.schedule);
message.command !== undefined && (obj.command = message.command);
if (message.extraSchedules) {
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
} else {
obj.extraSchedules = [];
}
message.name !== undefined && (obj.name = message.name);
return obj;
},
create<I extends Exact<DeepPartial<ICron>, I>>(base?: I): ICron {
return ICron.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron();
message.id = object.id ?? "";
message.schedule = object.schedule ?? "";
message.command = object.command ?? "";
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
message.name = object.name ?? "";
return message;
},
};
function createBaseAddCronRequest(): AddCronRequest {
return { crons: [] };
}
export const AddCronRequest = {
encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).ldelim();
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.crons.push(ICron.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): AddCronRequest {
return { crons: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
},
toJSON(message: AddCronRequest): unknown {
const obj: any = {};
if (message.crons) {
obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined);
} else {
obj.crons = [];
}
return obj;
},
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
return message;
},
};
function createBaseAddCronResponse(): AddCronResponse {
return {};
}
export const AddCronResponse = {
encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): AddCronResponse {
return {};
},
toJSON(_: AddCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
const message = createBaseAddCronResponse();
return message;
},
};
function createBaseDeleteCronRequest(): DeleteCronRequest {
return { ids: [] };
}
export const DeleteCronRequest = {
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
for (const v of message.ids) {
writer.uint32(10).string(v!);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.ids.push(reader.string());
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): DeleteCronRequest {
return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
},
toJSON(message: DeleteCronRequest): unknown {
const obj: any = {};
if (message.ids) {
obj.ids = message.ids.map((e) => e);
} else {
obj.ids = [];
}
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || [];
return message;
},
};
function createBaseDeleteCronResponse(): DeleteCronResponse {
return {};
}
export const DeleteCronResponse = {
encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(_: any): DeleteCronResponse {
return {};
},
toJSON(_: DeleteCronResponse): unknown {
const obj: any = {};
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
const message = createBaseDeleteCronResponse();
return message;
},
};
export type CronService = typeof CronService;
export const CronService = {
addCron: {
path: "/com.ql.cron.Cron/addCron",
requestStream: false,
responseStream: false,
requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
},
delCron: {
path: "/com.ql.cron.Cron/delCron",
requestStream: false,
responseStream: false,
requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
},
} as const;
export interface CronServer extends UntypedServiceImplementation {
addCron: handleUnaryCall<AddCronRequest, AddCronResponse>;
delCron: handleUnaryCall<DeleteCronRequest, DeleteCronResponse>;
}
export interface CronClient extends Client {
addCron(
request: AddCronRequest,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
addCron(
request: AddCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: AddCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
): ClientUnaryCall;
}
export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
service: typeof CronService;
};
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
-22
View File
@@ -1,22 +0,0 @@
syntax = "proto3";
package com.ql.health;
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
-254
View File
@@ -1,254 +0,0 @@
/* eslint-disable */
import {
CallOptions,
ChannelCredentials,
Client,
ClientOptions,
ClientReadableStream,
ClientUnaryCall,
handleServerStreamingCall,
handleUnaryCall,
makeGenericClientConstructor,
Metadata,
ServiceError,
UntypedServiceImplementation,
} from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = "com.ql.health";
export interface HealthCheckRequest {
service: string;
}
export interface HealthCheckResponse {
status: HealthCheckResponse_ServingStatus;
}
export enum HealthCheckResponse_ServingStatus {
UNKNOWN = 0,
SERVING = 1,
NOT_SERVING = 2,
SERVICE_UNKNOWN = 3,
UNRECOGNIZED = -1,
}
export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {
switch (object) {
case 0:
case "UNKNOWN":
return HealthCheckResponse_ServingStatus.UNKNOWN;
case 1:
case "SERVING":
return HealthCheckResponse_ServingStatus.SERVING;
case 2:
case "NOT_SERVING":
return HealthCheckResponse_ServingStatus.NOT_SERVING;
case 3:
case "SERVICE_UNKNOWN":
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
case -1:
case "UNRECOGNIZED":
default:
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
}
}
export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {
switch (object) {
case HealthCheckResponse_ServingStatus.UNKNOWN:
return "UNKNOWN";
case HealthCheckResponse_ServingStatus.SERVING:
return "SERVING";
case HealthCheckResponse_ServingStatus.NOT_SERVING:
return "NOT_SERVING";
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
return "SERVICE_UNKNOWN";
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
default:
return "UNRECOGNIZED";
}
}
function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: "" };
}
export const HealthCheckRequest = {
encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.service !== "") {
writer.uint32(10).string(message.service);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.service = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? String(object.service) : "" };
},
toJSON(message: HealthCheckRequest): unknown {
const obj: any = {};
message.service !== undefined && (obj.service = message.service);
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
const message = createBaseHealthCheckRequest();
message.service = object.service ?? "";
return message;
},
};
function createBaseHealthCheckResponse(): HealthCheckResponse {
return { status: 0 };
}
export const HealthCheckResponse = {
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.status !== 0) {
writer.uint32(8).int32(message.status);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 8) {
break;
}
message.status = reader.int32() as any;
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): HealthCheckResponse {
return { status: isSet(object.status) ? healthCheckResponse_ServingStatusFromJSON(object.status) : 0 };
},
toJSON(message: HealthCheckResponse): unknown {
const obj: any = {};
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0;
return message;
},
};
export type HealthService = typeof HealthService;
export const HealthService = {
check: {
path: "/com.ql.health.Health/Check",
requestStream: false,
responseStream: false,
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
watch: {
path: "/com.ql.health.Health/Watch",
requestStream: false,
responseStream: true,
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
} as const;
export interface HealthServer extends UntypedServiceImplementation {
check: handleUnaryCall<HealthCheckRequest, HealthCheckResponse>;
watch: handleServerStreamingCall<HealthCheckRequest, HealthCheckResponse>;
}
export interface HealthClient extends Client {
check(
request: HealthCheckRequest,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
): ClientUnaryCall;
watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;
watch(
request: HealthCheckRequest,
metadata?: Metadata,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
}
export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
service: typeof HealthService;
};
type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
export type DeepPartial<T> = T extends Builtin ? T
: T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin ? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
+12 -18
View File
@@ -1,36 +1,30 @@
import express from 'express'; import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import config from './config'; import config from './config';
import { HealthClient } from './protos/health';
import { credentials } from '@grpc/grpc-js';
const app = express(); const app = express();
const client = new HealthClient(
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
app.get('/api/health', (req, res) => { app.get('/api/public/panel/log', (req, res) => {
client.check({ service: 'cron' }, (err, response) => { exec(
if (err) { 'pm2 logs panel --lines 500 --nostream --timestamp',
return res.status(200).send({ code: 500, error: err }); (err, stdout, stderr) => {
if (err || stderr) {
return res.send({ code: 400, message: (err && err.message) || stderr });
} }
return res.status(200).send({ code: 200, data: response }); return res.send({ code: 200, data: stdout });
}); },
);
}); });
app app
.listen(config.publicPort, '0.0.0.0', async () => { .listen(config.publicPort, async () => {
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`); Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
console.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
console.error(err);
process.exit(1); process.exit(1);
}); });
+55
View File
@@ -0,0 +1,55 @@
import schedule from 'node-schedule';
import express from 'express';
import { exec } from 'child_process';
import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config';
const app = express();
const run = async () => {
CrontabModel.findAll({ where: {} })
.then((docs) => {
if (docs && docs.length > 0) {
for (let i = 0; i < docs.length; i++) {
const task = docs[i];
const _schedule = task.schedule && task.schedule.split(/ +/);
if (
_schedule &&
_schedule.length > 5 &&
task.status !== CrontabStatus.disabled &&
!task.isDisabled
) {
schedule.scheduleJob(task.schedule, function () {
let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) {
command = `task ${command}`;
}
exec(command);
});
}
}
}
})
.catch((err) => {
Logger.error(err);
process.exit(1);
});
};
app
.listen(config.cronPort, async () => {
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
})
.on('error', (err) => {
Logger.error(err);
process.exit(1);
});
-57
View File
@@ -1,57 +0,0 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data';
import { runCron } from '../shared/runCron';
import Logger from '../loaders/logger';
const addCron = (
call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
callback: sendUnaryData<AddCronResponse>,
) => {
for (const item of call.request.crons) {
const { id, schedule, command, extraSchedules, name } = item;
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.forEach((x) => x.cancel());
}
Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id,
name,
schedule,
command,
);
if (extraSchedules?.length) {
extraSchedules.forEach(x => {
Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id,
name,
x.schedule,
command,
);
})
}
scheduleStacks.set(id, [
nodeSchedule.scheduleJob(id, schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, { name, schedule, extraSchedules });
}),
...(extraSchedules?.length
? extraSchedules.map((x) =>
nodeSchedule.scheduleJob(id, x.schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, { name, schedule, extraSchedules });
}),
)
: []),
]);
}
callback(null, null);
};
export { addCron };
-41
View File
@@ -1,41 +0,0 @@
import { credentials } from '@grpc/grpc-js';
import {
AddCronRequest,
AddCronResponse,
CronClient,
DeleteCronRequest,
DeleteCronResponse,
} from '../protos/cron';
import config from '../config';
class Client {
private client = new CronClient(
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
return new Promise((resolve, reject) => {
this.client.addCron({ crons: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
return new Promise((resolve, reject) => {
this.client.delCron({ ids: request }, (err, res) => {
if (err) {
reject(err);
}
resolve(res);
});
});
}
}
export default new Client();
-6
View File
@@ -1,6 +0,0 @@
import nodeSchedule from 'node-schedule';
import { ToadScheduler } from 'toad-scheduler';
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
export const intervalSchedule = new ToadScheduler();
-24
View File
@@ -1,24 +0,0 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
import { scheduleStacks } from './data';
import Logger from '../loaders/logger';
const delCron = (
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
callback: sendUnaryData<DeleteCronResponse>,
) => {
for (const id of call.request.ids) {
if (scheduleStacks.has(id)) {
Logger.info(
'[schedule][取消定时任务], 任务ID: %s',
id,
);
scheduleStacks.get(id)?.forEach(x => x.cancel());
scheduleStacks.delete(id);
}
}
callback(null, null);
};
export { delCron };
-35
View File
@@ -1,35 +0,0 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
import config from '../config';
import { promiseExec } from '../config/util';
const check = async (
call: ServerUnaryCall<HealthCheckRequest, HealthCheckResponse>,
callback: sendUnaryData<HealthCheckResponse>,
) => {
switch (call.request.service) {
case 'cron':
const res = await promiseExec(
`curl -s --noproxy '*' http://0.0.0.0:${config.port}/api/system`,
);
if (res.includes('200')) {
return callback(null, { status: 1 });
}
const panelErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/panel-error.log`,
);
const scheduleErrLog = await promiseExec(
`tail -n 300 ~/.pm2/logs/schedule-error.log`,
);
return callback(
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
);
default:
return callback(null, { status: 1 });
}
};
export { check };
-25
View File
@@ -1,25 +0,0 @@
import { Server, ServerCredentials } from '@grpc/grpc-js';
import { CronService } from '../protos/cron';
import { addCron } from './addCron';
import { delCron } from './delCron';
import { HealthService } from '../protos/health';
import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server({ 'grpc.enable_http_proxy': 0 });
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.bindAsync(
`0.0.0.0:${config.cronPort}`,
ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
throw err;
}
server.start();
Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
-30
View File
@@ -1,30 +0,0 @@
import { Service, Inject } from 'typedi';
import path, { join } from 'path';
import config from '../config';
import { getFileContentByName } from '../config/util';
import { Response } from 'express';
import got from 'got';
@Service()
export default class ConfigService {
constructor() {}
public async getFile(filePath: string, res: Response) {
let content = '';
if (config.blackFileList.includes(filePath) || !filePath) {
res.send({ code: 403, message: '文件无法访问' });
}
if (filePath.startsWith('sample/')) {
const res = await got.get(
`https://gitlab.com/whyour/qinglong/-/raw/master/${filePath}`,
);
content = res.body;
} else if (filePath.startsWith('data/scripts/')) {
content = await getFileContentByName(join(config.rootPath, filePath));
} else {
content = await getFileContentByName(join(config.configPath, filePath));
}
res.send({ code: 200, data: content });
}
}
+221 -356
View File
@@ -2,33 +2,23 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync } from 'child_process'; import { exec, execSync, spawn } from 'child_process';
import fs from 'fs/promises'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { import { getFileContentByName, concurrentRun, fileExist } from '../config/util';
getFileContentByName, import { promises, existsSync } from 'fs';
fileExist, import { promisify } from 'util';
killTask, import { Op } from 'sequelize';
getUniqPath,
safeJSONParse,
} from '../config/util';
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path'; import path from 'path';
import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import pickBy from 'lodash/pickBy';
import omit from 'lodash/omit';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
private isNodeCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule, extra_schedules } = cron; const { schedule } = cron;
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) { if (schedule?.split(/ +/).length === 6) {
return true; return true;
} }
return false; return false;
@@ -38,18 +28,7 @@ export default class CronService {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
if (this.isNodeCron(doc)) { await this.set_crontab(this.isSixCron(doc));
await cronClient.addCron([
{
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
},
]);
}
await this.set_crontab();
return doc; return doc;
} }
@@ -58,28 +37,9 @@ export default class CronService {
} }
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
const doc = await this.getDb({ id: payload.id }); payload.saved = false;
const tab = new Crontab({ ...doc, ...payload }); const newDoc = await this.updateDb(payload);
tab.saved = false; await this.set_crontab(this.isSixCron(newDoc));
const newDoc = await this.updateDb(tab);
if (doc.isDisabled === 1) {
return newDoc;
}
if (this.isNodeCron(doc)) {
await cronClient.delCron([String(doc.id)]);
}
if (this.isNodeCron(newDoc)) {
await cronClient.addCron([
{
name: doc.name || '',
id: String(newDoc.id),
schedule: newDoc.schedule!,
command: this.makeCommand(newDoc),
extraSchedules: newDoc.extra_schedules || [],
},
]);
}
await this.set_crontab();
return newDoc; return newDoc;
} }
@@ -103,7 +63,7 @@ export default class CronService {
last_running_time: number; last_running_time: number;
last_execution_time: number; last_execution_time: number;
}) { }) {
let options: any = { const options: any = {
status, status,
pid, pid,
log_path, log_path,
@@ -113,28 +73,12 @@ export default class CronService {
options.last_running_time = last_running_time; options.last_running_time = last_running_time;
} }
for (const id of ids) { return await CrontabModel.update({ ...options }, { where: { id: ids } });
let cron;
try {
cron = await this.getDb({ id });
} catch (err) {}
if (!cron) {
continue;
}
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
options = omit(options, ['status', 'log_path', 'pid']);
}
await CrontabModel.update(
{ ...pickBy(options, (v) => v === 0 || !!v) },
{ where: { id } },
);
}
} }
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await cronClient.delCron(ids.map(String)); await this.set_crontab(true);
await this.set_crontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -169,80 +113,9 @@ export default class CronService {
} }
} }
private formatViewQuery(query: any, viewQuery: any) { public async crontabs(searchText?: string): Promise<Crontab[]> {
if (viewQuery.filters && viewQuery.filters.length > 0) { let query = {};
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
if (!query[primaryOperate]) {
query[primaryOperate] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
let q: any = {};
let operate2 = null;
let operate = null;
switch (operation) {
case 'Reg':
operate = Op.like;
operate2 = Op.or;
break;
case 'NotReg':
operate = Op.notLike;
operate2 = Op.and;
break;
case 'In':
q[Op.or] = [
{
[property]: Array.isArray(value) ? value : [value],
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
: {},
];
break;
case 'Nin':
q[Op.and] = [
{
[property]: {
[Op.notIn]: Array.isArray(value) ? value : [value],
},
},
property === 'status' && value.includes(2)
? { isDisabled: { [Op.ne]: 1 } }
: {},
];
break;
default:
break;
}
if (operate && operate2) {
q[property] = {
[Op.or]: [
{
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURI(value)}%` },
],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(colFn(property), operate, `%${encodeURI(value)}%`),
],
},
],
};
}
query[primaryOperate].push(q);
}
}
}
private formatSearchText(query: any, searchText: string | undefined) {
if (searchText) { if (searchText) {
if (!query[Op.and]) {
query[Op.and] = [];
}
let q: any = {};
const textArray = searchText.split(':'); const textArray = searchText.split(':');
switch (textArray[0]) { switch (textArray[0]) {
case 'name': case 'name':
@@ -250,21 +123,24 @@ export default class CronService {
case 'schedule': case 'schedule':
case 'label': case 'label':
const column = textArray[0] === 'label' ? 'labels' : textArray[0]; const column = textArray[0] === 'label' ? 'labels' : textArray[0];
q[column] = { query = {
[column]: {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${textArray[1]}%` }, { [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURI(textArray[1])}%` }, { [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
], ],
},
}; };
break; break;
default: default:
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` }, { [Op.like]: `%${encodeURIComponent(searchText)}%` },
], ],
}; };
q[Op.or] = [ query = {
[Op.or]: [
{ {
name: reg, name: reg,
}, },
@@ -277,114 +153,25 @@ export default class CronService {
{ {
labels: reg, labels: reg,
}, },
]; ],
};
break; break;
} }
query[Op.and].push(q);
} }
}
private formatFilterQuery(query: any, filterQuery: any) {
if (filterQuery) {
if (!query[Op.and]) {
query[Op.and] = [];
}
const filterKeys: any = Object.keys(filterQuery);
for (const key of filterKeys) {
let q: any = {};
if (!filterQuery[key]) continue;
if (key === 'status') {
if (filterQuery[key].includes(CrontabStatus.disabled)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
}
} else {
q[key] = filterQuery[key];
}
query[Op.and].push(q);
}
}
}
private formatViewSort(order: string[][], viewQuery: any) {
if (viewQuery.sorts && viewQuery.sorts.length > 0) {
for (const { property, type } of viewQuery.sorts) {
order.unshift([property, type]);
}
}
}
public async find({
log_path,
}: {
log_path: string;
}): Promise<Crontab | null> {
try { try {
const result = await CrontabModel.findOne({ where: { log_path } }); const result = await CrontabModel.findAll({
return result;
} catch (error) {
throw error;
}
}
public async crontabs(params?: {
searchValue: string;
page: string;
size: string;
sorter: string;
filters: string;
queryString: string;
}): Promise<{ data: Crontab[]; total: number }> {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const viewQuery = safeJSONParse(params?.queryString);
const filterQuery = safeJSONParse(params?.filters);
const sorterQuery = safeJSONParse(params?.sorter);
let query: any = {};
let order = [
['isPinned', 'DESC'],
['isDisabled', 'ASC'],
['status', 'ASC'],
['createdAt', 'DESC'],
];
this.formatViewQuery(query, viewQuery);
this.formatSearchText(query, searchText);
this.formatFilterQuery(query, filterQuery);
this.formatViewSort(order, viewQuery);
if (sorterQuery) {
const { field, type } = sorterQuery;
if (field && type) {
order.unshift([field, type]);
}
}
let condition: any = {
where: query, where: query,
order: order, order: [['createdAt', 'DESC']],
}; });
if (page && size) { return result as any;
condition.offset = (page - 1) * size;
condition.limit = size;
}
try {
const result = await CrontabModel.findAll(condition);
const count = await CrontabModel.count({ where: query });
return { data: result, total: count };
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
public async getDb(query: FindOptions<Crontab>['where']): Promise<Crontab> { public async getDb(query: any): Promise<Crontab> {
const doc: any = await CrontabModel.findOne({ where: { ...query } }); const doc: any = await CrontabModel.findOne({ where: { ...query } });
if (!doc) { return doc && (doc.get({ plain: true }) as Crontab);
throw new Error(`Cron ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async run(ids: number[]) { public async run(ids: number[]) {
@@ -392,9 +179,10 @@ export default class CronService {
{ status: CrontabStatus.queued }, { status: CrontabStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
); );
ids.forEach((id) => { concurrentRun(
this.runSingle(id); ids.map((id) => async () => await this.runSingle(id)),
}); 10,
);
} }
public async stop(ids: number[]) { public async stop(ids: number[]) {
@@ -402,11 +190,31 @@ export default class CronService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
await killTask(doc.pid); process.kill(-doc.pid);
} catch (error) { } catch (error) {
this.logger.error(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));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? `,耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`,
);
}
} }
await CrontabModel.update( await CrontabModel.update(
@@ -415,87 +223,112 @@ export default class CronService {
); );
} }
private async runSingle(cronId: number): Promise<number | void> { public async killTask(name: string) {
return taskLimit.manualRunWithCronLimit(() => { 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中不存在,所以截取前三个
pids = pids.slice(0, 3);
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(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
const params = {
name: cron.name,
command: cron.command,
schedule: cron.schedule,
extraSchedules: cron.extra_schedules,
};
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {
resolve(params); resolve();
return; return;
} }
this.logger.info(
`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
);
let { id, command, log_path } = cron; let { id, command, log_path } = cron;
const uniqPath = await getUniqPath(command, `${id}`); const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); const logFileExist = log_path && (await fileExist(absolutePath));
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
if (log_path?.split('/')?.every((x) => x !== uniqPath)) { this.logger.silly('Running job');
await fs.mkdir(logDirPath, { recursive: true }); this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) {
cmdStr = `task ${cmdStr}`;
} }
const logPath = `${uniqPath}/${logTime}.log`; if (cmdStr.endsWith('.js')) {
const absolutePath = path.resolve(config.logPath, `${logPath}`); cmdStr = `${cmdStr} now`;
const cp = spawn( }
`real_log_path=${logPath} no_delay=true ${this.makeCommand(
cron, const cp = spawn(cmdStr, { shell: '/bin/bash' });
true,
)}`,
{ shell: '/bin/bash' },
);
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath }, { status: CrontabStatus.running, pid: cp.pid },
{ where: { id } }, { where: { id } },
); );
cp.stdout.on('data', async (data) => { cp.stderr.on('data', (data) => {
await fs.appendFile(absolutePath, data.toString()); if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`);
}
}); });
cp.stderr.on('data', async (data) => { cp.on('error', (err) => {
await fs.appendFile(absolutePath, data.toString()); if (logFileExist) {
}); fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
cp.on('error', async (err) => { }
await fs.appendFile(absolutePath, JSON.stringify(err));
}); });
cp.on('exit', async (code) => { cp.on('exit', async (code, signal) => {
this.logger.info(
`${command} pid: ${cp.pid} exit ${code} signal ${signal}`,
);
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.idle, pid: undefined },
{ where: { id } }, { where: { id } },
); );
resolve({ ...params, pid: cp.pid, code }); resolve();
}); });
cp.on('close', async (code) => {
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
}); });
}); });
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await cronClient.delCron(ids.map(String)); await this.set_crontab(true);
await this.set_crontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } }); await this.set_crontab(true);
const sixCron = docs
.filter((x) => this.isNodeCron(x))
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -507,86 +340,130 @@ export default class CronService {
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`); const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { if (logFileExist) {
return await getFileContentByName(`${absolutePath}`); return getFileContentByName(`${absolutePath}`);
}
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 getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else { } else {
return '任务未运行'; return '';
} }
} }
public async logs(id: number) { public async logs(id: number) {
const doc = await this.getDb({ id }); const doc = await this.getDb({ id });
if (!doc || !doc.log_path) { if (!doc) {
return []; return [];
} }
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`); const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir); const dir = path.resolve(config.logPath, relativeDir);
const dirExist = await fileExist(dir); if (existsSync(dir)) {
if (dirExist) { let files = await promises.readdir(dir);
let files = await fs.readdir(dir); return files
return ( .map((x) => ({
await Promise.all(
files.map(async (x) => ({
filename: x, filename: x,
directory: relativeDir.replace(config.logPath, ''), directory: relativeDir.replace(config.logPath, ''),
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(), time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
})), }))
) .sort((a, b) => b.time - a.time);
).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 { } else {
return []; return [];
} }
} }
private makeCommand(tab: Crontab, realTime?: boolean) { private getKey(command: string): string {
let command = tab.command.trim(); const start =
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) { command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
command = `${TASK_PREFIX}${tab.command}`; const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
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(':');
} }
let commandVariable = `real_time=${Boolean(realTime)} no_tee=true ID=${ if (index) {
tab.id return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} `; } else {
if (tab.task_before) { return command.substring(start, end);
commandVariable += `task_before='${tab.task_before
.replace(/'/g, "'\\''")
.trim()}' `;
} }
if (tab.task_after) {
commandVariable += `task_after='${tab.task_after
.replace(/'/g, "'\\''")
.trim()}' `;
} }
const crontab_job_string = `${commandVariable}${command}`; private make_command(tab: Crontab) {
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(data?: { data: Crontab[]; total: number }) { private async set_crontab(needReloadSchedule: boolean = false) {
const tabs = data ?? (await this.crontabs()); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if ( if (tab.isDisabled === 1 || _schedule!.length !== 5) {
tab.isDisabled === 1 ||
_schedule!.length !== 5 ||
tab.extra_schedules?.length
) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';
crontab_string += this.makeCommand(tab); crontab_string += this.make_command(tab);
crontab_string += '\n'; crontab_string += '\n';
} else { } else {
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';
crontab_string += this.makeCommand(tab); crontab_string += this.make_command(tab);
crontab_string += '\n'; crontab_string += '\n';
} }
}); });
await fs.writeFile(config.crontabFile, crontab_string); this.logger.silly(crontab_string);
fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) {
exec(`pm2 reload schedule`);
}
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
@@ -624,19 +501,7 @@ export default class CronService {
}); });
} }
public async autosave_crontab() { public autosave_crontab() {
const tabs = await this.crontabs(); return this.set_crontab();
this.set_crontab(tabs);
const sixCron = tabs.data
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
} }
} }
-125
View File
@@ -1,125 +0,0 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import {
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import { FindOptions } from 'sequelize';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
}
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
await this.checkPosition(tab.position!);
return doc;
}
public async insert(payload: CrontabView): Promise<CrontabView> {
return await CrontabViewModel.create(payload, { returning: true });
}
public async update(payload: CrontabView): Promise<CrontabView> {
const doc = await this.getDb({ id: payload.id });
const tab = new CrontabView({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
return newDoc;
}
public async updateDb(payload: CrontabView): Promise<CrontabView> {
await CrontabViewModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id });
}
public async remove(ids: number[]) {
await CrontabViewModel.destroy({ where: { id: ids } });
}
public async list(): Promise<CrontabView[]> {
try {
const result = await CrontabViewModel.findAll({
where: {},
order: [['position', 'DESC']],
});
return result;
} catch (error) {
throw error;
}
}
public async getDb(
query: FindOptions<CrontabView>['where'],
): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
if (!doc) {
throw new Error(`CronView ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
}
public async disabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 1 }, { where: { id: ids } });
}
public async enabled(ids: number[]) {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.list();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async move({
id,
fromIndex,
toIndex,
}: {
fromIndex: number;
toIndex: number;
id: number;
}): Promise<CrontabView> {
let targetPosition: number;
const isUpward = fromIndex > toIndex;
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position! * 2
: views[toIndex].position! / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition);
return newDoc;
}
}
+48 -193
View File
@@ -8,20 +8,13 @@ import {
DependenceTypes, DependenceTypes,
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes,
} from '../data/dependence'; } from '../data/dependence';
import { spawn } from 'cross-spawn'; import _ from 'lodash';
import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import { FindOptions, Op } from 'sequelize'; import { Op } from 'sequelize';
import { import { concurrentRun } from '../config/util';
fileExist,
getPid,
killTask,
promiseExecSuccess,
} from '../config/util';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
@Service() @Service()
export default class DependenceService { export default class DependenceService {
@@ -32,7 +25,7 @@ export default class DependenceService {
public async create(payloads: Dependence[]): Promise<Dependence[]> { public async create(payloads: Dependence[]): Promise<Dependence[]> {
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
const tab = new Dependence({ ...x, status: DependenceStatus.queued }); const tab = new Dependence({ ...x, status: DependenceStatus.installing });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
@@ -53,7 +46,7 @@ export default class DependenceService {
const tab = new Dependence({ const tab = new Dependence({
...doc, ...doc,
...other, ...other,
status: DependenceStatus.queued, status: DependenceStatus.installing,
}); });
const newDoc = await this.updateDb(tab); const newDoc = await this.updateDb(tab);
this.installDependenceOneByOne([newDoc]); this.installDependenceOneByOne([newDoc]);
@@ -66,23 +59,12 @@ export default class DependenceService {
} }
public async remove(ids: number[], force = false): Promise<Dependence[]> { public async remove(ids: number[], force = false): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { id: ids } });
const unInstalledDeps = docs.filter(
(x) => x.status !== DependenceStatus.installed,
);
const installedDeps = docs.filter(
(x) => x.status === DependenceStatus.installed,
);
await this.removeDb(unInstalledDeps.map((x) => x.id!));
if (installedDeps.length) {
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] }, { status: DependenceStatus.removing, log: [] },
{ where: { id: ids } }, { where: { id: ids } },
); );
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs, false, force); this.installDependenceOneByOne(docs, false, force);
}
return docs; return docs;
} }
@@ -91,23 +73,13 @@ export default class DependenceService {
} }
public async dependencies( public async dependencies(
{ { searchValue, type }: { searchValue: string; type: string },
searchValue, sort: any = { position: -1 },
type,
status,
}: { searchValue: string; type: string; status: string },
sort: any = [],
query: any = {}, query: any = {},
): Promise<Dependence[]> { ): Promise<Dependence[]> {
let condition = { let condition = { ...query, type: DependenceTypes[type as any] };
...query,
type: DependenceTypes[type as any],
};
if (status) {
condition.status = status.split(',').map(Number);
}
if (searchValue) { if (searchValue) {
const encodeText = encodeURI(searchValue); const encodeText = encodeURIComponent(searchValue);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchValue}%` }, { [Op.like]: `%${searchValue}%` },
@@ -121,120 +93,78 @@ export default class DependenceService {
}; };
} }
try { try {
const result = await this.find(condition, sort); const result = await this.find(condition);
return result as any; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
public installDependenceOneByOne( private installDependenceOneByOne(
docs: Dependence[], docs: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
docs.forEach((dep) => { concurrentRun(
this.installOrUninstallDependency(dep, isInstall, force); docs.map(
}); (dep) => async () =>
await this.installOrUninstallDependencies([dep], isInstall, force),
),
1,
);
} }
public async reInstall(ids: number[]): Promise<Dependence[]> { public async reInstall(ids: number[]): Promise<Dependence[]> {
await DependenceModel.update( await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] }, { status: DependenceStatus.installing, log: [] },
{ where: { id: ids } }, { where: { id: ids } },
); );
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs, true, true); this.installDependenceOneByOne(docs);
return docs; return docs;
} }
public async cancel(ids: number[]) { private async find(query: any, sort?: any): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({ where: { id: ids } }); const docs = await DependenceModel.findAll({ where: { ...query } });
for (const doc of docs) {
taskLimit.removeQueuedDependency(doc);
const depInstallCommand = InstallDependenceCommandTypes[doc.type];
const depUnInstallCommand = unInstallDependenceCommandTypes[doc.type];
const installCmd = `${depInstallCommand} ${doc.name.trim()}`;
const unInstallCmd = `${depUnInstallCommand} ${doc.name.trim()}`;
const pids = await Promise.all([
getPid(installCmd),
getPid(unInstallCmd),
]);
for (const pid of pids) {
pid && (await killTask(pid));
}
}
await DependenceModel.update(
{ status: DependenceStatus.cancelled },
{ where: { id: ids } },
);
}
private async find(query: any, sort: any = []): Promise<Dependence[]> {
const docs = await DependenceModel.findAll({
where: { ...query },
order: [...sort, ['createdAt', 'DESC']],
});
return docs; return docs;
} }
public async getDb( public async getDb(query: any): Promise<Dependence> {
query: FindOptions<Dependence>['where'],
): Promise<Dependence> {
const doc: any = await DependenceModel.findOne({ where: { ...query } }); const doc: any = await DependenceModel.findOne({ where: { ...query } });
if (!doc) { return doc && (doc.get({ plain: true }) as Dependence);
throw new Error(`Dependency ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
private async updateLog(ids: number[], log: string): Promise<void> { private async updateLog(ids: number[], log: string): Promise<void> {
taskLimit.updateDepLog(async () => { const doc = await DependenceModel.findOne({ where: { id: ids } });
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
const newLog = doc?.log ? [...doc.log, log] : [log]; const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update( await DependenceModel.update({ log: newLog }, { where: { id: ids } });
{ log: newLog },
{ where: { id: doc.id } },
);
}
return null;
});
} }
public installOrUninstallDependency( public installOrUninstallDependencies(
dependency: Dependence, dependencies: Dependence[],
isInstall: boolean = true, isInstall: boolean = true,
force: boolean = false, force: boolean = false,
) { ) {
return taskLimit.runDependeny(dependency, () => {
return new Promise(async (resolve) => { return new Promise(async (resolve) => {
if (taskLimit.firstDependencyId !== dependency.id) { if (dependencies.length === 0) {
return resolve(null); resolve(null);
return;
} }
const socketMessageType = !force
taskLimit.removeQueuedDependency(dependency);
const depIds = [dependency.id!];
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: depIds } });
const socketMessageType = isInstall
? 'installDependence' ? 'installDependence'
: 'uninstallDependence'; : 'uninstallDependence';
let depName = dependency.name.trim(); const depNames = dependencies.map((x) => x.name).join(' ');
const depRunCommand = ( const depRunCommand = (
isInstall isInstall
? InstallDependenceCommandTypes ? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes : unInstallDependenceCommandTypes
)[dependency.type]; )[dependencies[0].type as any];
const actionText = isInstall ? '安装' : '删除'; const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[];
const startTime = dayjs(); const startTime = dayjs();
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format( const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)}\n\n`; )}\n\n`;
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -242,74 +172,9 @@ export default class DependenceService {
message, message,
references: depIds, references: depIds,
}); });
this.updateLog(depIds, message); await this.updateLog(depIds, message);
// 判断是否已经安装过依赖 const cp = spawn(`${depRunCommand} ${depNames}`, { shell: '/bin/bash' });
if (isInstall && !force) {
const getCommandPrefix = GetDependenceCommandTypes[dependency.type];
const depVersionStr = versionDependenceCommandTypes[dependency.type];
let depVersion = '';
if (depName.includes(depVersionStr)) {
const symbolRegx = new RegExp(
`(.*)${depVersionStr}([0-9\\.\\-\\+a-zA-Z]*)`,
);
const [, _depName, _depVersion] = depName.match(symbolRegx) || [];
if (_depVersion && _depName) {
depName = _depName;
depVersion = _depVersion;
}
}
const isNodeDependence = dependency.type === DependenceTypes.nodejs;
const isLinuxDependence = dependency.type === DependenceTypes.linux;
const isPythonDependence =
dependency.type === DependenceTypes.python3;
const depInfo = (
await promiseExecSuccess(
isNodeDependence
? `${getCommandPrefix} | grep "${depName}" | head -1`
: `${getCommandPrefix} ${depName}`,
)
)
.replace(/\s{2,}/, ' ')
.replace(/\s+$/, '');
if (
depInfo &&
((isNodeDependence && depInfo.split(' ')?.[0] === depName) ||
(isLinuxDependence &&
depInfo.toLocaleLowerCase().includes('installed')) ||
isPythonDependence) &&
(!depVersion || depInfo.includes(depVersion))
) {
const endTime = dayjs();
const _message = `检测到已经安装 ${depName}\n\n${depInfo}\n\n跳过安装\n\n依赖${actionText}成功,结束时间 ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)},耗时 ${endTime.diff(startTime, 'second')}`;
this.sockService.sendMessage({
type: socketMessageType,
message: _message,
references: depIds,
});
this.updateLog(depIds, _message);
await DependenceModel.update(
{ status: DependenceStatus.installed },
{ where: { id: depIds } },
);
return resolve(null);
}
}
const dependenceProxyFileExist = await fileExist(
config.dependenceProxyFile,
);
const proxyStr = dependenceProxyFileExist
? `source ${config.dependenceProxyFile} &&`
: '';
const cp = spawn(
`${proxyStr} ${depRunCommand} ${dependency.name.trim()}`,
{
shell: '/bin/bash',
},
);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -317,7 +182,7 @@ export default class DependenceService {
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
this.updateLog(depIds, data.toString()); await this.updateLog(depIds, data.toString());
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
@@ -326,7 +191,7 @@ export default class DependenceService {
message: data.toString(), message: data.toString(),
references: depIds, references: depIds,
}); });
this.updateLog(depIds, data.toString()); await this.updateLog(depIds, data.toString());
}); });
cp.on('error', async (err) => { cp.on('error', async (err) => {
@@ -335,10 +200,11 @@ export default class DependenceService {
message: JSON.stringify(err), message: JSON.stringify(err),
references: depIds, references: depIds,
}); });
this.updateLog(depIds, JSON.stringify(err)); await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
}); });
cp.on('exit', async (code) => { cp.on('close', async (code) => {
const endTime = dayjs(); const endTime = dayjs();
const isSucceed = code === 0; const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败'; const resultText = isSucceed ? '成功' : '失败';
@@ -351,7 +217,7 @@ export default class DependenceService {
message, message,
references: depIds, references: depIds,
}); });
this.updateLog(depIds, message); await this.updateLog(depIds, message);
let status = null; let status = null;
if (isSucceed) { if (isSucceed) {
@@ -363,17 +229,7 @@ export default class DependenceService {
? DependenceStatus.installFailed ? DependenceStatus.installFailed
: DependenceStatus.removeFailed; : DependenceStatus.removeFailed;
} }
const docs = await DependenceModel.findAll({ where: { id: depIds } }); await DependenceModel.update({ status }, { where: { id: depIds } });
const _docIds = docs
.filter((x) => x.status !== DependenceStatus.cancelled)
.map((x) => x.id!);
if (_docIds.length > 0) {
await DependenceModel.update(
{ status },
{ where: { id: _docIds } },
);
}
// 如果删除依赖成功或者强制删除 // 如果删除依赖成功或者强制删除
if ((isSucceed || force) && !isInstall) { if ((isSucceed || force) && !isInstall) {
@@ -383,6 +239,5 @@ export default class DependenceService {
resolve(null); resolve(null);
}); });
}); });
});
} }
} }
+39 -85
View File
@@ -1,18 +1,11 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises'; import * as fs from 'fs';
import { import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
Env, import _ from 'lodash';
EnvModel, import { Op } from 'sequelize';
EnvStatus,
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { FindOptions, Op } from 'sequelize';
@Service() @Service()
export default class EnvService { export default class EnvService {
@@ -20,22 +13,17 @@ export default class EnvService {
public async create(payloads: Env[]): Promise<Env[]> { public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
let position = initPosition; let position = initEnvPosition;
if ( if (envs && envs.length > 0 && envs[envs.length - 1].position) {
envs && position = envs[envs.length - 1].position as number;
envs.length > 0 &&
typeof envs[envs.length - 1].position === 'number'
) {
position = envs[envs.length - 1].position!;
} }
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
position = position - stepPosition; position = position / 2;
const tab = new Env({ ...x, position }); const tab = new Env({ ...x, position });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
await this.set_envs(); await this.set_envs();
await this.checkPosition(tabs[tabs.length - 1].position!);
return docs; return docs;
} }
@@ -49,9 +37,7 @@ export default class EnvService {
} }
public async update(payload: Env): Promise<Env> { public async update(payload: Env): Promise<Env> {
const doc = await this.getDb({ id: payload.id }); const newDoc = await this.updateDb(payload);
const tab = new Env({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.set_envs(); await this.set_envs();
return newDoc; return newDoc;
} }
@@ -81,47 +67,28 @@ export default class EnvService {
const envs = await this.envs(); const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) { if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward targetPosition = isUpward
? envs[0].position! + stepPosition ? envs[0].position * 2
: envs[toIndex].position! - stepPosition; : envs[toIndex].position / 2;
} else { } else {
targetPosition = isUpward targetPosition = isUpward
? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2 ? (envs[toIndex].position + envs[toIndex - 1].position) / 2
: (envs[toIndex].position! + envs[toIndex + 1].position!) / 2; : (envs[toIndex].position + envs[toIndex + 1].position) / 2;
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
position: this.getPrecisionPosition(targetPosition), position: targetPosition,
}); });
await this.checkPosition(targetPosition, envs[toIndex].position!);
return newDoc; return newDoc;
} }
private async checkPosition(position: number, edge: number = 0) { public async envs(
const precisionPosition = parseFloat(position.toPrecision(16)); searchText: string = '',
if ( sort: any = { position: -1 },
precisionPosition < minPosition || query: any = {},
precisionPosition > maxPosition || ): Promise<Env[]> {
Math.abs(precisionPosition - edge) < minPosition
) {
const envs = await this.envs();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURI(searchText); const encodeText = encodeURIComponent(searchText);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
@@ -145,10 +112,7 @@ export default class EnvService {
}; };
} }
try { try {
const result = await this.find(condition, [ const result = await this.find(condition, [['position', 'DESC']]);
['position', 'DESC'],
['createdAt', 'ASC'],
]);
return result as any; return result as any;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -163,12 +127,9 @@ export default class EnvService {
return docs; return docs;
} }
public async getDb(query: FindOptions<Env>['where']): Promise<Env> { public async getDb(query: any): Promise<Env> {
const doc: any = await EnvModel.findOne({ where: { ...query } }); const doc: any = await EnvModel.findOne({ where: { ...query } });
if (!doc) { return doc && (doc.get({ plain: true }) as Env);
throw new Error(`Env ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async disabled(ids: string[]) { public async disabled(ids: string[]) {
@@ -190,38 +151,31 @@ export default class EnvService {
} }
public async set_envs() { public async set_envs() {
const envs = await this.envs('', { const envs = await this.envs(
name: { [Op.not]: null }, '',
status: EnvStatus.normal, { position: -1 },
}); { name: { [Op.not]: null } },
const groups = groupBy(envs, 'name'); );
const groups = _.groupBy(envs, 'name');
let env_string = ''; let env_string = '';
let js_env_string = '';
let py_env_string = 'import os\n';
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
let value = group let value = _(group)
.map((x) => x.value) .filter((x) => x.status !== EnvStatus.disabled)
.map('value')
.join('&') .join('&')
.replace(/'/g, "'\\''") .replace(/(\\)[^\n]/g, '\\\\')
.replace(/(\\$)/, '\\\\')
.replace(/"/g, '\\"')
.trim(); .trim();
env_string += `export ${key}='${value}'\n`; env_string += `export ${key}="${value}"\n`;
const _env_value = `'${group
.map((x) => x.value)
.join('&')
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")}'`;
js_env_string += `process.env.${key}=${_env_value};\n`;
py_env_string += `os.environ['${key}']=${_env_value}\n`;
} }
} }
} }
await fs.writeFile(config.envFile, env_string); fs.writeFileSync(config.envFile, env_string);
await fs.writeFile(config.jsEnvFile, js_env_string);
await fs.writeFile(config.pyEnvFile, py_env_string);
} }
} }
+66 -446
View File
@@ -1,12 +1,11 @@
import crypto from 'crypto';
import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi';
import winston from 'winston';
import { parseBody, parseHeaders } from '../config/util';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user'; import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -18,30 +17,20 @@ export default class NotificationService {
['goCqHttpBot', this.goCqHttpBot], ['goCqHttpBot', this.goCqHttpBot],
['serverChan', this.serverChan], ['serverChan', this.serverChan],
['pushDeer', this.pushDeer], ['pushDeer', this.pushDeer],
['chat', this.chat],
['bark', this.bark], ['bark', this.bark],
['telegramBot', this.telegramBot], ['telegramBot', this.telegramBot],
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot], ['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp], ['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['wePlusBot', this.wePlusBot],
['email', this.email], ['email', this.email],
['pushMe', this.pushMe],
['webhook', this.webhook],
['lark', this.lark],
['chronocat', this.chronocat],
]); ]);
private timeout = 10000;
private title = ''; private title = '';
private content = ''; private content = '';
private params!: Omit<NotificationInfo, 'type'>; private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 10000,
retry: 1,
};
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
@@ -58,7 +47,7 @@ export default class NotificationService {
try { try {
return await notificationModeAction?.call(this); return await notificationModeAction?.call(this);
} catch (error: any) { } catch (error: any) {
throw error; return false;
} }
} }
return false; return false;
@@ -81,11 +70,11 @@ export default class NotificationService {
} }
private async gotify() { private async gotify() {
const { gotifyUrl, gotifyToken, gotifyPriority = 1 } = this.params; const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
try {
const res: any = await got const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, { .post(`${gotifyUrl}/message?token=${gotifyToken}`, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `title=${encodeURIComponent( body: `title=${encodeURIComponent(
this.title, this.title,
)}&message=${encodeURIComponent( )}&message=${encodeURIComponent(
@@ -96,34 +85,20 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
if (typeof res.id === 'number') { return typeof res.id === 'number';
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async goCqHttpBot() { private async goCqHttpBot() {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
try {
const res: any = await got const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, { .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { message: `${this.title}\n${this.content}` }, json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
}) })
.json(); .json();
if (res.retcode === 0) { return res.retcode === 0;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async serverChan() { private async serverChan() {
@@ -131,111 +106,53 @@ export default class NotificationService {
const url = serverChanKey.startsWith('SCT') const url = serverChanKey.startsWith('SCT')
? `https://sctapi.ftqq.com/${serverChanKey}.send` ? `https://sctapi.ftqq.com/${serverChanKey}.send`
: `https://sc.ftqq.com/${serverChanKey}.send`; : `https://sc.ftqq.com/${serverChanKey}.send`;
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `title=${this.title}&desp=${this.content}`, body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
if (res.errno === 0 || res.data.errno === 0) { return res.errno === 0 || res.data.errno === 0;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async pushDeer() { private async pushDeer() {
const { pushDeerKey, pushDeerUrl } = this.params; const { pushDeerKey } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`; const url = `https://api2.pushdeer.com/message/push`;
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent( body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title, this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`, )}&desp=${encodeURIComponent(this.content)}&type=markdown`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
if ( return (
res.content.result.length !== undefined && res.content.result.length !== undefined && res.content.result.length > 0
res.content.result.length > 0 );
) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async chat() {
const { chatUrl, chatToken } = this.params;
const url = `${chatUrl}${chatToken}`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
if (res.success) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async bark() { private async bark() {
let { let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
barkPush, if (!barkPush.startsWith('http') && !barkPush.startsWith('https')) {
barkIcon = '',
barkSound = '',
barkGroup = '',
barkLevel = '',
barkUrl = '',
barkArchive = '',
} = this.params;
if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
const url = `${barkPush}`; const url = `${barkPush}/${encodeURIComponent(
const body = { this.title,
title: this.title, )}/${encodeURIComponent(
body: this.content, this.content,
icon: barkIcon, )}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
sound: barkSound,
group: barkGroup,
isArchive: barkArchive,
level: barkLevel,
url: barkUrl,
};
try {
const res: any = await got const res: any = await got
.post(url, { .get(url, {
...this.gotOption, timeout: this.timeout,
json: body, retry: 0,
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
if (res.code === 200) { return res.code === 200;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async telegramBot() { private async telegramBot() {
@@ -248,8 +165,8 @@ export default class NotificationService {
telegramBotUserId, telegramBotUserId,
} = this.params; } = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : ''; const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `${ const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org' telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`; }/bot${telegramBotToken}/sendMessage`;
let agent; let agent;
if (telegramBotProxyHost && telegramBotProxyPort) { if (telegramBotProxyHost && telegramBotProxyPort) {
@@ -267,23 +184,16 @@ export default class NotificationService {
https: httpsAgent, https: httpsAgent,
}; };
} }
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`, body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent, agent,
}) })
.json(); .json();
if (res.ok) { return !!res.ok;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async dingtalkBot() { private async dingtalkBot() {
@@ -297,10 +207,10 @@ export default class NotificationService {
secretParam = `&timestamp=${dateNow}&sign=${result}`; secretParam = `&timestamp=${dateNow}&sign=${result}`;
} }
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`; const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -309,24 +219,16 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
if (res.errcode === 0) { return res.errcode === 0;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async weWorkBot() { private async weWorkBot() {
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = const { weWorkBotKey } = this.params;
this.params; const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -335,25 +237,18 @@ export default class NotificationService {
}, },
}) })
.json(); .json();
if (res.errcode === 0) { return res.errcode === 0;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async weWorkApp() { private async weWorkApp() {
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } = const { weWorkAppKey } = this.params;
this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] = const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(','); weWorkAppKey.split(',');
const url = `${weWorkOrigin}/cgi-bin/gettoken`; const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const tokenRes: any = await got const { access_token } = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
corpid, corpid,
corpsecret, corpsecret,
@@ -399,12 +294,12 @@ export default class NotificationService {
break; break;
} }
try {
const res: any = await got const res: any = await got
.post( .post(
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`, `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${access_token}`,
{ {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
touser, touser,
agentid, agentid,
@@ -415,93 +310,31 @@ export default class NotificationService {
) )
.json(); .json();
if (res.errcode === 0) { return res.errcode === 0;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async aibotk() {
const { aibotkKey, aibotkType, aibotkName } = this.params;
let url = '';
let json = {};
switch (aibotkType) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${aibotkKey}`,
roomName: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${aibotkKey}`,
name: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
}
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
...json,
},
})
.json();
if (res.code === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async iGot() { private async iGot() {
const { iGotPushKey } = this.params; const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
body: `title=${this.title}&content=${this.content}`, body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
if (res.ret === 0) { return res.ret === 0;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async pushPlus() { private async pushPlus() {
const { pushPlusToken, pushPlusUser } = this.params; const { pushPlusToken, pushPlusUser } = this.params;
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
try {
const res: any = await got const res: any = await got
.post(url, { .post(url, {
...this.gotOption, timeout: this.timeout,
retry: 0,
json: { json: {
token: `${pushPlusToken}`, token: `${pushPlusToken}`,
title: `${this.title}`, title: `${this.title}`,
@@ -511,84 +344,11 @@ export default class NotificationService {
}) })
.json(); .json();
if (res.code === 200) { return res.code === 200;
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async wePlusBot() {
const { wePlusBotToken, wePlusBotReceiver, wePlusBotVersion } = this.params;
let content = this.content;
let template = 'txt';
if (this.content.length > 800) {
template = 'html';
content = content.replace(/[\n\r]/g, '<br>');
}
const url = `https://www.weplusbot.com/send`;
try {
const res: any = await got
.post(url, {
...this.gotOption,
json: {
token: `${wePlusBotToken}`,
title: `${this.title}`,
template: `${template}`,
content: `${content}`,
receiver: `${wePlusBotReceiver || ''}`,
version: `${wePlusBotVersion || 'pro'}`,
},
})
.json();
if (res.code === 200) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async lark() {
let { larkKey } = this.params;
if (!larkKey.startsWith('http')) {
larkKey = `https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`;
}
try {
const res: any = await got
.post(larkKey, {
...this.gotOption,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
})
.json();
if (res.StatusCode === 0 || res.code === 0) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
} }
private async email() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser } = this.params;
try {
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
service: emailService, service: emailService,
auth: { auth: {
@@ -606,146 +366,6 @@ export default class NotificationService {
transporter.close(); transporter.close();
if (info.messageId) { return !!info.messageId;
return true;
} else {
throw new Error(JSON.stringify(info));
}
} catch (error: any) {
throw error;
}
}
private async pushMe() {
const { pushMeKey, pushMeUrl } = this.params;
try {
const res: any = await got.post(pushMeUrl || 'https://push.i-i.me/', {
...this.gotOption,
json: {
push_key: pushMeKey,
title: this.title,
content: this.content,
},
headers: { 'Content-Type': 'application/json' },
});
if (res.body === 'success') {
return true;
} else {
throw new Error(res.body);
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToken } = this.params;
try {
const user_ids = chronocatQQ
.match(/user_id=(\d+)/g)
?.map((match: any) => match.split('=')[1]);
const group_ids = chronocatQQ
.match(/group_id=(\d+)/g)
?.map((match: any) => match.split('=')[1]);
const url = `${chronocatURL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${chronocatToken}`,
};
for (const [chat_type, ids] of [
[1, user_ids],
[2, group_ids],
]) {
if (!ids) {
continue;
}
let _ids: any = ids;
for (const chat_id of _ids) {
const data = {
peer: {
chatType: chat_type,
peerUin: chat_id,
},
elements: [
{
elementType: 1,
textElement: {
content: `${this.title}\n\n${this.content}`,
},
},
],
};
const res: any = await got.post(url, {
...this.gotOption,
json: data,
headers,
});
if (res.statusCode === 200) {
return true;
} else {
throw new Error(res.body);
}
}
}
return false;
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async webhook() {
const {
webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
if (!webhookUrl.includes('$title') && !webhookBody.includes('$title')) {
throw new Error('Url 或者 Body 中必须包含 $title');
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(webhookBody, webhookContentType, (v) =>
v?.replaceAll('$title', this.title)?.replaceAll('$content', this.content),
);
const bodyParam = this.formatBody(webhookContentType, body);
const options = {
method: webhookMethod,
headers,
...this.gotOption,
allowGetBody: true,
...bodyParam,
};
try {
const formatUrl = webhookUrl
?.replaceAll('$title', encodeURIComponent(this.title))
?.replaceAll('$content', encodeURIComponent(this.content));
const res = await got(formatUrl, options);
if (String(res.statusCode).startsWith('20')) {
return true;
} else {
throw new Error(JSON.stringify(res));
}
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private formatBody(contentType: string, body: any): object {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
case 'text/plain':
return { body };
}
return {};
} }
} }
+18 -13
View File
@@ -26,7 +26,7 @@ export default class OpenService {
public async insert(payload: App): Promise<App> { public async insert(payload: App): Promise<App> {
const doc = await AppModel.create(payload, { returning: true }); const doc = await AppModel.create(payload, { returning: true });
return doc.get({ plain: true }); return doc.get({ plain: true }) as App;
} }
public async update(payload: App): Promise<App> { public async update(payload: App): Promise<App> {
@@ -45,10 +45,7 @@ export default class OpenService {
public async getDb(query: any): Promise<App> { public async getDb(query: any): Promise<App> {
const doc: any = await AppModel.findOne({ where: query }); const doc: any = await AppModel.findOne({ where: query });
if (!doc) { return doc && (doc.get({ plain: true }) as App);
throw new Error(`App ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async remove(ids: number[]) { public async remove(ids: number[]) {
@@ -78,7 +75,7 @@ export default class OpenService {
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURI(searchText); const encodeText = encodeURIComponent(searchText);
const reg = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
@@ -122,7 +119,7 @@ export default class OpenService {
(x) => x.expiration >= timestamp, (x) => x.expiration >= timestamp,
); );
let tokens = invalidTokens; let tokens = invalidTokens;
if (invalidTokens.length >= 5) { if (invalidTokens.length > 5) {
tokens = [ tokens = [
...invalidTokens.slice(0, 4), ...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration }, { ...invalidTokens[4], expiration },
@@ -148,7 +145,7 @@ export default class OpenService {
} }
} }
public async generateSystemToken(): Promise<{ public async findSystemToken(): Promise<{
value: string; value: string;
expiration: number; expiration: number;
}> { }> {
@@ -161,13 +158,21 @@ export default class OpenService {
scopes: ['crons', 'system'], scopes: ['crons', 'system'],
} as App); } as App);
} }
const { data } = await this.authToken({ const nowTime = Math.round(Date.now() / 1000);
let token;
if (
!systemApp.tokens ||
!systemApp.tokens.length ||
nowTime > [...systemApp.tokens].pop()!.expiration
) {
const authToken = await this.authToken({
client_id: systemApp.client_id, client_id: systemApp.client_id,
client_secret: systemApp.client_secret, client_secret: systemApp.client_secret,
}); });
return { token = authToken.data;
...data, } else {
value: data.token, token = [...systemApp.tokens].pop();
}; }
return token;
} }
} }
+39 -57
View File
@@ -1,19 +1,17 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams } from 'child_process'; import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { import {
ToadScheduler, ToadScheduler,
LongIntervalJob, LongIntervalJob,
AsyncTask,
SimpleIntervalSchedule, SimpleIntervalSchedule,
Task,
} from 'toad-scheduler'; } from 'toad-scheduler';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
export interface ScheduleTaskType { interface ScheduleTaskType {
id?: number; id: number;
command: string; command: string;
name?: string; name?: string;
schedule?: string; schedule?: string;
@@ -44,37 +42,26 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask( async runTask(command: string, callbacks: TaskCallbacks = {}) {
command: string,
callbacks: TaskCallbacks = {},
params: {
schedule?: string;
name?: string;
command?: string;
},
completionTime: 'start' | 'end' = 'end',
) {
return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
try { try {
const startTime = dayjs(); const startTime = dayjs();
await callbacks.onBefore?.(startTime); await callbacks.onBefore?.(startTime);
const cp = spawn(command, { shell: '/bin/bash' }); const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime); callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString()); await callbacks.onLog?.(data.toString());
}); });
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.info( this.logger.error(
'[panel][执行任务失败] 命令: %s, 错误信息: %j', '执行任务 %s 失败,时间:%s, 错误信息%j',
command, command,
new Date().toLocaleString(),
data.toString(), data.toString(),
); );
await callbacks.onError?.(data.toString()); await callbacks.onError?.(data.toString());
@@ -82,32 +69,41 @@ export default class ScheduleService {
cp.on('error', async (err) => { cp.on('error', async (err) => {
this.logger.error( this.logger.error(
'[panel][创建任务失败] 命令: %s, 错误信息: %j', '创建任务 %s 失败,时间:%s, 错误信息%j',
command, command,
new Date().toLocaleString(),
err, err,
); );
await callbacks.onError?.(JSON.stringify(err)); await callbacks.onError?.(JSON.stringify(err));
}); });
cp.on('exit', async (code) => { 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(); const endTime = dayjs();
this.logger.info(`${command} pid: ${cp.pid} closed ${code}`);
await callbacks.onEnd?.( await callbacks.onEnd?.(
cp, cp,
endTime, endTime,
endTime.diff(startTime, 'seconds'), endTime.diff(startTime, 'seconds'),
); );
resolve({ ...params, pid: cp.pid, code }); resolve(null);
}); });
} catch (error) { } catch (error) {
this.logger.error( await this.logger.error(
'[panel][执行任务失败] 命令: %s, 错误信息: %j', '执行任务%s失败,时间:%s, 错误信息%j',
command, command,
new Date().toLocaleString(),
error, error,
); );
await callbacks.onError?.(JSON.stringify(error)); await callbacks.onError?.(JSON.stringify(error));
resolve(null);
} }
}); });
});
} }
async createCronTask( async createCronTask(
@@ -117,7 +113,7 @@ export default class ScheduleService {
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s', '[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s',
_id, _id,
schedule, schedule,
name, name,
@@ -127,30 +123,19 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
this.runTask(command, callbacks, { await this.runTask(command, callbacks);
name,
schedule,
command,
});
}), }),
); );
if (runImmediately) { if (runImmediately) {
this.runTask(command, callbacks, { await this.runTask(command, callbacks);
name,
schedule,
command,
});
} }
} }
async cancelCronTask({ id = 0, name }: ScheduleTaskType) { async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[panel][取消定时任务], 任务名: %s', name); this.logger.info('[取消定时任务]任务名%s', name);
if (this.scheduleStacks.has(_id)) { this.scheduleStacks.has(_id) && this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id);
}
} }
async createIntervalTask( async createIntervalTask(
@@ -161,30 +146,30 @@ export default class ScheduleService {
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s', '[创建interval任务]任务ID: %s任务名: %s执行命令: %s',
_id, _id,
name, name,
command, command,
); );
const task = new Task( const task = new AsyncTask(
name, name,
() => { async () => {
this.runTask(command, callbacks, { return new Promise(async (resolve, reject) => {
name, await this.runTask(command, callbacks);
command,
}); });
}, },
(err) => { (err) => {
this.logger.error( this.logger.error(
'[执行任务失败] 命令: %s, 错误信息: %j', '执行任务%s失败,时间:%s, 错误信息%j',
command, command,
new Date().toLocaleString(),
err, err,
); );
}, },
); );
const job = new LongIntervalJob( const job = new LongIntervalJob(
{ runImmediately: false, ...schedule }, { ...schedule, runImmediately: false },
task, task,
_id, _id,
); );
@@ -192,16 +177,13 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
if (runImmediately) { if (runImmediately) {
this.runTask(command, callbacks, { await this.runTask(command, callbacks);
name,
command,
});
} }
} }
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) { async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name); this.logger.info('[取消interval任务]任务ID: %s任务名%s', _id, name);
this.intervalSchedule.removeById(_id); this.intervalSchedule.removeById(_id);
} }
+20 -30
View File
@@ -1,12 +1,11 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import path, { join } from 'path'; import fs from 'fs';
import path from 'path';
import SockService from './sock'; import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const';
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -20,7 +19,9 @@ export default class ScriptService {
private taskCallbacks(filePath: string): TaskCallbacks { private taskCallbacks(filePath: string): TaskCallbacks {
return { return {
onEnd: async (cp, endTime, diff) => { onEnd: async (cp, endTime, diff) => {
await rmPath(filePath); try {
fs.unlinkSync(filePath);
} catch (error) {}
}, },
onError: async (message: string) => { onError: async (message: string) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -39,35 +40,24 @@ export default class ScriptService {
public async runScript(filePath: string) { public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const command = `${TASK_COMMAND} ${relativePath} now`; const command = `task -l ${relativePath} now`;
const pid = await this.scheduleService.runTask( this.scheduleService.runTask(command, this.taskCallbacks(filePath));
`real_time=true ${command}`,
this.taskCallbacks(filePath),
{ command },
'start',
);
return { code: 200, data: pid };
}
public async stopScript(filePath: string, pid: number) {
if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
}
try {
await killTask(pid);
} catch (error) {}
return { code: 200 }; return { code: 200 };
} }
public async getFile(filePath: string, fileName: string) { public async stopScript(filePath: string) {
let _filePath = join(config.scriptPath, filePath, fileName); const relativePath = path.relative(config.scriptPath, filePath);
if (filePath.startsWith(config.dataPath)) { const err = await this.cronService.killTask(`task -l ${relativePath} now`);
_filePath = join(filePath, fileName);
} const str = err ? `\n${err}` : '';
const content = await getFileContentByName(_filePath); this.sockService.sendMessage({
return content; type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
});
return { code: 200 };
} }
} }
+47 -88
View File
@@ -1,47 +1,20 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import fs from 'fs/promises'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
import { Subscription } from '../data/subscription';
import { formatUrl } from '../config/subscription';
import config from '../config';
import { fileExist, rmPath } from '../config/util';
@Service() @Service()
export default class SshKeyService { export default class SshKeyService {
private homedir = os.homedir(); private homedir = os.homedir();
private sshPath = config.sshdPath; private sshPath = path.resolve(this.homedir, '.ssh');
private sshConfigFilePath = path.resolve(this.homedir, '.ssh', 'config'); private sshConfigFilePath = path.resolve(this.sshPath, 'config');
private sshConfigHeader = `Include ${path.join(this.sshPath, '*.config')}`;
constructor(@Inject('logger') private logger: winston.Logger) { constructor(@Inject('logger') private logger: winston.Logger) {}
this.initSshConfigFile();
}
private async initSshConfigFile() { private generatePrivateKeyFile(alias: string, key: string): void {
let config = '';
const _exist = await fileExist(this.sshConfigFilePath);
if (_exist) {
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
} else {
await fs.writeFile(this.sshConfigFilePath, '');
}
if (!config.includes(this.sshConfigHeader)) {
await fs.writeFile(
this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
);
}
}
private async generatePrivateKeyFile(
alias: string,
key: string,
): Promise<void> {
try { try {
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, { fs.writeFileSync(`${this.sshPath}/${alias}`, key, {
encoding: 'utf8', encoding: 'utf8',
mode: '400', mode: '400',
}); });
@@ -50,76 +23,62 @@ export default class SshKeyService {
} }
} }
private async removePrivateKeyFile(alias: string): Promise<void> { private getConfigRegx(alias: string) {
return new RegExp(
`Host ${alias}\n.*[^StrictHostKeyChecking]*.*[\n]*.*StrictHostKeyChecking no`,
'g',
);
}
private removePrivateKeyFile(alias: string): void {
try { try {
const filePath = path.join(this.sshPath, alias); fs.unlinkSync(`${this.sshPath}/${alias}`);
await rmPath(filePath);
} catch (error) { } catch (error) {
this.logger.error('删除私钥文件失败', error); this.logger.error('删除私钥文件失败', error);
} }
} }
private async generateSingleSshConfig( private generateSingleSshConfig(alias: string, host: string): string {
alias: string, return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
host: string,
proxy?: string,
) {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
}
const proxyStr = proxy
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
: '';
const config = `Host ${alias}\n Hostname ${host}\n IdentityFile ${path.join(
this.sshPath,
alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`;
await fs.writeFile(
`${path.join(this.sshPath, `${alias}.config`)}`,
config,
{
encoding: 'utf8',
},
);
} }
private async removeSshConfig(alias: string) { private generateSshConfig(configs: string[]) {
try { try {
const filePath = path.join(this.sshPath, `${alias}.config`); for (const config of configs) {
await rmPath(filePath); fs.appendFileSync(this.sshConfigFilePath, config, {
encoding: 'utf8',
});
}
} catch (error) {
this.logger.error('写入ssh配置文件失败', error);
}
}
private removeSshConfig(alias: string) {
try {
const configRegx = this.getConfigRegx(alias);
const data = fs
.readFileSync(this.sshConfigFilePath, { encoding: 'utf8' })
.replace(configRegx, '')
.replace(/\n[\n]+/g, '\n');
fs.writeFileSync(this.sshConfigFilePath, data, {
encoding: 'utf8',
});
} catch (error) { } catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error); this.logger.error(`删除ssh配置文件${alias}失败`, error);
} }
} }
public async addSSHKey( public addSSHKey(key: string, alias: string, host: string): void {
key: string, this.generatePrivateKeyFile(alias, key);
alias: string, const config = this.generateSingleSshConfig(alias, host);
host: string, this.removeSshConfig(alias);
proxy?: string, this.generateSshConfig([config]);
): Promise<void> {
await this.generatePrivateKeyFile(alias, key);
await this.generateSingleSshConfig(alias, host, proxy);
} }
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> { public removeSSHKey(alias: string, host: string): void {
await this.removePrivateKeyFile(alias); this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias); const config = this.generateSingleSshConfig(alias, host);
} this.removeSshConfig(config);
public async setSshConfig(docs: Subscription[]) {
for (const doc of docs) {
if (doc.type === 'private-repo' && doc.pull_type === 'ssh-key') {
const { alias, proxy } = doc;
const { host } = formatUrl(doc);
await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias);
await this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
await this.generateSingleSshConfig(alias, host, proxy);
}
}
} }
} }
+194 -94
View File
@@ -6,29 +6,29 @@ import {
SubscriptionModel, SubscriptionModel,
SubscriptionStatus, SubscriptionStatus,
} from '../data/subscription'; } from '../data/subscription';
import { ChildProcessWithoutNullStreams } from 'child_process'; import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs';
import cron_parser from 'cron-parser';
import { import {
getFileContentByName, getFileContentByName,
concurrentRun, concurrentRun,
fileExist, fileExist,
createFile, createFile,
killTask,
handleLogPath,
promiseExec,
rmPath,
} from '../config/util'; } from '../config/util';
import fs from 'fs/promises'; import { promises, existsSync } from 'fs';
import { FindOptions, Op } from 'sequelize'; import { promisify } from 'util';
import path, { join } from 'path'; import { Op } from 'sequelize';
import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler'; import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock'; import SockService from './sock';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
import { formatCommand, formatUrl } from '../config/subscription';
import { CrontabModel } from '../data/cron';
import CrontabService from './cron';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -37,16 +37,32 @@ export default class SubscriptionService {
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
private sshKeyService: SshKeyService, private sshKeyService: SshKeyService,
private crontabService: CrontabService,
) {} ) {}
public async list(searchText?: string): Promise<Subscription[]> { public async list(searchText?: string): Promise<Subscription[]> {
let query = {}; let query = {};
if (searchText) { 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 = { const reg = {
[Op.or]: [ [Op.or]: [
{ [Op.like]: `%${searchText}%` }, { [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` }, { [Op.like]: `%${encodeURIComponent(searchText)}%` },
], ],
}; };
query = { query = {
@@ -55,33 +71,80 @@ export default class SubscriptionService {
name: reg, name: reg,
}, },
{ {
url: reg, command: reg,
},
{
schedule: reg,
},
{
labels: reg,
}, },
], ],
}; };
break;
}
} }
try { try {
const result = await SubscriptionModel.findAll({ const result = await SubscriptionModel.findAll({
where: query, where: query,
order: [ order: [['createdAt', 'DESC']],
['is_disabled', 'ASC'],
['createdAt', 'DESC'],
],
}); });
return result; return result as any;
} catch (error) { } catch (error) {
throw 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( public async handleTask(
doc: Subscription, doc: Subscription,
needCreate = true, needCreate = true,
needAddKey = true,
runImmediately = false, runImmediately = false,
) { ) {
const { url } = formatUrl(doc); 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 = formatCommand(doc, url as string); doc.command = this.formatCommand(doc, url as string);
if (doc.schedule_type === 'crontab') { if (doc.schedule_type === 'crontab') {
this.scheduleService.cancelCronTask(doc as any); this.scheduleService.cancelCronTask(doc as any);
@@ -91,9 +154,9 @@ export default class SubscriptionService {
this.taskCallbacks(doc), this.taskCallbacks(doc),
runImmediately, runImmediately,
)); ));
} else if (doc.interval_schedule) { } else {
this.scheduleService.cancelIntervalTask(doc as any); this.scheduleService.cancelIntervalTask(doc as any);
const { type, value } = doc.interval_schedule; const { type, value } = doc.interval_schedule as any;
needCreate && needCreate &&
(await this.scheduleService.createIntervalTask( (await this.scheduleService.createIntervalTask(
doc as any, doc as any,
@@ -104,9 +167,28 @@ export default class SubscriptionService {
} }
} }
public async setSshConfig() { private async promiseExec(command: string): Promise<string> {
const docs = await SubscriptionModel.findAll(); return new Promise((resolve, reject) => {
await this.sshKeyService.setSshConfig(docs); 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 { private taskCallbacks(doc: Subscription): TaskCallbacks {
@@ -121,7 +203,7 @@ export default class SubscriptionService {
}, },
{ where: { id: doc.id } }, { where: { id: doc.id } },
); );
const absolutePath = await handleLogPath( const absolutePath = await this.handleLogPath(
logPath as string, logPath as string,
`## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`, `## 开始执行... ${startTime.format('YYYY-MM-DD HH:mm:ss')}\n`,
); );
@@ -130,15 +212,15 @@ export default class SubscriptionService {
let beforeStr = ''; let beforeStr = '';
try { try {
if (doc.sub_before) { if (doc.sub_before) {
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`); fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await promiseExec(doc.sub_before); beforeStr = await this.promiseExec(doc.sub_before);
} }
} catch (error: any) { } catch (error: any) {
beforeStr = beforeStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error); (error.stderr && error.stderr.toString()) || JSON.stringify(error);
} }
if (beforeStr) { if (beforeStr) {
await fs.appendFile(absolutePath, `${beforeStr}\n`); fs.appendFileSync(absolutePath, `${beforeStr}\n`);
} }
}, },
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => { onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
@@ -151,28 +233,28 @@ export default class SubscriptionService {
}, },
onEnd: async (cp, endTime, diff) => { onEnd: async (cp, endTime, diff) => {
const sub = await this.getDb({ id: doc.id }); const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string); const absolutePath = await this.handleLogPath(sub.log_path as string);
// 执行 sub_after // 执行 sub_after
let afterStr = ''; let afterStr = '';
try { try {
if (sub.sub_after) { if (sub.sub_after) {
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`); fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await promiseExec(sub.sub_after); afterStr = await this.promiseExec(sub.sub_after);
} }
} catch (error: any) { } catch (error: any) {
afterStr = afterStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error); (error.stderr && error.stderr.toString()) || JSON.stringify(error);
} }
if (afterStr) { if (afterStr) {
await fs.appendFile(absolutePath, `${afterStr}\n`); fs.appendFileSync(absolutePath, `${afterStr}\n`);
} }
await fs.appendFile( fs.appendFileSync(
absolutePath, absolutePath,
`\n## 执行结束... ${endTime.format( `\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}${LOG_END_SYMBOL}`, )} 耗时 ${diff}`,
); );
await SubscriptionModel.update( await SubscriptionModel.update(
@@ -188,13 +270,13 @@ export default class SubscriptionService {
}, },
onError: async (message: string) => { onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id }); const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string); const absolutePath = await this.handleLogPath(sub.log_path as string);
await fs.appendFile(absolutePath, `\n${message}`); fs.appendFileSync(absolutePath, `\n${message}`);
}, },
onLog: async (message: string) => { onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id }); const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string); const absolutePath = await this.handleLogPath(sub.log_path as string);
await fs.appendFile(absolutePath, `\n${message}`); fs.appendFileSync(absolutePath, `\n${message}`);
}, },
}; };
} }
@@ -203,7 +285,6 @@ export default class SubscriptionService {
const tab = new Subscription(payload); const tab = new Subscription(payload);
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.handleTask(doc); await this.handleTask(doc);
await this.setSshConfig();
return doc; return doc;
} }
@@ -212,11 +293,8 @@ export default class SubscriptionService {
} }
public async update(payload: Subscription): Promise<Subscription> { public async update(payload: Subscription): Promise<Subscription> {
const doc = await this.getDb({ id: payload.id }); const newDoc = await this.updateDb(payload);
const tab = new Subscription({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
await this.handleTask(newDoc, !newDoc.is_disabled); await this.handleTask(newDoc, !newDoc.is_disabled);
await this.setSshConfig();
return newDoc; return newDoc;
} }
@@ -256,34 +334,17 @@ export default class SubscriptionService {
); );
} }
public async remove(ids: number[], query: { force?: boolean }) { public async remove(ids: number[]) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false); await this.handleTask(doc, false, false);
} }
await SubscriptionModel.destroy({ where: { id: ids } }); await SubscriptionModel.destroy({ where: { id: ids } });
await this.setSshConfig();
if (query?.force === true) {
const crons = await CrontabModel.findAll({ where: { sub_id: ids } });
if (crons?.length) {
await this.crontabService.remove(crons.map((x) => x.id!));
}
for (const doc of docs) {
const filePath = join(config.scriptPath, doc.alias);
await rmPath(filePath);
}
}
} }
public async getDb( public async getDb(query: any): Promise<Subscription> {
query: FindOptions<Subscription>['where'], const doc: any = await SubscriptionModel.findOne({ where: { ...query } });
): Promise<Subscription> { return doc && (doc.get({ plain: true }) as Subscription);
const doc = await SubscriptionModel.findOne({ where: { ...query } });
if (!doc) {
throw new Error(`Subscription ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async run(ids: number[]) { public async run(ids: number[]) {
@@ -291,9 +352,10 @@ export default class SubscriptionService {
{ status: SubscriptionStatus.queued }, { status: SubscriptionStatus.queued },
{ where: { id: ids } }, { where: { id: ids } },
); );
ids.forEach((id) => { concurrentRun(
this.runSingle(id); ids.map((id) => async () => await this.runSingle(id)),
}); 10,
);
} }
public async stop(ids: number[]) { public async stop(ids: number[]) {
@@ -301,11 +363,20 @@ export default class SubscriptionService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
await killTask(doc.pid); process.kill(-doc.pid);
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.silly(error);
} }
} }
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
);
} }
await SubscriptionModel.update( await SubscriptionModel.update(
@@ -314,37 +385,69 @@ export default class SubscriptionService {
); );
} }
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) { private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId }); const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) { if (subscription.status !== SubscriptionStatus.queued) {
return; return;
} }
const command = formatCommand(subscription); const command = this.formatCommand(subscription);
this.scheduleService.runTask(command, this.taskCallbacks(subscription), { await this.scheduleService.runTask(
name: subscription.name,
schedule: subscription.schedule,
command, command,
}); this.taskCallbacks(subscription),
);
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc, false); await this.handleTask(doc, false);
} }
await SubscriptionModel.update({ is_disabled: 1 }, { where: { id: ids } });
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
const docs = await SubscriptionModel.findAll({ where: { id: ids } }); const docs = await SubscriptionModel.findAll({ where: { id: ids } });
await this.setSshConfig();
for (const doc of docs) { for (const doc of docs) {
await this.handleTask(doc); await this.handleTask(doc);
} }
await SubscriptionModel.update({ is_disabled: 0 }, { where: { id: ids } });
} }
public async log(id: number) { public async log(id: number) {
@@ -353,8 +456,8 @@ export default class SubscriptionService {
return ''; return '';
} }
const absolutePath = await handleLogPath(doc.log_path as string); const absolutePath = await this.handleLogPath(doc.log_path as string);
return await getFileContentByName(absolutePath); return getFileContentByName(absolutePath);
} }
public async logs(id: number) { public async logs(id: number) {
@@ -366,18 +469,15 @@ export default class SubscriptionService {
if (doc.log_path) { if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`); const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir); const dir = path.resolve(config.logPath, relativeDir);
const _exist = await fileExist(dir); if (existsSync(dir)) {
if (_exist) { let files = await promises.readdir(dir);
let files = await fs.readdir(dir); return files
return ( .map((x) => ({
await Promise.all(
files.map(async (x) => ({
filename: x, filename: x,
directory: relativeDir.replace(config.logPath, ''), directory: relativeDir.replace(config.logPath, ''),
time: (await fs.lstat(`${dir}/${x}`)).mtime.getTime(), time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
})), }))
) .sort((a, b) => b.time - a.time);
).sort((a, b) => b.time - a.time);
} }
} }
} }
+50 -305
View File
@@ -1,38 +1,15 @@
import { spawn } from 'cross-spawn'; import { Service, Inject } from 'typedi';
import { Response } from 'express';
import fs from 'fs';
import got from 'got';
import sum from 'lodash/sum';
import path from 'path';
import { Inject, Service } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const'; import * as fs from 'fs';
import { import _ from 'lodash';
getPid, import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
killTask,
parseContentVersion,
parseVersion,
promiseExec,
readDirs,
} from '../config/util';
import {
DependenceModel,
DependenceStatus,
DependenceTypes,
} from '../data/dependence';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import {
AuthDataType,
SystemInfo,
SystemInstance,
SystemModel,
SystemModelInfo,
} from '../data/system';
import taskLimit from '../shared/pLimit';
import NotificationService from './notify'; import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import got from 'got';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -45,23 +22,20 @@ export default class SystemService {
private sockService: SockService, private sockService: SockService,
) {} ) {}
public async getSystemConfig() { public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.systemConfig }); const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return doc; return doc || {};
} }
private async updateAuthDb(payload: SystemInfo): Promise<SystemInfo> { private async updateAuthDb(payload: AuthInfo): Promise<any> {
await SystemModel.upsert({ ...payload }); await AuthModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type }); const doc = await this.getDb({ type: payload.type });
return doc; return doc;
} }
public async getDb(query: any): Promise<SystemInfo> { public async getDb(query: any): Promise<any> {
const doc = await SystemModel.findOne({ where: { ...query } }); const doc: any = await AuthModel.findOne({ where: { ...query } });
if (!doc) { return doc && (doc.get({ plain: true }) as any);
throw new Error(`System ${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async updateNotificationMode(notificationInfo: NotificationInfo) { public async updateNotificationMode(notificationInfo: NotificationInfo) {
@@ -82,203 +56,56 @@ export default class SystemService {
} }
} }
public async updateLogRemoveFrequency(info: SystemModelInfo) { public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getSystemConfig(); const oDoc = await this.getLogRemoveFrequency();
const result = await this.updateAuthDb({ const result = await this.updateAuthDb({
...oDoc, ...oDoc,
info: { ...oDoc.info, ...info }, type: AuthDataType.removeLogFrequency,
info: { frequency },
}); });
const cron = { const cron = {
id: result.id || NaN, id: result.id,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${info.logRemoveFrequency}`, command: `ql rmlog ${frequency}`,
}; };
if (oDoc.info?.logRemoveFrequency) {
await this.scheduleService.cancelIntervalTask(cron); await this.scheduleService.cancelIntervalTask(cron);
} if (frequency > 0) {
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) { await this.scheduleService.createIntervalTask(cron, {
this.scheduleService.createIntervalTask(cron, { days: frequency,
days: info.logRemoveFrequency, runImmediately: true,
}); });
} }
return { code: 200, data: info }; return { code: 200, data: { ...cron } };
}
public async updateCronConcurrency(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
if (info.cronConcurrency) {
await taskLimit.setCustomLimit(info.cronConcurrency);
}
return { code: 200, data: info };
}
public async updateDependenceProxy(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
if (info.dependenceProxy) {
await fs.promises.writeFile(
config.dependenceProxyFile,
`export http_proxy="${info.dependenceProxy}"\nexport https_proxy="${info.dependenceProxy}"`,
);
} else {
await fs.promises.rm(config.dependenceProxyFile);
}
return { code: 200, data: info };
}
public async updateNodeMirror(info: SystemModelInfo, res?: Response) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let cmd = 'pnpm config delete registry';
if (info.nodeMirror) {
cmd = `pnpm config set registry ${info.nodeMirror}`;
}
let command = `cd && ${cmd}`;
const docs = await DependenceModel.findAll({
where: {
type: DependenceTypes.nodejs,
status: DependenceStatus.installed,
},
});
if (docs.length > 0) {
command += ` && pnpm i -g`;
}
this.scheduleService.runTask(
command,
{
onStart: async (cp) => {
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
res?.end();
},
onEnd: async () => {
this.sockService.sendMessage({
type: 'updateNodeMirror',
message: 'update node mirror end',
});
},
onError: async (message: string) => {
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
},
onLog: async (message: string) => {
this.sockService.sendMessage({ type: 'updateNodeMirror', message });
},
},
{
command,
},
);
}
public async updatePythonMirror(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let cmd = 'pip config unset global.index-url';
if (info.pythonMirror) {
cmd = `pip3 config set global.index-url ${info.pythonMirror}`;
}
await promiseExec(cmd);
return { code: 200, data: info };
}
public async updateLinuxMirror(
info: SystemModelInfo,
res?: Response,
onEnd?: () => void,
) {
const oDoc = await this.getSystemConfig();
await this.updateAuthDb({
...oDoc,
info: { ...oDoc.info, ...info },
});
let defaultDomain = 'https://dl-cdn.alpinelinux.org';
let targetDomain = 'https://dl-cdn.alpinelinux.org';
const content = await fs.promises.readFile('/etc/apk/repositories', {
encoding: 'utf-8',
});
const domainMatch = content.match(/(http.*)\/alpine\/.*/);
if (domainMatch) {
defaultDomain = domainMatch[1];
}
if (info.linuxMirror) {
targetDomain = info.linuxMirror;
}
const command = `sed -i 's/${defaultDomain.replace(
/\//g,
'\\/',
)}/${targetDomain.replace(
/\//g,
'\\/',
)}/g' /etc/apk/repositories && apk update -f`;
this.scheduleService.runTask(
command,
{
onStart: async (cp) => {
res?.setHeader('QL-Task-Pid', `${cp.pid}`);
res?.end();
},
onEnd: async () => {
this.sockService.sendMessage({
type: 'updateLinuxMirror',
message: 'update linux mirror end',
});
onEnd?.();
},
onError: async (message: string) => {
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
onLog: async (message: string) => {
this.sockService.sendMessage({ type: 'updateLinuxMirror', message });
},
},
{
command,
},
);
} }
public async checkUpdate() { public async checkUpdate() {
try { try {
const currentVersionContent = await parseVersion(config.versionFile); const versionRegx = /.*export const version = \'(.*)\'\;/;
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
let lastVersionContent; const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try { try {
const result = await got.get( const result = await got.get(config.lastVersionFile, {
`${config.lastVersionFile}?t=${Date.now()}`, timeout: 6000,
{ retry: 0,
timeout: 30000, });
}, const lastVersionFileContent = result.body;
); lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastVersionContent = await parseContentVersion(result.body); lastLog = lastVersionFileContent.match(logRegx)
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {} } catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
}
return { return {
code: 200, code: 200,
data: { data: {
hasNewVersion: this.checkHasNewVersion( hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion),
currentVersionContent.version, lastVersion,
lastVersionContent.version, lastLog,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
}, },
}; };
} catch (error: any) { } catch (error: any) {
@@ -309,8 +136,12 @@ export default class SystemService {
} }
public async updateSystem() { public async updateSystem() {
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' }); const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => { cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateSystemVersion', type: 'updateSystemVersion',
@@ -335,13 +166,6 @@ export default class SystemService {
return { code: 200 }; return { code: 200 };
} }
public async reloadSystem(target?: 'system' | 'data') {
const cmd = `real_time=true ql reload ${target || ''}`;
const cp = spawn(cmd, { shell: '/bin/bash' });
cp.unref();
return { code: 200 };
}
public async notify({ title, content }: { title: string; content: string }) { public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content); const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) { if (isSuccess) {
@@ -350,83 +174,4 @@ export default class SystemService {
return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' }; return { code: 400, message: '通知发送失败,请检查系统设置/通知配置' };
} }
} }
public async run({ command }: { command: string }, callback: TaskCallbacks) {
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
this.scheduleService.runTask(`real_time=true ${command}`, callback, {
command,
});
}
public async stop({ command, pid }: { command: string; pid: number }) {
if (!pid && !command) {
return { code: 400, message: '参数错误' };
}
if (pid) {
await killTask(pid);
return { code: 200 };
}
if (!command.startsWith(TASK_COMMAND)) {
command = `${TASK_COMMAND} ${command}`;
}
const _pid = await getPid(command);
if (_pid) {
await killTask(_pid);
return { code: 200 };
} else {
return { code: 400, message: '任务未找到' };
}
}
public async exportData(res: Response) {
try {
await promiseExec(
`cd ${config.rootPath} && tar -zcvf ${config.dataTgzFile} data/`,
);
res.download(config.dataTgzFile);
} catch (error: any) {
return res.send({ code: 400, message: error.message });
}
}
public async importData() {
try {
await promiseExec(`rm -rf ${path.join(config.tmpPath, 'data')}`);
const res = await promiseExec(
`cd ${config.tmpPath} && tar -zxvf data.tgz`,
);
return { code: 200, data: res };
} catch (error: any) {
return { code: 400, message: error.message };
}
}
public async getSystemLog(res: Response) {
const result = await readDirs(config.systemLogPath, config.systemLogPath);
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
res.set({
'Content-Length': sum(logs.map((x) => x.size)),
});
(function sendFiles(res, fileNames) {
if (fileNames.length === 0) {
res.end();
return;
}
const currentLog = fileNames.shift();
if (currentLog) {
const currentFileStream = fs.createReadStream(
path.join(config.systemLogPath, currentLog.title),
);
currentFileStream.on('end', () => {
sendFiles(res, fileNames);
});
currentFileStream.pipe(res, { end: false });
}
})(res, logs);
}
} }
+64 -109
View File
@@ -1,33 +1,19 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { import { createRandomString, getNetIp, getPlatform } from '../config/util';
createFile,
createRandomString,
fileExist,
getNetIp,
getPlatform,
safeJSONParse,
} from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs/promises'; import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
AuthDataType,
SystemInfo,
SystemModel,
SystemModelInfo,
LoginStatus,
} from '../data/system';
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';
import ScheduleService from './schedule'; import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import IP2Region from 'ip2region';
import requestIp from 'request-ip';
import uniq from 'lodash/uniq';
@Service() @Service()
export default class UserService { export default class UserService {
@@ -48,13 +34,12 @@ export default class UserService {
req: Request, req: Request,
needTwoFactor = true, needTwoFactor = true,
): Promise<any> { ): Promise<any> {
const _exist = await fileExist(config.authConfigFile); if (!fs.existsSync(config.authConfigFile)) {
if (!_exist) {
return this.initAuthInfo(); return this.initAuthInfo();
} }
let { username, password } = payloads; let { username, password } = payloads;
const content = await this.getAuthInfo(); const content = this.getAuthInfo();
const timestamp = Date.now(); const timestamp = Date.now();
if (content) { if (content) {
let { let {
@@ -80,46 +65,32 @@ export default class UserService {
return this.initAuthInfo(); return this.initAuthInfo();
} }
const retriesTime = Math.pow(3, retries) * 1000; if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) {
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 1000,
);
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: `失败次数过多,请${Math.round(
data: waitTime, (Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
)}秒后重试`,
data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
),
}; };
} }
if ( const { ip, address } = await getNetIp(req);
username === cUsername && if (username === cUsername && password === cPassword) {
password === cPassword && if (twoFactorActivated && needTwoFactor) {
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, { this.updateAuthInfo(content, {
isTwoFactorChecking: true, isTwoFactorChecking: true,
}); });
return { return {
code: 420, code: 420,
message: '', message: '请输入两步验证token',
}; };
} }
const ip = requestIp.getClientIp(req) || '';
const query = new IP2Region();
const ipAddress = query.search(ip);
let address = '';
if (ipAddress) {
const { country, province, city, isp } = ipAddress;
address = uniq([country, province, city, isp])
.filter(Boolean)
.join(' ');
}
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20; const expiration = twoFactorActivated ? 30 : 3;
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration, expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384', algorithm: 'HS384',
@@ -138,12 +109,13 @@ export default class UserService {
platform: req.platform, platform: req.platform,
isTwoFactorChecking: false, isTwoFactorChecking: false,
}); });
this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform req.platform
}端 登录成功,ip地址 ${ip}`, }端 登录成功,ip地址 ${ip}`,
); );
await this.getLoginLog();
await this.insertDb({ await this.insertDb({
type: AuthDataType.loginLog, type: AuthDataType.loginLog,
info: { info: {
@@ -166,12 +138,13 @@ export default class UserService {
lastaddr: address, lastaddr: address,
platform: req.platform, platform: req.platform,
}); });
this.notificationService.notify( await this.notificationService.notify(
'登录通知', '登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${ `你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform req.platform
}端 登录失败,ip地址 ${ip}`, }端 登录失败,ip地址 ${ip}`,
); );
await this.getLoginLog();
await this.insertDb({ await this.insertDb({
type: AuthDataType.loginLog, type: AuthDataType.loginLog,
info: { info: {
@@ -182,40 +155,29 @@ export default class UserService {
status: LoginStatus.fail, status: LoginStatus.fail,
}, },
}); });
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError }; return { code: 400, message: config.authError };
} }
}
} else { } else {
return this.initAuthInfo(); return this.initAuthInfo();
} }
} }
public async logout(platform: string): Promise<any> { public async logout(platform: string): Promise<any> {
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { this.updateAuthInfo(authInfo, {
token: '', token: '',
tokens: { ...authInfo.tokens, [platform]: '' }, tokens: { ...authInfo.tokens, [platform]: '' },
}); });
} }
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> { public async getLoginLog(): Promise<AuthInfo[]> {
const docs = await SystemModel.findAll({ const docs = await AuthModel.findAll({
where: { type: AuthDataType.loginLog }, where: { type: AuthDataType.loginLog },
}); });
if (docs && docs.length > 0) { if (docs && docs.length > 0) {
const result = docs.sort( const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
);
if (result.length > 100) { if (result.length > 100) {
await SystemModel.destroy({ await AuthModel.destroy({
where: { id: result[result.length - 1].id }, where: { id: result[result.length - 1].id },
}); });
} }
@@ -224,22 +186,23 @@ export default class UserService {
return []; return [];
} }
private async insertDb(payload: SystemInfo): Promise<SystemInfo> { private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
const doc = await SystemModel.create({ ...payload }, { returning: true }); const doc = await AuthModel.create({ ...payload }, { returning: true });
return doc; return doc;
} }
private async initAuthInfo() { private initAuthInfo() {
await fs.writeFile( const newPassword = createRandomString(16, 22);
fs.writeFileSync(
config.authConfigFile, config.authConfigFile,
JSON.stringify({ JSON.stringify({
username: 'admin', username: 'admin',
password: 'admin', password: newPassword,
}), }),
); );
return { return {
code: 100, code: 100,
message: '未找到认证文件,重新初始化', message: '已初始化密码,请前往auth.json查看并重新登录',
}; };
} }
@@ -253,41 +216,36 @@ export default class UserService {
if (password === 'admin') { if (password === 'admin') {
return { code: 400, message: '密码不能设置为admin' }; return { code: 400, message: '密码不能设置为admin' };
} }
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password }); this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' }; return { code: 200, message: '更新成功' };
} }
public async updateAvatar(avatar: string) { public async updateAvatar(avatar: string) {
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar }); this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' }; return { code: 200, data: avatar, message: '更新成功' };
} }
public async getUserInfo(): Promise<any> { public getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile); return new Promise((resolve) => {
if (!authFileExist) { fs.readFile(config.authConfigFile, 'utf8', (err, data) => {
await createFile( if (err) console.log(err);
config.authConfigFile, resolve(JSON.parse(data));
JSON.stringify({ });
username: 'admin', });
password: 'admin',
}),
);
}
return await this.getAuthInfo();
} }
public async initTwoFactor() { public initTwoFactor() {
const secret = authenticator.generateSecret(); const secret = authenticator.generateSecret();
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret); const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret }); this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth }; return { secret, url: otpauth };
} }
public async activeTwoFactor(code: string) { public activeTwoFactor(code: string) {
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
const isValid = authenticator.verify({ const isValid = authenticator.verify({
token: code, token: code,
secret: authInfo.twoFactorSecret, secret: authInfo.twoFactorSecret,
@@ -306,7 +264,7 @@ export default class UserService {
}: { username: string; password: string; code: string }, }: { username: string; password: string; code: string },
req: any, req: any,
) { ) {
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
const { isTwoFactorChecking, twoFactorSecret } = authInfo; const { isTwoFactorChecking, twoFactorSecret } = authInfo;
if (!isTwoFactorChecking) { if (!isTwoFactorChecking) {
return { code: 450, message: '未知错误' }; return { code: 450, message: '未知错误' };
@@ -328,8 +286,8 @@ export default class UserService {
} }
} }
public async deactiveTwoFactor() { public deactiveTwoFactor() {
const authInfo = await this.getAuthInfo(); const authInfo = this.getAuthInfo();
this.updateAuthInfo(authInfo, { this.updateAuthInfo(authInfo, {
twoFactorActivated: false, twoFactorActivated: false,
twoFactorActived: false, twoFactorActived: false,
@@ -338,13 +296,13 @@ export default class UserService {
return true; return true;
} }
private async getAuthInfo() { private getAuthInfo() {
const content = await fs.readFile(config.authConfigFile, 'utf8'); const content = fs.readFileSync(config.authConfigFile, 'utf8');
return safeJSONParse(content); return JSON.parse(content || '{}');
} }
private async updateAuthInfo(authInfo: any, info: any) { private updateAuthInfo(authInfo: any, info: any) {
await fs.writeFile( fs.writeFileSync(
config.authConfigFile, config.authConfigFile,
JSON.stringify({ ...authInfo, ...info }), JSON.stringify({ ...authInfo, ...info }),
); );
@@ -352,29 +310,26 @@ export default class UserService {
public async getNotificationMode(): Promise<NotificationInfo> { public async getNotificationMode(): Promise<NotificationInfo> {
const doc = await this.getDb({ type: AuthDataType.notification }); const doc = await this.getDb({ type: AuthDataType.notification });
return (doc.info || {}) as NotificationInfo; return (doc && doc.info) || {};
} }
private async updateAuthDb(payload: SystemInfo): Promise<any> { private async updateAuthDb(payload: AuthInfo): Promise<any> {
let doc = await SystemModel.findOne({ type: payload.type }); let doc = await AuthModel.findOne({ type: payload.type });
if (doc) { if (doc) {
const updateResult = await SystemModel.update(payload, { const updateResult = await AuthModel.update(payload, {
where: { id: doc.id }, where: { id: doc.id },
returning: true, returning: true,
}); });
doc = updateResult[1][0]; doc = updateResult[1][0];
} else { } else {
doc = await SystemModel.create(payload, { returning: true }); doc = await AuthModel.create(payload, { returning: true });
} }
return doc; return doc;
} }
public async getDb(query: any): Promise<SystemInfo> { public async getDb(query: any): Promise<any> {
const doc = await SystemModel.findOne({ where: { ...query } }); const doc: any = await AuthModel.findOne({ where: { ...query } });
if (!doc) { return doc && (doc.get({ plain: true }) as any);
throw new Error(`${JSON.stringify(query)} not found`);
}
return doc.get({ plain: true });
} }
public async updateNotificationMode(notificationInfo: NotificationInfo) { public async updateNotificationMode(notificationInfo: NotificationInfo) {
-122
View File
@@ -1,122 +0,0 @@
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
import os from 'os';
import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger';
import { Dependence } from '../data/dependence';
interface IDependencyFn<T> {
(): Promise<T>;
dependency?: Dependence;
}
class TaskLimit {
private dependenyLimit = new PQueue({ concurrency: 1 });
private queuedDependencyIds = new Set<number>([]);
private updateLogLimit = new PQueue({ concurrency: 1 });
private cronLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4),
});
private manualCronoLimit = new PQueue({
concurrency: Math.max(os.cpus().length, 4),
});
get cronLimitActiveCount() {
return this.cronLimit.pending;
}
get cronLimitPendingCount() {
return this.cronLimit.size;
}
get firstDependencyId() {
return [...this.queuedDependencyIds.values()][0];
}
constructor() {
this.setCustomLimit();
this.handleEvents();
}
private handleEvents() {
this.cronLimit.on('add', () => {
Logger.info(
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
});
this.cronLimit.on('active', () => {
Logger.info(
`[schedule][开始处理任务] 运行中任务数: ${
this.cronLimitActiveCount + 1
}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
});
this.cronLimit.on('completed', (param) => {
Logger.info(`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`);
});
this.cronLimit.on('error', (error) => {
Logger.error(`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`);
});
this.cronLimit.on('next', () => {
Logger.info(
`[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
});
this.cronLimit.on('idle', () => {
Logger.info(`[schedule][任务队列] 空闲中...`);
});
}
public removeQueuedDependency(dependency: Dependence) {
if (this.queuedDependencyIds.has(dependency.id!)) {
this.queuedDependencyIds.delete(dependency.id!);
}
}
public async setCustomLimit(limit?: number) {
if (limit) {
this.cronLimit.concurrency = limit;
this.manualCronoLimit.concurrency = limit;
return;
}
await SystemModel.sync();
const doc = await SystemModel.findOne({
where: { type: AuthDataType.systemConfig },
});
if (doc?.info?.cronConcurrency) {
this.cronLimit.concurrency = doc.info.cronConcurrency;
this.manualCronoLimit.concurrency = doc.info.cronConcurrency;
}
}
public async runWithCronLimit<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.cronLimit.add(fn, options);
}
public async manualRunWithCronLimit<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.manualCronoLimit.add(fn, options);
}
public runDependeny<T>(
dependency: Dependence,
fn: IDependencyFn<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
this.queuedDependencyIds.add(dependency.id!);
fn.dependency = dependency;
return this.dependenyLimit.add(fn, options);
}
public updateDepLog<T>(
fn: () => Promise<T>,
options?: Partial<QueueAddOptions>,
): Promise<T | void> {
return this.updateLogLimit.add(fn, options);
}
}
export default new TaskLimit();
-31
View File
@@ -1,31 +0,0 @@
import { spawn } from 'cross-spawn';
import taskLimit from './pLimit';
import Logger from '../loaders/logger';
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => {
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stderr.on('data', (data) => {
Logger.info(
'[schedule][执行任务失败] 命令: %s, 错误信息: %j',
cmd,
data.toString(),
);
});
cp.on('error', (err) => {
Logger.error(
'[schedule][创建任务失败] 命令: %s, 错误信息: %j',
cmd,
err,
);
});
cp.on('exit', async (code) => {
resolve({ ...options, command: cmd, pid: cp.pid, code });
});
});
});
}
-40
View File
@@ -1,40 +0,0 @@
import 'reflect-metadata';
import OpenService from './services/open';
import { Container } from 'typedi';
import LoggerInstance from './loaders/logger';
import fs from 'fs';
import config from './config';
import path from 'path';
import os from 'os';
const tokenFile = path.join(config.configPath, 'token.json');
async function getToken() {
try {
Container.set('logger', LoggerInstance);
const openService = Container.get(OpenService);
const appToken = await openService.generateSystemToken();
console.log(appToken.value);
await writeFile({
value: appToken.value,
expiration: appToken.expiration,
});
} catch (error) {
console.log(error);
}
}
async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => {
fs.writeFile(
tokenFile,
`${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve();
},
);
});
}
getToken();
-27
View File
@@ -1,27 +0,0 @@
import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config';
import express from 'express';
import depInjectorLoader from './loaders/depInjector';
import Logger from './loaders/logger';
async function startServer() {
const app = express();
depInjectorLoader();
await require('./loaders/update').default({ app });
app
.listen(config.updatePort, '0.0.0.0', () => {
Logger.debug(`✌️ 更新服务启动成功!`);
console.debug(`✌️ 更新服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
}
startServer();
-274
View File
@@ -1,274 +0,0 @@
#!/usr/bin/env zx
import path from 'path';
const dir_root = process.env.QL_DIR;
const file_auth_token = path.join(dir_root, 'static/auth.json');
const token_file = path.join(dir_root, 'static/build/token.js');
let token;
const createToken = async () => {
let tokenCommand = `tsx ${dir_root}/back/token.ts`;
if (await fs.exists(token_file)) {
tokenCommand = `node ${token_file}`;
}
token = (await $([tokenCommand])).stdout.trim();
};
const getToken = async () => {
if (fs.existsSync(file_auth_token)) {
const authTokenData = JSON.parse(fs.readFileSync(file_auth_token, 'utf8'));
token = authTokenData.value;
const expiration = authTokenData.expiration;
const currentTimeStamp = Math.floor(Date.now() / 1000);
if (currentTimeStamp >= expiration) {
await createToken();
}
} else {
await createToken();
}
};
export const addCronApi = async (schedule, command, name, subId = null) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
name,
command,
schedule,
sub_id: subId,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'POST',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log(`${name} -> 添加成功`);
} else {
console.log(`${name} -> 添加失败(${message})`);
}
} catch (error) {
console.error(`${name} -> 添加失败(${error.message})`);
}
};
export const updateCronApi = async (schedule, command, name, id) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
name,
command,
schedule,
id,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log(`${name} -> 更新成功`);
} else {
console.log(`${name} -> 更新失败(${message})`);
}
} catch (error) {
console.error(`${name} -> 更新失败(${error.message})`);
}
};
export const updateCronCommandApi = async (command, id) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
command,
id,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log(`${command} -> 更新成功`);
} else {
console.log(`${command} -> 更新失败(${message})`);
}
} catch (error) {
console.error(`${command} -> 更新失败(${error.message})`);
}
};
export const delCronApi = async (ids) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons?t=${currentTimeStamp}`,
{
method: 'DELETE',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(ids),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log('成功');
} else {
console.log(`失败(${message})`);
}
} catch (error) {
console.error(`删除失败(${error.message})`);
}
};
export const updateCron = async (
ids,
status,
pid,
logPath,
lastExecutingTime = 0,
runningTime = 0,
) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
ids,
status,
pid,
log_path: logPath,
last_execution_time: lastExecutingTime,
last_running_time: runningTime,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons/status?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code !== 200) {
console.log(`更新任务状态失败(${message})`);
}
} catch (error) {
console.error(`更新任务状态失败(${error.message})`);
}
};
export const notifyApi = async (title, content) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
const data = {
title,
content,
};
try {
const response = await fetch(
`http://0.0.0.0:5600/open/system/notify?t=${currentTimeStamp}`,
{
method: 'PUT',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify(data),
},
);
const responseData = await response.json();
const { code, message } = responseData;
if (code === 200) {
console.log('通知发送成功🎉');
} else {
console.log(`通知失败(${message})`);
}
} catch (error) {
console.error(`通知失败(${error.message})`);
}
};
export const findCronApi = async (params) => {
const currentTimeStamp = Math.floor(Date.now() / 1000);
try {
const response = await fetch(
`http://0.0.0.0:5600/open/crons/detail?${params}&t=${currentTimeStamp}`,
{
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
'User-Agent': 'Mozilla/5.0',
'Content-Type': 'application/json;charset=UTF-8',
},
},
);
const responseData = await response.json();
const { data } = responseData;
if (data === 'null') {
console.log('');
} else {
const { name } = data;
console.log(name);
}
} catch (error) {
console.error(`查找失败(${error.message})`);
}
};
await getToken();
+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) => {},
};
-22
View File
@@ -1,22 +0,0 @@
#!/usr/bin/env zx
import 'zx/globals';
let initialVars = [];
export const storeEnvVars = async () => {
const stdout = (await $`env`).lines();
initialVars = stdout.map((line) => line.split('=')[0]);
};
export const restoreEnvVars = async () => {
const stdout = (await $`env`).lines();
const currentVars = stdout.map((line) => line.split('=')[0]);
for (const key of currentVars) {
if (!initialVars.includes(key)) {
await $`unset ${key}`;
}
}
};
await storeEnvVars();
+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;
-302
View File
@@ -1,302 +0,0 @@
#!/usr/bin/env zx
import {
dirScripts,
dirLog,
handleTaskStart,
runTaskBefore,
runTaskAfter,
handleTaskEnd,
globalState,
formatLogTime,
} from './share.mjs';
import { basename, dirname } from 'path';
function defineProgram(fileParam) {
if (fileParam.endsWith('.js') || fileParam.endsWith('.mjs')) {
return 'node';
} else if (fileParam.endsWith('.py') || fileParam.endsWith('.pyc')) {
return 'python3';
} else if (fileParam.endsWith('.sh')) {
return 'bash';
} else if (fileParam.endsWith('.ts')) {
return $`command -v tsx`
.then(() => 'tsx')
.catch(() => 'ts-node-transpile-only');
} else {
return '';
}
}
async function randomDelay(fileParam) {
const randomDelayMax = process.env.RandomDelay;
if (randomDelayMax && randomDelayMax > 0) {
const fileExtensions = process.env.RandomDelayFileExtensions || 'js';
const ignoredMinutes = process.env.RandomDelayIgnoredMinutes || '0 30';
const currentMin = new Date().getMinutes();
if (
fileExtensions.split(' ').some((ext) => fileParam.endsWith(`.${ext}`))
) {
if (
ignoredMinutes.split(' ').some((min) => parseInt(min) === currentMin)
) {
return;
}
const delaySecond = Math.floor(Math.random() * randomDelayMax) + 1;
console.log(
`任务随机延迟 ${delaySecond} 秒,配置文件参数 RandomDelay 置空可取消延迟`,
);
await sleep(delaySecond * 1000);
}
}
}
async function genArrayScripts() {
const arrayScripts = [];
const arrayScriptsName = [];
const files = await fs.readdir(dirScripts);
for (const file of files) {
if (file.endsWith('.js') && file !== 'sendNotify.js') {
arrayScripts.push(file);
const content = await fs.readFile(`${dirScripts}/${file}`, 'utf8');
const match = content.match(/new Env\(['"]([^'"]+)['"]\)/);
arrayScriptsName.push(match ? match[1] : '<未识别出活动名称>');
}
}
return { arrayScripts, arrayScriptsName };
}
async function usage() {
const { arrayScripts, arrayScriptsName } = await genArrayScripts();
console.log(
`task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “${dirScripts}/” 目录后的相对路径(定时任务中请写作相对路径),用法为:`,
);
console.log(
`1.$cmdTask <fileName> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数`,
);
console.log(
`2.$cmdTask <fileName> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中`,
);
console.log(
`3.$cmdTask <fileName> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行`,
);
console.log(
`4.$cmdTask <fileName> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行`,
);
if (arrayScripts.length > 0) {
console.log(`\n当前有以下脚本可以运行:`);
arrayScripts.forEach((script, i) =>
console.log(`${i + 1}. ${arrayScriptsName[i]}${script}`),
);
} else {
console.log(`\n暂无脚本可以执行`);
}
}
export function parseDuration(d) {
if (typeof d == 'number') {
if (isNaN(d) || d < 0) throw new Error(`Invalid duration: "${d}".`);
return d;
} else if (/\d+s/.test(d)) {
return +d.slice(0, -1) * 1000;
} else if (/\d+ms/.test(d)) {
return +d.slice(0, -2);
} else if (/\d+m/.test(d)) {
return +d.slice(0, -1) * 1000 * 60;
} else if (/\d+h/.test(d)) {
return +d.slice(0, -1) * 1000 * 60 * 60;
} else if (/\d+d/.test(d)) {
return +d.slice(0, -1) * 1000 * 60 * 60 * 24;
}
throw new Error(`Unknown duration: "${d}".`);
}
async function runWithTimeout(command) {
if (globalState.commandTimeoutTime) {
const timeoutNumber = parseDuration(globalState.commandTimeoutTime);
await $([command]).timeout(timeoutNumber).nothrow().pipe(process.stdout);
} else {
await $([command]).nothrow().pipe(process.stdout);
}
}
async function runNormal(fileParam, scriptParams) {
if (
!scriptParams.includes('now') &&
process.env.realTime !== 'true' &&
process.env.noDelay !== 'true'
) {
await randomDelay(fileParam);
}
cd(dirScripts);
const relativePath = dirname(fileParam);
if (!fileParam.startsWith('/') && relativePath) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, '');
}
await runWithTimeout(
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
);
}
async function runConcurrent(fileParam, envParam, numParam, scriptParams) {
if (!envParam || !numParam) {
console.log(`缺少并发运行的环境变量参数 task xxx.js conc Test 1 3`);
return;
}
const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length);
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
const singleLogTime = formatLogTime(new Date());
cd(dirScripts);
const relativePath = dirname(fileParam);
if (relativePath && fileParam.includes('/')) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, '');
}
await Promise.all(
arrayRun.map(async (env, i) => {
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
i + 1
}.log`;
await runWithTimeout(
`${envParam}="${env.replace('"', '\\"')}" ${
globalState.whichProgram
} ${fileParam} ${scriptParams} &>${singleLogPath}`,
);
}),
);
for (let i = 0; i < arrayRun.length; i++) {
const singleLogPath = `${dirLog}/${globalState.logDir}/${singleLogTime}_${
i + 1
}.log`;
const log = await fs.readFile(singleLogPath, 'utf8');
console.log(log);
await fs.unlink(singleLogPath);
}
}
async function runDesignated(fileParam, envParam, numParam, scriptParams) {
if (!envParam || !numParam) {
console.log(`缺少单独运行的参数 task xxx.js desi Test 1 3`);
return;
}
const array = (process.env[envParam] || '').split('&');
const runArr = expandRange(numParam, array.length);
const arrayRun = runArr.map((i) => array[i - 1]).filter(Boolean);
const cookieStr = arrayRun.join('&');
cd(dirScripts);
const relativePath = dirname(fileParam);
if (relativePath && fileParam.includes('/')) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, '');
}
console.log('cookieStr', cookieStr.length, arrayRun.length);
// ${envParam}="${cookieStr.replace('"', '\\"')}"
await runWithTimeout(
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
);
}
async function runElse(fileParam, scriptParams) {
cd(dirScripts);
const relativePath = dirname(fileParam);
if (relativePath && fileParam.includes('/')) {
cd(relativePath);
fileParam = fileParam.replace(`${relativePath}/`, './');
}
await runWithTimeout(
`${globalState.whichProgram} ${fileParam} ${scriptParams}`,
);
}
function expandRange(rangeStr, max) {
const tempRangeStr = rangeStr
.replace(/-max/g, `-${max}`)
.replace(/max-/g, `${max}-`);
return tempRangeStr.split(' ').flatMap((part) => {
const rangeMatch = part.match(/^(\d+)([-~_])(\d+)$/);
if (rangeMatch) {
const [, start, , end] = rangeMatch.map(Number);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
return Number(part);
});
}
async function main(taskShellParams, scriptParams) {
const [fileParam, action, envParam, ...others] = taskShellParams;
if (taskShellParams.length === 0) {
return await usage();
}
if (fileParam && /\.(js|py|pyc|sh|ts)$/.test(fileParam)) {
const filePath = fileParam.startsWith('/')
? fileParam
: path.join(dirScripts, fileParam);
if (!(await fs.exists(filePath))) {
console.log(`文件不存在 ${fileParam}`);
return;
}
switch (action) {
case undefined:
return await runNormal(fileParam, scriptParams);
case 'now':
return await runNormal(fileParam, scriptParams);
case 'conc':
return await runConcurrent(
fileParam,
envParam,
others.join(' '),
scriptParams,
);
case 'desi':
return await runDesignated(
fileParam,
envParam,
others.join(' '),
scriptParams,
);
}
}
await runElse(fileParam, taskShellParams.slice(1).concat(scriptParams));
}
async function run() {
const taskArgv = minimist(process.argv.slice(3), {
'--': true,
});
const { _: taskShellParams, m, '--': scriptParams, GlobalState } = taskArgv;
const cacheState = JSON.parse(GlobalState || '{}');
for (const key in cacheState) {
globalState[key] = cacheState[key];
}
if (m) {
globalState.commandTimeoutTime = m;
}
globalState.whichProgram = await defineProgram(taskShellParams[0]);
await handleTaskStart();
await runTaskBefore();
await main(taskShellParams, scriptParams);
await runTaskAfter();
await handleTaskEnd();
}
async function singleHandle() {}
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
signals.forEach((sig) => process.on(sig, singleHandle));
await run();
-500
View File
@@ -1,500 +0,0 @@
#!/usr/bin/env zx
import 'zx/globals';
// $.verbose = true;
import { updateCron, notifyApi } from './api.mjs';
import { restoreEnvVars } from './env.mjs';
export const dirRoot = process.env.QL_DIR;
export const dirTmp = path.join(dirRoot, '.tmp');
export const dirData = process.env.QL_DATA_DIR
? process.env.QL_DATA_DIR.endsWith('/')
? process.env.QL_DATA_DIR.slice(-1)
: process.env.QL_DATA_DIR
: path.join(dirRoot, 'data');
export const dirShell = path.join(dirRoot, 'shell');
export const dirCli = path.join(dirRoot, 'cli');
export const dirSample = path.join(dirRoot, 'sample');
export const dirStatic = path.join(dirRoot, 'static');
export const dirConfig = path.join(dirData, 'config');
export const dirScripts = path.join(dirData, 'scripts');
export const dirRepo = path.join(dirData, 'repo');
export const dirRaw = path.join(dirData, 'raw');
export const dirLog = path.join(dirData, 'log');
export const dirDb = path.join(dirData, 'db');
export const dirDep = path.join(dirData, 'deps');
export const dirListTmp = path.join(dirLog, '.tmp');
export const dirUpdateLog = path.join(dirLog, 'update');
export const qlStaticRepo = path.join(dirRepo, 'static');
export const fileConfigSample = path.join(dirSample, 'config.sample.sh');
export const fileEnv = path.join(dirConfig, 'env.sh');
export const jsFileEnv = path.join(dirConfig, 'env.js');
export const fileConfigUser = path.join(dirConfig, 'config.sh');
export const fileAuthSample = path.join(dirSample, 'auth.sample.json');
export const fileAuthUser = path.join(dirConfig, 'auth.json');
export const fileAuthToken = path.join(dirConfig, 'token.json');
export const fileExtraShell = path.join(dirConfig, 'extra.sh');
export const fileTaskBefore = path.join(dirConfig, 'task_before.sh');
export const fileTaskAfter = path.join(dirConfig, 'task_after.sh');
export const fileTaskSample = path.join(dirSample, 'task.sample.sh');
export const fileExtraSample = path.join(dirSample, 'extra.sample.sh');
export const fileNotifyJsSample = path.join(dirSample, 'notify.js');
export const fileNotifyPySample = path.join(dirSample, 'notify.py');
export const fileTestJsSample = path.join(dirSample, 'ql_sample.js');
export const fileTestPySample = path.join(dirSample, 'ql_sample.py');
export const fileNotifyPy = path.join(dirScripts, 'notify.py');
export const fileNotifyJs = path.join(dirScripts, 'sendNotify.js');
export const fileTestJs = path.join(dirScripts, 'ql_sample.js');
export const fileTestPy = path.join(dirScripts, 'ql_sample.py');
export const nginxAppConf = path.join(dirRoot, 'docker/front.conf');
export const nginxConf = path.join(dirRoot, 'docker/nginx.conf');
export const depNotifyPy = path.join(dirDep, 'notify.py');
export const depNotifyJs = path.join(dirDep, 'sendNotify.js');
export const listCrontabUser = path.join(dirConfig, 'crontab.list');
export const listCrontabSample = path.join(dirSample, 'crontab.sample.list');
export const listOwnScripts = path.join(dirListTmp, 'own_scripts.list');
export const listOwnUser = path.join(dirListTmp, 'own_user.list');
export const listOwnAdd = path.join(dirListTmp, 'own_add.list');
export const listOwnDrop = path.join(dirListTmp, 'own_drop.list');
export const globalState = {};
export const initEnv = () => {
$.prefix +=
'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;';
$.prefix += 'export PYTHONUNBUFFERED=1;';
$.prefix += 'export TERM=xterm-color;';
};
export const importConfig = async () => {
if (await fs.exists(fileConfigUser)) {
$.prefix += (await fs.readFile(fileConfigUser, 'utf8'));
}
// if (process.env.LOAD_ENV !== 'false' && (await fs.exists(fileEnv))) {
// $.prefix += (await fs.readFile(fileEnv, 'utf8'));
// }
require(jsFileEnv)
globalState.qlBaseUrl = process.env.QlBaseUrl || '/';
globalState.qlPort = process.env.QlPort || '5700';
globalState.commandTimeoutTime = process.env.CommandTimeoutTime;
globalState.fileExtensions = process.env.RepoFileExtensions || 'js py';
globalState.proxyUrl = process.env.ProxyUrl || '';
globalState.currentBranch = process.env.QL_BRANCH;
if (process.env.DefaultCronRule) {
globalState.defaultCron = process.env.DefaultCronRule;
} else {
globalState.defaultCron = `${Math.floor(Math.random() * 60)} ${Math.floor(
Math.random() * 24,
)} * * *`;
}
globalState.cpuWarn = process.env.CpuWarn;
globalState.memWarn = process.env.MemoryWarn;
globalState.diskWarn = process.env.DiskWarn;
};
export const setProxy = (proxy) => {
if (proxy) {
globalState.proxyUrl = proxy;
}
if (globalState.proxyUrl) {
$`export http_proxy=${globalState.proxyUrl}`;
$`export https_proxy=${globalState.proxyUrl}`;
}
};
export const unsetProxy = () => {
$`unset http_proxy`;
$`unset https_proxy`;
};
export const makeDir = async (dir) => {
if (!(await fs.exists(dir))) {
await fs.mkdir(dir, { recursive: true });
}
};
export const detectTermux = () => {
globalState.isTermux = process.env.PATH?.includes('com.termux') ? 1 : 0;
};
export const detectMacos = () => {
globalState.isMacos = os.type() === 'Darwin' ? 1 : 0;
};
export const genRandomNum = (number) => {
return Math.floor(Math.random() * number);
};
export const fixConfig = async () => {
await makeDir(dirTmp);
await makeDir(dirStatic);
await makeDir(dirData);
await makeDir(dirConfig);
await makeDir(dirLog);
await makeDir(dirDb);
await makeDir(dirScripts);
await makeDir(dirListTmp);
await makeDir(dirRepo);
await makeDir(dirRaw);
await makeDir(dirUpdateLog);
await makeDir(dirDep);
if (!(await fs.exists(fileConfigUser))) {
console.log(
`复制一份 ${fileConfigSample}${fileConfigUser},随后请按注释编辑你的配置文件:${fileConfigUser}`,
);
await fs.copyFile(fileConfigSample, fileConfigUser);
}
if (!(await fs.exists(fileEnv))) {
console.log(
'检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...',
);
await fs.writeFile(fileEnv, '');
}
if (!(await fs.exists(fileTaskBefore))) {
console.log(`复制一份 ${fileTaskSample}${fileTaskBefore}`);
await fs.copyFile(fileTaskSample, fileTaskBefore);
}
if (!(await fs.exists(fileTaskAfter))) {
console.log(`复制一份 ${fileTaskSample}${fileTaskAfter}`);
await fs.copyFile(fileTaskSample, fileTaskAfter);
}
if (!(await fs.exists(fileExtraShell))) {
console.log(`复制一份 ${fileExtraSample}${fileExtraShell}`);
await fs.copyFile(fileExtraSample, fileExtraShell);
}
if (!(await fs.exists(fileAuthUser))) {
console.log(`复制一份 ${fileAuthSample}${fileAuthUser}`);
await fs.copyFile(fileAuthSample, fileAuthUser);
}
if (!(await fs.exists(fileNotifyPy))) {
console.log(`复制一份 ${fileNotifyPySample}${fileNotifyPy}`);
await fs.copyFile(fileNotifyPySample, fileNotifyPy);
}
if (!(await fs.exists(fileNotifyJs))) {
console.log(`复制一份 ${fileNotifyJsSample}${fileNotifyJs}`);
await fs.copyFile(fileNotifyJsSample, fileNotifyJs);
}
if (!(await fs.exists(fileTestJs))) {
await fs.copyFile(fileTestJsSample, fileTestJs);
}
if (!(await fs.exists(fileTestPy))) {
await fs.copyFile(fileTestPySample, fileTestPy);
}
if (await fs.exists('/etc/nginx/conf.d/default.conf')) {
console.log('检测到你可能未修改过默认nginx配置,将帮你删除');
await fs.unlink('/etc/nginx/conf.d/default.conf');
}
if (!(await fs.exists(depNotifyJs))) {
console.log(`复制一份 ${fileNotifyJsSample}${depNotifyJs}`);
await fs.copyFile(fileNotifyJsSample, depNotifyJs);
}
if (!(await fs.exists(depNotifyPy))) {
console.log(`复制一份 ${fileNotifyPySample}${depNotifyPy}`);
await fs.copyFile(fileNotifyPySample, depNotifyPy);
}
};
export const npmInstallSub = async () => {
if (globalState.isTermux === 1) {
await $`npm install --production --no-bin-links`;
} else if (!(await $`command -v pnpm`)) {
await $`npm install --production`;
} else {
await $`pnpm install --loglevel error --production`;
}
};
export const npmInstall = async (dirWork) => {
const dirCurrent = process.cwd();
await $`cd ${dirWork}`;
console.log(`安装 ${dirWork} 依赖包...`);
await npmInstallSub();
await $`cd ${dirCurrent}`;
};
export const diffAndCopy = async (copySource, copyTo) => {
if (
!(await fs.exists(copyTo)) ||
(await $`diff ${copySource} ${copyTo}`).exitCode !== 0
) {
await fs.copyFile(copySource, copyTo);
}
};
export const gitCloneScripts = async (url, dir, branch, proxy) => {
const partCmd = branch ? `-b ${branch}` : '';
console.log(`开始拉取仓库 ${globalState.uniqPath}${dir}`);
setProxy(proxy);
const res = await $`git clone -q --depth=1 ${partCmd} ${url} ${dir}`;
globalState.exitStatus = res.exitCode;
unsetProxy();
};
export const randomRange = (begin, end) => {
return Math.floor(Math.random() * (end - begin) + begin);
};
export const deletePm2 = async () => {
await $`cd ${dirRoot}`;
await $`pm2 delete ecosystem.config.js`;
};
export const reloadPm2 = async () => {
await $`cd ${dirRoot}`;
restoreEnvVars();
await $`pm2 flush &>/dev/null`;
await $`pm2 startOrGracefulReload ecosystem.config.js`;
};
export const reloadUpdate = async () => {
await $`cd ${dirRoot}`;
restoreEnvVars();
await $`pm2 flush &>/dev/null`;
await $`pm2 startOrGracefulReload other.config.js`;
};
export const diffTime = (beginTime, endTime) => {
let diffTime;
if (globalState.isMacos === 1) {
diffTime = (+new Date(endTime) - +new Date(beginTime)) / 1000;
} else {
diffTime =
(new Date(endTime).getTime() - new Date(beginTime).getTime()) / 1000;
}
return diffTime;
};
export const formatTime = (time) => {
// 秒
return new Date(time).toLocaleString();
};
function pad(n, min = 10) {
return n < min ? '0' + n : n;
}
export function formatDate(date) {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hour = pad(date.getHours());
const minute = pad(date.getMinutes());
const second = pad(date.getSeconds());
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
export const formatLogTime = (date) => {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hour = pad(date.getHours());
const minute = pad(date.getMinutes());
const second = pad(date.getSeconds());
const milliSecond = pad(date.getMilliseconds(), 100);
return `${year}-${month}-${day}-${hour}-${minute}-${second}-${milliSecond}`;
};
export const formatTimestamp = (date) => {
return Math.floor(date.getTime() / 1000);
};
export const patchVersion = async () => {
await $`git config --global pull.rebase false`;
if (await fs.exists(path.join(dirRoot, 'db/cookie.db'))) {
console.log('检测到旧的db文件,拷贝为新db...');
await $`mv ${path.join(dirRoot, 'db/cookie.db')} ${path.join(
dirRoot,
'db/env.db',
)}`;
await $`rm -rf ${path.join(dirRoot, 'db/cookie.db')}`;
}
if (await fs.exists(path.join(dirRoot, 'db'))) {
console.log('检测到旧的db目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
}
if (await fs.exists(path.join(dirRoot, 'scripts'))) {
console.log('检测到旧的scripts目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'scripts')} ${dirData}`;
}
if (await fs.exists(path.join(dirRoot, 'log'))) {
console.log('检测到旧的log目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'log')} ${dirData}`;
}
if (await fs.exists(path.join(dirRoot, 'config'))) {
console.log('检测到旧的config目录,拷贝到data目录...');
await $`cp -rf ${path.join(dirRoot, 'config')} ${dirData}`;
}
};
export const initNginx = async () => {
await fs.copyFile(nginxConf, '/etc/nginx/nginx.conf');
await fs.copyFile(nginxAppConf, '/etc/nginx/conf.d/front.conf');
let locationUrl = '/';
let aliasStr = '';
let rootStr = '';
let qlBaseUrl = globalState.qlBaseUrl;
let qlPort = globalState.qlPort;
if (qlBaseUrl !== '/') {
if (!qlBaseUrl.startsWith('/')) {
qlBaseUrl = `/${qlBaseUrl}`;
}
if (!qlBaseUrl.endsWith('/')) {
qlBaseUrl = `${qlBaseUrl}/`;
}
locationUrl = `^~${qlBaseUrl.slice(0, -1)}`;
aliasStr = `alias ${path.join(dirStatic, 'dist')};`;
const file = await fs.readFile(
path.join(dirStatic, 'dist/index.html'),
'utf8',
);
if (!file.includes(`<base href="${qlBaseUrl}">`)) {
await fs.writeFile(
path.join(dirStatic, 'dist/index.html'),
`<base href="${qlBaseUrl}">\n${file}`,
);
}
} else {
rootStr = `root ${path.join(dirStatic, 'dist')};`;
}
await $`sed -i "s,QL_ALIAS_CONFIG,${aliasStr},g" /etc/nginx/conf.d/front.conf`;
await $`sed -i "s,QL_ROOT_CONFIG,${rootStr},g" /etc/nginx/conf.d/front.conf`;
await $`sed -i "s,QL_BASE_URL_LOCATION,${locationUrl},g" /etc/nginx/conf.d/front.conf`;
let ipv6Str = '';
const ipv6 = await $`ip a | grep inet6`;
if (ipv6.stdout.trim()) {
ipv6Str = 'listen [::]:${qlPort} ipv6only=on;';
}
const ipv4Str = `listen ${qlPort};`;
await $`sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf`;
await $`sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf`;
};
async function checkServer() {
const cpuWarn = parseInt(process.env.cpuWarn || '0');
const memWarn = parseInt(process.env.memWarn || '0');
const diskWarn = parseInt(process.env.diskWarn || '0');
if (cpuWarn && memWarn && diskWarn) {
const topResult = await $`top -b -n 1`;
const cpuUse = parseInt(
topResult.stdout.match(/CPU\s+(\d+)\%/)?.[1] || '0',
);
const memFree = parseInt(
(await $`free -m`).stdout.match(/Mem:\s+(\d+)/)?.[1] || '0',
);
const memTotal = parseInt(
(await $`free -m`).stdout.match(/Mem:\s+\d+\s+(\d+)/)?.[1] || '0',
);
const diskUse = parseInt(
(await $`df -P`).stdout.match(/\/dev.*\s+(\d+)\%/)?.[1] || '0',
);
if (memFree && memTotal && diskUse && cpuUse) {
const memUse = Math.floor((memFree * 100) / memTotal);
if (cpuUse > cpuWarn || memFree < memWarn || diskUse > diskWarn) {
const resource = topResult.stdout
.split('\n')
.slice(7, 17)
.map((line) => line.replace(/\s+/g, ' '))
.join('\\n');
await notifyApi(
'服务器资源异常警告',
`当前CPU占用 ${cpuUse}% 内存占用 ${memUse}% 磁盘占用 ${diskUse}% \n资源占用详情 \n\n ${resource}`,
);
}
}
}
}
export const handleTaskStart = async () => {
if (globalState.ID) {
await updateCron(
[globalState.ID],
'0',
String(process.pid),
globalState.logPath,
globalState.beginTimestamp,
);
}
console.log(`## 开始执行... ${globalState.beginTime}\n`);
};
export const runTaskBefore = async () => {
if (globalState.isMacos === 0) {
await checkServer();
}
await $`. ${fileTaskBefore} "$@"`;
if (globalState.taskBefore) {
console.log('执行前置命令');
await $`eval ${globalState.taskBefore}`;
console.log('执行前置命令结束');
}
};
export const runTaskAfter = async () => {
await $`. ${fileTaskAfter} "$@"`;
if (globalState.taskAfter) {
console.log('执行后置命令');
await $`eval "${globalState.taskAfter}"`;
console.log('执行后置命令结束');
}
};
export const handleTaskEnd = async () => {
const etime = new Date();
const endTime = formatDate(etime);
const endTimestamp = formatTimestamp(etime);
let diffTime = endTimestamp - globalState.beginTimestamp;
if (diffTime === 0) {
diffTime = 1;
}
if (globalState.ID) {
await updateCron(
[globalState.ID],
'1',
`${process.pid}`,
globalState.logPath,
globalState.beginTimestamp,
diffTime,
);
}
console.log(`\n## 执行结束... ${endTime} 耗时 ${diffTime}`);
};
initEnv();
detectTermux();
detectMacos();
await importConfig();
-94
View File
@@ -1,94 +0,0 @@
#!/usr/bin/env zx
import 'zx/globals';
import { PassThrough } from 'node:stream';
import {
listCrontabUser,
formatLogTime,
formatTimestamp,
formatDate,
dirLog,
dirCli,
globalState,
handleTaskEnd,
} from './share.mjs';
import './api.mjs';
// $.verbose = true;
async function singleHandle() {}
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT', 'SIGTSTP'];
signals.forEach((sig) => process.on(sig, singleHandle));
export async function handleLogPath(fileParam) {
let ID = $.env.ID;
if (!ID) {
const grepResult =
await $`grep -E "task.* ${fileParam}" ${listCrontabUser}`.nothrow();
ID = grepResult.stdout.match(/ID=(\d+)/)?.[1];
}
globalState.ID = ID;
const suffix = ID && parseInt(ID, 10) > 0 ? `_${ID}` : '';
globalState.time = new Date();
const logTime = formatLogTime(globalState.time);
let logDirTmp = path.basename(fileParam);
let logDirTmpPath = '';
if (fileParam.includes('/')) {
logDirTmpPath = path.isAbsolute(fileParam)
? fileParam.substring(1)
: fileParam;
logDirTmpPath = path.basename(path.dirname(logDirTmpPath));
}
if (logDirTmpPath) {
logDirTmp = `${logDirTmpPath}_${logDirTmp}`;
}
const logDir = `${logDirTmp.replace(/\.[^/.]+$/, '')}${suffix}`;
globalState.logDir = logDir;
globalState.logPath = `${logDir}/${logTime}.log`;
if ($.env.real_log_path) {
globalState.logPath = realLogPath;
}
await $`mkdir -p ${dirLog}/${logDir}`;
}
export function initBeginTime() {
globalState.beginTime = formatDate(globalState.time);
globalState.beginTimestamp = formatTimestamp(globalState.time);
}
async function main() {
const taskArgv = minimist(process.argv.slice(3), {
'--': true,
});
const {
_: [scriptFile],
} = taskArgv;
await handleLogPath(scriptFile);
initBeginTime();
cd(`${dirCli}`);
const logStream = fs.createWriteStream(`${dirLog}/${globalState.logPath}`);
const passThrough = new PassThrough();
passThrough.pipe(logStream);
const p = $`./otask.mjs ${process.argv.slice(
3,
)} --GlobalState=${JSON.stringify(globalState)} 2>&1`.nothrow();
p.stdout.pipe(passThrough).pipe(process.stdout);
await p;
}
main()
.then(() => {
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
-82
View File
@@ -1,82 +0,0 @@
FROM python:3.10-alpine3.18 as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 pm2 tsx \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop
ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
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 \
LANG=C.UTF-8 \
SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data
EXPOSE 5700
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
COPY --from=builder /usr/local/bin/. /usr/local/bin/
RUN set -x \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
coreutils \
git \
curl \
wget \
tzdata \
perl \
openssl \
nginx \
nodejs \
jq \
openssh \
procps \
netcat-openbsd \
unzip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& ulimit -c 0
ARG SOURCE_COMMIT
RUN git clone --depth=1 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \
&& cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+17 -41
View File
@@ -1,40 +1,27 @@
FROM python:3.11-alpine3.18 as builder FROM node:alpine
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm git \
&& npm i -g pnpm@8.3.1 pm2 tsx \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.11-alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git ARG QL_URL=https://github.com/${QL_MAINTAINER}/qinglong.git
ARG QL_BRANCH=develop ARG QL_BRANCH=master
ENV PNPM_HOME=/root/.local/share/pnpm \ ENV PNPM_HOME=/root/.local/share/pnpm \
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \ PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/share/pnpm:/root/.local/share/pnpm/global/5/node_modules:$PNPM_HOME \
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 \ LANG=zh_CN.UTF-8 \
LANG=C.UTF-8 \
SHELL=/bin/bash \ SHELL=/bin/bash \
PS1="\u@\h:\w \$ " \ PS1="\u@\h:\w \$ " \
QL_DIR=/ql \ QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data WORKDIR ${QL_DIR}
EXPOSE 5700
COPY --from=builder /usr/local/lib/node_modules/. /usr/local/lib/node_modules/
COPY --from=builder /usr/local/bin/. /usr/local/bin/
RUN set -x \ RUN set -x \
&& 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 \
coreutils \ coreutils \
moreutils \
git \ git \
curl \ curl \
wget \ wget \
@@ -42,41 +29,30 @@ RUN set -x \
perl \ perl \
openssl \ openssl \
nginx \ nginx \
nodejs \ python3 \
jq \ jq \
openssh \ openssh \
procps \ py3-pip \
netcat-openbsd \
unzip \
npm \
&& rm -rf /var/cache/apk/* \ && rm -rf /var/cache/apk/* \
&& apk update \
&& ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& echo "Asia/Shanghai" > /etc/timezone \ && echo "Asia/Shanghai" > /etc/timezone \
&& git config --global user.email "qinglong@@users.noreply.github.com" \ && git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \ && git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \ && npm install -g pnpm \
&& rm -rf /root/.pnpm-store \ && pnpm add -g pm2 ts-node typescript tslib \
&& rm -rf /root/.local/share/pnpm/store \ && git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& rm -rf /root/.cache \
&& ulimit -c 0
ARG SOURCE_COMMIT
RUN git clone --depth=1 -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 \
&& git clone --depth=1 -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \ && pnpm install --prod \
&& 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 \
&& mkdir -p ${QL_DIR}/static \ && mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \ && cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
-6
View File
@@ -1,15 +1,9 @@
version: '2' version: '2'
services: services:
web: web:
# alpine 基础镜像版本
image: whyour/qinglong:latest image: whyour/qinglong:latest
# debian-slim 基础镜像版本
# image: whyour/qinglong:debian
volumes: volumes:
- ./data:/ql/data - ./data:/ql/data
ports: ports:
- "0.0.0.0:5700:5700" - "0.0.0.0:5700:5700"
environment:
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
QlBaseUrl: '/'
restart: unless-stopped restart: unless-stopped
+25 -12
View File
@@ -1,42 +1,55 @@
#!/bin/bash #!/bin/bash
dir_shell=/ql/shell dir_shell=/ql/shell
. $dir_shell/env.sh
. $dir_shell/share.sh . $dir_shell/share.sh
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 make_dir /run/nginx
init_nginx cp -fv $nginx_conf /etc/nginx/nginx.conf
fix_config cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
patch_version update_depend
echo
echo -e "======================3. 启动nginx========================\n" 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. 启动pm2服务========================\n" echo -e "======================4. 启动面板监控========================\n"
reload_update pm2 delete public &>/dev/null
reload_pm2 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 start $dir_static/build/app.js -n panel --source-map-support --time
echo -e "控制面板启动成功...\n"
echo -e "======================6. 启动定时任务========================\n"
pm2 delete schedule &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time
echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================5. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >$dir_log/bot.log 2>&1 & nohup ql bot >>$dir_log/start.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "====================6. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >$dir_log/extra.log 2>&1 & nohup ql extra >>$dir_log/start.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
echo -e "############################################################\n" echo -e "############################################################\n"
echo -e "容器启动成功..." echo -e "容器启动成功..."
echo -e "\n请先访问5700端口,登录成功面板之后再执行添加定时任务..."
echo -e "############################################################\n" echo -e "############################################################\n"
crond -f >/dev/null crond -f >/dev/null
+12 -45
View File
@@ -1,74 +1,43 @@
upstream baseApi { upstream api {
server 0.0.0.0:5600; server 0.0.0.0:5600;
} }
upstream publicApi { upstream public {
server 0.0.0.0:5400; server 0.0.0.0:5400;
} }
upstream updateApi {
server 0.0.0.0:5300;
}
map $http_upgrade $connection_upgrade { map $http_upgrade $connection_upgrade {
default keep-alive; default keep-alive;
'websocket' upgrade; 'websocket' upgrade;
} }
server { server {
IPV4_CONFIG listen 5700;
IPV6_CONFIG root /ql/static/dist;
ssl_session_timeout 5m; ssl_session_timeout 5m;
location QL_BASE_URLapi/update/ { location /api/public {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://updateApi/api/; proxy_pass http://public;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
} }
location QL_BASE_URLapi/public/ { location /api {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/; proxy_pass http://api;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
location QL_BASE_URLapi/ {
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://baseApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade; proxy_set_header Connection $connection_upgrade;
} }
location QL_BASE_URLopen/ { location /open {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/; proxy_pass http://api;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
} }
gzip on; gzip on;
@@ -79,12 +48,10 @@ server {
gzip_comp_level 6; gzip_comp_level 6;
gzip_buffers 16 8k; gzip_buffers 16 8k;
gzip_http_version 1.0; gzip_http_version 1.0;
QL_ROOT_CONFIG
location QL_BASE_URL_LOCATION { location / {
QL_ALIAS_CONFIG
index index.html index.htm; index index.html index.htm;
try_files $uri QL_BASE_URLindex.html; try_files $uri $uri/ /index.html;
} }
location ~ .*\.(html)$ { location ~ .*\.(html)$ {
+1 -1
View File
@@ -14,7 +14,7 @@ http {
server_tokens off; server_tokens off;
client_max_body_size 4096m; client_max_body_size 20m;
client_body_buffer_size 20m; client_body_buffer_size 20m;
keepalive_timeout 65; keepalive_timeout 65;
-42
View File
@@ -1,42 +0,0 @@
module.exports = {
apps: [
{
name: 'schedule',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/schedule/index.js',
env: {
http_proxy: '',
https_proxy: '',
HTTP_PROXY: '',
HTTPS_PROXY: '',
all_proxy: '',
ALL_PROXY: '',
},
},
{
name: 'public',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/public.js',
},
{
name: 'panel',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
source_map_support: true,
time: true,
script: 'static/build/app.js',
},
],
};
+2 -1
View File
@@ -1,5 +1,6 @@
{ {
"watch": ["back", ".env"], "watch": ["back", ".env"],
"ext": "js,ts,json", "ext": "js,ts,json",
"exec": "ts-node -P tsconfig.back.json ./back/app.ts" "ignore": ["src/**/*.spec.ts"],
"exec": "ts-node --transpile-only ./back/app.ts"
} }
-13
View File
@@ -1,13 +0,0 @@
module.exports = {
apps: [
{
name: 'update',
max_restarts: 10,
kill_timeout: 15000,
wait_ready: true,
listen_timeout: 10000,
time: true,
script: 'static/build/update.js',
},
],
};
+34 -86
View File
@@ -2,26 +2,19 @@
"private": true, "private": true,
"scripts": { "scripts": {
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "umi dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:update": "ts-node -P tsconfig.back.json ./back/update.ts", "start:public": "ts-node back/public.ts",
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts", "build:front": "umi build",
"start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
"schedule": "npm run build:back && node static/build/schedule/index.js", "schedule": "npm run build:back && node static/build/schedule.js",
"public": "npm run build:back && node static/build/public.js", "public": "npm run build:back && node static/build/public.js",
"update": "npm run build:back && node static/build/update.js",
"gen:proto": "protoc --experimental_allow_proto3_optional --plugin=./node_modules/.bin/protoc-gen-ts_proto ./back/protos/*.proto --ts_proto_out=./ --ts_proto_opt=outputServices=grpc-js,env=node,esModuleInterop=true",
"prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'", "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
"postinstall": "max setup 2>/dev/null || true", "postinstall": "umi generate tmp 2>/dev/null || true",
"test": "umi-test", "test": "umi-test",
"test:coverage": "umi-test --coverage" "test:coverage": "umi-test --coverage"
}, },
"bin": {
"task": "./cli/task.mjs"
},
"gitHooks": { "gitHooks": {
"pre-commit": "lint-staged" "pre-commit": "lint-staged"
}, },
@@ -35,144 +28,99 @@
}, },
"pnpm": { "pnpm": {
"peerDependencyRules": { "peerDependencyRules": {
"ignoreMissing": [
"react",
"react-dom",
"antd",
"dva",
"postcss",
"webpack",
"eslint",
"stylelint",
"redux",
"@babel/core",
"monaco-editor",
"rc-field-form",
"@types/lodash.merge",
"rollup",
"styled-components"
],
"allowedVersions": { "allowedVersions": {
"react": "18", "react": "17",
"react-dom": "18", "react-dom": "17"
"dva-core": "2"
} }
} }
}, },
"dependencies": { "dependencies": {
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1", "@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1", "@sentry/node": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"body-parser": "^1.19.2", "body-parser": "^1.19.2",
"celebrate": "^15.0.1", "celebrate": "^15.0.1",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron-parser": "^4.2.1", "cron-parser": "^4.2.1",
"cross-spawn": "^7.0.3",
"dayjs": "^1.11.2", "dayjs": "^1.11.2",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-rate-limit": "^7.0.0",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^1.2.0", "hpagent": "^0.1.2",
"http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "1.4.5-lts.1", "multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-queue-cjs": "7.3.4", "p-queue": "7.2.0",
"protobufjs": "^7.3.0",
"pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.25.5", "sequelize": "^6.20.1",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3", "sqlite3": "npm:@louislam/sqlite3@^15.0.6",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.6.0", "winston": "^3.6.0",
"winston-daily-rotate-file": "^4.7.1", "yargs": "^17.3.1"
"zx": "^8.1.4",
"tough-cookie": "^4.0.0",
"request-ip": "3.3.0",
"ip2region": "2.3.0"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "6.38.22", "@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "^4.3.1",
"@react-hook/resize-observer": "^1.2.6", "@sentry/react": "^7.0.0",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/file-saver": "2.0.2",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185", "@types/lodash": "^4.14.179",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12", "@types/nedb": "^1.8.12",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
"@types/node-schedule": "^1.3.2", "@types/node-schedule": "^1.3.2",
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^17.0.39",
"@types/react-copy-to-clipboard": "^5.0.4", "@types/react-dom": "^17.0.13",
"@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/request-ip": "0.0.41", "@umijs/plugin-antd": "^0.15.0",
"@uiw/codemirror-extensions-langs": "^4.21.9", "@umijs/plugin-esbuild": "^1.4.1",
"@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ahooks": "^3.7.8", "@umijs/test": "^3.5.21",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.24.8", "antd": "^4.20.5",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"axios": "^1.4.0", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"react-hotkeys-hook": "^4.4.1", "lint-staged": "^12.3.4",
"file-saver": "2.0.2", "nodemon": "^2.0.15",
"lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^3.0.1",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"pretty-bytes": "6.1.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"rc-virtual-list": "3.5.3", "react": "17.x",
"react": "18.2.0", "react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "18.2.0", "react-dom": "17.x",
"react-intl-universal": "^2.6.21",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"ts-proto": "^1.146.0", "typescript": "^4.6.2",
"tslib": "^2.4.0", "umi": "^3.5.21",
"tsx": "^4.7.3", "umi-request": "^1.4.0",
"typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
-16711
View File
File diff suppressed because it is too large Load Diff

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