Compare commits

...
209 Commits
Author SHA1 Message Date
whyour 892d91edc4 更新版本 v2.17.1 2024-02-13 15:19:52 +08:00
whyour 74916670eb 修复设置 nodejs 镜像源 2024-02-13 15:19:44 +08:00
whyour b31eab02d7 修复重启系统 2024-02-11 22:04:41 +08:00
whyour 9c258ab113 修复初始化更新镜像源配置 2024-02-11 21:46:27 +08:00
whyour 3bcba11b33 修改 workflow 编译 2024-02-08 22:10:11 +08:00
whyour 15140fd87d 静态资源编译增加 git gc 2024-02-08 21:28:46 +08:00
whyour e83058c3bc 兼容脚本、日志、配置文件详情接口 2024-02-08 20:47:03 +08:00
whyour 29f0c2c9ac 修复对比工具保存文件 2024-02-07 20:08:10 +08:00
whyour f25fe27473 脚本管理重命名增加默认值,增加新建、删除快捷键 2024-02-06 22:17:07 +08:00
whyour 0ae1f284ec 修改脚本详情和日志详情接口 2024-01-29 21:47:03 +08:00
whyour 49c441117a 修改 sqlite 版本 2024-01-28 20:22:55 +08:00
whyour 7bc5ff01a6 更新 readme 2024-01-25 23:54:03 +08:00
whyour 8128fb8a9f 修改依赖设置提示 2024-01-25 22:42:33 +08:00
秋澪andGitHub 315ae49360 添加发布端口和数据卷 (#2231) 2024-01-25 20:48:03 +08:00
Cp0204andGitHub 35c6475534 修复未开启一言时多余空行, 通知渠道改发送前检查 (#2222) 2024-01-25 13:14:26 +08:00
whyour 7f3e60423b 修复启动时依赖配置未初始化 2024-01-24 22:55:21 +08:00
whyour 0a723318a1 修复 workflow 缓存 2024-01-22 23:50:47 +08:00
whyour 2c1e7535b8 修复 debian 系统内更新源代码分支错误 2024-01-22 23:38:58 +08:00
whyour 48d1fba699 系统设置增加重启 2024-01-22 23:36:11 +08:00
whyour 06f861baaf 更新版本 v2.17.0 2024-01-22 22:51:33 +08:00
whyour bbbd353567 增加 python3.10 镜像 2024-01-22 22:51:22 +08:00
whyour 8283f62db2 修改获取配置文件接口,初始化系统配置 2024-01-18 13:51:06 +08:00
whyour 1a94a660a5 修改对比工具数据源,修复编辑器语言 2024-01-14 16:28:11 +08:00
whyour 5c393fef36 更新 readme 2024-01-13 21:34:13 +08:00
whyour 53346a3962 修复 notify.js smtp 通知 2024-01-13 20:52:04 +08:00
whyour 4e66a3aff5 修改 monaco-editor 静态资源地址 2024-01-10 22:12:41 +08:00
whyour b79d10b452 修改获取登录 ip 和 ip 地址方式 2024-01-08 19:05:12 +08:00
whyour 3dc3ece993 修改日志脚本文件大小展示 2024-01-07 16:54:05 +08:00
whyour 792cf9ad20 修复依赖设置样式 2024-01-02 22:07:12 +08:00
whyour 201f41e15b 修改镜像源设置样式 2024-01-01 22:33:20 +08:00
whyour 71e7ba9cb6 修改 nginx api 代理超时时间 2024-01-01 17:14:57 +08:00
whyour a7562259d1 更新 sqlite3 版本 2024-01-01 15:07:56 +08:00
whyour 18671264bf 移除 config.sh 中 PipMirror 和 NpmMirror配置 2023-12-25 22:05:33 +08:00
whyour 37bcb77be0 修复健康检查命令 2023-12-24 15:48:34 +08:00
whyour 563457cf1b JavaScript 通知文件 tunnel 替换为 hpagent,移除 scripts 目录下 package.json 2023-12-24 14:30:56 +08:00
whyour d2cf482d11 修复保存文件 2023-12-23 23:56:11 +08:00
whyour 2713942a68 脚本管理忽略符号文件 2023-12-23 00:28:31 +08:00
whyour 62168979e3 修复更新用户信息 2023-12-22 22:13:09 +08:00
whyour f8aba4b1fb 日志和脚本增加文件大小展示,修改脚本管理列表排序 2023-12-21 09:57:06 +08:00
whyour 52a89a21f6 降级 Alpine 为 3.18 2023-12-18 23:39:16 +08:00
whyour ca8790bbec 修复任务详情脚本行数展示 2023-12-17 17:53:15 +08:00
whyour be1d68c0d1 修改 schedule 服务代理环境变量 2023-12-10 21:53:23 +08:00
whyour 95afcbc9dd 更新 workflow 2023-12-09 21:29:04 +08:00
whyour 20249119a0 修复 sentry 配置 2023-12-09 12:02:58 +08:00
whyour 6e0ee6f627 移除 console 2023-12-05 23:23:13 +08:00
whyour 13e7dac333 修复自定义通知 body 中 key 被自动转为小写 2023-12-04 17:44:13 +08:00
whyour b89dc6dfc6 修改镜像源按钮增加 loading 2023-12-01 09:28:11 +08:00
whyour 897b710527 通知 TG_API_HOST 参数移除默认 https 协议,参数包含协议 2023-12-01 09:21:27 +08:00
whyour 5afec03548 修改获取切换 node 和 linux 源日志 2023-11-30 23:39:14 +08:00
whyour c6a25e5359 修改代理地址提示 2023-11-25 23:03:16 +08:00
whyour 3777a4e7b4 系统设置增加依赖代理和镜像设置 2023-11-25 21:49:33 +08:00
luckrnx09andGitHub 9b7d4cb8fe Enhance the script editor experience (#2186) 2023-11-25 14:24:08 +08:00
whyour 59f7b72bb1 移除 ql -l 参数 2023-11-23 19:40:52 +08:00
whyour 4d97f0f44d 重新安装依赖时不检查是否已安装 2023-11-22 22:54:36 +08:00
whyour de9e9ce627 webhook 通知 body 增加 text/plain 类型,修复多定时任务服务重启未初始化 2023-11-22 22:43:48 +08:00
whyour f056b611a4 修复删除依赖逻辑 2023-11-11 11:23:04 +08:00
whyour 659484511f 修复 js 通知文件 2023-11-10 22:57:17 +08:00
whyour 9ed107980c 系统设置增加依赖代理和镜像源设置 2023-11-06 23:55:16 +08:00
whyour 4a3ac7dc4b 移除随机延迟相关配置及 ProxyUrl 默认值,修复拉库通知可能失败 2023-11-05 01:02:31 +08:00
whyour c3a0f58003 更新 python 版本为 3.11 2023-11-04 21:21:21 +08:00
whyour 23781d8b34 修复获取日志列表 2023-11-03 21:31:48 +08:00
6dylan6andwhyour c1d6a0a40b 修复调用就结束执行的问题 2023-11-03 18:07:07 +08:00
Bad ChildandGitHub 82c9978514 修复 chronocat 通知成功判断 (#2166) 2023-11-03 17:57:10 +08:00
TungliesandGitHub 9f6d4943ea 修复: 邮件服务支持列表链接失效(#2167) 2023-11-03 17:55:59 +08:00
whyour c4a44a48c5 使用 npm 安装 pm2 tsx 2023-11-02 19:37:24 +08:00
whyour d2fef75e01 修改 trust proxy 2023-11-02 00:47:33 +08:00
whyour 085cb789b5 修复初始化设置通知,shell 映射,邮箱通知错误提示 2023-11-02 00:41:04 +08:00
whyour 20f615eadf fs 文件操作替换为 fs.promise 2023-11-01 16:44:34 +08:00
whyour 66a2769e7c 更新版本 v2.16.5 2023-10-30 15:04:16 +08:00
whyour 899a30eacd 修改重置环境变量逻辑 2023-10-30 15:04:14 +08:00
whyour 8760ac2964 修复定时任务详情日志列表样式 2023-10-26 23:03:58 +08:00
whyour 62a29867a3 更新 readme 2023-10-21 16:57:11 +08:00
whyour 4938635ef4 修复服务启动时环境变量污染 2023-10-20 17:54:32 +08:00
whyour 046239404f 更新版本 v2.16.4 2023-10-19 21:37:00 +08:00
whyour b90243f55c 修改 task 日志展示逻辑 2023-10-19 21:36:39 +08:00
whyour 43c0cd8132 修复 mac 任务无法正常退出 2023-10-16 23:44:39 +08:00
whyour 533e12a796 脚本推送增加自定义 webhook 方式 2023-10-15 20:42:05 +08:00
whyour bd004a0489 修复设置代理时,schedule 服务通信异常 2023-10-14 23:05:25 +08:00
whyour 41befcc0a8 更新 readme 2023-10-14 21:50:12 +08:00
whyour 9fb9b3d121 task 命令支持给脚本传参,使用 -- 分割,后面的参数都会传给脚本
例如 task x.js -n whyour,脚本就能接收到参数 -n whyour
2023-10-14 21:47:46 +08:00
5055045d22 增加chronocat无头模式的QQNT推送 用于无法使go-cqhttp的接替 (#2141)
* 增加chronocat无头模式的QQNT推送

* Chronocat发送时没有配置群号或个人消息号发送出错

* 增加系统通知、去除注释

* 增加系统通知

---------

Co-authored-by: child <wulincheng@javalc.com>
2023-10-14 20:56:32 +08:00
whyour 9f7beb934d 系统日志增加置顶置底按钮 2023-10-13 23:44:28 +08:00
whyour 26b06c17c5 修复清空文件夹报错 2023-10-13 15:35:50 +08:00
whyour b8a9b26ca3 修复定时任务任务前后命令校验 2023-10-12 00:55:22 +08:00
whyour b6376ed2e8 修复定时任务 task_after 命令无法停止 2023-10-12 00:45:49 +08:00
whyour 99f6073c8e 定时任务增加前后规则校验,不能包含 task 命令 2023-10-10 23:06:01 +08:00
whyour 5e73f0390f 修复定时任务详情页查看历史日志异常 2023-10-09 23:56:35 +08:00
whyour c35cfba8b0 修改任务队列重新设置并发 2023-10-08 22:18:54 +08:00
whyour aac109621a 修改任务队列日志打印 2023-10-08 22:11:46 +08:00
whyour a340964c82 修改获取登录 ip 判断 2023-10-08 00:10:46 +08:00
whyour 00818b694a 使用 spawn exit 方法监听进程退出 2023-10-07 22:51:44 +08:00
whyour ec5b885476 修改任务队列执行日志 2023-10-06 02:34:40 +08:00
whyour 9d55cb108c 定时任务详情增加额外定时展示 2023-09-29 23:16:07 +08:00
whyour 4c19054b30 修复 api 前缀 2023-09-29 16:26:03 +08:00
whyour 2a41f64d1b 修复 QlBaseUrl 末尾不加斜杠无法访问 2023-09-29 16:07:41 +08:00
whyour d3023d31e3 修复定时任务日志滚动 2023-09-27 23:04:16 +08:00
whyour eddc03e295 修复定时任务间隔较小,任务状态不准确 2023-09-26 22:51:33 +08:00
whyour 77a8e00b17 更新版本 v2.16.3 2023-09-24 13:39:53 +08:00
whyour 99281a061e 修复更新历史定时任务规则未生效 2023-09-24 13:39:52 +08:00
whyour 22eedebf14 修复任务管理创建视图 2023-09-24 00:47:56 +08:00
whyour d7e0531935 修复系统设置展示版本失败 2023-09-23 11:14:24 +08:00
whyour 16734326cf 修复 ws 变量错误 2023-09-22 20:44:47 +08:00
whyour 956cfe18be 环境变量名称增加复制功能 2023-09-22 00:47:36 +08:00
whyour a864a56917 修复调试脚本日志丢失 2023-09-22 00:46:16 +08:00
whyour ab3fc9b5f1 定时任务增加运行任务前和运行任务后参数 2023-09-19 22:44:32 +08:00
whyour 8d899f1a53 更新 workflow 2023-09-19 00:30:45 +08:00
whyour a1eef2c644 修复使用 ubuntu-latest linux/s390x 无法使用 npm 2023-09-18 13:09:14 +08:00
zui-langandGitHub aab096843c 消息通知 Bark 增加额外参数:时效性通知、跳转Url (#2112) 2023-09-17 01:01:34 +08:00
whyour acc7443004 定时任务支持多个定时规则 2023-09-16 00:15:50 +08:00
whyour 042d7d3b8e 修复任务管理创建视图 placeholder,更新 npm 包 2023-09-15 21:27:53 +08:00
whyour 0511a4af0d 增加 QlPort 环境变量,支持设置服务启动的端口,修复定时任务视图操作符筛选 2023-09-15 17:57:28 +08:00
whyour 748a099087 修复日志滚动逻辑 2023-09-12 22:28:26 +08:00
whyour 742d0b2135 更新版本 v2.16.2 2023-09-02 00:41:16 +08:00
whyour 2081364325 修改定时任务置顶样式 2023-09-02 00:41:13 +08:00
whyour 39dc31916e 修复日志弹框样式 2023-09-01 23:16:12 +08:00
whyour 5395cebdb4 修复环境变量有空格时并发数量错误 2023-09-01 22:35:15 +08:00
whyour fe934fa4af 修复环境变量中单引号转义 2023-09-01 15:15:25 +08:00
whyour 461fa8e131 修改系统任务日志 2023-08-29 23:29:02 +08:00
whyour 4618a19c04 修改本地监听 ip 2023-08-29 21:50:23 +08:00
whyour e2bd15683e 修复 JSON.parse 错误,修复删除环境变量名称过长 2023-08-27 12:41:06 +08:00
whyour 648b9c4520 修改系统日志时间打印 2023-08-26 20:51:17 +08:00
whyour b4e4e84bbd 修复多语言翻译 2023-08-26 15:29:44 +08:00
whyour aa5d6f3cb6 系统设置增加系统语言切换 2023-08-26 00:16:15 +08:00
whyour e9416c23df 增加任务运行数日志 2023-08-25 19:25:12 +08:00
NANSIYVandGitHub ef4999be55 修复 python 企业微信推送错误 (#2087)
close #2086
2023-08-25 10:37:03 +08:00
whyour e7be4999b0 修复定时任务日志滚动 2023-08-25 09:05:32 +08:00
whyour 5907553670 修改定时任务置顶样式 2023-08-25 00:46:28 +08:00
whyour 0b9066525a 修复任务运行错误时没有日志,修改 tooltip 样式 2023-08-24 23:14:22 +08:00
whyour ac904cae61 修复停止订阅执行日志 2023-08-23 22:57:18 +08:00
whyour 4d5fa320ea 修改私有仓库 ssh 配置 2023-08-23 18:06:28 +08:00
whyour 003defedcf 修复环境变量中$变量被计算 2023-08-23 14:55:57 +08:00
whyour 40e8041401 修复退出调试自动删除调试日志目录 2023-08-22 22:18:18 +08:00
whyour a8174e89be 更新版本 v2.16.1 2023-08-21 22:51:00 +08:00
whyour cce83f7a4c 修复日志中多语言文本判断 2023-08-21 22:50:57 +08:00
whyour 4f7649f157 系统设置增加系统运行日志 2023-08-21 00:10:43 +08:00
whyour b002cbef3a 修复手机端日志弹框样式 2023-08-19 09:29:35 +08:00
whyour 1d93fe0de0 修复添加订阅粘贴 ql 链接 2023-08-18 09:17:18 +08:00
whyour 2d936f1341 修复定时任务页面样式 2023-08-17 08:26:48 +08:00
whyour b40673e9ac 修改日志弹框样式 2023-08-16 22:14:45 +08:00
whyour 637874e426 初始化数据增加错误捕获 2023-08-11 22:50:53 +08:00
whyour de71d1a258 修复任务筛选条件切换 2023-08-11 22:25:14 +08:00
whyour e174e190ee 修复 python 企业微信推送,debian 版本关于页面报错 2023-08-11 21:40:01 +08:00
whyour 89164a3bd2 更新版本 v2.16.0 2023-08-06 20:57:10 +08:00
whyour a9b9c4d7ad 修复错误信息展示 2023-08-06 20:56:59 +08:00
whyour f947866c32 修复全部任务多语言 2023-08-06 14:55:51 +08:00
whyour a2e33d1ed4 修复 editor 判断语言 2023-08-05 13:18:28 +08:00
whyour b1a242c8ee 更新多语言 2023-08-05 12:33:43 +08:00
whyour 0bf28e9ab3 task 支持运行 mjs 文件 2023-08-05 11:02:29 +08:00
whyour 624c9374e5 修复定时任务列表虚拟滚动 2023-08-04 22:24:54 +08:00
雨思andGitHub a9da8fffc1 PushMe消息通道增加params参数,以支持markdown消息 (#2044) 2023-08-01 21:33:19 +08:00
whyour b4e5db9da9 删除订阅支持自动删除任务和脚本 2023-07-30 21:15:46 +08:00
whyour 2e27d4057e 定时任务增加关联订阅 2023-07-30 13:38:38 +08:00
whyour e7d023a7e0 支持多语言英文 2023-07-29 18:26:30 +08:00
whyour 39bfd39559 修复任务详情日志列表卡顿 2023-07-27 23:15:48 +08:00
whyour 6832cb4e22 修复 6 位 cron 不以 task 开头运行失败 2023-07-25 22:18:35 +08:00
雨思andGitHub 590dbba19b PushMe 推送通道修复 (#2034) 2023-07-25 17:44:27 +08:00
whyour 3987ef8d97 更新版本 v2.15.20 2023-07-24 22:02:33 +08:00
whyour 8b0dedaf8c 修复 linux 依赖是否存在判断 2023-07-24 22:02:26 +08:00
whyour 04edd97967 修复 node 依赖匹配判断 2023-07-21 21:22:48 +08:00
whyour 6131fe0110 更新版本 v2.15.19 2023-07-20 23:14:16 +08:00
whyour 665c344ae4 修复依赖是否已经安装判断 2023-07-20 22:54:11 +08:00
雨思andGitHub 373b8c97d7 增加PushMe消息通道 (#2018),修复系统设置保存通知 2023-07-20 13:19:39 +08:00
whyour 56eb0c5408 更新版本 v2.15.18 2023-07-19 21:17:22 +08:00
whyour fe55929959 更新 readme 2023-07-19 21:02:52 +08:00
whyour 01e2bd007d 修复重启提示持续时间 2023-07-18 21:39:03 +08:00
whyour 4e091b0c3e 修改 nginx body 限制,服务重启延迟时间 2023-07-18 15:30:43 +08:00
whyour db94cd3799 修复备份数据超时时间 2023-07-18 13:43:02 +08:00
whyour a15192b9b2 升级 @umijs/max 版本 2023-07-17 23:18:48 +08:00
whyour efd4f1d5ab umi-request 替换为 axios 2023-07-17 23:13:06 +08:00
whyour bd166ee794 修复 pm2 重启逻辑 2023-07-17 13:25:56 +08:00
whyour 93e94ea94c 系统设置增加数据恢复功能 2023-07-16 22:02:30 +08:00
whyour 88b87de391 增加数据备份功能 2023-07-16 00:23:29 +08:00
whyour 8affff96f3 修改系统内更新系统逻辑 2023-07-13 22:09:45 +08:00
whyour 936b565fb1 修改系统更新时间和版本获取 2023-07-12 00:05:46 +08:00
whyour b69ff2895e 增加依赖是否已经安装判断 2023-07-10 23:48:05 +08:00
whyour f3791cbb62 修复编译错误 2023-07-09 17:46:02 +08:00
whyour b0f3b51736 修复服务启动时定时删除日志失效 2023-07-08 20:58:36 +08:00
whyour 3aa112e373 修改 node 进程错误监控 2023-07-08 00:27:05 +08:00
whyour 683482e067 修改 ql update 逻辑 2023-07-08 00:18:09 +08:00
whyour 5a2caeb66b 更新版本 v2.15.17 2023-07-02 16:09:09 +08:00
whyour ee6e5bd8b4 修改任务状态值 2023-07-02 16:09:01 +08:00
whyour f970322f0a 修复定时任务状态筛选 2023-07-02 12:19:34 +08:00
whyour 1718120623 修改更新仓库逻辑 2023-07-01 20:48:28 +08:00
whyour 702c3160ec 修改并发逻辑,系统设置增加定时任务并发设置 2023-07-01 15:26:20 +08:00
whyour db227e56bf 更新版本 v2.15.16 2023-06-18 15:51:42 +08:00
whyour 84828865f7 修复依赖安装流程 2023-06-18 15:51:35 +08:00
whyour 87455cebc0 修改应用设置 loading 默认值 2023-06-17 20:26:40 +08:00
whyour 9f4435b237 修改初始化逻辑 2023-06-16 02:12:25 +08:00
whyour e5bc4b3b04 修复关闭全部任务视图,默认视图筛选错误 2023-06-14 22:38:15 +08:00
whyour 11e1b50000 修改订阅通知 2023-06-11 20:05:39 +08:00
whyour bec226ae13 增加初始化文件写入,修复 shell 变量 2023-06-11 19:58:30 +08:00
whyour c9ecdc6c97 企业微信通知增加代理地址配置 QYWX_ORIGIN 2023-06-11 15:03:07 +08:00
whyour 4cf64e7af0 增加 demo 环境变量 2023-06-10 22:20:19 +08:00
whyour 4ce01c5f0e 修复初始化界面样式 2023-06-10 16:16:36 +08:00
whyour 2eff99abe1 修复侧边栏样式 2023-06-10 13:27:21 +08:00
whyour 5bf57185d9 更新 readme 2023-06-10 12:52:53 +08:00
whyour c59dac19ae 修复 shell 变量无引号报错 2023-06-09 19:50:38 +08:00
whyour f1ef8e72ae 修复解析脚本名称逻辑 2023-06-09 19:45:25 +08:00
whyour 5b1accffb7 修改 api 限流策略,修复检查检查日志 2023-06-07 22:45:37 +08:00
whyour f64ff83414 修复列表查询字符转码 2023-06-04 22:11:48 +08:00
whyour 26c914d1db 修复侧边栏样式 2023-06-03 01:24:52 +08:00
whyour 8f75a54110 修改 textarea 样式 2023-06-03 00:48:04 +08:00
whyour 874b6e3a56 修复错误页样式 2023-06-01 23:02:36 +08:00
whyour f305d45225 修复依赖安装状态流转 2023-05-31 15:07:40 +08:00
whyour a7f97bd729 替换 spawn 为 cross-spawn 2023-05-31 14:25:20 +08:00
whyour 0bbb7c7b89 修复获取 cpu 核数 2023-05-30 17:45:47 +08:00
whyour f7465759c7 降级 p-limit,修复 esm 问题 2023-05-30 17:31:31 +08:00
whyour 998e82d323 增加 api 限流配置 2023-05-30 16:41:19 +08:00
whyour f8dfee8945 重构任务并发执行逻辑 2023-05-30 16:32:00 +08:00
158 changed files with 11207 additions and 6437 deletions
+101 -35
View File
@@ -2,54 +2,51 @@ name: Build And Push Docker Image
on:
push:
paths-ignore:
- "*.md"
branches:
- 'master'
- 'develop'
- "master"
- "develop"
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
- "v*"
schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
- cron: "00 20 * * *"
workflow_dispatch:
jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- 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 }}
target_repo_url: git@gitlab.com:whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- 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 }}
target_repo_url: git@gitee.com:whyour/qinglong.git
ssh_private_key: ${{ secrets.GITLAB_SSH_PK }}
build-static:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: '8.3.1'
version: "8.3.1"
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
cache: "pnpm"
- name: build front and back
run: |
@@ -81,40 +78,43 @@ jobs:
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:
needs: build-static
runs-on: ubuntu-latest
# 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
runs-on: ubuntu-20.04
# runs-on: self-hosted
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: '8.3.1'
- uses: actions/setup-node@v3
version: "8.3.1"
- uses: actions/setup-node@v4
with:
cache: 'pnpm'
cache: "pnpm"
- name: Setup timezone
uses: zcong1993/setup-timezone@master
uses: szenius/set-timezone@v1.2
with:
timezone: Asia/Shanghai
timezoneLinux: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -122,7 +122,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: |
${{ github.repository }}
@@ -142,14 +142,14 @@ jobs:
type=semver,pattern={{major}}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
uses: docker/build-push-action@v5
with:
build-args: |
MAINTAINER=${{ github.repository_owner }}
@@ -157,6 +157,7 @@ jobs:
SOURCE_COMMIT=${{ github.sha }}
network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x,linux/386
# platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x,linux/386
context: .
file: ./docker/Dockerfile
push: true
@@ -166,4 +167,69 @@ jobs:
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: echo ${{ steps.docker_build.outputs.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 }}
+1 -7
View File
@@ -22,10 +22,4 @@
.env
.history
.version.ts
/config
/log
/db
/manual_log
/scripts
/bak
/.tmp
-1
View File
@@ -1,2 +1 @@
sentrycli_cdnurl=https://npmmirror.com/mirrors/sentry-cli/
strict-peer-dependencies=false
+17
View File
@@ -6,3 +6,20 @@ package.json
.umi
.umi-production
.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
+12 -3
View File
@@ -4,13 +4,16 @@ const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({
hash: true,
jsMinifier: 'terser',
antd: {},
locale: {
antd: true,
title: true,
baseNavigator: true,
},
outputPath: 'static/dist',
fastRefresh: true,
favicons: [`https://qn.whyour.cn/favicon.svg`],
mfsu: {
strategy: 'eager',
},
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: {
[`${baseUrl}api/public`]: {
@@ -44,4 +47,10 @@ export default defineConfig({
'https://gw.alipayobjects.com/os/lib/react/18.2.0/umd/react.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',
},
],
});
+137 -139
View File
@@ -1,30 +1,29 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<h1 align="center">Green Dragon</h1>
<div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<h1 align="center">Qinglong</h1>
[简体中文](./README.md) | English
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-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[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>
[![](https://user-images.githubusercontent.com/22700758/229290661-03aabe84-8780-4ef0-8e75-2146f4636130.jpeg)](https://whyour.cn)
[简体中文](./README.md) | English
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
## Features
@@ -36,100 +35,47 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
- Support dark mode
- Support cell phone operation
## Deployment
## Version
### Local Deployment
### 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
# To be refined, see the development steps first (not supported on windows yet)
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### Podman Deployment
### npm
1. podman installation
The npm version supports `debian/ubuntu/centos/alpine` systems and requires `node/python3` to be installed.
```bash
https://podman.io/getting-started/installation
npm i @whyour/qinglong
```
2. start the container
## Built-in commands
- task
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
# 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
```
### Docker Deployment
1. docker installation
```bash
sudo curl -sSL get.docker.com | sh
```
2. configure domestic mirror sources
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. start the container
```bash
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### Docker-compose Deployment
1. docker-compose installation
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. start the container
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# start
docker-compose up -d
# stop
docker-compose down
```
3. access
Open your browser and visit http://{ip}:5700
## Use
1. built-in commands
- ql
```bash
# Update and restart Green Dragon
@@ -150,36 +96,106 @@ ql check
ql resetlet
# Disable two-step login
ql resettfa
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
```
2. parameter description
| **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 |
* file_url: Script address
* repo_url: Repository address
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
* branch: Pull the branch of the repository
* days: Number of days of logs to be kept
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Deployment
### Docker (Recommended)
```bash
# curl -sSL get.docker.com | sh
docker run -dit \
-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 \
--restart unless-stopped \
whyour/qinglong:latest
```
### Docker-compose (Recommended)
```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
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# start
docker-compose up -d
# stop
docker-compose down
```
### Podman (Recommended)
```bash
# 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
```
### Local
It is recommended to use a pure system installation to avoid losing the original system data, you need to install node/npm/python3/pip3 yourself
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# Centos
curl --silent --location https://rpm.nodesource.com/setup_20.x | sudo 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
```
## Development
```bash
$ git clone https://github.com/whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
Open your browser and visit <http://127.0.0.1:5700>
## Links
@@ -191,24 +207,6 @@ task -l <file_path>
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## Development
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# Recommended use pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
Open your browser and visit http://127.0.0.1:5700
## Communication
[telegram channel](https://t.me/jiao_long)
## Name Origin
The Green Dragon, also known as the Canglong, is one of the four elephants and one of the [four spirits of the heavens](https://zh.wikipedia.org/wiki/%E5%A4%A9%E4%B9%8B%E5%9B%9B%E7%81%B5) in traditional Chinese culture. According to the Five Elements, it is a spirit animal representing the East as a green dragon, the five elements are wood, and the season represented is spring, with the eight trigrams dominating vibration. Like the Ying Long, the Cang Long has feathered wings. According to the Zhang Guo Xing Zong (Zhang Guo Xing Zong), "a true dragon is one that has complementary wings".
+135 -140
View File
@@ -1,30 +1,31 @@
<p align="center">
<a href="https://github.com/whyour/qinglong">
<img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a>
</p>
<div align="center">
<img width="100" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
<h1 align="center">青龙</h1>
<div align="center">
简体中文 | [English](./README-en.md)
支持 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]
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-url]: https://hub.docker.com/r/whyour/qinglong
[docker-version-image]: https://img.shields.io/docker/v/whyour/qinglong?style=flat
[docker-version-url]: https://hub.docker.com/r/whyour/qinglong/tags?page=1&ordering=last_updated
[docker-stars-image]: https://img.shields.io/docker/stars/whyour/qinglong?style=flat
[docker-stars-url]: https://hub.docker.com/r/whyour/qinglong
[docker-image-size-image]: https://img.shields.io/docker/image-size/whyour/qinglong?style=flat
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
[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>
[![](https://user-images.githubusercontent.com/22700758/229290661-03aabe84-8780-4ef0-8e75-2146f4636130.jpeg)](https://whyour.cn)
简体中文 | [English](./README-en.md)
![cover](https://user-images.githubusercontent.com/22700758/244847235-8dc1ca21-e03f-4606-9458-0541fab60413.png)
## 功能
@@ -36,104 +37,45 @@
- 支持暗黑模式
- 支持手机端操作
## 部署
## 版本
### 本机部署
### docker
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
```bash
# 待完善,可先参考开发步骤 (windows暂时不支持)
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian
```
### podman 部署
### npm
1. podman 安装
npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node/python3`
```bash
https://podman.io/getting-started/installation
npm i @whyour/qinglong
```
2. 启动容器
## 内置命令
- task
```bash
podman run -dit \
--network bridge \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \
--hostname qinglong \
docker.io/whyour/qinglong:latest
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
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
```
### docker 部署
1. docker 安装
```bash
sudo curl -sSL get.docker.com | sh
```
2. 配置国内镜像源
```bash
mkdir -p /etc/docker
tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": [
"https://0b27f0a81a00f3560fbdc00ddd2f99e0.mirror.swr.myhuaweicloud.com",
"https://ypzju6vq.mirror.aliyuncs.com",
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn"
]
}
EOF
systemctl daemon-reload
systemctl restart docker
```
3. 启动容器
```bash
docker run -dit \
-v $PWD/ql/data:/ql/data \
-p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
-e QlBaseUrl="/" \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose 部署
1. docker-compose 安装
```bash
sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
```
2. 启动容器
```bash
mkdir qinglong
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
3. 访问
打开你的浏览器,访问 http://{ip}:5700
## 使用
1. 内置命令
- ql
```bash
# 更新并重启青龙
@@ -154,35 +96,106 @@ ql check
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
```
2. 参数说明
| **参数** | **说明** |
|------------|---------------------------------------------------------------------------------------------|
| file_url | 脚本地址 |
| repo_url | 仓库地址 |
| whitelist | 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割 |
| blacklist | 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割 |
| dependence | 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割 |
| extensions | 拉取仓库的文件后缀,多个竖线分割 |
| branch | 拉取仓库的分支 |
| days | 需要保留的日志的天数 |
| file_path | 任务执行时的文件路径 |
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
* extensions: 拉取仓库的文件后缀,多个竖线分割
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 部署
### docker (推荐)
```bash
# curl -sSL get.docker.com | sh
docker run -dit \
-v $PWD/ql/data:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \
# 部署路径非必须,比如 /test
-e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
whyour/qinglong:latest
```
### docker-compose (推荐)
```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
wget https://raw.githubusercontent.com/whyour/qinglong/master/docker/docker-compose.yml
# 启动
docker-compose up -d
# 停止
docker-compose down
```
### podman (推荐)
```bash
# 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
```
### 本机
建议使用纯净系统安装,避免系统原有数据丢失,需要自己安装 node/npm/python3/pip3
```bash
# Debian/Ubuntu
curl -sL https://deb.nodesource.com/setup_20.x | sudo -E bash -
# Centos
curl --silent --location https://rpm.nodesource.com/setup_20.x | sudo 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
```
## 开发
```bash
$ git clone https://github.com/whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm@8.3.1
$ pnpm install
$ pnpm start
```
打开你的浏览器,访问 <http://127.0.0.1:5700>
## 链接
@@ -194,24 +207,6 @@ task -l <file_path>
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
## 开发
```bash
$ git clone git@github.com:whyour/qinglong.git
$ cd qinglong
$ cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
$ npm install -g pnpm@8.3.1
$ 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)之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
+38 -20
View File
@@ -3,20 +3,36 @@ import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import { celebrate, Joi } from 'celebrate';
import { join } from 'path';
import { SAMPLE_FILES } from '../config/const';
import ConfigService from '../services/config';
const route = Router();
export default (app: Router) => {
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(
'/files',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const fileList = fs.readdirSync(config.configPath, 'utf-8');
const fileList = await fs.readdir(config.configPath, 'utf-8');
res.send({
code: 200,
data: fileList
@@ -32,24 +48,11 @@ export default (app: Router) => {
);
route.get(
'/:file',
'/detail',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let content = '';
if (config.blackFileList.includes(req.params.file)) {
res.send({ code: 403, message: '文件无法访问' });
}
if (req.params.file.includes('sample')) {
content = getFileContentByName(
join(config.samplePath, req.params.file),
);
} else {
content = getFileContentByName(
join(config.configPath, req.params.file),
);
}
res.send({ code: 200, data: content });
const configService = Container.get(ConfigService);
await configService.getFile(req.query.path as string, res);
} catch (e) {
return next(e);
}
@@ -71,12 +74,27 @@ export default (app: Router) => {
if (config.blackFileList.includes(name)) {
res.send({ code: 403, message: '文件无法访问' });
}
const path = join(config.configPath, name);
fs.writeFileSync(path, content);
let path = join(config.configPath, name);
if (name.startsWith('data/scripts/')) {
path = join(config.rootPath, name);
}
await fs.writeFile(path, content);
res.send({ code: 200, message: '保存成功' });
} catch (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);
}
},
);
};
+8 -3
View File
@@ -176,6 +176,9 @@ export default (app: Router) => {
name: Joi.string().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) => {
@@ -333,6 +336,9 @@ export default (app: Router) => {
schedule: Joi.string().required(),
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(),
}),
}),
@@ -452,13 +458,12 @@ export default (app: Router) => {
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const cronService = Container.get(CronService);
const data = await cronService.status({
...req.body,
status: parseInt(req.body.status),
pid: parseInt(req.body.pid) || '',
status: req.body.status ? parseInt(req.body.status) : undefined,
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
});
return res.send({ code: 200, data });
} catch (e) {
+2 -1
View File
@@ -6,6 +6,7 @@ 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 storage = multer.diskStorage({
@@ -200,7 +201,7 @@ export default (app: Router) => {
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent);
const parseContent = safeJSONParse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
+25 -11
View File
@@ -3,7 +3,7 @@ import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import config from '../config';
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { getFileContentByName, readDirs, rmPath } from '../config/util';
import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router();
@@ -15,7 +15,7 @@ export default (app: Router) => {
route.get('/', async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const result = readDirs(config.logPath, config.logPath, blacklist);
const result = await readDirs(config.logPath, config.logPath, blacklist);
res.send({
code: 200,
data: result,
@@ -26,10 +26,29 @@ 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(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (blacklist.includes(req.path)) {
return res.send({ code: 403, message: '暂无权限' });
@@ -39,7 +58,7 @@ export default (app: Router) => {
(req.query.path || '') as string,
req.params.file,
);
const content = getFileContentByName(filePath);
const content = await getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
@@ -53,7 +72,7 @@ export default (app: Router) => {
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
type: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -64,16 +83,11 @@ export default (app: Router) => {
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
};
+56 -24
View File
@@ -4,13 +4,13 @@ import {
readDirs,
getLastModifyFilePath,
readDir,
emptyDir,
rmPath,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import { celebrate, Joi } from 'celebrate';
import path, { join, parse } from 'path';
import ScriptService from '../services/script';
@@ -34,15 +34,33 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger');
try {
let result = [];
const blacklist = ['node_modules', '.git'];
const blacklist = [
'node_modules',
'.git',
'.pnpm',
'pnpm-lock.yaml',
'yarn.lock',
'package-lock.json',
];
if (req.query.path) {
const targetPath = path.join(
config.scriptPath,
req.query.path as string,
);
result = readDir(targetPath, config.scriptPath, blacklist);
result = await readDir(targetPath, config.scriptPath, blacklist);
} else {
result = readDirs(config.scriptPath, config.scriptPath, blacklist);
result = await readDirs(
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({
code: 200,
@@ -54,17 +72,31 @@ 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(
'/:file',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const filePath = join(
config.scriptPath,
const scriptService = Container.get(ScriptService);
const content = await scriptService.getFile(
req.query.path as string,
req.params.file,
);
const content = getFileContentByName(filePath);
res.send({ code: 200, data: content });
} catch (e) {
return next(e);
@@ -104,12 +136,12 @@ export default (app: Router) => {
}
if (req.file) {
fs.renameSync(req.file.path, join(path, req.file.filename));
await fs.rename(req.file.path, join(path, req.file.filename));
return res.send({ code: 200 });
}
if (directory) {
fs.mkdirSync(join(path, directory), { recursive: true });
await fs.mkdir(join(path, directory), { recursive: true });
return res.send({ code: 200 });
}
@@ -121,16 +153,17 @@ export default (app: Router) => {
`${originFilename.replace(/\//g, '')}`,
);
const filePath = join(path, `${filename.replace(/\//g, '')}`);
if (fs.existsSync(originFilePath)) {
fs.copyFileSync(
const fileExists = await fileExist(filePath);
if (fileExists) {
await fs.copyFile(
originFilePath,
join(config.bakPath, originFilename.replace(/\//g, '')),
);
if (filename !== originFilename) {
fs.unlinkSync(originFilePath);
await rmPath(originFilePath);
}
}
fs.writeFileSync(filePath, content);
await fs.writeFile(filePath, content);
return res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -156,7 +189,7 @@ export default (app: Router) => {
path: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.writeFileSync(filePath, content);
await fs.writeFile(filePath, content);
return res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -182,11 +215,7 @@ export default (app: Router) => {
type: string;
};
const filePath = join(config.scriptPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
res.send({ code: 200 });
} catch (e) {
return next(e);
@@ -239,7 +268,7 @@ export default (app: Router) => {
let { filename, content, path } = req.body;
const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
fs.writeFileSync(filePath, content || '', { encoding: 'utf8' });
await fs.writeFile(filePath, content || '', { encoding: 'utf8' });
const scriptService = Container.get(ScriptService);
const result = await scriptService.runScript(filePath);
@@ -260,14 +289,17 @@ export default (app: Router) => {
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path, pid } = req.body;
const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const logPath = join(config.logPath, path, `${name}.swap`);
const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath, pid);
setTimeout(() => {
rmPath(logPath);
}, 3000);
res.send(result);
} catch (e) {
return next(e);
@@ -294,7 +326,7 @@ export default (app: Router) => {
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
await fs.rename(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
+5 -1
View File
@@ -210,12 +210,16 @@ export default (app: Router) => {
'/',
celebrate({
body: Joi.array().items(Joi.number().required()),
query: Joi.object({
force: Joi.boolean().optional(),
t: Joi.number()
})
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const subscriptionService = Container.get(SubscriptionService);
const data = await subscriptionService.remove(req.body);
const data = await subscriptionService.remove(req.body, req.query);
return res.send({ code: 200, data });
} catch (e) {
return next(e);
+171 -35
View File
@@ -1,12 +1,11 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
import { Logger } from 'winston';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import config from '../config';
import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
import {
getUniqPath,
handleLogPath,
@@ -14,8 +13,18 @@ import {
promiseExec,
} from '../config/util';
import dayjs from 'dayjs';
import multer from 'multer';
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) => {
app.use('/system', route);
@@ -25,30 +34,14 @@ export default (app: Router) => {
try {
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
const { version, changeLog, changeLogLink } = await parseVersion(
config.versionFile,
);
const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
)
).replace('\n', '');
const lastCommitId = (
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
).replace('\n', '');
const branch = (
await promiseExec(
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
)
).replace('\n', '');
const { version, changeLog, changeLogLink, publishTime } =
await parseVersion(config.versionFile);
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
authInfo.password === 'admin'
) {
isInitialized = false;
}
@@ -57,9 +50,8 @@ export default (app: Router) => {
data: {
isInitialized,
version,
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
publishTime: dayjs(publishTime).unix(),
branch: process.env.QL_BRANCH || 'master',
changeLog,
changeLogLink,
},
@@ -71,12 +63,12 @@ export default (app: Router) => {
});
route.get(
'/log/remove',
'/config',
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const systemService = Container.get(SystemService);
const data = await systemService.getLogRemoveFrequency();
const data = await systemService.getSystemConfig();
res.send({ code: 200, data });
} catch (e) {
return next(e);
@@ -85,19 +77,16 @@ export default (app: Router) => {
);
route.put(
'/log/remove',
'/config/log-remove-frequency',
celebrate({
body: Joi.object({
frequency: Joi.number().required(),
logRemoveFrequency: Joi.number().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.updateLogRemoveFrequency(
req.body.frequency,
);
const result = await systemService.updateLogRemoveFrequency(req.body);
res.send(result);
} catch (e) {
return next(e);
@@ -105,6 +94,96 @@ 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(
'/update-check',
async (req: Request, res: Response, next: NextFunction) => {
@@ -133,6 +212,25 @@ 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(
'/notify',
celebrate({
@@ -163,7 +261,10 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
const uniqPath = await getUniqPath(req.body.command);
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');
@@ -179,12 +280,12 @@ export default (app: Router) => {
onError: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
res.write(`\n${message}`);
const absolutePath = await handleLogPath(logPath);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
},
);
@@ -212,4 +313,39 @@ export default (app: Router) => {
}
},
);
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,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import path from 'path';
import { v4 as uuidV4 } from 'uuid';
import rateLimit from 'express-rate-limit';
import config from '../config';
const route = Router();
@@ -26,6 +27,10 @@ export default (app: Router) => {
route.post(
'/login',
rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
}),
celebrate({
body: Joi.object({
username: Joi.string().required(),
@@ -69,6 +74,9 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
if (process.env.DeployEnv === 'demo') {
return res.send({ code: 450, message: '未知错误' });
}
const userService = Container.get(UserService);
await userService.updateUsernameAndPassword(req.body);
res.send({ code: 200, message: '更新成功' });
+2 -1
View File
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config';
import express from 'express';
import Logger from './loaders/logger';
import path from 'path';
async function startServer() {
const app = express();
@@ -18,10 +17,12 @@ async function startServer() {
const server = app
.listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`);
console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
+18
View File
@@ -5,3 +5,21 @@ 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',
},
];
+14 -1
View File
@@ -1,6 +1,6 @@
import dotenv from 'dotenv';
import path from 'path';
import { createRandomString } from './util';
import { createRandomString } from './share';
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
@@ -20,6 +20,8 @@ const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
const dataPath = path.join(rootPath, 'data/');
const shellPath = path.join(rootPath, 'shell/');
const tmpPath = path.join(rootPath, '.tmp/');
const samplePath = path.join(rootPath, 'sample/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
@@ -28,6 +30,7 @@ const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/');
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 confFile = path.join(configPath, 'config.sh');
@@ -42,6 +45,9 @@ const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'version.yaml');
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) {
throw new Error("⚠️ Couldn't find .env file ⚠️");
@@ -59,6 +65,11 @@ export default {
prefix: '/api',
},
rootPath,
tmpPath,
dataPath,
dataTgzFile,
shareShellFile,
dependenceProxyFile,
configString,
loginFaild,
authError,
@@ -80,6 +91,7 @@ export default {
'config.sh.sample',
'cookie.sh',
'crontab.list',
'dependence-proxy.sh',
'env.sh',
'token.json',
],
@@ -102,4 +114,5 @@ export default {
lastVersionFile,
sqliteFile,
sshdPath,
systemLogPath,
};
+10 -2
View File
@@ -1,11 +1,19 @@
import { Request, Response } from 'express';
import { pick } from 'lodash';
import pick from 'lodash/pick';
let pickedEnv: Record<string, string>;
function getPickedEnv() {
if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl']);
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;
}
+84
View File
@@ -0,0 +1,84 @@
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('');
}
+4 -2
View File
@@ -34,8 +34,10 @@ export function formatCommand(doc: Subscription, url?: string) {
if (type === 'file') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${isNil(autoAddCron) ? true : Boolean(autoAddCron)
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}" "${
isNil(autoAddCron) ? true : Boolean(autoAddCron)
}" "${isNil(autoDelCron) ? true : Boolean(autoDelCron)}"`;
}
return command;
+140 -175
View File
@@ -1,4 +1,4 @@
import * as fs from 'fs';
import * as fs from 'fs/promises';
import * as path from 'path';
import got from 'got';
import iconv from 'iconv-lite';
@@ -9,23 +9,28 @@ import { promisify } from 'util';
import { load } from 'js-yaml';
import config from './index';
import { TASK_COMMAND } from './const';
import Logger from '../loaders/logger';
export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf8');
export * from './share';
export async function getFileContentByName(fileName: string) {
const _exsit = await fileExist(fileName);
if (_exsit) {
return await fs.readFile(fileName, 'utf8');
}
return '';
}
export function getLastModifyFilePath(dir: string) {
export async function getLastModifyFilePath(dir: string) {
let filePath = '';
if (fs.existsSync(dir)) {
const arr = fs.readdirSync(dir);
const _exsit = await fileExist(dir);
if (_exsit) {
const arr = await fs.readdir(dir);
arr.forEach((item) => {
arr.forEach(async (item) => {
const fullpath = path.join(dir, item);
const stats = fs.statSync(fullpath);
const stats = await fs.stat(fullpath);
if (stats.isFile()) {
if (stats.mtimeMs >= 0) {
filePath = fullpath;
@@ -36,91 +41,6 @@ export function getLastModifyFilePath(dir: string) {
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) {
const { authorization = '' } = req.headers;
if (authorization && authorization.split(' ')[0] === 'Bearer') {
@@ -141,7 +61,8 @@ export async function getNetIp(req: any) {
...req.ips,
req.socket.remoteAddress,
]),
];
].filter(Boolean);
let ip = ipArray[0];
if (ipArray.length > 1) {
@@ -162,34 +83,42 @@ export async function getNetIp(req: any) {
break;
}
}
ip = ip.substr(ip.lastIndexOf(':') + 1, ip.length);
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = '';
}
if (!ip) {
return { address: `获取失败`, ip };
}
try {
const baiduApi = got
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
const csdnApi = got
.get(`https://searchplugin.csdn.net/api/v1/ip/get?ip=${ip}`, {
timeout: 10000,
retry: 0,
})
.text();
const ipApi = got
const pconlineApi = got
.get(`https://whois.pconline.com.cn/ipJson.jsp?ip=${ip}&json=true`, {
timeout: 10000,
retry: 0,
})
.buffer();
const [data, ipApiBody] = await await Promise.all<any>([baiduApi, ipApi]);
const ipRegx = /.*IP :(.*)\n/;
const addrRegx = /.*数据二 :(.*)\n/;
if (data && ipRegx.test(data) && addrRegx.test(data)) {
const ip = data.match(ipRegx)[1];
const addr = data.match(addrRegx)[1];
return { address: addr, ip };
} else if (ipApiBody) {
const { addr, ip } = JSON.parse(iconv.decode(ipApiBody, 'GBK'));
return { address: `${addr}`, ip };
} else {
return { address: `获取失败`, ip };
const [csdnBody, pconlineBody] = await await Promise.all<any>([
csdnApi,
pconlineApi,
]);
const csdnRes = JSON.parse(csdnBody);
const pconlineRes = JSON.parse(iconv.decode(pconlineBody, 'GBK'));
let address = '';
if (csdnBody && csdnRes.code == 200) {
address = csdnRes.data.address;
} else if (pconlineRes && pconlineRes.addr) {
address = pconlineRes.addr;
}
return { address, ip };
} catch (error) {
return { address: `获取失败`, ip };
}
@@ -229,22 +158,17 @@ export function getPlatform(userAgent: string): 'mobile' | 'desktop' {
}
export async function fileExist(file: any) {
return new Promise((resolve) => {
try {
fs.accessSync(file);
resolve(true);
await fs.access(file);
return true;
} catch (error) {
resolve(false);
return false;
}
});
}
export async function createFile(file: string, data: string = '') {
return new Promise((resolve) => {
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, data);
resolve(true);
});
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, data);
}
export async function handleLogPath(
@@ -307,61 +231,76 @@ interface IFile {
type: 'directory' | 'file';
parent: string;
mtime: number;
size?: number;
children?: IFile[];
}
export function dirSort(a: IFile, b: IFile) {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
export function 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 function readDirs(
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 = fs.readdirSync(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
const files = await fs.readdir(dir);
const result: IFile[] = [];
for (const file of files) {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const stats = await fs.stat(subPath);
const key = path.join(relativePath, file);
if (blacklist.includes(file) || stats.isSymbolicLink()) {
continue;
}
if (stats.isDirectory()) {
return {
const children = await readDirs(subPath, baseDir, blacklist, sort);
result.push({
title: file,
key,
type: 'directory',
parent: relativePath,
mtime: stats.mtime.getTime(),
children: readDirs(subPath, baseDir).sort(dirSort),
};
}
return {
children: children.sort(sort),
});
} else {
result.push({
title: file,
type: 'file',
isLeaf: true,
key,
parent: relativePath,
size: stats.size,
mtime: stats.mtime.getTime(),
};
});
return result.sort(dirSort);
}
}
export function readDir(
return result.sort(sort);
}
export async function readDir(
dir: string,
baseDir: string = '',
blacklist: string[] = [],
) {
const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir);
const files = await fs.readdir(dir);
const result: any = files
.filter((x) => !blacklist.includes(x))
.map((file: string) => {
.map(async (file: string) => {
const subPath = path.join(dir, file);
const stats = fs.statSync(subPath);
const stats = await fs.stat(subPath);
const key = path.join(relativePath, file);
return {
title: file,
@@ -373,30 +312,28 @@ export function readDir(
return result;
}
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
export async function promiseExec(command: string): Promise<string> {
try {
const { stderr, stdout } = await promisify(exec)(command, {
maxBuffer: 200 * 1024 * 1024,
encoding: 'utf8',
});
fs.rmdirSync(path);
return stdout || stderr;
} catch (error) {
return JSON.stringify(error);
}
}
export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
export 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) {
@@ -428,9 +365,12 @@ export function parseBody(
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
| 'application/x-www-form-urlencoded'
| 'text/plain',
) {
if (!body) return '';
if (contentType === 'text/plain' || !body) {
return body;
}
const parsed: any = {};
let key;
@@ -440,7 +380,7 @@ export function parseBody(
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
key = line.substring(0, i).trim();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
@@ -483,11 +423,11 @@ export function psTree(pid: number): Promise<number[]> {
export async function killTask(pid: number) {
const pids = await psTree(pid);
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
if (pids.length) {
try {
[pid, ...pids].forEach((x) => {
process.kill(x, 2);
[pid, ...pids].reverse().forEach((x) => {
process.kill(x, 15);
});
} catch (error) {}
} else {
@@ -505,21 +445,22 @@ interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
publishTime: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as 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): Promise<string> {
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);
if (/^\d\d*\d$/.test(id)) {
export async function getUniqPath(
command: string,
id: string,
): Promise<string> {
if (/^\d+$/.test(id)) {
id = `_${id}`;
} else {
id = '';
@@ -551,3 +492,27 @@ export async function getUniqPath(command: string): Promise<string> {
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
@@ -1,38 +0,0 @@
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,
},
});
+14 -5
View File
@@ -18,10 +18,13 @@ export class Crontab {
last_running_time?: number;
last_execution_time?: number;
sub_id?: number;
extra_schedules?: Array<{ schedule: string }>;
task_before?: string;
task_after?: string;
constructor(options: Crontab) {
this.name = options.name;
this.command = options.command;
this.command = options.command.trim();
this.schedule = options.schedule;
this.saved = options.saved;
this.id = options.id;
@@ -39,17 +42,20 @@ export class Crontab {
this.last_running_time = options.last_running_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 {
'running',
'idle',
'running' = 0,
'queued' = 0.5,
'idle' = 1,
'disabled',
'queued',
}
interface CronInstance extends Model<Crontab, Crontab>, Crontab {}
export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: {
unique: 'compositeIndex',
@@ -75,4 +81,7 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
last_running_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,
});
+1 -1
View File
@@ -39,7 +39,7 @@ export class CrontabView {
}
}
interface CronViewInstance
export interface CronViewInstance
extends Model<CrontabView, CrontabView>,
CrontabView {}
export const CrontabViewModel = sequelize.define<CronViewInstance>(
+21 -9
View File
@@ -4,9 +4,9 @@ import { DataTypes, Model, ModelDefined } from 'sequelize';
export class Dependence {
timestamp?: string;
id?: number;
status?: DependenceStatus;
type?: DependenceTypes;
name?: number;
status: DependenceStatus;
type: DependenceTypes;
name: string;
log?: string[];
remark?: string;
@@ -15,10 +15,10 @@ export class Dependence {
this.status =
typeof options.status === 'number' && DependenceStatus[options.status]
? options.status
: DependenceStatus.installing;
: DependenceStatus.queued;
this.type = options.type || DependenceTypes.nodejs;
this.timestamp = new Date().toString();
this.name = options.name;
this.name = options.name.trim();
this.log = options.log || [];
this.remark = options.remark || '';
}
@@ -42,17 +42,29 @@ export enum DependenceTypes {
export enum InstallDependenceCommandTypes {
'pnpm add -g',
'pip3 install',
'apk add',
'pip3 install --disable-pip-version-check --root-user-action=ignore',
'apk add --no-check-certificate',
}
export enum GetDependenceCommandTypes {
'pnpm ls -g ',
'pip3 show --disable-pip-version-check',
'apk info -es',
}
export enum versionDependenceCommandTypes {
'@',
'==',
'=',
}
export enum unInstallDependenceCommandTypes {
'pnpm remove -g',
'pip3 uninstall -y',
'pip3 uninstall --disable-pip-version-check --root-user-action=ignore -y',
'apk del',
}
interface DependenceInstance
export interface DependenceInstance
extends Model<Dependence, Dependence>,
Dependence {}
export const DependenceModel = sequelize.define<DependenceInstance>(
+1 -1
View File
@@ -34,7 +34,7 @@ export const initPosition = 4500000000000000;
export const stepPosition = 10000000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {}
export interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', {
value: { type: DataTypes.STRING, unique: 'compositeIndex' },
timestamp: DataTypes.STRING,
+20 -1
View File
@@ -15,8 +15,10 @@ export enum NotificationMode {
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
'pushMe' = 'pushMe',
'feishu' = 'feishu',
'webhook' = 'webhook',
'chronocat' = 'Chronocat',
}
abstract class NotificationBaseInfo {
@@ -54,6 +56,8 @@ export class BarkNotification extends NotificationBaseInfo {
public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = '';
public barkGroup = 'qinglong';
public barkLevel = 'active';
public barkUrl = '';
}
export class TelegramBotNotification extends NotificationBaseInfo {
@@ -62,7 +66,7 @@ export class TelegramBotNotification extends NotificationBaseInfo {
public telegramBotProxyHost = '';
public telegramBotProxyPort = '';
public telegramBotProxyAuth = '';
public telegramBotApiHost = 'api.telegram.org';
public telegramBotApiHost = 'https://api.telegram.org';
}
export class DingtalkBotNotification extends NotificationBaseInfo {
@@ -72,10 +76,12 @@ export class DingtalkBotNotification extends NotificationBaseInfo {
export class WeWorkBotNotification extends NotificationBaseInfo {
public weWorkBotKey = '';
public weWorkOrigin = '';
}
export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
public weWorkOrigin = '';
}
export class AibotkNotification extends NotificationBaseInfo {
@@ -99,6 +105,16 @@ export class EmailNotification extends NotificationBaseInfo {
public emailPass: string = '';
}
export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: 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 = '';
@@ -129,5 +145,8 @@ export interface NotificationInfo
IGotNotification,
PushPlusNotification,
EmailNotification,
PushMeNotification,
WebhookNotification,
ChronocatNotification,
LarkNotification {}
+1 -8
View File
@@ -26,14 +26,7 @@ export interface AppToken {
export type AppScope = 'envs' | 'crons' | 'configs' | 'scripts' | 'logs';
export enum CrontabStatus {
'running',
'idle',
'disabled',
'queued',
}
interface AppInstance extends Model<App, App>, App {}
export interface AppInstance extends Model<App, App>, App {}
export const AppModel = sequelize.define<AppInstance>('App', {
name: { type: DataTypes.STRING, unique: 'name' },
scopes: DataTypes.JSON,
+4 -1
View File
@@ -16,4 +16,7 @@ export type SockMessageType =
| 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd';
| 'runSubscriptionEnd'
| 'reloadSystem'
| 'updateNodeMirror'
| 'updateLinuxMirror';
+1 -1
View File
@@ -70,7 +70,7 @@ export enum SubscriptionStatus {
'queued',
}
interface SubscriptionInstance
export interface SubscriptionInstance
extends Model<Subscription, Subscription>,
Subscription {}
export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
+61
View File
@@ -0,0 +1,61 @@
import { sequelize } from '.';
import { DataTypes, Model, ModelDefined } from 'sequelize';
import { NotificationInfo } from './notify';
export class AuthInfo {
ip?: string;
type: AuthDataType;
info?: SystemModelInfo;
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',
'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<AuthInfo, AuthInfo>, AuthInfo { }
export const SystemModel = sequelize.define<SystemInstance>('Auth', {
ip: DataTypes.STRING,
type: DataTypes.STRING,
info: {
type: DataTypes.JSON,
allowNull: true,
},
});
+2
View File
@@ -3,3 +3,5 @@ declare namespace Express {
platform: 'desktop' | 'mobile';
}
}
declare module 'pstree.remy';
+12 -2
View File
@@ -7,18 +7,28 @@ import linkDeps from './deps';
import initTask from './initTask';
export default async ({ expressApp }: { expressApp: Application }) => {
await depInjectorLoader();
try {
depInjectorLoader();
Logger.info('✌️ Dependency Injector loaded');
console.log('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp });
expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded');
console.log('✌️ Express loaded');
await initData();
Logger.info('✌️ init data loaded');
console.log('✌️ init data loaded');
await linkDeps();
Logger.info('✌️ link deps loaded');
console.log('✌️ link deps loaded');
initTask();
Logger.info('✌️ init task loaded');
console.log('✌️ init task loaded');
} catch (error) {
Logger.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed, ${error}`);
console.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed ${error}`);
}
};
+16 -6
View File
@@ -5,7 +5,7 @@ import { EnvModel } from '../data/env';
import { CrontabModel } from '../data/cron';
import { DependenceModel } from '../data/dependence';
import { AppModel } from '../data/open';
import { AuthModel } from '../data/auth';
import { SystemModel } from '../data/system';
import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView';
@@ -17,7 +17,7 @@ export default async () => {
await CrontabModel.sync();
await DependenceModel.sync();
await AppModel.sync();
await AuthModel.sync();
await SystemModel.sync();
await EnvModel.sync();
await SubscriptionModel.sync();
await CrontabViewModel.sync();
@@ -49,6 +49,15 @@ export default async () => {
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 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
@@ -66,7 +75,7 @@ export default async () => {
const dependenceCount = await DependenceModel.count();
const envCount = await EnvModel.count();
const appCount = await AppModel.count();
const authCount = await AuthModel.count();
const authCount = await SystemModel.count();
if (crondbExist && cronCount === 0) {
const cronDb = new DataStore({
filename: cronDbFile,
@@ -118,13 +127,14 @@ export default async () => {
});
authDb.persistence.compactDatafile();
authDb.find({}).exec(async (err, docs) => {
await AuthModel.bulkCreate(docs, { ignoreDuplicates: true });
await SystemModel.bulkCreate(docs, { ignoreDuplicates: true });
});
}
console.log('✌️ DB loaded');
Logger.info('✌️ DB loaded');
} catch (error) {
Logger.info('✌️ DB load failed');
Logger.info(error);
console.error('✌️ DB load failed');
Logger.error(error);
}
};
+11 -14
View File
@@ -1,20 +1,19 @@
import path from 'path';
import fs from 'fs';
import fs from 'fs/promises';
import chokidar from 'chokidar';
import config from '../config/index';
import { promiseExec } from '../config/util';
import { fileExist, promiseExec, rmPath } from '../config/util';
function linkToNodeModule(src: string, dst?: string) {
async function linkToNodeModule(src: string, dst?: string) {
const target = path.join(config.rootPath, 'node_modules', dst || src);
const source = path.join(config.rootPath, src);
fs.lstat(target, (err, stat) => {
if (!stat) {
fs.symlink(source, target, 'dir', (err) => {
if (err) throw err;
});
try {
const stats = await fs.lstat(target);
if (!stats) {
await fs.symlink(source, target, 'dir');
}
});
} catch (error) {}
}
async function linkCommand() {
@@ -34,16 +33,14 @@ async function linkCommand() {
for (const link of linkShell) {
const source = path.join(config.rootPath, 'shell', link.src);
const target = path.join(commandDir, link.dest);
if (fs.existsSync(target)) {
fs.unlinkSync(target);
}
fs.symlink(source, target, (err) => {});
await rmPath(target);
await fs.symlink(source, target);
}
}
export default async (src: string = 'deps') => {
await linkCommand();
linkToNodeModule(src);
await linkToNodeModule(src);
const source = path.join(config.rootPath, src);
const watcher = chokidar.watch(source, {
+10 -24
View File
@@ -4,22 +4,21 @@ import cors from 'cors';
import routes from '../api';
import config from '../config';
import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs';
import { getPlatform, getToken } from '../config/util';
import fs from 'fs/promises';
import { getPlatform, getToken, safeJSONParse } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
import rewrite from 'express-urlrewrite';
import UserService from '../services/user';
import handler from 'serve-handler';
import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env';
import { errors } from 'celebrate';
import path from 'path';
import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv';
import Logger from './logger';
export default ({ app }: { app: Application }) => {
app.enable('trust proxy');
app.set('trust proxy', 'loopback');
app.use(cors());
app.get(`${config.api.prefix}/env.js`, serveEnv);
app.use(`${config.api.prefix}/static`, express.static(config.uploadPath));
@@ -27,9 +26,10 @@ export default ({ app }: { app: Application }) => {
app.use(
'/api/public',
createProxyMiddleware({
target: `http://localhost:${config.publicPort}/api`,
target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true,
pathRewrite: { '/api/public': '' },
logProvider: () => Logger,
}),
);
@@ -38,7 +38,7 @@ export default ({ app }: { app: Application }) => {
app.use(
jwt({
secret: config.secret as string,
secret: config.secret,
algorithms: ['HS384'],
}).unless({
path: [...config.apiWhiteList, /^\/open\//],
@@ -83,9 +83,9 @@ export default ({ app }: { app: Application }) => {
return next();
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const data = await fs.readFile(config.authConfigFile, 'utf8');
if (data && headerToken) {
const { token = '', tokens = {} } = JSON.parse(data);
const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[req.platform] === headerToken) {
return next();
}
@@ -105,14 +105,12 @@ export default ({ app }: { app: Application }) => {
}
const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count();
let isInitialized = true;
if (
Object.keys(authInfo).length === 2 &&
authInfo.username === 'admin' &&
authInfo.password === 'admin' &&
envCount === 0
authInfo.password === 'admin'
) {
isInitialized = false;
}
@@ -173,18 +171,6 @@ export default ({ app }: { app: Application }) => {
},
);
app.use(
Sentry.Handlers.errorHandler({
shouldHandleError(error) {
// 排除 SequelizeUniqueConstraintError / NotFound
return (
!['SequelizeUniqueConstraintError'].includes(error.name) ||
!['Not Found'].includes(error.message)
);
},
}),
);
app.use(
(
err: Error & { status: number },
+51 -17
View File
@@ -9,11 +9,58 @@ import { Op } from 'sequelize';
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 () => {
const cronService = Container.get(CronService);
const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService);
const installDependencies = () => {
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({
where: {},
order: [
['type', 'DESC'],
['createdAt', 'DESC'],
],
raw: true,
}).then(async (docs) => {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
setTimeout(() => {
dependenceService.installDependenceOneByOne(docs);
}, 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({
@@ -30,23 +77,7 @@ export default async () => {
});
// 初始化更新所有任务状态为空闲
await CrontabModel.update(
{ status: CrontabStatus.idle },
{ where: { status: [CrontabStatus.running, CrontabStatus.queued] } },
);
// 初始化时安装所有处于安装中,安装成功,安装失败的依赖
DependenceModel.findAll({
where: {},
order: [['type', 'DESC']],
raw: true,
}).then(async (docs) => {
await DependenceModel.update(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: docs.map((x) => x.id!) } },
);
dependenceService.installDependenceOneByOne(docs);
});
await CrontabModel.update({ status: CrontabStatus.idle }, { where: {} });
// 初始化时执行一次所有的 ql repo 任务
CrontabModel.findAll({
@@ -127,4 +158,7 @@ export default async () => {
// 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab();
await envService.set_envs();
// 初始化增加系统配置
await SystemModel.upsert({ type: AuthDataType.systemConfig });
};
+63 -19
View File
@@ -1,11 +1,10 @@
import fs from 'fs';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import dotenv from 'dotenv';
import Logger from './logger';
import { fileExist } from '../config/util';
const rootPath = process.env.QL_DIR as string;;
const rootPath = process.env.QL_DIR as string;
const dataPath = path.join(rootPath, 'data/');
const configPath = path.join(dataPath, 'config/');
const scriptPath = path.join(dataPath, 'scripts/');
@@ -13,13 +12,22 @@ const logPath = path.join(dataPath, 'log/');
const uploadPath = path.join(dataPath, 'upload/');
const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const 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 sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d');
const systemLogPath = path.join(dataPath, 'syslog');
export default async () => {
const authFileExist = await fileExist(authConfigFile);
@@ -31,44 +39,80 @@ export default async () => {
const sshDirExist = await fileExist(sshPath);
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) {
fs.mkdirSync(configPath);
}
if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
}
if (!confFileExist) {
fs.writeFileSync(confFile, fs.readFileSync(sampleConfigFile));
await fs.mkdir(configPath);
}
if (!scriptDirExist) {
fs.mkdirSync(scriptPath);
await fs.mkdir(scriptPath);
}
if (!logDirExist) {
fs.mkdirSync(logPath);
await fs.mkdir(logPath);
}
if (!tmpDirExist) {
await fs.mkdir(tmpPath);
}
if (!uploadDirExist) {
fs.mkdirSync(uploadPath);
await fs.mkdir(uploadPath);
}
if (!sshDirExist) {
fs.mkdirSync(sshPath);
await fs.mkdir(sshPath);
}
if (!bakDirExist) {
fs.mkdirSync(bakPath);
await fs.mkdir(bakPath);
}
if (!sshdDirExist) {
fs.mkdirSync(sshdPath);
await fs.mkdir(sshdPath);
}
dotenv.config({ path: confFile });
if (!systemLogDirExist) {
await fs.mkdir(systemLogPath);
}
// 初始化文件
if (!authFileExist) {
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
}
if (!confFileExist) {
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));
}
if (!scriptNotifyJsFileExist) {
await fs.writeFile(
scriptNotifyJsFile,
await fs.readFile(sampleNotifyJsFile),
);
}
if (!scriptNotifyPyFileExist) {
await fs.writeFile(
scriptNotifyPyFile,
await fs.readFile(sampleNotifyPyFile),
);
}
if (!TaskBeforeFileExist) {
await fs.writeFile(TaskBeforeFile, await fs.readFile(sampleTaskShellFile));
}
if (!TaskAfterFileExist) {
await fs.writeFile(TaskAfterFile, await fs.readFile(sampleTaskShellFile));
}
Logger.info('✌️ Init file down');
console.log('✌️ Init file down');
};
+7 -7
View File
@@ -1,6 +1,6 @@
import { Container } from 'typedi';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import ScheduleService, { ScheduleTaskType } from '../services/schedule';
import SubscriptionService from '../services/subscription';
import config from '../config';
import { fileExist } from '../config/util';
@@ -22,23 +22,23 @@ export default async () => {
id: NaN,
name: '生成token',
command: tokenCommand,
};
} as ScheduleTaskType;
await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, {
days: 28,
});
// 运行删除日志任务
const data = await systemService.getLogRemoveFrequency();
if (data && data.info && data.info.frequency) {
const data = await systemService.getSystemConfig();
if (data && data.info && data.info.logRemoveFrequency) {
const rmlogCron = {
id: data.id,
id: data.id as number,
name: '删除日志',
command: `ql rmlog ${data.info.frequency}`,
command: `ql rmlog ${data.info.logRemoveFrequency}`,
};
await scheduleService.cancelIntervalTask(rmlogCron);
scheduleService.createIntervalTask(rmlogCron, {
days: data.info.frequency,
days: data.info.logRemoveFrequency,
});
}
+27 -21
View File
@@ -1,32 +1,38 @@
import winston from 'winston';
import 'winston-daily-rotate-file';
import config from '../config';
import path from 'path';
const transports = [];
if (process.env.NODE_ENV !== 'development') {
transports.push(new winston.transports.Console());
} else {
transports.push(
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat(),
),
}),
);
const levelMap: Record<string, string> = {
info: '🔵',
warn: '🟡',
error: '🔴',
debug: '🔶'
}
const customFormat = winston.format.combine(
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({
level: config.logs.level,
levels: winston.config.npm.levels,
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss',
}),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json(),
),
transports,
transports: [
new winston.transports.DailyRotateFile({
filename: path.join(config.systemLogPath, '%DATE%.log'),
...defaultOptions,
})
],
});
export default LoggerInstance;
+10 -4
View File
@@ -1,6 +1,5 @@
import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
@@ -10,12 +9,18 @@ export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
ignoreErrors: [
/SequelizeUniqueConstraintError/i,
/Validation error/i,
/UnauthorizedError/i,
/celebrate request validation failed/i,
],
dsn: 'https://8b5c84cfef3e22541bc84de0ed00497b@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
new Sentry.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 0.1,
tracesSampleRate: 0.8,
release: version,
});
@@ -23,4 +28,5 @@ export default async ({ expressApp }: { expressApp: Application }) => {
expressApp.use(Sentry.Handlers.tracingHandler());
Logger.info('✌️ Sentry loaded');
console.log('✌️ Sentry loaded');
};
+26 -10
View File
@@ -5,18 +5,34 @@ import Sock from './sock';
export default async ({ server }: { server: Server }) => {
await Sock({ server });
Logger.info('✌️ Sock loaded');
let exitTime = 0;
let timer: NodeJS.Timeout;
process.on('SIGINT', () => {
Logger.info('✌️ Server need close');
server.close(() => {
setTimeout(() => {
process.exit();
}, 10000);
});
setTimeout(() => {
console.log('Forcing server close !!!');
process.on('SIGINT', (singal) => {
Logger.warn(`Server need close, singal ${singal}`);
console.warn(`Server need close, singal ${singal}`);
exitTime++;
if (exitTime >= 3) {
Logger.warn('Forcing server close');
console.warn('Forcing server close');
clearTimeout(timer);
process.exit(1);
}
server.close(() => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
process.exit();
}, 15000);
});
});
process.on('uncaughtException', (error) => {
Logger.error('Uncaught exception:', error);
});
process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled rejection:', reason, promise);
});
};
+5 -6
View File
@@ -1,26 +1,25 @@
import sockJs from 'sockjs';
import { Server } from 'http';
import Logger from './logger';
import { Container } from 'typedi';
import SockService from '../services/sock';
import config from '../config/index';
import fs from 'fs';
import { getPlatform } from '../config/util';
import fs from 'fs/promises';
import { getPlatform, safeJSONParse } from '../config/util';
export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
const sockService = Container.get(SockService);
echo.on('connection', (conn) => {
echo.on('connection', async (conn) => {
if (!conn.headers || !conn.url || !conn.pathname) {
conn.close('404');
}
const data = fs.readFileSync(config.authConfigFile, 'utf8');
const data = await fs.readFile(config.authConfigFile, 'utf8');
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) {
const { token = '', tokens = {} } = JSON.parse(data);
const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[platform] === headerToken) {
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
sockService.addClient(conn);
+4
View File
@@ -7,10 +7,14 @@ service Cron {
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; }
+158 -138
View File
@@ -10,78 +10,161 @@ import {
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
} from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = 'com.ql.cron';
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 AddCronResponse {
}
export interface DeleteCronRequest {
ids: string[];
}
export interface DeleteCronResponse {}
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: '' };
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
}
export const ICron = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== '') {
if (message.id !== "") {
writer.uint32(10).string(message.id);
}
if (message.schedule !== '') {
if (message.schedule !== "") {
writer.uint32(18).string(message.schedule);
}
if (message.command !== '') {
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);
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) {
if (tag !== 10) {
break;
}
message.id = reader.string();
continue;
case 2:
if (tag != 18) {
if (tag !== 18) {
break;
}
message.schedule = reader.string();
continue;
case 3:
if (tag != 26) {
if (tag !== 26) {
break;
}
message.command = reader.string();
continue;
case 4:
if (tag !== 34) {
break;
}
if ((tag & 7) == 4 || tag == 0) {
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);
@@ -91,9 +174,13 @@ export const ICron = {
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) : '',
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) : "",
};
},
@@ -102,6 +189,12 @@ export const ICron = {
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;
},
@@ -111,9 +204,11 @@ export const ICron = {
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.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;
},
};
@@ -123,10 +218,7 @@ function createBaseAddCronRequest(): AddCronRequest {
}
export const AddCronRequest = {
encode(
message: AddCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
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();
}
@@ -134,22 +226,21 @@ export const AddCronRequest = {
},
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
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) {
if (tag !== 10) {
break;
}
message.crons.push(ICron.decode(reader, reader.uint32()));
continue;
}
if ((tag & 7) == 4 || tag == 0) {
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
@@ -158,32 +249,24 @@ export const AddCronRequest = {
},
fromJSON(object: any): AddCronRequest {
return {
crons: Array.isArray(object?.crons)
? object.crons.map((e: any) => ICron.fromJSON(e))
: [],
};
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));
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 {
create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(
object: I,
): AddCronRequest {
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
return message;
@@ -195,23 +278,19 @@ function createBaseAddCronResponse(): AddCronResponse {
}
export const AddCronResponse = {
encode(
_: AddCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
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);
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) {
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
@@ -228,15 +307,11 @@ export const AddCronResponse = {
return obj;
},
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(
base?: I,
): AddCronResponse {
create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(
_: I,
): AddCronResponse {
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
const message = createBaseAddCronResponse();
return message;
},
@@ -247,10 +322,7 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
}
export const DeleteCronRequest = {
encode(
message: DeleteCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
for (const v of message.ids) {
writer.uint32(10).string(v!);
}
@@ -258,22 +330,21 @@ export const DeleteCronRequest = {
},
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
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) {
if (tag !== 10) {
break;
}
message.ids.push(reader.string());
continue;
}
if ((tag & 7) == 4 || tag == 0) {
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
@@ -282,11 +353,7 @@ export const DeleteCronRequest = {
},
fromJSON(object: any): DeleteCronRequest {
return {
ids: Array.isArray(object?.ids)
? object.ids.map((e: any) => String(e))
: [],
};
return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
},
toJSON(message: DeleteCronRequest): unknown {
@@ -299,15 +366,11 @@ export const DeleteCronRequest = {
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
base?: I,
): DeleteCronRequest {
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(
object: I,
): DeleteCronRequest {
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || [];
return message;
@@ -319,23 +382,19 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
}
export const DeleteCronResponse = {
encode(
_: DeleteCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
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);
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) {
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
@@ -352,15 +411,11 @@ export const DeleteCronResponse = {
return obj;
},
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
base?: I,
): DeleteCronResponse {
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(
_: I,
): DeleteCronResponse {
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
const message = createBaseDeleteCronResponse();
return message;
},
@@ -369,25 +424,21 @@ export const DeleteCronResponse = {
export type CronService = typeof CronService;
export const CronService = {
addCron: {
path: '/com.ql.cron.Cron/addCron',
path: "/com.ql.cron.Cron/addCron",
requestStream: false,
responseStream: false,
requestSerialize: (value: AddCronRequest) =>
Buffer.from(AddCronRequest.encode(value).finish()),
requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) =>
Buffer.from(AddCronResponse.encode(value).finish()),
responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
},
delCron: {
path: '/com.ql.cron.Cron/delCron',
path: "/com.ql.cron.Cron/delCron",
requestStream: false,
responseStream: false,
requestSerialize: (value: DeleteCronRequest) =>
Buffer.from(DeleteCronRequest.encode(value).finish()),
requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) =>
Buffer.from(DeleteCronResponse.encode(value).finish()),
responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
},
} as const;
@@ -415,67 +466,36 @@ export interface CronClient extends Client {
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
): ClientUnaryCall;
delCron(
request: DeleteCronRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
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;
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;
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]> }
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;
};
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;
+51 -114
View File
@@ -12,10 +12,10 @@ import {
Metadata,
ServiceError,
UntypedServiceImplementation,
} from '@grpc/grpc-js';
import _m0 from 'protobufjs/minimal';
} from "@grpc/grpc-js";
import _m0 from "protobufjs/minimal";
export const protobufPackage = 'com.ql.health';
export const protobufPackage = "com.ql.health";
export interface HealthCheckRequest {
service: string;
@@ -33,79 +33,71 @@ export enum HealthCheckResponse_ServingStatus {
UNRECOGNIZED = -1,
}
export function healthCheckResponse_ServingStatusFromJSON(
object: any,
): HealthCheckResponse_ServingStatus {
export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {
switch (object) {
case 0:
case 'UNKNOWN':
case "UNKNOWN":
return HealthCheckResponse_ServingStatus.UNKNOWN;
case 1:
case 'SERVING':
case "SERVING":
return HealthCheckResponse_ServingStatus.SERVING;
case 2:
case 'NOT_SERVING':
case "NOT_SERVING":
return HealthCheckResponse_ServingStatus.NOT_SERVING;
case 3:
case 'SERVICE_UNKNOWN':
case "SERVICE_UNKNOWN":
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
case -1:
case 'UNRECOGNIZED':
case "UNRECOGNIZED":
default:
return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
}
}
export function healthCheckResponse_ServingStatusToJSON(
object: HealthCheckResponse_ServingStatus,
): string {
export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {
switch (object) {
case HealthCheckResponse_ServingStatus.UNKNOWN:
return 'UNKNOWN';
return "UNKNOWN";
case HealthCheckResponse_ServingStatus.SERVING:
return 'SERVING';
return "SERVING";
case HealthCheckResponse_ServingStatus.NOT_SERVING:
return 'NOT_SERVING';
return "NOT_SERVING";
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
return 'SERVICE_UNKNOWN';
return "SERVICE_UNKNOWN";
case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
default:
return 'UNRECOGNIZED';
return "UNRECOGNIZED";
}
}
function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: '' };
return { service: "" };
}
export const HealthCheckRequest = {
encode(
message: HealthCheckRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.service !== '') {
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);
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) {
if (tag !== 10) {
break;
}
message.service = reader.string();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
@@ -114,7 +106,7 @@ export const HealthCheckRequest = {
},
fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? String(object.service) : '' };
return { service: isSet(object.service) ? String(object.service) : "" };
},
toJSON(message: HealthCheckRequest): unknown {
@@ -123,17 +115,13 @@ export const HealthCheckRequest = {
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
base?: I,
): HealthCheckRequest {
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(
object: I,
): HealthCheckRequest {
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
const message = createBaseHealthCheckRequest();
message.service = object.service ?? '';
message.service = object.service ?? "";
return message;
},
};
@@ -143,10 +131,7 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
}
export const HealthCheckResponse = {
encode(
message: HealthCheckResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.status !== 0) {
writer.uint32(8).int32(message.status);
}
@@ -154,22 +139,21 @@ export const HealthCheckResponse = {
},
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
const reader =
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
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) {
if (tag !== 8) {
break;
}
message.status = reader.int32() as any;
continue;
}
if ((tag & 7) == 4 || tag == 0) {
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
@@ -178,29 +162,20 @@ export const HealthCheckResponse = {
},
fromJSON(object: any): HealthCheckResponse {
return {
status: isSet(object.status)
? healthCheckResponse_ServingStatusFromJSON(object.status)
: 0,
};
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));
message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
return obj;
},
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
base?: I,
): HealthCheckResponse {
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(
object: I,
): HealthCheckResponse {
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0;
return message;
@@ -210,25 +185,21 @@ export const HealthCheckResponse = {
export type HealthService = typeof HealthService;
export const HealthService = {
check: {
path: '/com.ql.health.Health/Check',
path: "/com.ql.health.Health/Check",
requestStream: false,
responseStream: false,
requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
watch: {
path: '/com.ql.health.Health/Watch',
path: "/com.ql.health.Health/Watch",
requestStream: false,
responseStream: true,
requestSerialize: (value: HealthCheckRequest) =>
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) =>
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
},
} as const;
@@ -241,32 +212,20 @@ export interface HealthServer extends UntypedServiceImplementation {
export interface HealthClient extends Client {
check(
request: HealthCheckRequest,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
): ClientUnaryCall;
check(
request: HealthCheckRequest,
metadata: Metadata,
options: Partial<CallOptions>,
callback: (
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
): ClientUnaryCall;
watch(
request: HealthCheckRequest,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;
watch(
request: HealthCheckRequest,
metadata?: Metadata,
@@ -274,43 +233,21 @@ export interface HealthClient extends Client {
): ClientReadableStream<HealthCheckResponse>;
}
export const HealthClient = makeGenericClientConstructor(
HealthService,
'com.ql.health.Health',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): HealthClient;
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;
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]> }
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;
};
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;
+5 -2
View File
@@ -6,14 +6,15 @@ import { credentials } from '@grpc/grpc-js';
const app = express();
const client = new HealthClient(
`localhost:${config.cronPort}`,
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
app.get('/api/health', (req, res) => {
client.check({ service: 'cron' }, (err, response) => {
if (err) {
return res.status(500).send({ code: 500, error: err });
return res.status(200).send({ code: 500, error: err });
}
return res.status(200).send({ code: 200, data: response });
});
@@ -25,9 +26,11 @@ app
await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`);
console.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready');
})
.on('error', (err) => {
Logger.error(err);
console.error(err);
process.exit(1);
});
+37 -7
View File
@@ -2,23 +2,53 @@ import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data';
import { exec } from 'child_process';
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 } = item;
const { id, schedule, command, extraSchedules, name } = item;
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
scheduleStacks.get(id)?.forEach((x) => x.cancel());
}
scheduleStacks.set(
Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id,
nodeSchedule.scheduleJob(id, schedule, async () => {
exec(`ID=${id} ${command}`);
}),
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);
+2 -1
View File
@@ -10,8 +10,9 @@ import config from '../config';
class Client {
private client = new CronClient(
`localhost:${config.cronPort}`,
`0.0.0.0:${config.cronPort}`,
credentials.createInsecure(),
{ 'grpc.enable_http_proxy': 0 },
);
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
+1 -1
View File
@@ -1,6 +1,6 @@
import nodeSchedule from 'node-schedule';
import { ToadScheduler } from 'toad-scheduler';
export const scheduleStacks = new Map<string, nodeSchedule.Job>();
export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
export const intervalSchedule = new ToadScheduler();
+6 -1
View File
@@ -1,6 +1,7 @@
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>,
@@ -8,7 +9,11 @@ const delCron = (
) => {
for (const id of call.request.ids) {
if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel();
Logger.info(
'[schedule][取消定时任务], 任务ID: %s',
id,
);
scheduleStacks.get(id)?.forEach(x => x.cancel());
scheduleStacks.delete(id);
}
}
+5 -3
View File
@@ -1,6 +1,5 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { HealthCheckRequest, HealthCheckResponse } from '../protos/health';
import { exec } from 'child_process';
import config from '../config';
import { promiseExec } from '../config/util';
@@ -11,19 +10,22 @@ const check = async (
switch (call.request.service) {
case 'cron':
const res = await promiseExec(
`curl -sf http://localhost:${config.port}/api/system`,
`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}`));
return callback(
new Error(`${scheduleErrLog || ''}\n${panelErrLog || ''}\n${res}`.trim()),
);
default:
return callback(null, { status: 1 });
+3 -2
View File
@@ -7,11 +7,11 @@ import { check } from './health';
import config from '../config';
import Logger from '../loaders/logger';
const server = new Server();
const server = new Server({ 'grpc.enable_http_proxy': 0 });
server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron });
server.bindAsync(
`localhost:${config.cronPort}`,
`0.0.0.0:${config.cronPort}`,
ServerCredentials.createInsecure(),
(err, port) => {
if (err) {
@@ -19,6 +19,7 @@ server.bindAsync(
}
server.start();
Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready');
},
);
+30
View File
@@ -0,0 +1,30 @@
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 });
}
}
+128 -97
View File
@@ -2,28 +2,33 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process';
import fs from 'fs';
import { exec, execSync } from 'child_process';
import fs from 'fs/promises';
import cron_parser from 'cron-parser';
import {
getFileContentByName,
concurrentRun,
fileExist,
killTask,
getUniqPath,
safeJSONParse,
} from '../config/util';
import { promises, existsSync } from 'fs';
import { Op, where, col as colFn, FindOptions } from 'sequelize';
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
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 pickBy from 'lodash/pickBy';
import omit from 'lodash/omit';
@Service()
export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {}
private isSixCron(cron: Crontab) {
const { schedule } = cron;
if (schedule?.split(/ +/).length === 6) {
private isNodeCron(cron: Crontab) {
const { schedule, extra_schedules } = cron;
if (Number(schedule?.split(/ +/).length) > 5 || extra_schedules?.length) {
return true;
}
return false;
@@ -33,9 +38,15 @@ export default class CronService {
const tab = new Crontab(payload);
tab.saved = false;
const doc = await this.insert(tab);
if (this.isSixCron(doc)) {
if (this.isNodeCron(doc)) {
await cronClient.addCron([
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command },
{
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
},
]);
}
await this.set_crontab();
@@ -54,15 +65,17 @@ export default class CronService {
if (doc.isDisabled === 1) {
return newDoc;
}
if (this.isSixCron(doc)) {
await cronClient.delCron([String(newDoc.id)]);
if (this.isNodeCron(doc)) {
await cronClient.delCron([String(doc.id)]);
}
if (this.isSixCron(newDoc)) {
if (this.isNodeCron(newDoc)) {
await cronClient.addCron([
{
name: doc.name || '',
id: String(newDoc.id),
schedule: newDoc.schedule!,
command: newDoc.command,
command: this.makeCommand(newDoc),
extraSchedules: newDoc.extra_schedules || [],
},
]);
}
@@ -90,7 +103,7 @@ export default class CronService {
last_running_time: number;
last_execution_time: number;
}) {
const options: any = {
let options: any = {
status,
pid,
log_path,
@@ -100,7 +113,16 @@ export default class CronService {
options.last_running_time = last_running_time;
}
return await CrontabModel.update({ ...options }, { where: { id: ids } });
for (const id of ids) {
const cron = await this.getDb({ id });
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[]) {
@@ -164,7 +186,7 @@ export default class CronService {
case 'In':
q[Op.or] = [
{
[property]: value,
[property]: Array.isArray(value) ? value : [value],
},
property === 'status' && value.includes(2)
? { isDisabled: 1 }
@@ -175,7 +197,7 @@ export default class CronService {
q[Op.and] = [
{
[property]: {
[Op.notIn]: value,
[Op.notIn]: Array.isArray(value) ? value : [value],
},
},
property === 'status' && value.includes(2)
@@ -192,17 +214,13 @@ export default class CronService {
{
[operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
{ [operate]: `%${encodeURI(value)}%` },
],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
where(colFn(property), operate, `%${encodeURI(value)}%`),
],
},
],
@@ -229,7 +247,7 @@ export default class CronService {
q[column] = {
[Op.or]: [
{ [Op.like]: `%${textArray[1]}%` },
{ [Op.like]: `%${encodeURIComponent(textArray[1])}%` },
{ [Op.like]: `%${encodeURI(textArray[1])}%` },
],
};
break;
@@ -237,7 +255,7 @@ export default class CronService {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` },
],
};
q[Op.or] = [
@@ -270,7 +288,7 @@ export default class CronService {
let q: any = {};
if (!filterQuery[key]) continue;
if (key === 'status') {
if (filterQuery[key].includes(2)) {
if (filterQuery[key].includes(CrontabStatus.disabled)) {
q = { [Op.or]: [{ [key]: filterQuery[key] }, { isDisabled: 1 }] };
} else {
q = { [Op.and]: [{ [key]: filterQuery[key] }, { isDisabled: 0 }] };
@@ -315,9 +333,9 @@ export default class CronService {
const searchText = params?.searchValue;
const page = Number(params?.page || '0');
const size = Number(params?.size || '0');
const viewQuery = JSON.parse(params?.queryString || '{}');
const filterQuery = JSON.parse(params?.filters || '{}');
const sorterQuery = JSON.parse(params?.sorter || '{}');
const viewQuery = safeJSONParse(params?.queryString);
const filterQuery = safeJSONParse(params?.filters);
const sorterQuery = safeJSONParse(params?.sorter);
let query: any = {};
let order = [
@@ -365,10 +383,9 @@ export default class CronService {
{ status: CrontabStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
ids.forEach((id) => {
this.runSingle(id);
});
}
public async stop(ids: number[]) {
@@ -378,7 +395,7 @@ export default class CronService {
try {
await killTask(doc.pid);
} catch (error) {
this.logger.silly(error);
this.logger.error(error);
}
}
}
@@ -389,64 +406,58 @@ export default class CronService {
);
}
private async runSingle(cronId: number): Promise<number> {
private async runSingle(cronId: number): Promise<number | void> {
return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => {
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) {
resolve();
resolve(params);
return;
}
this.logger.info(
`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`,
);
let { id, command, log_path } = cron;
const absolutePath = path.resolve(config.logPath, `${log_path}`);
const logFileExist = log_path && (await fileExist(absolutePath));
this.logger.silly('Running job');
this.logger.silly('ID: ' + id);
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
const uniqPath = await getUniqPath(command, `${id}`);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
if (log_path?.split('/')?.every((x) => x !== uniqPath)) {
await fs.mkdir(logDirPath, { recursive: true });
}
const logPath = `${uniqPath}/${logTime}.log`;
const absolutePath = path.resolve(config.logPath, `${logPath}`);
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' });
const cp = spawn(
`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`,
{ shell: '/bin/bash' },
);
await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid },
{ status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
{ where: { id } },
);
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
}
cp.stderr.on('data', async (data) => {
await fs.appendFile(`${absolutePath}`, `${data.toString()}`);
});
cp.on('error', (err) => {
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, signal) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
cp.on('exit', async (code) => {
await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined },
{ where: { id } },
);
resolve();
resolve({ ...params, pid: cp.pid, code });
});
});
});
}
@@ -461,11 +472,13 @@ export default class CronService {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
const docs = await CrontabModel.findAll({ where: { id: ids } });
const sixCron = docs
.filter((x) => this.isSixCron(x))
.filter((x) => this.isNodeCron(x))
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
await this.set_crontab();
@@ -480,7 +493,7 @@ export default class CronService {
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) {
return getFileContentByName(`${absolutePath}`);
return await getFileContentByName(`${absolutePath}`);
} else {
return '任务未运行';
}
@@ -494,28 +507,41 @@ export default class CronService {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
const dirExist = await fileExist(dir);
if (dirExist) {
let files = await fs.readdir(dir);
return (
await Promise.all(
files.map(async (x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
time: (await fs.stat(`${dir}/${x}`)).mtime.getTime(),
})),
)
).sort((a, b) => b.time - a.time);
} else {
return [];
}
}
private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
private makeCommand(tab: Crontab) {
let command = tab.command.trim();
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`;
let commandVariable = `no_tee=true ID=${tab.id} `;
if (tab.task_before) {
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}`;
return crontab_job_string;
}
@@ -524,22 +550,25 @@ export default class CronService {
var crontab_string = '';
tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule!.length !== 5) {
if (
tab.isDisabled === 1 ||
_schedule!.length !== 5 ||
tab.extra_schedules?.length
) {
crontab_string += '# ';
crontab_string += tab.schedule;
crontab_string += ' ';
crontab_string += this.make_command(tab);
crontab_string += this.makeCommand(tab);
crontab_string += '\n';
} else {
crontab_string += tab.schedule;
crontab_string += ' ';
crontab_string += this.make_command(tab);
crontab_string += this.makeCommand(tab);
crontab_string += '\n';
}
});
this.logger.silly(crontab_string);
fs.writeFileSync(config.crontabFile, crontab_string);
await fs.writeFile(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`);
await CrontabModel.update({ saved: true }, { where: {} });
@@ -584,11 +613,13 @@ export default class CronService {
this.set_crontab(tabs);
const sixCron = tabs.data
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
.filter((x) => this.isNodeCron(x) && x.isDisabled !== 1)
.map((doc) => ({
name: doc.name || '',
id: String(doc.id),
schedule: doc.schedule!,
command: doc.command,
command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || [],
}));
await cronClient.addCron(sixCron);
}
+4 -2
View File
@@ -32,7 +32,7 @@ export default class CronViewService {
}
public async update(payload: CrontabView): Promise<CrontabView> {
const doc = await this.getDb({ id: payload.id })
const doc = await this.getDb({ id: payload.id });
const tab = new CrontabView({ ...doc, ...payload });
const newDoc = await this.updateDb(tab);
return newDoc;
@@ -59,7 +59,9 @@ export default class CronViewService {
}
}
public async getDb(query: FindOptions<CrontabView>['where']): Promise<CrontabView> {
public async getDb(
query: FindOptions<CrontabView>['where'],
): Promise<CrontabView> {
const doc: any = await CrontabViewModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as CrontabView);
}
+118 -40
View File
@@ -8,12 +8,15 @@ import {
DependenceTypes,
unInstallDependenceCommandTypes,
DependenceModel,
GetDependenceCommandTypes,
versionDependenceCommandTypes,
} from '../data/dependence';
import { spawn } from 'child_process';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import { FindOptions, Op } from 'sequelize';
import { concurrentRun } from '../config/util';
import { fileExist, promiseExecSuccess } from '../config/util';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
@Service()
export default class DependenceService {
@@ -58,12 +61,23 @@ export default class DependenceService {
}
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(
{ status: DependenceStatus.queued, log: [] },
{ where: { id: ids } },
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs, false, force);
}
return docs;
}
@@ -78,7 +92,7 @@ export default class DependenceService {
): Promise<Dependence[]> {
let condition = { ...query, type: DependenceTypes[type as any] };
if (searchValue) {
const encodeText = encodeURIComponent(searchValue);
const encodeText = encodeURI(searchValue);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchValue}%` },
@@ -104,20 +118,9 @@ export default class DependenceService {
isInstall: boolean = true,
force: boolean = false,
) {
concurrentRun(
docs.map((dep) => async () => {
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: dep.id } });
return await this.installOrUninstallDependencies(
[dep],
isInstall,
force,
);
}),
1,
);
docs.forEach((dep) => {
this.installOrUninstallDependency(dep, isInstall, force);
});
}
public async reInstall(ids: number[]): Promise<Dependence[]> {
@@ -127,7 +130,7 @@ export default class DependenceService {
);
const docs = await DependenceModel.findAll({ where: { id: ids } });
this.installDependenceOneByOne(docs);
this.installDependenceOneByOne(docs, true, true);
return docs;
}
@@ -147,35 +150,45 @@ export default class DependenceService {
}
private async updateLog(ids: number[], log: string): Promise<void> {
const doc = await DependenceModel.findOne({ where: { id: ids } });
taskLimit.updateDepLog(async () => {
const docs = await DependenceModel.findAll({ where: { id: ids } });
for (const doc of docs) {
const newLog = doc?.log ? [...doc.log, log] : [log];
await DependenceModel.update({ log: newLog }, { where: { id: ids } });
await DependenceModel.update(
{ log: newLog },
{ where: { id: doc.id } },
);
}
return null;
});
}
public installOrUninstallDependencies(
dependencies: Dependence[],
public installOrUninstallDependency(
dependency: Dependence,
isInstall: boolean = true,
force: boolean = false,
) {
return taskLimit.runOneByOne(() => {
return new Promise(async (resolve) => {
if (dependencies.length === 0) {
resolve(null);
return;
}
const socketMessageType = !force
const depIds = [dependency.id!];
const status = isInstall
? DependenceStatus.installing
: DependenceStatus.removing;
await DependenceModel.update({ status }, { where: { id: depIds } });
const socketMessageType = isInstall
? 'installDependence'
: 'uninstallDependence';
const depNames = dependencies.map((x) => x.name).join(' ');
let depName = dependency.name.trim();
const depRunCommand = (
isInstall
? InstallDependenceCommandTypes
: unInstallDependenceCommandTypes
)[dependencies[0].type as any];
)[dependency.type];
const actionText = isInstall ? '安装' : '删除';
const depIds = dependencies.map((x) => x.id) as number[];
const startTime = dayjs();
const message = `开始${actionText}依赖 ${depNames},开始时间 ${startTime.format(
const message = `开始${actionText}依赖 ${depName},开始时间 ${startTime.format(
'YYYY-MM-DD HH:mm:ss',
)}\n\n`;
this.sockService.sendMessage({
@@ -183,9 +196,74 @@ export default class DependenceService {
message,
references: depIds,
});
await this.updateLog(depIds, message);
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) => {
this.sockService.sendMessage({
@@ -193,7 +271,7 @@ export default class DependenceService {
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
this.updateLog(depIds, data.toString());
});
cp.stderr.on('data', async (data) => {
@@ -202,7 +280,7 @@ export default class DependenceService {
message: data.toString(),
references: depIds,
});
await this.updateLog(depIds, data.toString());
this.updateLog(depIds, data.toString());
});
cp.on('error', async (err) => {
@@ -211,11 +289,10 @@ export default class DependenceService {
message: JSON.stringify(err),
references: depIds,
});
await this.updateLog(depIds, JSON.stringify(err));
resolve(null);
this.updateLog(depIds, JSON.stringify(err));
});
cp.on('close', async (code) => {
cp.on('exit', async (code) => {
const endTime = dayjs();
const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败';
@@ -228,7 +305,7 @@ export default class DependenceService {
message,
references: depIds,
});
await this.updateLog(depIds, message);
this.updateLog(depIds, message);
let status = null;
if (isSucceed) {
@@ -250,5 +327,6 @@ export default class DependenceService {
resolve(null);
});
});
});
}
}
+5 -7
View File
@@ -1,7 +1,7 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import {
Env,
EnvModel,
@@ -121,7 +121,7 @@ export default class EnvService {
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const encodeText = encodeURI(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
@@ -202,14 +202,12 @@ export default class EnvService {
let value = group
.map((x) => x.value)
.join('&')
.replace(/(\\)[^n]/g, '\\\\')
.replace(/(\\$)/, '\\\\')
.replace(/"/g, '\\"')
.replace(/'/g, "'\\''")
.trim();
env_string += `export ${key}="${value}"\n`;
env_string += `export ${key}='${value}'\n`;
}
}
}
fs.writeFileSync(config.envFile, env_string);
await fs.writeFile(config.envFile, env_string);
}
}
+106 -17
View File
@@ -1,12 +1,12 @@
import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
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 UserService from './user';
@Service()
export default class NotificationService {
@@ -28,8 +28,10 @@ export default class NotificationService {
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
['pushMe', this.pushMe],
['webhook', this.webhook],
['lark', this.lark],
['chronocat', this.chronocat],
]);
private title = '';
@@ -55,7 +57,7 @@ export default class NotificationService {
try {
return await notificationModeAction?.call(this);
} catch (error: any) {
return false;
throw error;
}
}
return false;
@@ -194,7 +196,8 @@ export default class NotificationService {
}
private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params;
let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
this.params;
if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`;
}
@@ -202,7 +205,7 @@ export default class NotificationService {
this.title,
)}/${encodeURIComponent(
this.content,
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`;
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}&level=${barkLevel}&url=${barkUrl}`;
try {
const res: any = await got
@@ -231,8 +234,8 @@ export default class NotificationService {
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
const url = `${
telegramBotApiHost ? telegramBotApiHost : 'https://api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
@@ -303,8 +306,9 @@ export default class NotificationService {
}
private async weWorkBot() {
const { weWorkBotKey } = this.params;
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const { weWorkBotKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
this.params;
const url = `${weWorkOrigin}/cgi-bin/webhook/send?key=${weWorkBotKey}`;
try {
const res: any = await got
.post(url, {
@@ -328,10 +332,11 @@ export default class NotificationService {
}
private async weWorkApp() {
const { weWorkAppKey } = this.params;
const { weWorkAppKey, weWorkOrigin = 'https://qyapi.weixin.qq.com' } =
this.params;
const [corpid, corpsecret, touser, agentid, thumb_media_id = '1'] =
weWorkAppKey.split(',');
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const url = `${weWorkOrigin}/cgi-bin/gettoken`;
const tokenRes: any = await got
.post(url, {
...this.gotOption,
@@ -383,7 +388,7 @@ export default class NotificationService {
try {
const res: any = await got
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
`${weWorkOrigin}/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
...this.gotOption,
json: {
@@ -556,6 +561,87 @@ export default class NotificationService {
} else {
throw new Error(JSON.stringify(info));
}
} catch (error: any) {
throw error;
}
}
private async pushMe() {
const { pushMeKey } = this.params;
try {
const res: any = await got.post(
`https://push.i-i.me/?push_key=${pushMeKey}`,
{
...this.gotOption,
json: {
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);
}
@@ -574,9 +660,11 @@ export default class NotificationService {
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
throw new Error('Url 或者 Body 中必须包含 $title');
}
const headers = parseHeaders(webhookHeaders);
const body = parseBody(formatBody, webhookContentType);
const bodyParam = this.formatBody(webhookContentType, body);
@@ -607,6 +695,7 @@ export default class NotificationService {
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
case 'text/plain':
return { body };
}
return {};
+1 -1
View File
@@ -75,7 +75,7 @@ export default class OpenService {
): Promise<App[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
const encodeText = encodeURI(searchText);
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
+42 -28
View File
@@ -1,7 +1,7 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import nodeSchedule from 'node-schedule';
import { ChildProcessWithoutNullStreams, exec, spawn } from 'child_process';
import { ChildProcessWithoutNullStreams } from 'child_process';
import {
ToadScheduler,
LongIntervalJob,
@@ -9,8 +9,10 @@ import {
Task,
} from 'toad-scheduler';
import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn';
interface ScheduleTaskType {
export interface ScheduleTaskType {
id: number;
command: string;
name?: string;
@@ -45,16 +47,23 @@ export default class ScheduleService {
async runTask(
command: string,
callbacks: TaskCallbacks = {},
params: {
schedule?: string;
name?: string;
command?: string;
},
completionTime: 'start' | 'end' = 'end',
) {
return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve, reject) => {
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
try {
const startTime = dayjs();
await callbacks.onBefore?.(startTime);
const cp = spawn(command, { shell: '/bin/bash' });
// TODO:
callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
@@ -64,9 +73,8 @@ export default class ScheduleService {
cp.stderr.on('data', async (data) => {
this.logger.info(
'[执行任务失败] %s,时间:%s, 错误信息%j',
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
command,
new Date().toLocaleString(),
data.toString(),
);
await callbacks.onError?.(data.toString());
@@ -74,39 +82,32 @@ export default class ScheduleService {
cp.on('error', async (err) => {
this.logger.error(
'[创建任务失败] %s,时间:%s, 错误信息%j',
'[panel][创建任务失败] 命令: %s, 错误信息: %j',
command,
new Date().toLocaleString(),
err,
);
await callbacks.onError?.(JSON.stringify(err));
});
cp.on('exit', async (code, signal) => {
this.logger.info(
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
);
});
cp.on('close', async (code) => {
cp.on('exit', async (code) => {
const endTime = dayjs();
await callbacks.onEnd?.(
cp,
endTime,
endTime.diff(startTime, 'seconds'),
);
resolve(null);
resolve({ ...params, pid: cp.pid, code });
});
} catch (error) {
await this.logger.error(
'执行任务%s失败,时间:%s, 错误信息%j',
this.logger.error(
'[panel][执行任务失败] 命令: %s, 错误信息: %j',
command,
new Date().toLocaleString(),
error,
);
await callbacks.onError?.(JSON.stringify(error));
}
});
});
}
async createCronTask(
@@ -116,7 +117,7 @@ export default class ScheduleService {
) {
const _id = this.formatId(id);
this.logger.info(
'[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s',
'[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
_id,
schedule,
name,
@@ -126,18 +127,26 @@ export default class ScheduleService {
this.scheduleStacks.set(
_id,
nodeSchedule.scheduleJob(_id, schedule, async () => {
await this.runTask(command, callbacks);
this.runTask(command, callbacks, {
name,
schedule,
command,
});
}),
);
if (runImmediately) {
await this.runTask(command, callbacks);
this.runTask(command, callbacks, {
name,
schedule,
command,
});
}
}
async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id);
this.logger.info('[取消定时任务]任务名%s', name);
this.logger.info('[panel][取消定时任务], 任务名: %s', name);
if (this.scheduleStacks.has(_id)) {
this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id);
@@ -152,7 +161,7 @@ export default class ScheduleService {
) {
const _id = this.formatId(id);
this.logger.info(
'[创建interval任务]任务ID: %s任务名: %s执行命令: %s',
'[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
_id,
name,
command,
@@ -160,13 +169,15 @@ export default class ScheduleService {
const task = new Task(
name,
() => {
this.runTask(command, callbacks);
this.runTask(command, callbacks, {
name,
command,
});
},
(err) => {
this.logger.error(
'执行任务%s失败,时间:%s, 错误信息%j',
'[执行任务失败] 命令: %s, 错误信息: %j',
command,
new Date().toLocaleString(),
err,
);
},
@@ -181,13 +192,16 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job);
if (runImmediately) {
this.runTask(command, callbacks);
this.runTask(command, callbacks, {
name,
command,
});
}
}
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
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);
}
+12 -9
View File
@@ -1,13 +1,12 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs from 'fs';
import path from 'path';
import path, { join } from 'path';
import SockService from './sock';
import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
import { getFileContentByName, getPid, killTask, rmPath } from '../config/util';
@Service()
export default class ScriptService {
@@ -21,9 +20,7 @@ export default class ScriptService {
private taskCallbacks(filePath: string): TaskCallbacks {
return {
onEnd: async (cp, endTime, diff) => {
try {
fs.unlinkSync(filePath);
} catch (error) {}
await rmPath(filePath);
},
onError: async (message: string) => {
this.sockService.sendMessage({
@@ -42,10 +39,11 @@ export default class ScriptService {
public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath);
const command = `${TASK_COMMAND} -l ${relativePath} now`;
const command = `${TASK_COMMAND} ${relativePath} now`;
const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
{ command },
'start',
);
@@ -53,10 +51,9 @@ export default class ScriptService {
}
public async stopScript(filePath: string, pid: number) {
let str = '';
if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
pid = (await getPid(`${TASK_COMMAND} ${relativePath} now`)) as number;
}
try {
await killTask(pid);
@@ -64,4 +61,10 @@ export default class ScriptService {
return { code: 200 };
}
public async getFile(filePath: string, fileName: string) {
const _filePath = join(config.scriptPath, filePath, fileName);
const content = await getFileContentByName(_filePath);
return content;
}
}
+41 -32
View File
@@ -1,11 +1,12 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import fs, { existsSync } from 'fs';
import fs from 'fs/promises';
import os from 'os';
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()
export default class SshKeyService {
@@ -18,15 +19,16 @@ export default class SshKeyService {
this.initSshConfigFile();
}
private initSshConfigFile() {
private async initSshConfigFile() {
let config = '';
if (existsSync(this.sshConfigFilePath)) {
config = fs.readFileSync(this.sshConfigFilePath, { encoding: 'utf-8' });
const _exist = await fileExist(this.sshConfigFilePath);
if (_exist) {
config = await fs.readFile(this.sshConfigFilePath, { encoding: 'utf-8' });
} else {
fs.writeFileSync(this.sshConfigFilePath, '');
await fs.writeFile(this.sshConfigFilePath, '');
}
if (!config.includes(this.sshConfigHeader)) {
fs.writeFileSync(
await fs.writeFile(
this.sshConfigFilePath,
`${this.sshConfigHeader}\n\n${config}`,
{ encoding: 'utf-8' },
@@ -34,9 +36,12 @@ export default class SshKeyService {
}
}
private generatePrivateKeyFile(alias: string, key: string): void {
private async generatePrivateKeyFile(
alias: string,
key: string,
): Promise<void> {
try {
fs.writeFileSync(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
await fs.writeFile(path.join(this.sshPath, alias), `${key}${os.EOL}`, {
encoding: 'utf8',
mode: '400',
});
@@ -45,20 +50,22 @@ export default class SshKeyService {
}
}
private removePrivateKeyFile(alias: string): void {
private async removePrivateKeyFile(alias: string): Promise<void> {
try {
const filePath = path.join(this.sshPath, alias);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
} catch (error) {
this.logger.error('删除私钥文件失败', error);
}
}
private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
private async generateSingleSshConfig(
alias: string,
host: string,
proxy?: string,
) {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
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`
@@ -67,49 +74,51 @@ export default class SshKeyService {
this.sshPath,
alias,
)}\n StrictHostKeyChecking no\n${proxyStr}`;
fs.writeFileSync(`${path.join(this.sshPath, `${alias}.config`)}`, config, {
await fs.writeFile(
`${path.join(this.sshPath, `${alias}.config`)}`,
config,
{
encoding: 'utf8',
});
},
);
}
private removeSshConfig(alias: string) {
private async removeSshConfig(alias: string) {
try {
const filePath = path.join(this.sshPath, `${alias}.config`);
if (existsSync(filePath)) {
fs.unlinkSync(filePath);
}
await rmPath(filePath);
} catch (error) {
this.logger.error(`删除ssh配置文件${alias}失败`, error);
}
}
public addSSHKey(
public async addSSHKey(
key: string,
alias: string,
host: string,
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key);
this.generateSingleSshConfig(alias, host, proxy);
): Promise<void> {
await this.generatePrivateKeyFile(alias, key);
await this.generateSingleSshConfig(alias, host, proxy);
}
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
this.removeSshConfig(alias);
public async removeSSHKey(alias: string, host: string, proxy?: string): Promise<void> {
await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias);
}
public setSshConfig(docs: Subscription[]) {
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);
this.removePrivateKeyFile(alias);
this.removeSshConfig(alias);
this.generatePrivateKeyFile(
await this.removePrivateKeyFile(alias);
await this.removeSshConfig(alias);
await this.generatePrivateKeyFile(
alias,
(doc.pull_option as any).private_key,
);
this.generateSingleSshConfig(alias, host, proxy);
await this.generateSingleSshConfig(alias, host, proxy);
}
}
}
+50 -57
View File
@@ -6,13 +6,7 @@ import {
SubscriptionModel,
SubscriptionStatus,
} from '../data/subscription';
import {
ChildProcessWithoutNullStreams,
exec,
execSync,
spawn,
} from 'child_process';
import fs from 'fs';
import { ChildProcessWithoutNullStreams } from 'child_process';
import {
getFileContentByName,
concurrentRun,
@@ -20,10 +14,12 @@ import {
createFile,
killTask,
handleLogPath,
promiseExec,
rmPath,
} from '../config/util';
import { promises, existsSync } from 'fs';
import fs from 'fs/promises';
import { FindOptions, Op } from 'sequelize';
import path from 'path';
import path, { join } from 'path';
import ScheduleService, { TaskCallbacks } from './schedule';
import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
@@ -31,6 +27,8 @@ import SshKeyService from './sshKey';
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()
export default class SubscriptionService {
@@ -39,6 +37,7 @@ export default class SubscriptionService {
private scheduleService: ScheduleService,
private sockService: SockService,
private sshKeyService: SshKeyService,
private crontabService: CrontabService,
) {}
public async list(searchText?: string): Promise<Subscription[]> {
@@ -47,7 +46,7 @@ export default class SubscriptionService {
const reg = {
[Op.or]: [
{ [Op.like]: `%${searchText}%` },
{ [Op.like]: `%${encodeURIComponent(searchText)}%` },
{ [Op.like]: `%${encodeURI(searchText)}%` },
],
};
query = {
@@ -107,19 +106,7 @@ export default class SubscriptionService {
public async setSshConfig() {
const docs = await SubscriptionModel.findAll();
this.sshKeyService.setSshConfig(docs);
}
private async promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
command,
{ maxBuffer: 200 * 1024 * 1024, encoding: 'utf8' },
(err, stdout, stderr) => {
resolve(stdout || stderr || JSON.stringify(err));
},
);
});
await this.sshKeyService.setSshConfig(docs);
}
private taskCallbacks(doc: Subscription): TaskCallbacks {
@@ -143,15 +130,15 @@ export default class SubscriptionService {
let beforeStr = '';
try {
if (doc.sub_before) {
fs.appendFileSync(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await this.promiseExec(doc.sub_before);
await fs.appendFile(absolutePath, `\n## 执行before命令...\n\n`);
beforeStr = await promiseExec(doc.sub_before);
}
} catch (error: any) {
beforeStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (beforeStr) {
fs.appendFileSync(absolutePath, `${beforeStr}\n`);
await fs.appendFile(absolutePath, `${beforeStr}\n`);
}
},
onStart: async (cp: ChildProcessWithoutNullStreams, startTime) => {
@@ -170,18 +157,18 @@ export default class SubscriptionService {
let afterStr = '';
try {
if (sub.sub_after) {
fs.appendFileSync(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await this.promiseExec(sub.sub_after);
await fs.appendFile(absolutePath, `\n\n## 执行after命令...\n\n`);
afterStr = await promiseExec(sub.sub_after);
}
} catch (error: any) {
afterStr =
(error.stderr && error.stderr.toString()) || JSON.stringify(error);
}
if (afterStr) {
fs.appendFileSync(absolutePath, `${afterStr}\n`);
await fs.appendFile(absolutePath, `${afterStr}\n`);
}
fs.appendFileSync(
await fs.appendFile(
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
@@ -202,12 +189,12 @@ export default class SubscriptionService {
onError: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
onLog: async (message: string) => {
const sub = await this.getDb({ id: doc.id });
const absolutePath = await handleLogPath(sub.log_path as string);
fs.appendFileSync(absolutePath, `\n${message}`);
await fs.appendFile(absolutePath, `\n${message}`);
},
};
}
@@ -269,13 +256,24 @@ export default class SubscriptionService {
);
}
public async remove(ids: number[]) {
public async remove(ids: number[], query: any) {
const docs = await SubscriptionModel.findAll({ where: { id: ids } });
for (const doc of docs) {
await this.handleTask(doc, false);
}
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(
@@ -290,10 +288,9 @@ export default class SubscriptionService {
{ status: SubscriptionStatus.queued },
{ where: { id: ids } },
);
concurrentRun(
ids.map((id) => async () => await this.runSingle(id)),
10,
);
ids.forEach((id) => {
this.runSingle(id);
});
}
public async stop(ids: number[]) {
@@ -303,17 +300,9 @@ export default class SubscriptionService {
try {
await killTask(doc.pid);
} catch (error) {
this.logger.silly(error);
this.logger.error(error);
}
}
const absolutePath = await handleLogPath(doc.log_path as string);
fs.appendFileSync(
`${absolutePath}`,
`\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
);
}
await SubscriptionModel.update(
@@ -330,10 +319,11 @@ export default class SubscriptionService {
const command = formatCommand(subscription);
await this.scheduleService.runTask(
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
name: subscription.name,
schedule: subscription.schedule,
command,
this.taskCallbacks(subscription),
);
});
}
public async disabled(ids: number[]) {
@@ -361,7 +351,7 @@ export default class SubscriptionService {
}
const absolutePath = await handleLogPath(doc.log_path as string);
return getFileContentByName(absolutePath);
return await getFileContentByName(absolutePath);
}
public async logs(id: number) {
@@ -373,15 +363,18 @@ export default class SubscriptionService {
if (doc.log_path) {
const relativeDir = path.dirname(`${doc.log_path}`);
const dir = path.resolve(config.logPath, relativeDir);
if (existsSync(dir)) {
let files = await promises.readdir(dir);
return files
.map((x) => ({
const _exist = await fileExist(dir);
if (_exist) {
let files = await fs.readdir(dir);
return (
await Promise.all(
files.map(async (x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
time: (await fs.stat(`${dir}/${x}`)).mtime.getTime(),
})),
)
).sort((a, b) => b.time - a.time);
}
}
}
+257 -21
View File
@@ -1,12 +1,18 @@
import { Response } from 'express';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import {
AuthDataType,
AuthInfo,
SystemInstance,
SystemModel,
SystemModelInfo,
} from '../data/system';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import ScheduleService, { TaskCallbacks } from './schedule';
import { spawn } from 'child_process';
import { spawn } from 'cross-spawn';
import SockService from './sock';
import got from 'got';
import {
@@ -14,8 +20,16 @@ import {
killTask,
parseContentVersion,
parseVersion,
promiseExec,
readDirs,
} from '../config/util';
import { TASK_COMMAND } from '../config/const';
import taskLimit from '../shared/pLimit';
import tar from 'tar';
import path from 'path';
import fs from 'fs';
import sum from 'lodash/sum';
import { DependenceModel, DependenceStatus, DependenceTypes } from '../data/dependence';
@Service()
export default class SystemService {
@@ -28,20 +42,20 @@ export default class SystemService {
private sockService: SockService,
) {}
public async getLogRemoveFrequency() {
const doc = await this.getDb({ type: AuthDataType.removeLogFrequency });
return doc || {};
public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig });
return doc || ({} as SystemInstance);
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
await AuthModel.upsert({ ...payload });
private async updateAuthDb(payload: AuthInfo): Promise<SystemInstance> {
await SystemModel.upsert({ ...payload });
const doc = await this.getDb({ type: payload.type });
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
public async getDb(query: any): Promise<SystemInstance> {
const doc: any = await SystemModel.findOne({ where: { ...query } });
return doc && doc.get({ plain: true });
}
public async updateNotificationMode(notificationInfo: NotificationInfo) {
@@ -62,25 +76,169 @@ export default class SystemService {
}
}
public async updateLogRemoveFrequency(frequency: number) {
const oDoc = await this.getLogRemoveFrequency();
public async updateLogRemoveFrequency(info: SystemModelInfo) {
const oDoc = await this.getSystemConfig();
const result = await this.updateAuthDb({
...oDoc,
type: AuthDataType.removeLogFrequency,
info: { frequency },
info: { ...oDoc.info, ...info },
});
const cron = {
id: result.id,
id: result.id || NaN,
name: '删除日志',
command: `ql rmlog ${frequency}`,
command: `ql rmlog ${info.logRemoveFrequency}`,
};
if (oDoc.info?.logRemoveFrequency) {
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
}
if (info.logRemoveFrequency && info.logRemoveFrequency > 0) {
this.scheduleService.createIntervalTask(cron, {
days: frequency,
days: info.logRemoveFrequency,
});
}
return { code: 200, data: { ...cron } };
return { code: 200, data: info };
}
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() {
@@ -142,7 +300,7 @@ export default class SystemService {
}
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
const cp = spawn('real_time=true ql update false', { shell: '/bin/bash' });
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
@@ -168,6 +326,34 @@ export default class SystemService {
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.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.stderr.on('data', (data) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: data.toString(),
});
});
cp.on('error', (err) => {
this.sockService.sendMessage({
type: 'reloadSystem',
message: JSON.stringify(err),
});
});
return { code: 200 };
}
public async notify({ title, content }: { title: string; content: string }) {
const isSuccess = await this.notificationService.notify(title, content);
if (isSuccess) {
@@ -187,6 +373,9 @@ export default class SystemService {
this.scheduleService.runTask(
`real_log_path=${logPath} real_time=true ${command}`,
callback,
{
command,
},
);
}
@@ -211,4 +400,51 @@ export default class SystemService {
return { code: 400, message: '任务未找到' };
}
}
public async exportData(res: Response) {
try {
await tar.create(
{ gzip: true, file: config.dataTgzFile, cwd: config.rootPath },
['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')}`);
await tar.x({ file: config.dataTgzFile, cwd: config.tmpPath });
return { code: 200 };
} 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);
}
}
+57 -38
View File
@@ -5,19 +5,28 @@ import {
fileExist,
getNetIp,
getPlatform,
safeJSONParse,
} from '../config/util';
import config from '../config';
import * as fs from 'fs';
import * as fs from 'fs/promises';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import {
AuthDataType,
AuthInfo,
SystemModel,
SystemModelInfo,
LoginStatus,
} from '../data/system';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
import { Request } from 'express';
import ScheduleService from './schedule';
import { spawn } from 'child_process';
import SockService from './sock';
import dayjs from 'dayjs';
import IP2Region from 'ip2region';
import requestIp from 'request-ip';
import uniq from 'lodash/uniq';
@Service()
export default class UserService {
@@ -38,12 +47,13 @@ export default class UserService {
req: Request,
needTwoFactor = true,
): Promise<any> {
if (!fs.existsSync(config.authConfigFile)) {
const _exist = await fileExist(config.authConfigFile);
if (!_exist) {
return this.initAuthInfo();
}
let { username, password } = payloads;
const content = this.getAuthInfo();
const content = await this.getAuthInfo();
const timestamp = Date.now();
if (content) {
let {
@@ -96,7 +106,16 @@ export default class UserService {
};
}
const { ip, address } = await getNetIp(req);
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 expiration = twoFactorActivated ? 60 : 20;
@@ -118,13 +137,12 @@ export default class UserService {
platform: req.platform,
isTwoFactorChecking: false,
});
await this.notificationService.notify(
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录成功,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
@@ -147,13 +165,12 @@ export default class UserService {
lastaddr: address,
platform: req.platform,
});
await this.notificationService.notify(
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
}端 登录失败,ip地址 ${ip}`,
);
await this.getLoginLog();
await this.insertDb({
type: AuthDataType.loginLog,
info: {
@@ -181,21 +198,23 @@ export default class UserService {
}
public async logout(platform: string): Promise<any> {
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, {
token: '',
tokens: { ...authInfo.tokens, [platform]: '' },
});
}
public async getLoginLog(): Promise<AuthInfo[]> {
const docs = await AuthModel.findAll({
public async getLoginLog(): Promise<Array<SystemModelInfo | undefined>> {
const docs = await SystemModel.findAll({
where: { type: AuthDataType.loginLog },
});
if (docs && docs.length > 0) {
const result = docs.sort((a, b) => b.info.timestamp - a.info.timestamp);
const result = docs.sort(
(a, b) => b.info!.timestamp! - a.info!.timestamp!,
);
if (result.length > 100) {
await AuthModel.destroy({
await SystemModel.destroy({
where: { id: result[result.length - 1].id },
});
}
@@ -205,12 +224,12 @@ export default class UserService {
}
private async insertDb(payload: AuthInfo): Promise<AuthInfo> {
const doc = await AuthModel.create({ ...payload }, { returning: true });
const doc = await SystemModel.create({ ...payload }, { returning: true });
return doc;
}
private initAuthInfo() {
fs.writeFileSync(
private async initAuthInfo() {
await fs.writeFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
@@ -233,13 +252,13 @@ export default class UserService {
if (password === 'admin') {
return { code: 400, message: '密码不能设置为admin' };
}
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' };
}
public async updateAvatar(avatar: string) {
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' };
}
@@ -247,7 +266,7 @@ export default class UserService {
public async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
fs.writeFileSync(
await fs.writeFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
@@ -255,19 +274,19 @@ export default class UserService {
}),
);
}
return this.getAuthInfo();
return await this.getAuthInfo();
}
public initTwoFactor() {
public async initTwoFactor() {
const secret = authenticator.generateSecret();
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth };
}
public activeTwoFactor(code: string) {
const authInfo = this.getAuthInfo();
public async activeTwoFactor(code: string) {
const authInfo = await this.getAuthInfo();
const isValid = authenticator.verify({
token: code,
secret: authInfo.twoFactorSecret,
@@ -286,7 +305,7 @@ export default class UserService {
}: { username: string; password: string; code: string },
req: any,
) {
const authInfo = this.getAuthInfo();
const authInfo = await this.getAuthInfo();
const { isTwoFactorChecking, twoFactorSecret } = authInfo;
if (!isTwoFactorChecking) {
return { code: 450, message: '未知错误' };
@@ -308,8 +327,8 @@ export default class UserService {
}
}
public deactiveTwoFactor() {
const authInfo = this.getAuthInfo();
public async deactiveTwoFactor() {
const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, {
twoFactorActivated: false,
twoFactorActived: false,
@@ -318,13 +337,13 @@ export default class UserService {
return true;
}
private getAuthInfo() {
const content = fs.readFileSync(config.authConfigFile, 'utf8');
return JSON.parse(content || '{}');
private async getAuthInfo() {
const content = await fs.readFile(config.authConfigFile, 'utf8');
return safeJSONParse(content);
}
private updateAuthInfo(authInfo: any, info: any) {
fs.writeFileSync(
private async updateAuthInfo(authInfo: any, info: any) {
await fs.writeFile(
config.authConfigFile,
JSON.stringify({ ...authInfo, ...info }),
);
@@ -336,21 +355,21 @@ export default class UserService {
}
private async updateAuthDb(payload: AuthInfo): Promise<any> {
let doc = await AuthModel.findOne({ type: payload.type });
let doc = await SystemModel.findOne({ type: payload.type });
if (doc) {
const updateResult = await AuthModel.update(payload, {
const updateResult = await SystemModel.update(payload, {
where: { id: doc.id },
returning: true,
});
doc = updateResult[1][0];
} else {
doc = await AuthModel.create(payload, { returning: true });
doc = await SystemModel.create(payload, { returning: true });
}
return doc;
}
public async getDb(query: any): Promise<any> {
const doc: any = await AuthModel.findOne({ where: { ...query } });
const doc: any = await SystemModel.findOne({ where: { ...query } });
return doc && (doc.get({ plain: true }) as any);
}
+84
View File
@@ -0,0 +1,84 @@
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
import os from 'os';
import { AuthDataType, SystemModel } from '../data/system';
import Logger from '../loaders/logger';
class TaskLimit {
private oneLimit = new PQueue({ concurrency: 1 });
private updateLogLimit = new PQueue({ concurrency: 1 });
private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
get cronLimitActiveCount() {
return this.cronLimit.pending;
}
get cronLimitPendingCount() {
return this.cronLimit.size;
}
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 async setCustomLimit(limit?: number) {
if (limit) {
this.cronLimit.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;
}
}
public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.cronLimit.add(fn, options);
}
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.oneLimit.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
@@ -0,0 +1,31 @@
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 });
});
});
});
}
+76
View File
@@ -0,0 +1,76 @@
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 \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine3.18
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}
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 \
&& npm install -g pnpm@8.3.1 pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm
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"]
+10 -8
View File
@@ -1,13 +1,13 @@
FROM python:3.10-alpine as builder
FROM python:3.11-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 \
&& cd /tmp/build \
&& pnpm --registry https://registry.npmmirror.com install --prod
&& pnpm install --prod
FROM python:3.10-alpine
FROM python:3.11-alpine3.18
ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}"
@@ -23,8 +23,11 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH}
VOLUME /ql/data
EXPOSE 5700
RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \
&& apk upgrade \
&& apk --no-cache add -f bash \
@@ -41,6 +44,7 @@ RUN set -x \
openssh \
procps \
netcat-openbsd \
unzip \
npm \
&& rm -rf /var/cache/apk/* \
&& apk update \
@@ -49,9 +53,7 @@ RUN set -x \
&& git config --global user.email "qinglong@@users.noreply.github.com" \
&& git config --global user.name "qinglong" \
&& git config --global http.postBuffer 524288000 \
&& npm install -g pnpm@8.3.1 \
&& cd && pnpm config set registry https://registry.npmmirror.com \
&& pnpm add -g pm2 tsx \
&& npm install -g pnpm@8.3.1 pm2 tsx \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
@@ -73,6 +75,6 @@ COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
HEALTHCHECK --interval=5s --timeout=2s --retries=20 \
CMD curl -sf http://127.0.0.1:5400/api/health || exit 1
CMD curl -sf --noproxy '*' http://127.0.0.1:5400/api/health || exit 1
ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+3 -15
View File
@@ -1,11 +1,10 @@
#!/bin/bash
dir_shell=/ql/shell
. $dir_shell/env.sh
. $dir_shell/share.sh
link_shell
export isFirstStartServer=true
echo -e "======================1. 检测配置文件========================\n"
make_dir /etc/nginx/conf.d
make_dir /run/nginx
@@ -16,17 +15,6 @@ pm2 l &>/dev/null
echo -e "======================2. 安装依赖========================\n"
patch_version
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
current_npm_registry=$(cd && pnpm config get registry)
is_equal_registry=$(echo $current_npm_registry | grep "${NpmMirror}")
if [[ "$is_equal_registry" == "" ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
update_depend
echo
echo -e "======================3. 启动nginx========================\n"
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
@@ -37,13 +25,13 @@ reload_pm2
if [[ $AutoStartBot == true ]]; then
echo -e "======================5. 启动bot========================\n"
nohup ql -l bot >$dir_log/bot.log 2>&1 &
nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n"
fi
if [[ $EnableExtraShell == true ]]; then
echo -e "====================6. 执行自定义脚本========================\n"
nohup ql -l extra >$dir_log/extra.log 2>&1 &
nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n"
fi
+13 -1
View File
@@ -12,7 +12,7 @@ map $http_upgrade $connection_upgrade {
}
server {
listen 5700;
IPV4_CONFIG
IPV6_CONFIG
ssl_session_timeout 5m;
@@ -22,6 +22,10 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://publicApi/api/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
location QL_BASE_URLapi/ {
@@ -30,6 +34,10 @@ server {
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 Connection $connection_upgrade;
@@ -41,6 +49,10 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://baseApi/open/;
proxy_buffering off;
proxy_redirect default;
proxy_connect_timeout 1800;
proxy_send_timeout 1800;
proxy_read_timeout 1800;
}
gzip on;
+1 -1
View File
@@ -14,7 +14,7 @@ http {
server_tokens off;
client_max_body_size 20m;
client_max_body_size 4096m;
client_body_buffer_size 20m;
keepalive_timeout 65;
+8
View File
@@ -9,6 +9,14 @@ module.exports = {
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',
+1 -2
View File
@@ -1,6 +1,5 @@
{
"watch": ["back", ".env"],
"ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node --transpile-only ./back/app.ts"
"exec": "ts-node -P tsconfig.back.json ./back/app.ts"
}
+29 -12
View File
@@ -4,8 +4,8 @@
"start": "concurrently -n w: npm:start:*",
"start:front": "max dev",
"start:back": "nodemon",
"start:public": "ts-node --transpile-only ./back/public.ts",
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts",
"start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
"start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
"build:front": "max build",
"build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js",
@@ -58,29 +58,31 @@
"@grpc/grpc-js": "^1.8.13",
"@otplib/preset-default": "^12.0.1",
"@sentry/node": "^7.12.1",
"@sentry/tracing": "^7.12.1",
"body-parser": "^1.19.2",
"celebrate": "^15.0.1",
"chokidar": "^3.5.3",
"cors": "^2.8.5",
"cron-parser": "^4.2.1",
"cross-spawn": "^7.0.3",
"dayjs": "^1.11.2",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"express-jwt": "^6.1.1",
"express-rate-limit": "^7.0.0",
"express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0",
"got": "^11.8.2",
"hpagent": "^0.1.2",
"hpagent": "^1.2.0",
"http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"multer": "^1.4.4",
"multer": "1.4.5-lts.1",
"nedb": "^1.8.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"p-queue-cjs": "7.3.4",
"protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13",
@@ -88,11 +90,16 @@
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3",
"tar": "^6.1.15",
"toad-scheduler": "^1.6.0",
"typedi": "^0.10.0",
"uuid": "^8.3.2",
"winston": "^3.6.0",
"yargs": "^17.3.1"
"winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.3.1",
"tough-cookie": "^4.0.0",
"request-ip": "3.3.0",
"ip2region": "2.3.0"
},
"devDependencies": {
"@ant-design/icons": "^4.7.0",
@@ -102,8 +109,10 @@
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/cross-spawn": "^6.0.2",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/file-saver": "2.0.2",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.185",
@@ -119,38 +128,46 @@
"@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.55",
"@types/request-ip": "0.0.41",
"@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45",
"ahooks": "^3.7.8",
"ansi-to-react": "^6.1.6",
"antd": "^4.24.8",
"antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2",
"axios": "^1.4.0",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"react-hotkeys-hook": "^4.4.1",
"file-saver": "2.0.2",
"lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15",
"nodemon": "^3.0.1",
"prettier": "^2.5.1",
"pretty-bytes": "6.1.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6",
"rc-virtual-list": "3.5.3",
"react": "18.2.0",
"react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "18.2.0",
"react-intl-universal": "^2.6.21",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"ts-proto": "^1.146.0",
"tslib": "^2.4.0",
"tsx": "^3.12.3",
"typescript": "4.8.4",
"umi-request": "^1.4.0",
"typescript": "5.2.2",
"vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0",
+3073 -2136
View File
File diff suppressed because it is too large Load Diff
+26 -18
View File
@@ -21,20 +21,19 @@ DiskWarn=90
## 设置定时任务执行的超时时间,例如1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime=""
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间
## 默认给javascript任务加随机延迟,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay="300"
## 在运行 task 命令时,随机延迟启动任务的最大延迟时间,如 RandomDelay="300" ,表示任务将在 1-300 秒内随机延迟一个秒数,然后再运行,取消延迟赋值为空
RandomDelay=""
## 需要随机延迟运行任务的文件后缀,直接写后缀名即可,多个后缀用空格分开,例如: js py ts
## 默认仅给javascript任务加随机延迟,其它任务按定时规则准点运行。全部任务随机延迟赋值为空
RandomDelayFileExtensions="js"
RandomDelayFileExtensions=""
## 每小时的第几分钟准点运行任务,当在这些时间运行任务时将忽略 RandomDelay 配置,不会被随机延迟
## 默认是第0分钟和第30分钟,例如21:00或21:30分的任务将会准点运行。不需要准点运行赋值为空
RandomDelayIgnoredMinutes="0 30"
RandomDelayIgnoredMinutes=""
## 如果你自己会写shell脚本,并且希望在每次运行 ql update 命令时,额外运行你的 shell 脚本,请赋值为 "true",默认为true
EnableExtraShell="true"
## 如果你自己会写shell脚本,并且希望在每次容器启动时,额外运行你的 shell 脚本,请赋值为 "true"
EnableExtraShell=""
## 是否自动启动bot,默认不启动,设置为true时自动启动,目前需要自行克隆bot仓库所需代码,存到ql/repo目录下,文件夹命名为dockerbot
AutoStartBot=""
@@ -42,12 +41,6 @@ AutoStartBot=""
## 是否使用第三方bot,默认不使用,使用时填入仓库地址,存到ql/repo目录下,文件夹命名为diybot
BotRepoUrl=""
## 安装python依赖时指定pip源
PipMirror="https://pypi.doubanio.com/simple/"
## 安装node依赖时指定npm源
NpmMirror="https://registry.npmmirror.com"
## 通知环境变量
## 1. Server酱
## https://sct.ftqq.com
@@ -91,6 +84,10 @@ export TG_API_HOST=""
export DD_BOT_TOKEN=""
export DD_BOT_SECRET=""
## 企业微信反向代理地址
## (环境变量名 QYWX_ORIGIN)
export QYWX_ORIGIN=""
## 5. 企业微信机器人
## 官方说明文档:https://work.weixin.qq.com/api/doc/90000/90136/91770
## 下方填写密钥,企业微信推送 webhook 后面的 key
@@ -152,11 +149,8 @@ export AIBOTK_TYPE=""
export AIBOTK_NAME=""
## 14. SMTP
## 暂时只支持在 Python 中调用 notify.py 以发送 SMTP 邮件通知
## smtp_server 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
export SMTP_SERVER=""
## smtp_ssl 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
export SMTP_SSL="false"
## 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
export SMTP_SERVICE=""
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
@@ -164,4 +158,18 @@ export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 15. PushMe
## 官方说明文档:https://push.i-i.me/
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
export PUSHME_KEY=""
## 13. CHRONOCAT
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
export CHRONOCAT_URL=""
export CHRONOCAT_QQ=""
export CHRONOCAT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+347 -44
View File
@@ -11,6 +11,7 @@
*/
const querystring = require('querystring');
const got = require('got');
const $ = new Env();
const timeout = 15000; // 超时时间(单位毫秒)
// =======================================gotify通知设置区域==============================================
@@ -55,6 +56,10 @@ let BARK_ICON = 'https://qn.whyour.cn/logo.png';
let BARK_SOUND = '';
// BARK app推送消息的分组, 默认为"QingLong"
let BARK_GROUP = 'QingLong';
// BARK app推送消息的时效性, 默认为"active"
let BARK_LEVEL = 'active';
// BARK app推送消息的跳转URL
let BARK_URL = '';
// =======================================telegram机器人通知设置区域===========================================
// 此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
@@ -68,7 +73,7 @@ let TG_PROXY_HOST = ''; //例如:127.0.0.1(环境变量名:TG_PROXY_HOST)
let TG_PROXY_PORT = ''; // 例如:1080(环境变量名:TG_PROXY_PORT)
let TG_PROXY_AUTH = ''; // tg代理配置认证参数
// Telegram api自建的反向代理地址(不懂可忽略,telegram机器人通知推送功能中非必填),默认tg官方api(环境变量名:TG_API_HOST)
let TG_API_HOST = 'api.telegram.org';
let TG_API_HOST = 'https://api.telegram.org';
// =======================================钉钉机器人通知设置区域===========================================
// 此处填你钉钉 bot 的webhook,例如:5a544165465465645d0f31dca676e7bd07415asdasd
// (环境变量名 DD_BOT_TOKEN)
@@ -76,6 +81,10 @@ let DD_BOT_TOKEN = '';
// 密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串
let DD_BOT_SECRET = '';
// =======================================企业微信基础设置===========================================
// 企业微信反向代理地址
// (环境变量名 QYWX_ORIGIN)
let QYWX_ORIGIN = '';
// =======================================企业微信机器人通知设置区域===========================================
// 此处填你企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa
// (环境变量名 QYWX_KEY)
@@ -104,13 +113,6 @@ let IGOT_PUSH_KEY = '';
let PUSH_PLUS_TOKEN = '';
let PUSH_PLUS_USER = '';
// =======================================Cool Push设置区域=======================================
//官方文档:https://cp.xuthus.cc/docs
//QQ_SKEY: Cool Push登录授权后推送消息的调用代码Skey
//QQ_MODE: 推送模式详情请登录获取QQ_SKEY后见https://cp.xuthus.cc/feat
let QQ_SKEY = '';
let QQ_MODE = '';
// =======================================智能微秘书设置区域=======================================
// 官方文档:http://wechat.aibotk.com/docs/about
// AIBOTK_KEY 填写智能微秘书个人中心的apikey
@@ -126,17 +128,37 @@ let AIBOTK_NAME = '';
let FSKEY = '';
// =======================================SMTP 邮件设置区域=======================================
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
// SMTP_SERVICE: 邮箱服务名称,比如126、163、Gmail、QQ等,支持列表 https://github.com/nodemailer/nodemailer/blob/master/lib/well-known/services.json
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
let SMTP_SERVER = '';
let SMTP_SSL = 'false';
let SMTP_SERVICE = '';
let SMTP_EMAIL = '';
let SMTP_PASSWORD = '';
let SMTP_NAME = '';
// =======================================PushMe通知设置区域===========================================
// 官方文档:https://push.i-i.me/
// 此处填你的PushMe KEY.
let PUSHME_KEY = '';
// =======================================CHRONOCAT通知设置区域===========================================
// CHRONOCAT_URL Red协议连接地址 例: http://127.0.0.1:16530
// CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
let CHRONOCAT_TOKEN = ''; // CHRONOCAT 生成的访问密钥
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
// =======================================自定义通知设置区域=======================================
// 自定义通知 接收回调的URL
let WEBHOOK_URL = '';
let WEBHOOK_BODY = '';
let WEBHOOK_HEADERS = '';
let WEBHOOK_METHOD = '';
let WEBHOOK_CONTENT_TYPE = '';
// ==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
@@ -202,6 +224,12 @@ if (process.env.BARK_PUSH) {
if (process.env.BARK_GROUP) {
BARK_GROUP = process.env.BARK_GROUP;
}
if (process.env.BARK_LEVEL) {
BARK_LEVEL = process.env.BARK_LEVEL;
}
if (process.env.BARK_URL) {
BARK_URL = process.env.BARK_URL;
}
} else {
if (
BARK_PUSH &&
@@ -230,6 +258,12 @@ if (process.env.DD_BOT_TOKEN) {
}
}
if (process.env.QYWX_ORIGIN) {
QYWX_ORIGIN = process.env.QYWX_ORIGIN;
} else {
QYWX_ORIGIN = 'https://qyapi.weixin.qq.com';
}
if (process.env.QYWX_KEY) {
QYWX_KEY = process.env.QYWX_KEY;
}
@@ -263,11 +297,8 @@ if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
if (process.env.SMTP_SERVER) {
SMTP_SERVER = process.env.SMTP_SERVER;
}
if (process.env.SMTP_SSL) {
SMTP_SSL = process.env.SMTP_SSL;
if (process.env.SMTP_SERVICE) {
SMTP_SERVICE = process.env.SMTP_SERVICE;
}
if (process.env.SMTP_EMAIL) {
SMTP_EMAIL = process.env.SMTP_EMAIL;
@@ -278,6 +309,35 @@ if (process.env.SMTP_PASSWORD) {
if (process.env.SMTP_NAME) {
SMTP_NAME = process.env.SMTP_NAME;
}
if (process.env.PUSHME_KEY) {
PUSHME_KEY = process.env.PUSHME_KEY;
}
if (process.env.CHRONOCAT_URL) {
CHRONOCAT_URL = process.env.CHRONOCAT_URL;
}
if (process.env.CHRONOCAT_QQ) {
CHRONOCAT_QQ = process.env.CHRONOCAT_QQ;
}
if (process.env.CHRONOCAT_TOKEN) {
CHRONOCAT_TOKEN = process.env.CHRONOCAT_TOKEN;
}
if (process.env.WEBHOOK_URL) {
WEBHOOK_URL = process.env.WEBHOOK_URL;
}
if (process.env.WEBHOOK_BODY) {
WEBHOOK_BODY = process.env.WEBHOOK_BODY;
}
if (process.env.WEBHOOK_HEADERS) {
WEBHOOK_HEADERS = process.env.WEBHOOK_HEADERS;
}
if (process.env.WEBHOOK_METHOD) {
WEBHOOK_METHOD = process.env.WEBHOOK_METHOD;
}
if (process.env.WEBHOOK_CONTENT_TYPE) {
WEBHOOK_CONTENT_TYPE = process.env.WEBHOOK_CONTENT_TYPE;
}
// ==========================云端环境变量的判断与接收=========================
/**
@@ -298,11 +358,11 @@ async function sendNotify(
desp += author; // 增加作者信息,防止被贩卖等
// 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
let skipTitle = process.env.SKIP_PUSH_TITLE
let skipTitle = process.env.SKIP_PUSH_TITLE;
if (skipTitle) {
if (skipTitle.split('\n').includes(text)) {
console.info(text + "在SKIP_PUSH_TITLE环境变量内,跳过推送!");
return
console.info(text + '在SKIP_PUSH_TITLE环境变量内,跳过推送!');
return;
}
}
@@ -326,6 +386,9 @@ async function sendNotify(
aibotkNotify(text, desp), // 智能微秘书
fsBotNotify(text, desp), // 飞书机器人
smtpNotify(text, desp), // SMTP 邮件
pushMeNotify(text, desp, params), // PushMe
chronocatNotify(text, desp), // Chronocat
webhookNotify(text, desp), // 自定义通知
]);
}
@@ -533,7 +596,7 @@ function BarkNotify(text, desp, params = {}) {
const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp,
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify(
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&level=${BARK_LEVEL}&url=${BARK_URL}&${querystring.stringify(
params,
)}`,
headers: {
@@ -570,7 +633,7 @@ function tgBotNotify(text, desp) {
return new Promise((resolve) => {
if (TG_BOT_TOKEN && TG_USER_ID) {
const options = {
url: `https://${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
url: `${TG_API_HOST}/bot${TG_BOT_TOKEN}/sendMessage`,
json: {
chat_id: `${TG_USER_ID}`,
text: `${text}\n\n${desp}`,
@@ -582,15 +645,19 @@ function tgBotNotify(text, desp) {
timeout,
};
if (TG_PROXY_HOST && TG_PROXY_PORT) {
const tunnel = require('tunnel');
const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
const options = {
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 256,
maxFreeSockets: 256,
proxy: `http://${TG_PROXY_AUTH}${TG_PROXY_HOST}:${TG_PROXY_PORT}`,
};
const httpAgent = new HttpProxyAgent(options);
const httpsAgent = new HttpsProxyAgent(options);
const agent = {
https: tunnel.httpsOverHttp({
proxy: {
host: TG_PROXY_HOST,
port: TG_PROXY_PORT * 1,
proxyAuth: TG_PROXY_AUTH,
},
}),
http: httpAgent,
https: httpsAgent,
};
Object.assign(options, { agent });
}
@@ -692,7 +759,7 @@ function ddBotNotify(text, desp) {
function qywxBotNotify(text, desp) {
return new Promise((resolve) => {
const options = {
url: `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${QYWX_KEY}`,
url: `${QYWX_ORIGIN}/cgi-bin/webhook/send?key=${QYWX_KEY}`,
json: {
msgtype: 'text',
text: {
@@ -754,7 +821,7 @@ function qywxamNotify(text, desp) {
if (QYWX_AM) {
const QYWX_AM_AY = QYWX_AM.split(',');
const options_accesstoken = {
url: `https://qyapi.weixin.qq.com/cgi-bin/gettoken`,
url: `${QYWX_ORIGIN}/cgi-bin/gettoken`,
json: {
corpid: `${QYWX_AM_AY[0]}`,
corpsecret: `${QYWX_AM_AY[1]}`,
@@ -819,7 +886,7 @@ function qywxamNotify(text, desp) {
};
}
options = {
url: `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${accesstoken}`,
url: `${QYWX_ORIGIN}/cgi-bin/message/send?access_token=${accesstoken}`,
json: {
touser: `${ChangeUserId(desp)}`,
agentid: `${QYWX_AM_AY[3]}`,
@@ -1062,31 +1129,31 @@ function fsBotNotify(text, desp) {
}
async function smtpNotify(text, desp) {
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
if (![SMTP_EMAIL, SMTP_PASSWORD].every(Boolean) || !SMTP_SERVICE) {
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
{
const transporter = nodemailer.createTransport({
service: SMTP_SERVICE,
auth: {
user: SMTP_EMAIL,
pass: SMTP_PASSWORD,
},
},
);
});
const addr = SMTP_NAME ? `"${SMTP_NAME}" <${SMTP_EMAIL}>` : SMTP_EMAIL;
const info = await transporter.sendMail({
from: addr,
to: addr,
subject: text,
text: desp,
html: `${desp.replace(/\n/g, '<br/>')}`,
});
if (!!info.messageId) {
transporter.close();
if (info.messageId) {
console.log('SMTP发送通知消息成功🎉\n');
return true;
}
@@ -1097,16 +1164,252 @@ async function smtpNotify(text, desp) {
}
}
function smtpNotify(text, desp) {
function pushMeNotify(text, desp, params = {}) {
return new Promise((resolve) => {
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
if (PUSHME_KEY) {
const options = {
url: `https://push.i-i.me?push_key=${PUSHME_KEY}`,
json: { title: text, content: desp, ...params },
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('PushMeNotify发送通知调用API失败!!\n');
console.log(err);
} else {
if (data === 'success') {
console.log('PushMe发送通知消息成功🎉\n');
} else {
console.log(`${data}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function chronocatNotify(title, desp) {
return new Promise((resolve) => {
if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {
resolve();
return;
}
const user_ids = CHRONOCAT_QQ.match(/user_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
const group_ids = CHRONOCAT_QQ.match(/group_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
const url = `${CHRONOCAT_URL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${CHRONOCAT_TOKEN}`,
};
for (const [chat_type, ids] of [
[1, user_ids],
[2, group_ids],
]) {
if (!ids) {
continue;
}
for (const chat_id of ids) {
const data = {
peer: {
chatType: chat_type,
peerUin: chat_id,
},
elements: [
{
elementType: 1,
textElement: {
content: `${title}\n\n${desp}`,
},
},
],
};
const options = {
url: url,
json: data,
headers,
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('Chronocat发送QQ通知消息失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (chat_type === 1) {
console.log(`QQ个人消息:${ids}推送成功!`);
} else {
console.log(`QQ群消息:${ids}推送成功!`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}
}
});
}
function webhookNotify(text, desp) {
return new Promise((resolve) => {
const { formatBody, formatUrl } = formatNotifyContentFun(
WEBHOOK_URL,
WEBHOOK_BODY,
text,
desp,
);
if (!formatUrl && !formatBody) {
resolve();
return;
}
const headers = parseHeaders(WEBHOOK_HEADERS);
const body = parseBody(formatBody, WEBHOOK_CONTENT_TYPE);
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
const options = {
method: WEBHOOK_METHOD,
headers,
allowGetBody: true,
...bodyParam,
timeout,
retry: 1,
};
if (WEBHOOK_METHOD) {
got(formatUrl, options).then((resp) => {
try {
if (resp.statusCode !== 200) {
console.log(`自定义发送通知消息失败!!\n${resp.body}`);
} else {
console.log(`自定义发送通知消息成功🎉。\n${resp.body}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(resp.body);
}
});
} else {
resolve();
}
});
}
function parseHeaders(headers) {
if (!headers) return {};
const parsed = {};
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 parseBody(body, contentType) {
if (contentType === 'text/plain' || !body) {
return body;
}
const parsed = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
function formatBodyFun(contentType, body) {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
function formatNotifyContentFun(url, body, title, content) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
.replaceAll('$title', encodeURIComponent(title))
.replaceAll('$content', encodeURIComponent(content)),
formatBody: body
.replaceAll('$title', title)
.replaceAll('$content', content),
};
}
module.exports = {
sendNotify,
BARK_PUSH,
+292 -47
View File
@@ -40,6 +40,8 @@ push_config = {
'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音
'BARK_ICON': '', # bark 推送图标
'BARK_LEVEL': '', # bark 推送时效性
'BARK_URL': '', # bark 推送跳转URL
'CONSOLE': True, # 控制台输出
@@ -76,6 +78,8 @@ push_config = {
'QMSG_KEY': '', # qmsg 酱的 QMSG_KEY
'QMSG_TYPE': '', # qmsg 酱的 QMSG_TYPE
'QYWX_ORIGIN': '', # 企业微信代理地址
'QYWX_AM': '', # 企业微信应用
'QYWX_KEY': '', # 企业微信机器人
@@ -96,6 +100,18 @@ push_config = {
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
'CHRONOCAT_QQ': '', # qq号
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
'CHRONOCAT_URL': '', # CHRONOCAT的url地址
'WEBHOOK_URL': '', # 自定义通知 请求地址
'WEBHOOK_BODY': '', # 自定义通知 请求体
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
}
notify_function = []
# fmt: on
@@ -126,6 +142,8 @@ def bark(title: str, content: str) -> None:
"BARK_GROUP": "group",
"BARK_SOUND": "sound",
"BARK_ICON": "icon",
"BARK_LEVEL": "level",
"BARK_URL": "url",
}
params = ""
for pair in filter(
@@ -164,8 +182,7 @@ def dingding_bot(title: str, content: str) -> None:
timestamp = str(round(time.time() * 1000))
secret_enc = push_config.get("DD_BOT_SECRET").encode("utf-8")
string_to_sign = "{}\n{}".format(
timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign = "{}\n{}".format(timestamp, push_config.get("DD_BOT_SECRET"))
string_to_sign_enc = string_to_sign.encode("utf-8")
hmac_code = hmac.new(
secret_enc, string_to_sign_enc, digestmod=hashlib.sha256
@@ -231,8 +248,11 @@ def gotify(title: str, content: str) -> None:
print("gotify 服务启动")
url = f'{push_config.get("GOTIFY_URL")}/message?token={push_config.get("GOTIFY_TOKEN")}'
data = {"title": title, "message": content,
"priority": push_config.get("GOTIFY_PRIORITY")}
data = {
"title": title,
"message": content,
"priority": push_config.get("GOTIFY_PRIORITY"),
}
response = requests.post(url, data=data).json()
if response.get("id"):
@@ -291,9 +311,13 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务的 DEER_KEY 未设置!!\n取消推送")
return
print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown",
"pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push'
data = {
"text": title,
"desp": content,
"type": "markdown",
"pushkey": push_config.get("DEER_KEY"),
}
url = "https://api2.pushdeer.com/message/push"
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
@@ -313,7 +337,7 @@ def chat(title: str, content: str) -> None:
print("chat 服务的 CHAT_URL或CHAT_TOKEN 未设置!!\n取消推送")
return
print("chat 服务启动")
data = 'payload=' + json.dumps({'text': title + '\n' + content})
data = "payload=" + json.dumps({"text": title + "\n" + content})
url = push_config.get("CHAT_URL") + push_config.get("CHAT_TOKEN")
response = requests.post(url, data=data)
@@ -347,11 +371,9 @@ def pushplus_bot(title: str, content: str) -> None:
print("PUSHPLUS 推送成功!")
else:
url_old = "http://pushplus.hxtrip.com/send"
headers["Accept"] = "application/json"
response = requests.post(
url=url_old, data=body, headers=headers).json()
response = requests.post(url=url_old, data=body, headers=headers).json()
if response["code"] == 200:
print("PUSHPLUS(hxtrip) 推送成功!")
@@ -370,8 +392,7 @@ def qmsg_bot(title: str, content: str) -> None:
print("qmsg 服务启动")
url = f'https://qmsg.zendee.cn/{push_config.get("QMSG_TYPE")}/{push_config.get("QMSG_KEY")}'
payload = {
"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
payload = {"msg": f'{title}\n\n{content.replace("----", "-")}'.encode("utf-8")}
response = requests.post(url=url, params=payload).json()
if response["code"] == 0:
@@ -420,9 +441,12 @@ class WeCom:
self.CORPID = corpid
self.CORPSECRET = corpsecret
self.AGENTID = agentid
self.ORIGIN = "https://qyapi.weixin.qq.com"
if push_config.get("QYWX_ORIGIN"):
self.ORIGIN = push_config.get("QYWX_ORIGIN")
def get_access_token(self):
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
url = f"{self.ORIGIN}/cgi-bin/gettoken"
values = {
"corpid": self.CORPID,
"corpsecret": self.CORPSECRET,
@@ -433,8 +457,7 @@ class WeCom:
def send_text(self, message, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
)
send_values = {
"touser": touser,
@@ -450,8 +473,7 @@ class WeCom:
def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = (
"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token="
+ self.get_access_token()
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
)
send_values = {
"touser": touser,
@@ -485,7 +507,11 @@ def wecom_bot(title: str, content: str) -> None:
return
print("企业微信机器人服务启动")
url = f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
origin = "https://qyapi.weixin.qq.com"
if push_config.get("QYWX_ORIGIN"):
origin = push_config.get("QYWX_ORIGIN")
url = f"{origin}/cgi-bin/webhook/send?key={push_config.get('QYWX_KEY')}"
headers = {"Content-Type": "application/json;charset=utf-8"}
data = {"msgtype": "text", "text": {"content": f"{title}\n\n{content}"}}
response = requests.post(
@@ -508,7 +534,7 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 服务启动")
if push_config.get("TG_API_HOST"):
url = f"https://{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
url = f"{push_config.get('TG_API_HOST')}/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
else:
url = (
f"https://api.telegram.org/bot{push_config.get('TG_BOT_TOKEN')}/sendMessage"
@@ -547,24 +573,28 @@ def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息。
"""
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
if (
not push_config.get("AIBOTK_KEY")
or not push_config.get("AIBOTK_TYPE")
or not push_config.get("AIBOTK_NAME")
):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room':
if push_config.get("AIBOTK_TYPE") == "room":
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n{title}\n{content}'}
"message": {"type": 1, "content": f"【青龙快讯】\n\n{title}\n{content}"},
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
@@ -580,29 +610,225 @@ def smtp(title: str, content: str) -> None:
"""
使用 SMTP 邮件 推送消息。
"""
if not push_config.get("SMTP_SERVER") or not push_config.get("SMTP_SSL") or not push_config.get("SMTP_EMAIL") or not push_config.get("SMTP_PASSWORD") or not push_config.get("SMTP_NAME"):
print("SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送")
if (
not push_config.get("SMTP_SERVER")
or not push_config.get("SMTP_SSL")
or not push_config.get("SMTP_EMAIL")
or not push_config.get("SMTP_PASSWORD")
or not push_config.get("SMTP_NAME")
):
print(
"SMTP 邮件 的 SMTP_SERVER 或者 SMTP_SSL 或者 SMTP_EMAIL 或者 SMTP_PASSWORD 或者 SMTP_NAME 未设置!!\n取消推送"
)
return
print("SMTP 邮件 服务启动")
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = formataddr((Header(push_config.get(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['To'] = formataddr((Header(push_config.get(
"SMTP_NAME"), 'utf-8').encode(), push_config.get("SMTP_EMAIL")))
message['Subject'] = Header(title, 'utf-8')
message = MIMEText(content, "plain", "utf-8")
message["From"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["To"] = formataddr(
(
Header(push_config.get("SMTP_NAME"), "utf-8").encode(),
push_config.get("SMTP_EMAIL"),
)
)
message["Subject"] = Header(title, "utf-8")
try:
smtp_server = smtplib.SMTP_SSL(push_config.get("SMTP_SERVER")) if push_config.get(
"SMTP_SSL") == 'true' else smtplib.SMTP(push_config.get("SMTP_SERVER"))
smtp_server.login(push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_PASSWORD"))
smtp_server.sendmail(push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"), message.as_bytes())
smtp_server = (
smtplib.SMTP_SSL(push_config.get("SMTP_SERVER"))
if push_config.get("SMTP_SSL") == "true"
else smtplib.SMTP(push_config.get("SMTP_SERVER"))
)
smtp_server.login(
push_config.get("SMTP_EMAIL"), push_config.get("SMTP_PASSWORD")
)
smtp_server.sendmail(
push_config.get("SMTP_EMAIL"),
push_config.get("SMTP_EMAIL"),
message.as_bytes(),
)
smtp_server.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f'SMTP 邮件 推送失败!{e}')
print(f"SMTP 邮件 推送失败!{e}")
def pushme(title: str, content: str) -> None:
"""
使用 PushMe 推送消息。
"""
if not push_config.get("PUSHME_KEY"):
print("PushMe 服务的 PUSHME_KEY 未设置!!\n取消推送")
return
print("PushMe 服务启动")
url = f'https://push.i-i.me/?push_key={push_config.get("PUSHME_KEY")}'
data = {
"title": title,
"content": content,
}
response = requests.post(url, data=data)
if response.status_code == 200 and response.text == "success":
print("PushMe 推送成功!")
else:
print(f"PushMe 推送失败!{response.status_code} {response.text}")
def chronocat(title: str, content: str) -> None:
"""
使用 CHRONOCAT 推送消息。
"""
if (
not push_config.get("CHRONOCAT_URL")
or not push_config.get("CHRONOCAT_QQ")
or not push_config.get("CHRONOCAT_TOKEN")
):
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
return
print("CHRONOCAT 服务启动")
user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send'
headers = {
"Content-Type": "application/json",
"Authorization": f'Bearer {push_config.get("CHRONOCAT_TOKEN")}',
}
for chat_type, ids in [(1, user_ids), (2, group_ids)]:
if not ids:
continue
for chat_id in ids:
data = {
"peer": {"chatType": chat_type, "peerUin": chat_id},
"elements": [
{
"elementType": 1,
"textElement": {"content": f"{title}\n\n{content}"},
}
],
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
if chat_type == 1:
print(f"QQ个人消息:{ids}推送成功!")
else:
print(f"QQ群消息:{ids}推送成功!")
else:
if chat_type == 1:
print(f"QQ个人消息:{ids}推送失败!")
else:
print(f"QQ群消息:{ids}推送失败!")
def parse_headers(headers):
if not headers:
return {}
parsed = {}
lines = headers.split("\n")
for line in lines:
i = line.find(":")
if i == -1:
continue
key = line[:i].strip().lower()
val = line[i + 1 :].strip()
parsed[key] = parsed.get(key, "") + ", " + val if key in parsed else val
return parsed
def parse_body(body, content_type):
if not body or content_type == "text/plain":
return body
parsed = {}
lines = body.split("\n")
for line in lines:
i = line.find(":")
if i == -1:
continue
key = line[:i].strip()
val = line[i + 1 :].strip()
if not key or key in parsed:
continue
try:
json_value = json.loads(val)
parsed[key] = json_value
except:
parsed[key] = val
if content_type == "application/x-www-form-urlencoded":
data = urlencode(parsed, doseq=True)
return data
if content_type == "application/json":
data = json.dumps(parsed)
return data
return parsed
def format_notify_content(url, body, title, content):
if "$title" not in url and "$title" not in body:
return {}
formatted_url = url.replace("$title", urllib.parse.quote_plus(title)).replace(
"$content", urllib.parse.quote_plus(content)
)
formatted_body = body.replace("$title", title).replace("$content", content)
return formatted_url, formatted_body
def custom_notify(title: str, content: str) -> None:
"""
通过 自定义通知 推送消息。
"""
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
return
print("自定义通知服务启动")
WEBHOOK_URL = push_config.get("WEBHOOK_URL")
WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD")
WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE")
WEBHOOK_BODY = push_config.get("WEBHOOK_BODY")
WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS")
formatUrl, formatBody = format_notify_content(
WEBHOOK_URL, WEBHOOK_BODY, title, content
)
if not formatUrl and not formatBody:
print("请求头或者请求体中必须包含 $title 和 $content")
return
headers = parse_headers(WEBHOOK_HEADERS)
body = parse_body(formatBody, WEBHOOK_CONTENT_TYPE)
response = requests.request(
method=WEBHOOK_METHOD, url=formatUrl, headers=headers, timeout=15, data=body
)
if response.status_code == 200:
print("自定义通知推送成功!")
else:
print(f"自定义通知推送失败!{response.status_code} {response.text}")
def one() -> str:
@@ -615,6 +841,7 @@ def one() -> str:
return res["hitokoto"] + " ----" + res["from"]
def add_notify_function():
if push_config.get("BARK_PUSH"):
notify_function.append(bark)
if push_config.get("CONSOLE"):
@@ -645,10 +872,30 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
if (
push_config.get("AIBOTK_KEY")
and push_config.get("AIBOTK_TYPE")
and push_config.get("AIBOTK_NAME")
):
notify_function.append(aibotk)
if push_config.get("SMTP_SERVER") and push_config.get("SMTP_SSL") and push_config.get("SMTP_EMAIL") and push_config.get("SMTP_PASSWORD") and push_config.get("SMTP_NAME"):
if (
push_config.get("SMTP_SERVER")
and push_config.get("SMTP_SSL")
and push_config.get("SMTP_EMAIL")
and push_config.get("SMTP_PASSWORD")
and push_config.get("SMTP_NAME")
):
notify_function.append(smtp)
if push_config.get("PUSHME_KEY"):
notify_function.append(pushme)
if (
push_config.get("CHRONOCAT_URL")
and push_config.get("CHRONOCAT_QQ")
and push_config.get("CHRONOCAT_TOKEN")
):
notify_function.append(chronocat)
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify)
def send(title: str, content: str) -> None:
@@ -659,18 +906,16 @@ def send(title: str, content: str) -> None:
# 根据标题跳过一些消息推送,环境变量:SKIP_PUSH_TITLE 用回车分隔
skipTitle = os.getenv("SKIP_PUSH_TITLE")
if skipTitle:
if (title in re.split("\n", skipTitle)):
if title in re.split("\n", skipTitle):
print(f"{title} 在SKIP_PUSH_TITLE环境变量内,跳过推送!")
return
hitokoto = push_config.get("HITOKOTO")
content += "\n\n" + one() if hitokoto else ""
text = one() if hitokoto else ""
content += "\n\n" + text
add_notify_function()
ts = [
threading.Thread(target=mode, args=(
title, content), name=mode.__name__)
threading.Thread(target=mode, args=(title, content), name=mode.__name__)
for mode in notify_function
]
[t.start() for t in ts]
-12
View File
@@ -1,12 +0,0 @@
{
"name": "dependence",
"author": "",
"license": "ISC",
"dependencies": {
"got": "^11.5.1",
"nodemailer": "^6.8.0",
"tough-cookie": "^4.0.0",
"tunnel": "0.0.6",
"ws": "^7.4.3"
}
}
+19 -19
View File
@@ -30,10 +30,10 @@ add_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}')
local sub_id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local sub_id=$4
local schedule="$1"
local command="$2"
local name="$3"
local sub_id="$4"
fi
if [[ ! $sub_id ]]; then
@@ -49,7 +49,7 @@ add_cron_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"sub_id\":$sub_id}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
@@ -69,10 +69,10 @@ update_cron_api() {
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
local schedule="$1"
local command="$2"
local name="$3"
local id="$4"
fi
local api=$(
@@ -85,7 +85,7 @@ update_cron_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--data-raw "{\"name\":\"${name//\"/\\\"}\",\"command\":\"${command//\"/\\\"}\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
@@ -103,8 +103,8 @@ update_cron_command_api() {
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
local command="$1"
local id="$2"
fi
local api=$(
@@ -117,7 +117,7 @@ update_cron_command_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--data-raw "{\"command\":\"${command//\"/\\\"}\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
@@ -130,7 +130,7 @@ update_cron_command_api() {
}
del_cron_api() {
local ids=$1
local ids="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
@@ -183,8 +183,8 @@ update_cron() {
}
notify_api() {
local title=$1
local content=$2
local title="$1"
local content="$2"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
@@ -196,20 +196,20 @@ notify_api() {
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--data-raw "{\"title\":\"${title//\"/\\\"}\",\"content\":\"${content//\"/\\\"}\"}" \
--compressed
)
code=$(echo "$api" | jq -r .code)
message=$(echo "$api" | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功"
echo -e "通知发送成功🎉"
else
echo -e "通知失败(${message})"
fi
}
find_cron_api() {
local params=$1
local params="$1"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/detail?$params&t=$currentTimeStamp" \
-3
View File
@@ -25,9 +25,6 @@ fi
echo -e "\nbot文件下载成功...\n"
echo -e "3、安装python3依赖...\n"
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
+1 -15
View File
@@ -9,9 +9,6 @@ reset_env() {
npm_install_2 $dir_root
echo -e "---> 青龙依赖安装完成\n"
echo -e "---> 3. 开始安装脚本依赖\n"
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
echo -e "---> 脚本依赖安装完成\n"
}
@@ -76,20 +73,9 @@ check_pm2() {
main() {
echo -e "=====> 开始检测"
npm i -g pnpm@8.3.1
npm i -g pnpm@8.3.1 pm2 tsx
patch_version
apk add procps netcat-openbsd
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
cd && pnpm config set registry $NpmMirror
pnpm install -g
fi
pnpm add -g pm2 tsx
reset_env
copy_dep
check_ql
Executable
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
store_env_vars() {
initial_vars=($(env | cut -d= -f1))
}
restore_env_vars() {
for key in $(env | cut -d= -f1); do
if ! [[ " ${initial_vars[@]} " =~ " $key " ]]; then
unset "$key"
fi
done
}
store_env_vars
+29 -19
View File
@@ -38,7 +38,7 @@ gen_array_scripts() {
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
array_scripts_name[i]=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
@@ -86,10 +86,15 @@ check_server() {
fi
}
env_str_to_array() {
local IFS="&"
read -ra array <<<"${!env_param}"
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]] && [[ "$no_delay" != "true" ]]; then
random_delay "$file_param"
fi
@@ -100,7 +105,7 @@ run_normal() {
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
$timeoutCmd $which_program $file_param "${script_params[@]}"
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
@@ -113,8 +118,7 @@ run_concurrent() {
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
env_str_to_array
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
@@ -125,11 +129,13 @@ run_concurrent() {
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local cookieStr=$(
IFS="&"
echo "${array_run[*]}"
)
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
env_str_to_array
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
cd $dir_scripts
@@ -139,9 +145,9 @@ run_concurrent() {
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
export "${env_param}=${array[i]}"
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
done
wait
@@ -161,8 +167,7 @@ run_designated() {
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
env_str_to_array
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
@@ -173,8 +178,11 @@ run_designated() {
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local cookieStr=$(
IFS="&"
echo "${array_run[*]}"
)
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
cd $dir_scripts
local relative_path="${file_param%/*}"
@@ -182,7 +190,7 @@ run_designated() {
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
$timeoutCmd $which_program $file_param "${script_params[@]}"
}
## 运行其他命令
@@ -233,6 +241,8 @@ main() {
fi
}
handle_task_before "$@"
main "$@"
handle_task_after "$@"
handle_task_start "${task_shell_params[@]}"
run_task_before "${task_shell_params[@]}"
main "${task_shell_params[@]}"
run_task_after "${task_shell_params[@]}"
handle_task_end "${task_shell_params[@]}"
+43 -87
View File
@@ -2,6 +2,7 @@
## 目录
dir_root=$QL_DIR
dir_tmp=$dir_root/.tmp
dir_data=$dir_root/data
dir_shell=$dir_root/shell
dir_sample=$dir_root/sample
@@ -18,7 +19,6 @@ dir_update_log=$dir_log/update
ql_static_repo=$dir_repo/static
## 文件
file_ecosystem_js=$dir_root/ecosystem.config.js
file_config_sample=$dir_sample/config.sample.sh
file_env=$dir_config/env.sh
file_sharecode=$dir_config/sharecode.sh
@@ -68,9 +68,10 @@ import_config() {
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-"/"}
ql_port=${QlPort:-"5700"}
command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
proxy_url=${ProxyUrl:-""}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
@@ -98,9 +99,6 @@ set_proxy() {
unset_proxy() {
unset http_proxy
unset https_proxy
unset ftp_proxy
unset all_proxy
unset no_proxy
}
make_dir() {
@@ -175,6 +173,7 @@ define_cmd() {
}
fix_config() {
make_dir $dir_tmp
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
@@ -263,6 +262,7 @@ npm_install_sub() {
else
pnpm install --loglevel error --production
fi
exit_status=$?
}
npm_install_2() {
@@ -283,90 +283,22 @@ diff_and_copy() {
fi
}
update_depend() {
local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
cd $dir_current
}
git_clone_scripts() {
local url="$1"
local dir="$2"
local branch="$3"
local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
echo -e "开始拉取仓库 ${uniq_path}$dir\n"
set_proxy "$proxy"
git clone --depth=1 $part_cmd $url $dir
exit_status=$?
reset_branch "$branch" "$dir"
unset_proxy
}
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
local proxy="$3"
cd $dir_work
echo -e "开始更新仓库:$dir_work"
set_proxy "$proxy"
if [[ ! $branch ]]; then
branch=$(cd $dir_work && git remote show origin | grep 'HEAD branch' | cut -d' ' -f5)
fi
local pre_commit_id=$(git rev-parse --short HEAD)
reset_branch "$branch" "$dir_work"
git fetch --depth 1 origin $branch
exit_status=$?
reset_branch "$branch" "$dir_work"
local cur_commit_id=$(git rev-parse --short HEAD)
if [[ $cur_commit_id != $pre_commit_id ]]; then
exit_status=0
fi
unset_proxy
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
cd $dir_work
if [[ -d "$dir_work/.git" ]]; then
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
else
git init
git remote add origin $url &>/dev/null
fi
cd $dir_current
}
reset_branch() {
local branch="$1"
local part_cmd="origin/${branch}"
git remote set-branches origin $branch
git reset --hard $part_cmd &>/dev/null
git checkout -b $branch $part_cmd &>/dev/null
}
random_range() {
local beg=$1
local end=$2
@@ -375,10 +307,9 @@ random_range() {
reload_pm2() {
cd $dir_root
# 代理会影响 grpc 服务
unset_proxy
restore_env_vars
pm2 flush &>/dev/null
pm2 startOrGracefulReload $file_ecosystem_js
pm2 startOrGracefulReload ecosystem.config.js
}
diff_time() {
@@ -430,8 +361,6 @@ format_timestamp() {
patch_version() {
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
@@ -471,8 +400,18 @@ init_nginx() {
local aliasStr=""
local rootStr=""
if [[ $ql_base_url != "/" ]]; then
if [[ $ql_base_url != /* ]]; then
ql_base_url="/$ql_base_url"
fi
if [[ $ql_base_url != */ ]]; then
ql_base_url="$ql_base_url/"
fi
location_url="^~${ql_base_url%*/}"
aliasStr="alias ${dir_static}/dist;"
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
mv temp.html "${dir_static}/dist/index.html"
fi
else
rootStr="root ${dir_static}/dist;"
fi
@@ -481,27 +420,45 @@ init_nginx() {
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6)
ipv6Str=""
local ipv6=$(ip a | grep inet6)
local ipv6Str=""
if [[ $ipv6 ]]; then
ipv6Str="listen [::]:5700 ipv6only=on;"
ipv6Str="listen [::]:${ql_port} ipv6only=on;"
fi
local ipv4Str="listen ${ql_port};"
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
}
handle_task_before() {
handle_task_start() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n"
}
run_task_before() {
[[ $is_macos -eq 0 ]] && check_server
. $file_task_before "$@"
if [[ $task_before ]]; then
echo -e "执行前置命令\n"
eval "$task_before"
echo -e "\n执行前置命令结束\n"
fi
}
handle_task_after() {
run_task_after() {
. $file_task_after "$@"
if [[ $task_after ]]; then
echo -e "\n执行后置命令\n"
eval "$task_after"
echo -e "\n执行后置命令结束"
fi
}
handle_task_end() {
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
@@ -509,8 +466,7 @@ handle_task_after() {
[[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     "
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
}
+23 -10
View File
@@ -5,16 +5,16 @@ dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 3 20 15 14
trap "single_hanle" 2 3 20 15 14 19 1
single_hanle() {
eval handle_task_after "$@" "$cmd"
eval handle_task_end "$@" "$cmd"
exit 1
}
## 选择python3还是node
define_program() {
local file_param=$1
if [[ $file_param == *.js ]]; then
if [[ $file_param == *.js ]] || [[ $file_param == *.mjs ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
@@ -70,10 +70,10 @@ handle_log_path() {
log_path="$real_log_path"
fi
cmd=">> $dir_log/$log_path 2>&1"
make_dir "$dir_log/$log_dir"
if [[ "$show_log" == "true" ]]; then
cmd="2>&1 | tee -a $dir_log/$log_path"
make_dir "$dir_log/$log_dir"
if [[ "$no_tee" == "true" ]]; then
cmd=">> $dir_log/$log_path 2>&1"
fi
if [[ "$real_time" == "true" ]]; then
@@ -95,6 +95,21 @@ format_params() {
fi
fi
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
# 分割 task 内置参数和脚本参数
task_shell_params=()
script_params=()
found_double_dash=false
for arg in "$@"; do
if $found_double_dash; then
script_params+=("$arg")
elif [ "$arg" == "--" ]; then
found_double_dash=true
else
task_shell_params+=("$arg")
fi
done
}
init_begin_time() {
@@ -119,11 +134,9 @@ if [[ $max_time ]]; then
fi
format_params "$@"
define_program "$@"
handle_log_path "$@"
define_program "${task_shell_params[@]}"
handle_log_path "${task_shell_params[@]}"
init_begin_time
eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
exit 0
+96 -66
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
dir_shell=$QL_DIR/shell
. $dir_shell/env.sh
. $dir_shell/share.sh
. $dir_shell/api.sh
@@ -31,7 +32,7 @@ output_list_add_drop() {
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
echo -e "检测到有${type}的定时任务:"
cat $list
echo
fi
@@ -44,7 +45,7 @@ del_cron() {
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
echo -e "开始尝试自动删除失效的定时任务..."
for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task.* $cron" | perl -pe "s|.*ID=(.*) $cmd_task.* $cron\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
@@ -54,7 +55,7 @@ del_cron() {
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
@@ -75,7 +76,7 @@ del_cron() {
add_cron() {
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
echo -e "开始尝试自动添加定时任务..."
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
@@ -92,7 +93,7 @@ add_cron() {
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
@@ -130,18 +131,14 @@ update_repo() {
make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" "${proxy}"
else
rm -rf ${repo_path} &>/dev/null
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
echo -e "\n拉取 ${uniq_path} 成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
echo -e "\n拉取 ${uniq_path} 失败,请检查日志...\n"
fi
}
@@ -193,7 +190,7 @@ update_raw() {
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:.*\('\''\|"\)\([^"'\'']*\)\('\''\|"\).*:\2:' | sed 's:"::g' | sed "s:'::g" | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
@@ -233,62 +230,95 @@ usage() {
echo -e "9. $cmd_update resettfa # 禁用两步登录"
}
## 更新qinglong
update_qinglong() {
local mirror="gitee"
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
mirror="github"
fi
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
reload_qinglong() {
local reload_target="${1}"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]]; then
primary_branch="develop"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
if [[ "$reload_target" == 'system' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
fi
if [[ "$reload_target" == 'data' ]]; then
rm -rf ${dir_root}/data
cp -rf ${dir_tmp}/data ${dir_root}/
fi
reload_pm2
}
## 更新qinglong
update_qinglong() {
rm -rf ${dir_tmp}/*
local mirror="gitee"
local downloadQLUrl="https://gitee.com/whyour/qinglong/repository/archive"
local downloadStaticUrl="https://gitee.com/whyour/qinglong-static/repository/archive"
local githubStatus=$(curl -s -m 2 -IL "https://google.com" | grep 200)
if [[ ! -z $githubStatus ]]; then
mirror="github"
downloadQLUrl="https://github.com/whyour/qinglong/archive/refs/heads"
downloadStaticUrl="https://github.com/whyour/qinglong-static/archive/refs/heads"
fi
echo -e "使用 ${mirror} 源更新...\n"
local primary_branch="master"
if [[ "${QL_BRANCH}" == "develop" ]] || [[ "${QL_BRANCH}" == "debian" ]] || [[ "${QL_BRANCH}" == "debian-dev" ]]; then
primary_branch="${QL_BRANCH}"
fi
wget -cqO "${dir_tmp}/ql.zip" "${downloadQLUrl}/${primary_branch}.zip"
exit_status=$?
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh
update_depend
echo -e "更新青龙源文件成功...\n"
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
update_qinglong_static "$1" "$primary_branch"
update_qinglong_static
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
echo -e "更新青龙源文件失败,请检查网络...\n"
fi
}
update_qinglong_static() {
local no_restart="$1"
local primary_branch="$2"
local url="https://${mirror}.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
rm -rf ${ql_static_repo} &>/dev/null
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
wget -cqO "${dir_tmp}/static.zip" "${downloadStaticUrl}/${primary_branch}.zip"
exit_status=$?
if [[ $exit_status -eq 0 ]]; then
echo -e "更新青龙静态资源成功...\n"
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
check_update_dep
else
echo -e "更新青龙静态资源失败,请检查网络...\n"
fi
}
check_update_dep() {
echo -e "\n开始检测依赖...\n"
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n依赖检测安装成功...\n"
echo -e "更新包下载成功..."
if [[ "$needRestart" == 'true' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
cp -rf ${dir_tmp}/qinglong-static-${primary_branch}/* ${dir_static}/
cp -f $file_config_sample $dir_config/config.sample.sh
reload_pm2
fi
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
echo -e "\n依赖检测安装失败,请检查网络...\n"
fi
}
@@ -363,10 +393,10 @@ gen_list_repo() {
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
files=$(echo "$files" | egrep "$path")
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
files=$(echo "$files" | egrep -v "$blackword")
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
@@ -374,7 +404,7 @@ gen_list_repo() {
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
results=$(eval $cmd | sed 's/^..//' | egrep "$dependence")
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
@@ -387,7 +417,7 @@ gen_list_repo() {
fi
for file in ${files}; do
filename=$(basename $file)
filename=$(basename "$file")
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
# cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task.* ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
@@ -442,8 +472,13 @@ main() {
local log_path="${log_dir}/${log_time}.log"
local file_path="$dir_log/$log_path"
cmd="2>&1 | tee -a $file_path"
if [[ "$no_tee" == "true" ]]; then
cmd=">> $file_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
fi
if [[ "$real_time" == "true" ]]; then
cmd=""
fi
local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format")
@@ -456,15 +491,14 @@ main() {
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
fi
case $p1 in
update)
fix_config
eval update_qinglong "$2" $cmd
local needRestart=${p2:-"true"}
eval update_qinglong $cmd
;;
reload)
eval reload_qinglong "$p2" $cmd
;;
extra)
eval run_extra_shell $cmd
@@ -521,10 +555,6 @@ main() {
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time 秒     " $cmd
fi
if [[ -f $file_path ]]; then
cat $file_path
fi
}
main "$@"
+22
View File
@@ -1,4 +1,26 @@
const baseUrl = window.__ENV__QlBaseUrl || '/';
import { setLocale } from '@umijs/max';
import intl from 'react-intl-universal';
export function rootContainer(container: any) {
const locales = {
'en': require('./locales/en-US.json'),
'zh': require('./locales/zh-CN.json'),
};
let currentLocale = intl.determineLocale({
urlLocaleKey: 'lang',
cookieLocaleKey: 'lang',
localStorageLocaleKey: 'lang',
}).slice(0, 2);
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
currentLocale = 'zh';
}
intl.init({ currentLocale, locales });
setLocale(currentLocale === 'zh' ? 'zh-CN' : 'en-US');
return container;
}
export function modifyClientRenderOpts(memo: any) {
return {
+5 -1
View File
@@ -1,3 +1,4 @@
import intl from 'react-intl-universal';
import React, { useRef, useState, useEffect } from 'react';
import { Tooltip, Typography } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
@@ -28,7 +29,10 @@ const Copy = ({ text }: { text: string }) => {
return (
<Link onClick={copyText} style={{ marginLeft: 1 }}>
<CopyToClipboard text={text}>
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
<Tooltip
key="copy"
title={copied ? intl.get('复制成功') : intl.get('复制')}
>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Tooltip>
</CopyToClipboard>
+22
View File
@@ -0,0 +1,22 @@
import { useRequest } from 'ahooks';
import { Service, Options } from 'ahooks/lib/useRequest/src/types';
import { Spin, Typography } from 'antd';
export default function Name<
TData extends { data?: { name: string } },
TParams,
>({
service,
options,
}: {
service: Service<TData, [TParams]>;
options: Options<TData, [TParams]>;
}) {
const { loading, data } = useRequest(service, options);
return (
<Spin spinning={loading}>
<Typography.Text ellipsis={true}>{data?.data?.name}</Typography.Text>
</Spin>
);
}
+2 -1
View File
@@ -1,3 +1,4 @@
import intl from 'react-intl-universal';
import { Tag, Input } from 'antd';
import { TweenOneGroup } from 'rc-tween-one';
import { PlusOutlined } from '@ant-design/icons';
@@ -101,7 +102,7 @@ const EditableTagGroup = ({
onClick={showInput}
style={{ borderStyle: 'dashed', cursor: 'pointer' }}
>
<PlusOutlined />
<PlusOutlined /> {intl.get('新建')}
</Tag>
)}
</>
-404
View File
@@ -1,404 +0,0 @@
import React, {
useRef,
useEffect,
useContext,
createContext,
useReducer,
useState,
useMemo,
} from 'react';
import { throttle, isNumber, debounce } from 'lodash';
const initialState = {
// 行高度
rowHeight: 0,
// 当前的scrollTop
curScrollTop: 0,
// 总行数
totalLen: 0,
};
function reducer(state, action) {
const { curScrollTop, totalLen, ifScrollTopClear, scrollTop } = action;
let stateScrollTop = state.curScrollTop;
switch (action.type) {
// 改变trs 即 改变渲染的列表trs
case 'changeTrs':
return {
...state,
curScrollTop,
};
// 更改totalLen
case 'changeTotalLen':
if (totalLen === 0) {
stateScrollTop = 0;
}
return {
...state,
totalLen,
curScrollTop: stateScrollTop,
};
case 'reset':
return {
...state,
curScrollTop: ifScrollTopClear ? 0 : scrollTop ?? state.curScrollTop,
};
default:
throw new Error();
}
}
// ==============全局常量 ================== //
const DEFAULT_VID = 'vtable';
const vidMap = new Map();
let preData = 0;
// ===============context ============== //
const ScrollContext = createContext({
dispatch: undefined,
renderLen: 1,
start: 0,
offsetStart: 0,
// =============
rowHeight: initialState.rowHeight,
totalLen: 0,
vid: DEFAULT_VID,
});
// =============组件 =================== //
function VCell(props: any): JSX.Element {
const { children, ...restProps } = props;
return (
<td {...restProps}>
<div>{children[1]}</div>
</td>
);
}
function VRow(props: any, ref: any): JSX.Element {
const { rowHeight } = useContext(ScrollContext);
const { children, style, ...restProps } = props;
const trRef = useRef<HTMLTableRowElement>(null);
return (
<tr
{...restProps}
ref={Object.prototype.hasOwnProperty.call(ref, 'current') ? ref : trRef}
style={{
...style,
height: rowHeight || 'auto',
boxSizing: 'border-box',
}}
>
{children}
</tr>
);
}
function VWrapper(props: any): JSX.Element {
const { children, ...restProps } = props;
const { renderLen, start, dispatch, vid, totalLen } =
useContext(ScrollContext);
const contents = useMemo(() => {
return children[1];
}, [children]);
const contentsLen = useMemo(() => {
return contents?.length ?? 0;
}, [contents]);
useEffect(() => {
if (totalLen !== contentsLen) {
dispatch({
type: 'changeTotalLen',
totalLen: contentsLen ?? 0,
});
}
}, [contentsLen, dispatch, vid, totalLen]);
let tempNode = null;
if (Array.isArray(contents) && contents.length) {
tempNode = [
children[0],
contents.slice(start, start + (renderLen ?? 1)).map((item) => {
if (Array.isArray(item)) {
// 兼容antd v4.3.5 --- rc-table 7.8.1及以下
return item[0];
}
// 处理antd ^v4.4.0 --- rc-table ^7.8.2
return item;
}),
];
} else {
tempNode = children;
}
return <tbody {...restProps}>{tempNode}</tbody>;
}
function VTable(props: any, otherParams): JSX.Element {
const { style, children, ...rest } = props;
const { width, ...rest_style } = style;
const { vid, scrollY, resetScrollTopWhenDataChange, rowHeight, scrollTop } =
otherParams ?? {};
const [state, dispatch] = useReducer(reducer, {
...initialState,
curScrollTop: scrollTop,
rowHeight,
});
const wrap_tableRef = useRef<HTMLDivElement>(null);
const tableRef = useRef<HTMLTableElement>(null);
const ifChangeRef = useRef(false);
// 数据的总条数
const [totalLen, setTotalLen] = useState<number>(
children[1]?.props?.data?.length ?? 0,
);
useEffect(() => {
setTotalLen(state.totalLen);
}, [state.totalLen]);
// 组件卸载的清除操作
useEffect(() => {
return () => {
vidMap.delete(vid);
};
}, [vid]);
// 数据变更
useEffect(() => {
ifChangeRef.current = true;
// console.log('数据变更')
if (isNumber(children[1]?.props?.data?.length)) {
dispatch({
type: 'changeTotalLen',
totalLen: children[1]?.props?.data?.length ?? 0,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [children[1].props.data]);
// table总高度
const tableHeight = useMemo<string | number>(() => {
let temp: string | number = 'auto';
if (rowHeight && totalLen) {
temp = rowHeight * totalLen;
}
return temp;
}, [totalLen]);
// table的scrollY值
const [tableScrollY, setTableScrollY] = useState(0);
// tableScrollY 随scrollY / tableHeight 进行变更
useEffect(() => {
let temp = 0;
if (typeof scrollY === 'string') {
temp =
(wrap_tableRef.current?.parentNode as HTMLElement)?.offsetHeight ?? 0;
} else {
temp = scrollY;
}
// if (isNumber(tableHeight) && tableHeight < temp) {
// temp = tableHeight;
// }
// 处理tableScrollY <= 0的情况
if (temp <= 0) {
temp = 0;
}
setTableScrollY(temp);
}, [scrollY, tableHeight]);
// 渲染的条数
const renderLen = useMemo<number>(() => {
let temp = 1;
if (rowHeight && totalLen && tableScrollY) {
if (tableScrollY <= 0) {
temp = 0;
} else {
const tempRenderLen = ((tableScrollY / rowHeight) | 0) + 10;
// console.log('tempRenderLen', tempRenderLen)
// temp = tempRenderLen > totalLen ? totalLen : tempRenderLen;
temp = tempRenderLen;
}
}
return temp;
}, [totalLen, tableScrollY]);
// 渲染中的第一条
let start = rowHeight ? (state.curScrollTop / rowHeight) | 0 : 0;
start = start < 5 ? 0 : start - 5 + 1;
// 偏移量
let offsetStart = state.curScrollTop % (rowHeight * 5);
if (start > 0) {
offsetStart = offsetStart % rowHeight;
}
// console.log(offsetStart)
// offsetStart= offsetStart%rowHeight
// 用来优化向上滚动出现的空白
if (state.curScrollTop && state.curScrollTop >= rowHeight * 5) {
// start -= 1
// if (offsetStart >= rowHeight) {
// offsetStart +=
// } else {
offsetStart += rowHeight * 4;
// }
} else {
start = 0;
}
// console.log(state.curScrollTop, start, offsetStart)
// 数据变更 操作scrollTop
useEffect(() => {
const scrollNode = wrap_tableRef.current?.parentNode as HTMLElement;
if (ifChangeRef?.current) {
// console.log(scrollNode)
ifChangeRef.current = false;
if (resetScrollTopWhenDataChange) {
// 重置scrollTop
if (scrollNode) {
scrollNode.scrollTop = 0;
}
dispatch({ type: 'reset', ifScrollTopClear: true });
} else {
// console.log(preData)
// scrollNode.scrollTop = preData+53
// 不重置scrollTop 不清空curScrollTop
dispatch({ type: 'reset', ifScrollTopClear: false });
}
}
if (vidMap.has(vid)) {
vidMap.set(vid, {
...vidMap.get(vid),
scrollNode,
});
}
}, [totalLen, resetScrollTopWhenDataChange, vid, children]);
useEffect(() => {
const throttleScroll = throttle((e) => {
const scrollTop: number = e?.target?.scrollTop ?? 0;
// const scrollHeight: number = e?.target?.scrollHeight ?? 0
// const clientHeight: number = e?.target?.clientHeight ?? 0
if (scrollTop) {
preData = scrollTop;
}
// 到底了 没有滚动条就不会触发reachEnd. 建议设置scrolly高度少点或者数据量多点.
// 若renderLen大于totalLen, 置空curScrollTop. => table paddingTop会置空.
dispatch({
type: 'changeTrs',
curScrollTop: renderLen <= totalLen ? scrollTop : 0,
});
}, 60);
const ref = wrap_tableRef?.current?.parentNode as HTMLElement;
if (ref) {
ref.addEventListener('scroll', throttleScroll, { passive: true });
}
return () => {
ref.removeEventListener('scroll', throttleScroll);
};
}, [renderLen, totalLen]);
return (
<div
className="virtuallist"
ref={wrap_tableRef}
style={{
width: '100%',
position: 'relative',
height: tableHeight,
boxSizing: 'border-box',
paddingTop: state.curScrollTop,
}}
>
<ScrollContext.Provider
value={{
dispatch,
start,
offsetStart,
renderLen,
totalLen,
vid,
rowHeight,
}}
>
<table
{...rest}
ref={tableRef}
style={{
...rest_style,
width,
position: 'relative',
transform: `translateY(-${offsetStart}px)`,
}}
>
{children}
</table>
</ScrollContext.Provider>
</div>
);
}
// ================导出===================
export function VList(props: {
height: number;
// 唯一标识
vid?: string;
rowHeight: number | string;
reset?: boolean;
scrollTop?: number | string;
}): any {
const { vid = DEFAULT_VID, rowHeight, height, reset, scrollTop } = props;
const resetScrollTopWhenDataChange = reset ?? true;
if (!vidMap.has(vid)) {
vidMap.set(vid, { _id: vid });
}
return {
table: (p) =>
VTable(p, {
vid,
scrollY: height,
rowHeight,
resetScrollTopWhenDataChange,
scrollTop,
}),
body: {
wrapper: VWrapper,
row: VRow,
cell: VCell,
},
};
}
+14
View File
@@ -0,0 +1,14 @@
import { RefObject, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer';
export default <T extends HTMLElement>(target: RefObject<T>) => {
const [height, setHeight] = useState<number>(0);
useResizeObserver(target, (entry) => {
let _height = entry.target.clientHeight;
if (height !== _height) {
setHeight(_height);
}
});
return height;
};
+2 -2
View File
@@ -1,9 +1,9 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import { RefObject, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer';
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(
target: MutableRefObject<T>,
target: RefObject<T>,
extraHeight?: number,
) => {
const [height, setHeight] = useState<number>(0);
+14 -23
View File
@@ -1,14 +1,5 @@
import {
FormOutlined,
FieldTimeOutlined,
DiffOutlined,
SettingOutlined,
CodeOutlined,
FolderOutlined,
RadiusSettingOutlined,
ControlOutlined,
ContainerOutlined,
} from '@ant-design/icons';
import intl from 'react-intl-universal';
import { SettingOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
@@ -16,74 +7,74 @@ export default {
route: {
routes: [
{
name: '登录',
name: intl.get('登录'),
path: '/login',
hideInMenu: true,
component: '@/pages/login/index',
},
{
name: '初始化',
name: intl.get('初始化'),
path: '/initialization',
hideInMenu: true,
component: '@/pages/initialization/index',
},
{
name: '错误',
name: intl.get('错误'),
path: '/error',
hideInMenu: true,
component: '@/pages/error/index',
},
{
path: '/crontab',
name: '定时任务',
name: intl.get('定时任务'),
icon: <IconFont type="ql-icon-crontab" />,
component: '@/pages/crontab/index',
},
{
path: '/subscription',
name: '订阅管理',
name: intl.get('订阅管理'),
icon: <IconFont type="ql-icon-subs" />,
component: '@/pages/subscription/index',
},
{
path: '/env',
name: '环境变量',
name: intl.get('环境变量'),
icon: <IconFont type="ql-icon-env" />,
component: '@/pages/env/index',
},
{
path: '/config',
name: '配置文件',
name: intl.get('配置文件'),
icon: <IconFont type="ql-icon-config" />,
component: '@/pages/config/index',
},
{
path: '/script',
name: '脚本管理',
name: intl.get('脚本管理'),
icon: <IconFont type="ql-icon-script" />,
component: '@/pages/script/index',
},
{
path: '/dependence',
name: '依赖管理',
name: intl.get('依赖管理'),
icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index',
},
{
path: '/log',
name: '日志管理',
name: intl.get('日志管理'),
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/diff',
name: '对比工具',
name: intl.get('对比工具'),
icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index',
},
{
path: '/setting',
name: '系统设置',
name: intl.get('系统设置'),
icon: <SettingOutlined />,
component: '@/pages/password/index',
},
+81 -11
View File
@@ -1,6 +1,5 @@
@import '~antd/es/style/themes/default.less';
@import '~@/styles/variable.less';
@import '~codemirror/lib/codemirror.css';
@font-face {
font-family: 'Source Code Pro';
@@ -25,16 +24,35 @@ body {
-webkit-overflow-scrolling: touch;
}
.ant-modal-header {
padding-right: 54px;
}
.ant-modal-body {
max-height: calc(90vh - 110px);
max-height: calc(90vh - var(--vh-offset, 110px));
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
overflow-y: auto;
}
.log-modal {
.ant-modal {
padding-bottom: 0 !important;
width: 580px !important;
&.ant-modal {
max-width: 1000px !important;
width: 80vw !important;
}
.ant-modal-body {
overflow-y: auto;
min-height: 300px;
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
padding: 0;
display: flex;
.log-container {
width: 100%;
padding: 24px;
overflow-y: auto;
}
}
pre {
@@ -87,10 +105,12 @@ body {
}
.ant-tooltip {
max-width: 500px !important;
max-width: 300px !important;
.ant-tooltip-inner {
word-break: break-all !important;
max-height: 300px !important;
overflow-y: auto !important;
}
}
@@ -165,6 +185,7 @@ body {
.react-codemirror2,
.CodeMirror {
height: 100%;
overflow: auto;
}
}
}
@@ -182,8 +203,14 @@ body {
}
}
.log-modal {
&.ant-modal {
width: calc(100vw - 16px) !important;
}
}
.ant-tooltip {
max-width: 250px !important;
max-width: 300px !important;
}
}
@@ -332,7 +359,10 @@ select:-webkit-autofill:focus {
}
.ant-pro-sider-logo {
padding-inline: 5px !important;
.title {
display: flex;
height: 32px;
margin: 0 5px;
font-weight: 600;
@@ -340,6 +370,11 @@ select:-webkit-autofill:focus {
line-height: 32px;
vertical-align: middle;
animation: pro-layout-title-hide 0.3s;
a {
display: inline-flex;
align-items: center;
}
}
img {
@@ -353,12 +388,16 @@ select:-webkit-autofill:focus {
// 移动端logo被拉伸
width: auto !important;
}
.ant-image {
display: inline-flex;
}
}
pre {
word-break: break-all !important;
white-space: break-spaces !important;
padding: 0 !important;
word-break: break-all;
white-space: break-spaces;
padding: 0;
}
.virtuallist {
@@ -392,3 +431,34 @@ pre {
.ant-table-filter-column {
justify-content: unset;
}
textarea.ant-input {
word-break: break-all;
}
.ant-pro-sider-collapsed,
body[data-mode='phone'] header {
.title {
display: none;
}
}
.ant-design-pro .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed {
box-shadow: none;
border-right: 1px solid #eee;
}
.ant-table.ant-table-middle .ant-table-title,
.ant-table.ant-table-middle .ant-table-footer,
.ant-table.ant-table-middle .ant-table-thead > tr > th,
.ant-table.ant-table-middle .ant-table-tbody > tr > td,
.ant-table.ant-table-middle tfoot > tr > th,
.ant-table.ant-table-middle tfoot > tr > td {
padding: 12px 16px;
}
body[data-dark='true'] {
.ant-popover-arrow-content {
--antd-arrow-background-color: rgb(24, 26, 27);
}
}
+21 -40
View File
@@ -1,4 +1,5 @@
import React, { useEffect, useState, useRef } from 'react';
import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps';
@@ -28,12 +29,9 @@ import {
MenuProps,
} from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
import WebSocketManager from '../utils/websocket';
export interface SharedContext {
headerStyle: React.CSSProperties;
@@ -42,15 +40,13 @@ export interface SharedContext {
user: any;
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: number;
publishTime: number;
version: string;
changeLog: string;
changeLogLink: string;
@@ -63,8 +59,6 @@ export default function () {
const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const {
@@ -183,32 +177,14 @@ export default function () {
useEffect(() => {
if (!user || !user.username) return;
ws.current = new SockJS(
`${window.location.origin}/api/ws?token=${localStorage.getItem(
const ws = WebSocketManager.getInstance(
`${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem(
config.authKey,
)}`,
);
ws.current.onmessage = (e: any) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('WS connection succeeded !!!');
} else {
console.log('WS connection Failed !!!', e);
}
}
setSocketMessage(data);
} catch (error) {
console.log('websocket连接失败', e);
}
};
const wsCurrent = ws.current;
return () => {
wsCurrent.close();
ws.close();
};
}, [user]);
@@ -249,7 +225,6 @@ export default function () {
user,
reloadUser,
reloadTheme,
ws: ws.current,
}}
/>
);
@@ -265,7 +240,7 @@ export default function () {
const menu: MenuProps = {
items: [
{
label: '退出登录',
label: intl.get('退出登录'),
className: 'side-menu-user-drop-menu',
onClick: logout,
key: 'logout',
@@ -283,7 +258,8 @@ export default function () {
logo={
<>
<Image preview={false} src="https://qn.whyour.cn/logo.png" />
<span className="title"></span>
<div className="title">
<span className="title">{intl.get('青龙')}</span>
<a
href={systemInfo?.changeLogLink}
target="_blank"
@@ -293,7 +269,11 @@ export default function () {
}}
>
<Tooltip
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
title={
systemInfo?.branch === 'develop'
? intl.get('开发版')
: intl.get('正式版')
}
>
<Badge size="small" dot={systemInfo?.branch === 'develop'}>
<span
@@ -310,6 +290,7 @@ export default function () {
</Badge>
</Tooltip>
</a>
</div>
</>
}
title={false}
@@ -325,8 +306,9 @@ export default function () {
}}
pageTitleRender={(props, pageName, info) => {
const title =
(config.documentTitleMap as any)[location.pathname] || '未找到';
return `${title} - 青龙`;
(config.documentTitleMap as any)[location.pathname] ||
intl.get('未找到');
return `${title} - ${intl.get('青龙')}`;
}}
onCollapse={setCollapsed}
collapsed={collapsed}
@@ -338,7 +320,7 @@ export default function () {
shape="square"
size="small"
icon={<UserOutlined />}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
@@ -360,7 +342,7 @@ export default function () {
shape="square"
size="small"
icon={<UserOutlined />}
src={user.avatar ? `/api/static/${user.avatar}` : ''}
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
/>
<span style={{ marginLeft: 5 }}>{user.username}</span>
</span>
@@ -383,7 +365,6 @@ export default function () {
user,
reloadUser,
reloadTheme,
socketMessage,
systemInfo,
}}
/>

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