Compare commits

..
186 Commits
Author SHA1 Message Date
whyour e70cd6579f 更新版本 v2.15.6 2023-02-01 22:46:23 +08:00
whyour 99c047e9c7 修复定时任务表格滚动 2023-02-01 22:45:47 +08:00
whyour 978ae326f8 更新 sqlite3 依赖 2023-02-01 15:30:53 +08:00
whyour b01f27f2d3 修改更新列表后刷新逻辑 2023-01-31 23:52:02 +08:00
whyour 4d34e0dd96 修复环境变量滚动卡顿 2023-01-31 23:46:42 +08:00
whyour 9b0065e533 修复定时任务过多搜索卡顿 2023-01-31 23:30:47 +08:00
whyour 34a51feee9 修改定时任务列表title 2023-01-31 17:42:30 +08:00
whyour 679091de59 修改定时任务输入提示 2023-01-31 17:41:35 +08:00
whyour c6220cb920 修改定时任务查询日志逻辑 2023-01-29 18:07:57 +08:00
whyour 633b30269c 修复定时任务脚本跳转 2023-01-29 10:57:08 +08:00
whyour 8ad26267d8 修改 task before/after 逻辑 2023-01-20 23:38:32 +08:00
OceanJiangandGitHub 7cfb125682 实现 Node.js 部分 SMTP 推送 (#1776) 2023-01-14 19:53:36 +08:00
Wankko ReeandGitHub a2dc5de5ce 添加基于 SMTP 邮件 的消息通知服务 (#1763) 2023-01-14 19:48:41 +08:00
whyour 4f41ff29ec 修改资源通知逻辑 2023-01-14 19:46:33 +08:00
whyour 13d95b8c19 修复日志滚动逻辑 2023-01-11 23:46:47 +08:00
whyour 335c13ab44 修改日志滚动逻辑 2023-01-11 23:44:04 +08:00
whyour 015cdc10a7 publicPath使用相对目录 2023-01-11 22:52:51 +08:00
whyour a5075ab28f 修改新建任务命令提示 2023-01-11 22:14:32 +08:00
whyour 2e3c3274c7 修复antd兼容性,日志详情自动滚动 2023-01-11 22:03:44 +08:00
whyour 443c581955 修改定时任务 shouldCellUpdate 2023-01-08 22:30:29 +08:00
whyour 5e41faf590 修改 monaco-editor 版本 2023-01-08 22:16:22 +08:00
whyour 0579fb83d3 修改环境变量值省略展示 2023-01-08 21:56:29 +08:00
whyour 2864845d5b 优化定时任务表格渲染 2023-01-08 18:15:18 +08:00
whyour 659dddace7 梅子留酸软齿牙,芭蕉分绿与窗纱。 2023-01-08 16:49:21 +08:00
whyour b096f30558 更新 dockerfile 2023-01-08 16:39:01 +08:00
whyour dcfb72143a 修复定时任务每页数据量大时搜索卡顿 2023-01-07 18:19:03 +08:00
whyour c63bcd7706 修改docker编译 2023-01-07 18:16:39 +08:00
whyour bac306f9b3 更新版本 v2.15.5 2023-01-04 21:09:52 +08:00
whyour a6fd96e97b 修复定时规则修改 2023-01-04 21:09:27 +08:00
whyour 7edd91f923 修改服务异常判断逻辑 2023-01-04 19:27:15 +08:00
whyour 66df445bd5 修改添加任务命令提示 2023-01-04 17:28:26 +08:00
whyour 5c03034bb4 修复定时任务不以task开头时,任务无效 2023-01-04 17:25:30 +08:00
whyour 0eab181d46 修改update逻辑 2022-12-29 12:15:25 +08:00
whyour f6f95308c7 monaco editor改为本地加载 2022-12-28 22:12:37 +08:00
whyour e93cad91b4 更新版本 v2.15.4 2022-12-28 11:15:20 +08:00
whyour 08ee61b179 修改 pub 脚本 2022-12-28 11:14:57 +08:00
whyour 0ab756665e 修改版本文件 2022-12-28 11:06:47 +08:00
whyour 3570cddce0 关于增加更新日志查看 2022-12-27 17:10:21 +08:00
whyour a2d9f1a5db 脚本管理支持重命名文件/文件夹 2022-12-27 16:02:15 +08:00
whyour 0c6a214e55 修复启动public服务命令 2022-12-27 12:24:24 +08:00
whyour 095fcdbe69 修复任务脚本快捷跳转 2022-12-17 12:46:26 +08:00
whyour 3afb1d18f6 更新版本 v2.15.3 2022-12-10 19:06:39 +08:00
whyour 4526c84330 任务视图支持标签筛选 2022-12-10 19:00:03 +08:00
whyour a9cc1cb4b9 修改token获取逻辑 2022-12-10 17:12:45 +08:00
whyour 7acf1eace3 修改添加任务视图筛选条件交互 2022-12-09 23:34:23 +08:00
whyour bb2e8cb287 修复调试保存文件默认目录和任务视图创建 2022-12-09 22:59:38 +08:00
whyour 3b389259c1 修复调试脚本日志 2022-12-06 00:22:05 +08:00
whyour 310ce55b09 修改默认基础镜像 2022-12-05 23:49:33 +08:00
CoolBoyandwhyour 4662e3fec2 修复notify.js中智能微秘书 (#1736) 2022-12-05 23:44:34 +08:00
whyour 10fab9d415 修改进程默认退出信号 2022-12-05 21:00:14 +08:00
whyour acecf01cbe 修复 killTask 返回值 2022-12-05 15:38:21 +08:00
whyour b95fb9cda4 修改退出进程逻辑 2022-12-05 15:26:22 +08:00
whyour 23fd595582 修改 timeout 命令默认信号 2022-12-05 13:18:10 +08:00
whyour 468d05cb10 更新版本 v2.15.2 2022-12-02 18:07:54 +08:00
whyour 88c2e5d160 更新 readme 2022-12-02 18:07:21 +08:00
whyour 8cf491b802 task 命令增加 -m 参数,支持设置任务超时时间 2022-12-01 18:26:24 +08:00
whyour 5886e78299 修复获取更新分支 2022-12-01 16:25:02 +08:00
whyour 591ef9781c 修改获取 cpu 占用 2022-12-01 16:16:17 +08:00
whyour 52fef96c1c 修改check逻辑 2022-11-30 21:29:58 +08:00
whyour d5fd9c57ec 修复shell语法 2022-11-29 22:50:11 +08:00
whyour 8a7d6a1c13 修改资源检查逻辑 2022-11-29 21:01:28 +08:00
whyour 1745c72981 修改 check/update 逻辑 2022-11-29 00:05:50 +08:00
whyour b98b7646cb 修复日志样式 2022-11-28 23:59:23 +08:00
whyour 1446e925ec 任务视图增加系统视图 2022-11-26 20:34:07 +08:00
whyour 799cda1e3e 移除未使用依赖 2022-11-24 22:09:33 +08:00
whyour d2e0231341 修改 workflow node_modules 缓存 2022-11-22 21:33:03 +08:00
whyour 9e0aa655a0 修改 workflow 依赖缓存 2022-11-22 21:00:53 +08:00
whyour e303c73d44 workflow 增加 digest 打印 2022-11-22 20:03:56 +08:00
whyour e5e8e66d6b 增加 docker action 缓存 2022-11-22 19:53:02 +08:00
whyour 0bb3381122 修复拉去仓库逻辑 2022-11-22 16:51:57 +08:00
whyour 2c5bf2bd6b 更新 readme 2022-11-22 14:42:27 +08:00
whyour 83763b00ae 上传 pnpm-lock 2022-11-22 13:41:39 +08:00
whyour 47fc795c97 增加action缓存 2022-11-22 13:37:44 +08:00
whyour 6a59bf7af3 更新版本 v2.15.1 2022-11-20 21:04:58 +08:00
whyour 6f185570d6 pushDeer支持自架版 2022-11-20 20:05:02 +08:00
whyour 8b8336d9d0 系统升级增加gitee源 2022-11-17 22:01:47 +08:00
whyour a1e1d6ed3f 更新 readme 2022-11-17 11:47:44 +08:00
whyour df3e209534 修改 workflow 2022-11-17 11:37:45 +08:00
whyour 851408860e 修复 workflow 2022-11-17 11:18:15 +08:00
whyour d92ea23b0e 修改 workflow 2022-11-17 11:11:05 +08:00
whyour d83641db92 修复workflow 2022-11-17 00:28:28 +08:00
whyour 48f262e1f3 增加 workflow 2022-11-17 00:23:24 +08:00
whyour bc49a7db50 同步gitee 2022-11-16 23:04:39 +08:00
whyour 49975b461c 修改typescript版本 2022-11-16 22:52:34 +08:00
whyour 3ccb635956 修复移动端最后更新时间展示 2022-11-16 22:34:28 +08:00
whyour 074a90e0ac 修改同步gitlab私钥 2022-11-16 00:02:40 +08:00
whyour 526f927436 增加 gitlab workflow 2022-11-15 23:55:28 +08:00
whyour fae7fad915 更新版本 v2.15.0 2022-11-15 12:15:06 +08:00
whyour 56132ceb8b 修复初始化新增字段 2022-11-15 12:14:33 +08:00
whyour 3c3fce0a5a 修复初始化表字段 2022-11-15 11:32:42 +08:00
whyour 8139384961 移除filterRelation默认值 2022-11-14 23:31:27 +08:00
whyour 734cc94048 修改filterRelation字段限制 2022-11-14 22:59:01 +08:00
whyour 91dbb7770d 修改订阅github ssh生成逻辑 2022-11-14 22:00:16 +08:00
whyour 707709837d 初始化更新不存在filterRelation的任务视图 2022-11-14 21:36:31 +08:00
whyour e868e65208 修改日志管理默认排序 2022-11-14 15:12:17 +08:00
whyour b19ad2a13b 修复debounce方法引入错误 2022-11-14 11:35:51 +08:00
whyour 98ccccf6ab 修复openapi token生成逻辑 2022-11-14 09:28:30 +08:00
whyour 0a6166c557 订阅增加代理参数 2022-11-13 23:58:42 +08:00
whyour 5ee00e52a8 修复新建视图筛选条件样式 2022-11-13 22:12:03 +08:00
whyour ff2b4e0b2f 订阅支持添加代理 2022-11-12 22:45:46 +08:00
whyour 8fdd8db51b 修改创建任务视图样式 2022-11-11 15:03:27 +08:00
whyour f113eb7ba8 修改系统设置样式 2022-11-11 14:55:09 +08:00
whyour 40bc25ed5f 修复获取最后commit时间 2022-11-10 10:52:57 +08:00
whyour cd3eab5d35 修复版本信息展示 2022-11-10 01:34:18 +08:00
whyour 7038e15ad2 支持定时任务视图筛选条件关系切换 2022-11-10 01:31:21 +08:00
whyour c72abd29ec 任务视图筛选条件增加条件关系 2022-11-09 20:43:22 +08:00
whyour 5efc3d2228 更新版本 v2.14.12 2022-11-05 16:43:28 +08:00
whyour 8ac3f83c79 修复可能出现移动或者创建环境变量出错 2022-11-05 16:41:19 +08:00
whyour b31b054d0c 更新版本 v2.14.11 2022-11-05 15:24:16 +08:00
whyour fb4a87f5ce 修复版本文件缓存 2022-11-05 15:23:17 +08:00
whyour 049c73880b 修改pre样式 2022-11-05 14:57:21 +08:00
whyour 7b2c54f6a6 修改版本信息获取 2022-11-05 12:14:46 +08:00
whyour 23bda39812 关于增加版本信息展示 2022-11-05 11:01:58 +08:00
whyour 90af5801ee 修复表格滚动 2022-11-04 19:14:03 +08:00
whyour 543c241bc6 更新版本 v2.14.10 2022-11-03 21:16:46 +08:00
whyour 19a58a3510 环境变量值增加复制功能 2022-11-03 17:34:36 +08:00
whyour 73c6fd18ed 修复资源预警通知 2022-11-03 13:42:14 +08:00
whyour 409297c6aa 修改调试日志样式 2022-11-02 19:54:49 +08:00
whyour 4272b9ea35 修复task命令参数 2022-11-02 19:40:16 +08:00
whyour 3c5c5b0bbc 修复&符号转义 2022-11-02 19:25:03 +08:00
whyour 802b2d722e 修复任务命令中的&和引号 2022-11-02 18:51:09 +08:00
whyour 8f90d3f8ff 修复环境变量位置计算逻辑 2022-11-02 00:02:15 +08:00
whyour f93cbbf508 修复 notify.js 飞书通知 2022-10-30 18:59:48 +08:00
whyour 2ba8756bd3 修改cdn协议 2022-10-30 12:00:12 +08:00
whyour 8013de56c9 修改sqlite配置 2022-10-30 10:54:39 +08:00
whyour 4af9b22442 更新版本 v2.14.9 2022-10-29 18:50:16 +08:00
whyour 8a392170cf webhook通知支持嵌套body 2022-10-29 18:43:16 +08:00
whyour 6385e053ae 增加飞书机器人通知 2022-10-29 16:41:37 +08:00
Leo_chenandGitHub 2789119882 添加智能微秘书通知 (#1680) 2022-10-29 15:45:16 +08:00
whyour 3e0553c3e8 修改登陆逻辑 2022-10-29 10:05:54 +08:00
whyour 5e16ace1e7 修改检查更新日志 2022-10-27 10:34:55 +08:00
whyour 0ca23cc4ca 修改环境变量默认排序 2022-10-27 00:02:50 +08:00
whyour 4949457001 修复ql命令日志打印 2022-10-26 23:50:07 +08:00
whyour 8ae13fe823 增加 -max 指定环境变量 2022-10-26 14:27:14 +08:00
whyour fdf672092a 修改获取version文件超时时间 2022-10-25 23:52:30 +08:00
whyour 48dd6ea826 修复表格滚动高度 2022-10-25 23:12:38 +08:00
whyour 68bb4f15e2 修复shell获取磁盘使用量 2022-10-24 22:45:06 +08:00
whyour 9c6d9ec22d 更新版本 v2.14.8 2022-10-22 18:36:38 +08:00
whyour ac075ace73 修复调试脚本语言判断 2022-10-22 00:31:34 +08:00
whyour eb58774ee9 优化task.sh 2022-10-22 00:25:54 +08:00
whyour 04613bf9a9 修复tsconfig 2022-10-20 15:30:22 +08:00
whyour 4194f1964d 修改日志结束符判定 2022-10-20 15:16:17 +08:00
whyour 42c64c82d7 修复 ql 命令日志目录 2022-10-16 21:59:19 +08:00
whyour d44a8966f2 修复环境变量名称验证 2022-10-16 09:22:37 +08:00
whyour ff44330aa1 修复 ql 命令日志目录 2022-10-15 18:40:15 +08:00
whyour ed259e8168 修复 task 命令执行时参数引号丢失 2022-10-15 18:29:34 +08:00
whyour 444836975d 修改 ql 命令打印日志逻辑 2022-10-14 23:02:20 +08:00
whyour 3756fa93e4 修复 ql 命令 2022-10-14 21:17:04 +08:00
whyour 064b52b306 修改 timezone action 2022-10-14 20:55:58 +08:00
whyour 648dc51474 更新ql指令 2022-10-14 15:17:37 +08:00
whyour 7c0bc32759 修改环境变量导入 2022-10-13 23:41:54 +08:00
whyour d4b6cc5e4d 修复调试脚本选择脚本 2022-10-13 23:39:28 +08:00
whyour 75fca715e3 增加环境变量导入 2022-10-11 22:16:36 +08:00
whyour 6e9a4a29ef 修复shell语法 2022-10-09 13:49:19 +08:00
whyour 78f643c50f 更新版本v2.14.7 2022-10-01 23:12:57 +08:00
whyour 2db4f1ba87 修改资源判定逻辑 2022-10-01 23:04:12 +08:00
whyour 5741dbb78c 修复获取task_before参数 2022-09-30 15:32:42 +08:00
whyour 88cf671465 更新版本v2.14.6 2022-09-29 23:06:31 +08:00
whyour 29f082dea2 增加config资源配置 2022-09-29 23:03:22 +08:00
whyour 9a55968ed6 修改资源异常推送 2022-09-29 13:29:22 +08:00
whyour 6ec6e25855 修改资源提示 2022-09-29 00:59:27 +08:00
whyour af48bc378b 修复获取cpu占用 2022-09-29 00:57:31 +08:00
whyour 3f5ae4bcb9 修复 echo 2022-09-29 00:44:07 +08:00
whyour c6624e9001 增加资源占用提示 2022-09-29 00:43:07 +08:00
whyour 5530ce76e3 添加系统资源消耗提示 2022-09-29 00:32:17 +08:00
whyour 654b51e476 修复脚本和日志搜索逻辑 2022-09-25 21:03:11 +08:00
whyour 6428ac3624 修复SplitPane类型错误 2022-09-25 14:02:27 +08:00
whyour c36450f436 修复ant-tree高度获取 2022-09-25 13:38:10 +08:00
whyour dcea231249 更新版本 v2.14.5 2022-09-24 21:00:47 +08:00
whyour f236119bd6 修改输入框自动填充样式 2022-09-24 14:40:13 +08:00
whyour cb870fad85 修改检查更新逻辑 2022-09-24 14:20:35 +08:00
whyour 44e2caedfc 修改更新日志清除评率 2022-09-24 13:59:56 +08:00
whyour c0b5192296 修改移动端脚本高亮 2022-09-24 11:27:16 +08:00
whyour ba43882c0b 修复脚本管理删除 2022-09-23 23:16:57 +08:00
whyour dfc706e16d 日志管理支持删除日志文件和目录 2022-09-23 20:09:10 +08:00
whyour 25b03d4345 脚本管理支持删除文件夹 2022-09-23 19:12:51 +08:00
whyour 4e8f36d9a4 修复登录错误提示 2022-09-23 17:44:48 +08:00
whyour 8f4d67ffa7 修改task命令ID获取 2022-09-23 14:37:56 +08:00
whyour ad47277149 修复登录验证 2022-09-23 12:15:15 +08:00
whyour 98c2a37ab6 修复初始化逻辑 2022-09-23 00:17:53 +08:00
whyour f2fea47336 重构前端错误提示 2022-09-22 23:59:23 +08:00
whyour e274d3e2f9 sentry增加版本号 2022-09-22 12:14:42 +08:00
whyour 36f4c3c02c 修改任务启动参数 2022-09-21 21:28:31 +08:00
whyour ae891f8e55 修复docker启动逻辑 2022-09-21 17:42:13 +08:00
whyour c874dc9705 更新readme 2022-09-21 16:03:53 +08:00
whyour 23bfbeb995 修改新建订阅提示 2022-09-21 14:54:45 +08:00
112 changed files with 19732 additions and 3208 deletions
+1
View File
@@ -10,3 +10,4 @@ SECRET='whyour'
QINIU_AK='' QINIU_AK=''
QINIU_SK='' QINIU_SK=''
QINIU_SCOPE='' QINIU_SCOPE=''
TEMP=''
+64 -12
View File
@@ -5,7 +5,6 @@ on:
branches: branches:
- 'master' - 'master'
- 'develop' - 'develop'
# Sequence of patterns matched against refs/tags
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
schedule: schedule:
@@ -13,20 +12,50 @@ on:
# note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30 # note: 这里是GMT时间,北京时间减去八小时即可。如北京时间 22:30 => GMT 14:30
# minute hour day month dayOfWeek # minute hour day month dayOfWeek
- cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00 - cron: '00 14 * * *' # GMT 14:00 => 北京时间 22:00
#- cron: '30 16 * * *' # GMT 16:30(前一天) => 北京时间 00:30
workflow_dispatch: workflow_dispatch:
jobs: jobs:
to_gitlab:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitlab.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
to_gitee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
git@gitee.com:whyour/qinglong.git
ssh_private_key:
${{ secrets.GITLAB_SSH_PK }}
build-static: build-static:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: build front and back - name: build front and back
run: | run: |
npm i -g pnpm pnpm install --frozen-lockfile
pnpm install
pnpm build:front pnpm build:front
pnpm build:back pnpm build:back
@@ -34,6 +63,9 @@ jobs:
env: env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
GITHUB_BRANCH: ${{ github.ref_name }} GITHUB_BRANCH: ${{ github.ref_name }}
REPO_GITEE: git@gitee.com:whyour/qinglong-static.git
REPO_GITLAB: git@gitlab.com:whyour/qinglong-static.git
PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PK }}
run: | run: |
mkdir -p tmp mkdir -p tmp
cd ./tmp cd ./tmp
@@ -44,6 +76,16 @@ jobs:
git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')" git commit --allow-empty -m "copy static at $(date +'%Y-%m-%d %H:%M:%S')"
git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH} git push --force --quiet "https://${{ secrets.API_TOKEN }}@${GITHUB_REPO}.git" ${GITHUB_BRANCH}:${GITHUB_BRANCH}
mkdir -p ~/.ssh
echo "${PRIVATE_KEY}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
export GIT_SSH_COMMAND="ssh -v -i ~/.ssh/id_rsa -o StrictHostKeyChecking=no -l git"
git remote add gitee "${REPO_GITEE}"
git remote add gitlab "${REPO_GITLAB}"
git push --force --quiet gitee ${GITHUB_BRANCH}:${GITHUB_BRANCH}
git push --force --quiet gitlab ${GITHUB_BRANCH}:${GITHUB_BRANCH}
build: build:
needs: build-static needs: build-static
@@ -55,14 +97,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-node@v3 - uses: pnpm/action-setup@v2
- name: Set time zone
uses: szenius/set-timezone@v1.0
with: with:
timezoneLinux: "Asia/Shanghai" version: latest
timezoneMacos: "Asia/Shanghai" - uses: actions/setup-node@v3
timezoneWindows: "China Standard Time" with:
cache: 'pnpm'
- name: Setup timezone
uses: zcong1993/setup-timezone@master
with:
timezone: Asia/Shanghai
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v2
@@ -108,9 +153,16 @@ jobs:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
QL_BRANCH=${{ github.ref_name }} QL_BRANCH=${{ github.ref_name }}
SOURCE_COMMIT=${{ github.sha }}
network: host network: host
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64
context: docker/ context: .
file: ./docker/Dockerfile
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=whyour/qinglong:cache
cache-to: type=registry,ref=whyour/qinglong:cache,mode=max
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
-1
View File
@@ -5,7 +5,6 @@
/npm-debug.log* /npm-debug.log*
/yarn-error.log /yarn-error.log
/yarn.lock /yarn.lock
/pnpm-lock.yaml
/package-lock.json /package-lock.json
# production # production
+2 -1
View File
@@ -6,10 +6,11 @@ export default defineConfig({
antd: {}, antd: {},
outputPath: 'static/dist', outputPath: 'static/dist',
fastRefresh: true, fastRefresh: true,
favicons: ['/images/favicon.svg'], favicons: ['./images/favicon.svg'],
mfsu: { mfsu: {
strategy: 'eager', strategy: 'eager',
}, },
publicPath: process.env.NODE_ENV === 'production' ? './' : '/',
proxy: { proxy: {
'/api/public': { '/api/public': {
target: 'http://127.0.0.1:5400/', target: 'http://127.0.0.1:5400/',
+9 -5
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a> </a>
</p> </p>
@@ -22,7 +22,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn) [![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
[简体中文](./README.md) | English [简体中文](./README.md) | English
@@ -41,7 +41,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
### Local Deployment ### Local Deployment
```bash ```bash
# To be completed # To be refined, see the development steps first (not supported on windows yet)
``` ```
### Podman Deployment ### Podman Deployment
@@ -61,7 +61,6 @@ podman run -dit \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
``` ```
@@ -162,6 +161,10 @@ task <file_path> now
task <file_path> conc <env_name> <account_number>(Optional) 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 # Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number> 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 2. parameter description
@@ -176,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: File path for task execution * 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 * 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 * account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Links ## Links
@@ -183,7 +187,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org) - [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+9 -5
View File
@@ -1,6 +1,6 @@
<p align="center"> <p align="center">
<a href="https://github.com/whyour/qinglong"> <a href="https://github.com/whyour/qinglong">
<img width="150" src="https://z3.ax1x.com/2021/11/18/I7MpAe.png"> <img width="150" src="https://user-images.githubusercontent.com/22700758/191449379-f9f56204-0e31-4a16-be5a-331f52696a73.png">
</a> </a>
</p> </p>
@@ -22,7 +22,7 @@
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong [docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div> </div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn) [![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
简体中文 | [English](./README-en.md) 简体中文 | [English](./README-en.md)
@@ -41,7 +41,7 @@
### 本机部署 ### 本机部署
```bash ```bash
# 待完善 # 待完善,可先参考开发步骤 (windows暂时不支持)
``` ```
### podman 部署 ### podman 部署
@@ -61,7 +61,6 @@ podman run -dit \
-p 5700:5700 \ -p 5700:5700 \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
``` ```
@@ -161,6 +160,10 @@ task <file_path> now
task <file_path> conc <env_name> <account_number>(可选的) task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行 # 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number> task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
``` ```
2. 参数说明 2. 参数说明
@@ -176,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: 任务执行时的文件路径 * file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称 * env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号 * account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 链接 ## 链接
@@ -183,7 +187,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui) - [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design) - [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/) - [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org) - [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader) - [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server) - [admin-server](https://github.com/sunpu007/admin-server)
+8 -2
View File
@@ -30,6 +30,7 @@ export default (app: Router) => {
name: Joi.string().required(), name: Joi.string().required(),
sorts: Joi.array().optional().allow(null), sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(), filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -51,13 +52,18 @@ export default (app: Router) => {
id: Joi.number().required(), id: Joi.number().required(),
sorts: Joi.array().optional().allow(null), sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(), filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const cronViewService = Container.get(CronViewService); const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body); if (req.body.type === 1) {
return res.send({ code: 200, data }); return res.send({ code: 400, message: '参数错误' });
} else {
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
}
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
+49 -2
View File
@@ -3,8 +3,21 @@ import { Container } from 'typedi';
import EnvService from '../services/env'; import EnvService from '../services/env';
import { Logger } from 'winston'; import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
const route = Router(); const route = Router();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, config.scriptPath);
},
filename: function (req, file, cb) {
cb(null, file.originalname);
},
});
const upload = multer({ storage: storage });
export default (app: Router) => { export default (app: Router) => {
app.use('/envs', route); app.use('/envs', route);
@@ -26,7 +39,9 @@ export default (app: Router) => {
body: Joi.array().items( body: Joi.array().items(
Joi.object({ Joi.object({
value: Joi.string().required(), value: Joi.string().required(),
name: Joi.string().required(), name: Joi.string()
.required()
.pattern(/^[a-zA-Z_][0-9a-zA-Z_]*$/),
remarks: Joi.string().optional().allow(''), remarks: Joi.string().optional().allow(''),
}), }),
), ),
@@ -94,7 +109,6 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => { async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const data = await envService.move(req.params.id, req.body); const data = await envService.move(req.params.id, req.body);
@@ -177,4 +191,37 @@ export default (app: Router) => {
} }
}, },
); );
route.post(
'/upload',
upload.single('env'),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent);
const data = Array.isArray(parseContent)
? parseContent
: [parseContent];
if (data.every((x) => x.name && x.value)) {
const result = await envService.create(
data.map((x) => ({
name: x.name,
value: x.value,
remarks: x.remarks,
})),
);
return res.send({ code: 200, data: result });
} else {
return res.send({
code: 400,
message: '文件缺少name或者value字段,参考导出文件格式',
});
}
} catch (e) {
return next(e);
}
},
);
}; };
+32 -1
View File
@@ -3,8 +3,9 @@ import { Container } from 'typedi';
import { Logger } from 'winston'; import { Logger } from 'winston';
import * as fs from 'fs'; import * as fs from 'fs';
import config from '../config'; import config from '../config';
import { getFileContentByName, readDirs } from '../config/util'; import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { join } from 'path'; import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router(); const route = Router();
const blacklist = ['.tmp']; const blacklist = ['.tmp'];
@@ -45,4 +46,34 @@ export default (app: Router) => {
} }
}, },
); );
route.delete(
'/',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.logPath, path, filename);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+39 -5
View File
@@ -4,6 +4,7 @@ import {
readDirs, readDirs,
getLastModifyFilePath, getLastModifyFilePath,
readDir, readDir,
emptyDir,
} from '../config/util'; } from '../config/util';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi'; import { Container } from 'typedi';
@@ -169,17 +170,23 @@ export default (app: Router) => {
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
path: Joi.string().allow(''), path: Joi.string().allow(''),
type: Joi.string().optional(),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, path } = req.body as { let { filename, path, type } = req.body as {
filename: string; filename: string;
path: string; path: string;
type: string;
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath); if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
res.send({ code: 200 }); res.send({ code: 200 });
} catch (e) { } catch (e) {
return next(e); return next(e);
@@ -248,23 +255,50 @@ export default (app: Router) => {
celebrate({ celebrate({
body: Joi.object({ body: Joi.object({
filename: Joi.string().required(), filename: Joi.string().required(),
content: Joi.string().optional().allow(''),
path: Joi.string().optional().allow(''), path: Joi.string().optional().allow(''),
pid: Joi.number().optional().allow(''),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let { filename, content, path } = req.body; let { filename, path, pid } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath); const result = await scriptService.stopScript(filePath, pid);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
} }
}, },
); );
route.put(
'/rename',
celebrate({
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
newFilename: Joi.string().required(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
let { filename, path, type, newFilename } = req.body as {
filename: string;
path: string;
type: string;
newFilename: string;
};
const filePath = join(config.scriptPath, path, filename);
const newPath = join(config.scriptPath, path, newFilename);
fs.renameSync(filePath, newPath);
res.send({ code: 200 });
} catch (e) {
return next(e);
}
},
);
}; };
+2
View File
@@ -49,6 +49,7 @@ export default (app: Router) => {
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
schedule_type: Joi.string().required(), schedule_type: Joi.string().required(),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -177,6 +178,7 @@ export default (app: Router) => {
sub_before: Joi.string().optional().allow('').allow(null), sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null), sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(), alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
+24 -4
View File
@@ -7,6 +7,9 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate'; import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user'; import UserService from '../services/user';
import { EnvModel } from '../data/env'; import { EnvModel } from '../data/env';
import { parseVersion, promiseExec } from '../config/util';
import dayjs from 'dayjs';
const route = Router(); const route = Router();
export default (app: Router) => { export default (app: Router) => {
@@ -18,10 +21,22 @@ export default (app: Router) => {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getUserInfo();
const envCount = await EnvModel.count(); const envCount = await EnvModel.count();
const versionRegx = /.*export const version = \'(.*)\'\;/; const { version, changeLog, changeLogLink } = await parseVersion(
config.versionFile,
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); );
const version = currentVersionFile.match(versionRegx)![1]; const lastCommitTime = (
await promiseExec(
`cd ${config.rootPath} && git show -s --format=%ai | head -1`,
)
).replace('\n', '');
const lastCommitId = (
await promiseExec(`cd ${config.rootPath} && git rev-parse --short HEAD`)
).replace('\n', '');
const branch = (
await promiseExec(
`cd ${config.rootPath} && git symbolic-ref --short HEAD`,
)
).replace('\n', '');
let isInitialized = true; let isInitialized = true;
if ( if (
@@ -37,6 +52,11 @@ export default (app: Router) => {
data: { data: {
isInitialized, isInitialized,
version, version,
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
changeLog,
changeLogLink,
}, },
}); });
} catch (e) { } catch (e) {
+1 -1
View File
@@ -17,7 +17,7 @@ async function startServer() {
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.debug(`✌️ Back server launched on port ${config.port}`); Logger.debug(`✌️ 后端服务启动成功!`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+7
View File
@@ -0,0 +1,7 @@
export const LOG_END_SYMBOL = '\n          ';
export const TASK_COMMAND = 'task';
export const QL_COMMAND = 'ql';
export const TASK_PREFIX = `${TASK_COMMAND} `;
export const QL_PREFIX = `${QL_COMMAND} `;
+3 -3
View File
@@ -11,10 +11,10 @@ if (!process.env.QL_DIR) {
if (qlHomePath.endsWith('/static/')) { if (qlHomePath.endsWith('/static/')) {
qlHomePath = path.join(qlHomePath, '../'); qlHomePath = path.join(qlHomePath, '../');
} }
process.env.QL_DIR = qlHomePath; process.env.QL_DIR = qlHomePath.replace(/\/$/g, '');
} }
const lastVersionFile = `http://qn.whyour.cn/version.ts?v=${Date.now()}`; const lastVersionFile = `https://qn.whyour.cn/version.yaml`;
const rootPath = process.env.QL_DIR as string; const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') }); const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
@@ -40,7 +40,7 @@ const sqliteFile = path.join(samplePath, 'database.sqlite');
const authError = '错误的用户名密码,请重试'; const authError = '错误的用户名密码,请重试';
const loginFaild = '请先登录!'; const loginFaild = '请先登录!';
const configString = 'config sample crontab shareCode diy'; const configString = 'config sample crontab shareCode diy';
const versionFile = path.join(rootPath, 'src/version.ts'); const versionFile = path.join(rootPath, 'version.yaml');
if (envFound.error) { if (envFound.error) {
throw new Error("⚠️ Couldn't find .env file ⚠️"); throw new Error("⚠️ Couldn't find .env file ⚠️");
+113 -30
View File
@@ -4,6 +4,9 @@ import got from 'got';
import iconv from 'iconv-lite'; import iconv from 'iconv-lite';
import { exec } from 'child_process'; import { exec } from 'child_process';
import FormData from 'form-data'; import FormData from 'form-data';
import psTreeFun from 'pstree.remy';
import { promisify } from 'util';
import { load } from 'js-yaml';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -284,6 +287,20 @@ enum FileType {
'file', 'file',
} }
interface IFile {
title: string;
key: string;
type: 'directory' | 'file';
parent: string;
mtime: number;
children?: IFile[];
}
export function dirSort(a: IFile, b: IFile) {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1;
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1;
}
export function readDirs( export function readDirs(
dir: string, dir: string,
baseDir: string = '', baseDir: string = '',
@@ -302,24 +319,21 @@ export function readDirs(
title: file, title: file,
key, key,
type: 'directory', type: 'directory',
disabled: true,
parent: relativePath, parent: relativePath,
children: readDirs(subPath, baseDir).sort( mtime: stats.mtime.getTime(),
(a: any, b: any) => children: readDirs(subPath, baseDir).sort(dirSort),
(FileType as any)[a.type] - (FileType as any)[b.type],
),
}; };
} }
return { return {
title: file, title: file,
type: 'file', type: 'file',
isLeaf: true,
key, key,
parent: relativePath, parent: relativePath,
mtime: stats.mtime.getTime(),
}; };
}); });
return result.sort( return result.sort(dirSort);
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
);
} }
export function readDir( export function readDir(
@@ -345,6 +359,20 @@ export function readDir(
return result; return result;
} }
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> { export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
exec( exec(
@@ -365,22 +393,29 @@ export function parseHeaders(headers: string) {
let val; let val;
let i; let i;
headers && headers.split('\n').forEach(function parser(line) { headers &&
i = line.indexOf(':'); headers.split('\n').forEach(function parser(line) {
key = line.substring(0, i).trim().toLowerCase(); i = line.indexOf(':');
val = line.substring(i + 1).trim(); key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) { if (!key) {
return; return;
} }
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val; parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
}); });
return parsed; return parsed;
}; }
export function parseBody(body: string, contentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded') { export function parseBody(
body: string,
contentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded',
) {
if (!body) return ''; if (!body) return '';
const parsed: any = {}; const parsed: any = {};
@@ -388,22 +423,28 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
let val; let val;
let i; let i;
body && body.split('\n').forEach(function parser(line) { body &&
i = line.indexOf(':'); body.split('\n').forEach(function parser(line) {
key = line.substring(0, i).trim().toLowerCase(); i = line.indexOf(':');
val = line.substring(i + 1).trim(); key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) { if (!key || parsed[key]) {
return; return;
} }
parsed[key] = val; try {
}); const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) { switch (contentType) {
case 'multipart/form-data': case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => { return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]) p.append(c, parsed[c]);
return p; return p;
}, new FormData()); }, new FormData());
case 'application/x-www-form-urlencoded': case 'application/x-www-form-urlencoded':
@@ -413,4 +454,46 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
} }
return parsed; return parsed;
}; }
export function psTree(pid: number): Promise<number[]> {
return new Promise((resolve, reject) => {
psTreeFun(pid, (err: any, pids: number[]) => {
if (err) {
reject(err);
}
resolve(pids.filter((x) => !isNaN(x)));
});
});
}
export async function killTask(pid: number) {
const pids = await psTree(pid);
// SIGALRM 14 时钟信号
if (pids.length) {
process.kill(pids[0], 14);
} else {
process.kill(pid, 14);
}
}
export async function getPid(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
let pid = (await execAsync(taskCommand)).stdout;
return Number(pid);
}
interface IVersion {
version: string;
changeLogLink: string;
changeLog: string;
}
export async function parseVersion(path: string): Promise<IVersion> {
return load(await promisify(fs.readFile)(path, 'utf8')) as IVersion;
}
export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion;
}
+16 -1
View File
@@ -1,13 +1,19 @@
import { sequelize } from '.'; import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize'; import { DataTypes, Model } from 'sequelize';
export enum CronViewType {
'系统' = 1,
'个人',
}
interface SortType { interface SortType {
type: 'ASC' | 'DESC'; type: 'ASC' | 'DESC';
value: string; value: string;
} }
interface FilterType { interface FilterType {
type: 'or' | 'and'; property: string;
operation: string;
value: string; value: string;
} }
@@ -18,6 +24,8 @@ export class CrontabView {
isDisabled?: 1 | 0; isDisabled?: 1 | 0;
filters?: FilterType[]; filters?: FilterType[];
sorts?: SortType[]; sorts?: SortType[];
filterRelation?: 'and' | 'or';
type?: CronViewType;
constructor(options: CrontabView) { constructor(options: CrontabView) {
this.name = options.name; this.name = options.name;
@@ -26,6 +34,8 @@ export class CrontabView {
this.isDisabled = options.isDisabled || 0; this.isDisabled = options.isDisabled || 0;
this.filters = options.filters; this.filters = options.filters;
this.sorts = options.sorts; this.sorts = options.sorts;
this.filterRelation = options.filterRelation;
this.type = options.type || CronViewType.;
} }
} }
@@ -43,5 +53,10 @@ export const CrontabViewModel = sequelize.define<CronViewInstance>(
isDisabled: DataTypes.NUMBER, isDisabled: DataTypes.NUMBER,
filters: DataTypes.JSON, filters: DataTypes.JSON,
sorts: DataTypes.JSON, sorts: DataTypes.JSON,
filterRelation: {
type: DataTypes.STRING,
allowNull: true,
},
type: DataTypes.NUMBER,
}, },
); );
+4 -1
View File
@@ -26,7 +26,10 @@ export enum EnvStatus {
'disabled', 'disabled',
} }
export const initEnvPosition = 9999999999; export const maxPosition = 9000000000000000;
export const initPosition = 4500000000000000;
export const stepPosition = 10000000;
export const minPosition = 100;
interface EnvInstance extends Model<Env, Env>, Env {} interface EnvInstance extends Model<Env, Env>, Env {}
export const EnvModel = sequelize.define<EnvInstance>('Env', { export const EnvModel = sequelize.define<EnvInstance>('Env', {
+12 -5
View File
@@ -1,15 +1,22 @@
import { Sequelize } from 'sequelize'; import { Sequelize, Transaction } from 'sequelize';
import config from '../config/index'; import config from '../config/index';
export const sequelize = new Sequelize({ export const sequelize = new Sequelize({
dialect: 'sqlite', dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`, storage: `${config.dbPath}database.sqlite`,
logging: false, logging: false,
pool: { retry: {
max: 6, max: 10,
min: 0, match: ['SQLITE_BUSY: database is locked'],
idle: 30000,
}, },
pool: {
max: 5,
min: 2,
idle: 30000,
acquire: 30000,
evict: 10000,
},
transactionType: Transaction.TYPES.IMMEDIATE,
}); });
export type ResponseType<T> = { code: number; data?: T; message?: string }; export type ResponseType<T> = { code: number; data?: T; message?: string };
+21 -3
View File
@@ -11,9 +11,11 @@ export enum NotificationMode {
'dingtalkBot' = 'dingtalkBot', 'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot', 'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp', 'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot', 'iGot' = 'iGot',
'pushPlus' = 'pushPlus', 'pushPlus' = 'pushPlus',
'email' = 'email', 'email' = 'email',
'feishu' = 'feishu',
'webhook' = 'webhook', 'webhook' = 'webhook',
} }
@@ -39,6 +41,7 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class PushDeerNotification extends NotificationBaseInfo { export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = ''; public pushDeerKey = '';
public pushDeerUrl = '';
} }
export class ChatNotification extends NotificationBaseInfo { export class ChatNotification extends NotificationBaseInfo {
@@ -48,7 +51,7 @@ export class ChatNotification extends NotificationBaseInfo {
export class BarkNotification extends NotificationBaseInfo { export class BarkNotification extends NotificationBaseInfo {
public barkPush = ''; public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png'; public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
} }
@@ -75,6 +78,12 @@ export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = ''; public weWorkAppKey = '';
} }
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
}
export class IGotNotification extends NotificationBaseInfo { export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = ''; public iGotPushKey = '';
} }
@@ -95,7 +104,14 @@ export class WebhookNotification extends NotificationBaseInfo {
public webhookBody: string = ''; public webhookBody: string = '';
public webhookUrl: string = ''; public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET'; public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' = 'application/json'; public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
} }
export interface NotificationInfo export interface NotificationInfo
@@ -109,7 +125,9 @@ export interface NotificationInfo
DingtalkBotNotification, DingtalkBotNotification,
WeWorkBotNotification, WeWorkBotNotification,
WeWorkAppNotification, WeWorkAppNotification,
AibotkNotification,
IGotNotification, IGotNotification,
PushPlusNotification, PushPlusNotification,
EmailNotification, EmailNotification,
WebhookNotification {} WebhookNotification,
LarkNotification {}
+3
View File
@@ -28,6 +28,7 @@ export class Subscription {
extensions?: string; extensions?: string;
sub_before?: string; sub_before?: string;
sub_after?: string; sub_after?: string;
proxy?: string;
constructor(options: Subscription) { constructor(options: Subscription) {
this.id = options.id; this.id = options.id;
@@ -54,6 +55,7 @@ export class Subscription {
this.extensions = options.extensions; this.extensions = options.extensions;
this.sub_before = options.sub_before; this.sub_before = options.sub_before;
this.sub_after = options.sub_after; this.sub_after = options.sub_after;
this.proxy = options.proxy;
} }
} }
@@ -102,5 +104,6 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
log_path: DataTypes.STRING, log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING, schedule_type: DataTypes.STRING,
alias: { type: DataTypes.STRING, unique: 'alias' }, alias: { type: DataTypes.STRING, unique: 'alias' },
proxy: { type: DataTypes.STRING, allowNull: true },
}, },
); );
+15 -6
View File
@@ -10,6 +10,7 @@ import { fileExist } from '../config/util';
import { SubscriptionModel } from '../data/subscription'; import { SubscriptionModel } from '../data/subscription';
import { CrontabViewModel } from '../data/cronView'; import { CrontabViewModel } from '../data/cronView';
import config from '../config'; import config from '../config';
import { sequelize } from '../data';
export default async () => { export default async () => {
try { try {
@@ -21,12 +22,20 @@ export default async () => {
await SubscriptionModel.sync(); await SubscriptionModel.sync();
await CrontabViewModel.sync(); await CrontabViewModel.sync();
// try { // 初始化新增字段
// const queryInterface = sequelize.getQueryInterface(); try {
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true }); await sequelize.query(
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true }); 'alter table CrontabViews add column filterRelation VARCHAR(255)',
// await queryInterface.addIndex('Apps', ['name'], { unique: true }); );
// } catch (error) { } } catch (error) {}
try {
await sequelize.query(
'alter table Subscriptions add column proxy VARCHAR(255)',
);
} catch (error) {}
try {
await sequelize.query('alter table CrontabViews add column type NUMBER');
} catch (error) {}
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
+18 -2
View File
@@ -4,16 +4,32 @@ import { Container } from 'typedi';
import { Crontab, CrontabModel, CrontabStatus } from '../data/cron'; import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import CronService from '../services/cron'; import CronService from '../services/cron';
import EnvService from '../services/env'; import EnvService from '../services/env';
import _ from 'lodash'; import groupBy from 'lodash/groupBy';
import { DependenceModel } from '../data/dependence'; import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import config from '../config'; import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
// 初始化新增默认全部任务视图
CrontabViewModel.findAll({
where: { type: CronViewType., name: '全部任务' },
raw: true,
}).then((docs) => {
if (docs.length === 0) {
CrontabViewModel.create({
name: '全部任务',
type: CronViewType.,
position: initPosition / 2,
});
}
});
// 初始化更新所有任务状态为空闲 // 初始化更新所有任务状态为空闲
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle }, { status: CrontabStatus.idle },
@@ -26,7 +42,7 @@ export default async () => {
order: [['type', 'DESC']], order: [['type', 'DESC']],
raw: true, raw: true,
}).then(async (docs) => { }).then(async (docs) => {
const groups = _.groupBy(docs, 'type'); const groups = groupBy(docs, 'type');
const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a)); const keys = Object.keys(groups).sort((a, b) => parseInt(b) - parseInt(a));
for (const key of keys) { for (const key of keys) {
const group = groups[key]; const group = groups[key];
+2 -5
View File
@@ -1,5 +1,4 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system'; import SystemService from '../services/system';
import ScheduleService from '../services/schedule'; import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
@@ -18,13 +17,12 @@ export default async () => {
tokenCommand = `node ${tokenFile}`; tokenCommand = `node ${tokenFile}`;
} }
const cron = { const cron = {
id: 'token', id: NaN,
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
}; };
scheduleService.createIntervalTask(cron as any, { scheduleService.createIntervalTask(cron, {
days: 28, days: 28,
runImmediately: true,
}); });
// 运行删除日志任务 // 运行删除日志任务
@@ -37,7 +35,6 @@ export default async () => {
}; };
scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: data.info.frequency, days: data.info.frequency,
runImmediately: true,
}); });
} }
+8 -3
View File
@@ -2,16 +2,21 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing'; import * as Tracing from '@sentry/tracing';
import Logger from './logger'; import Logger from './logger';
import config from '../config';
import fs from 'fs';
import { parseVersion } from '../config/util';
export default async ({ expressApp }: { expressApp: Application }) => {
const { version } = await parseVersion(config.versionFile);
export default ({ expressApp }: { expressApp: Application }) => {
Sentry.init({ Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819', dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [ integrations: [
new Sentry.Integrations.Http({ tracing: true }), new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }), new Tracing.Integrations.Express({ app: expressApp }),
], ],
tracesSampleRate: 0.1,
tracesSampleRate: 1.0, release: version,
}); });
expressApp.use(Sentry.Handlers.requestHandler()); expressApp.use(Sentry.Handlers.requestHandler());
+7 -10
View File
@@ -6,15 +6,12 @@ import config from './config';
const app = express(); const app = express();
app.get('/api/public/panel/log', (req, res) => { app.get('/api/public/panel/log', (req, res) => {
exec( exec('tail -n 300 ~/.pm2/logs/panel-error.log', (err, stdout, stderr) => {
'pm2 logs panel --lines 500 --nostream --timestamp', if (err || stderr) {
(err, stdout, stderr) => { return res.send({ code: 400, message: (err && err.message) || stderr });
if (err || stderr) { }
return res.send({ code: 400, message: (err && err.message) || stderr }); return res.send({ code: 200, data: stdout });
} });
return res.send({ code: 200, data: stdout });
},
);
}); });
app app
@@ -22,7 +19,7 @@ app
await require('./loaders/sentry').default({ expressApp: app }); await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`); Logger.debug(`✌️ 公共服务启动成功!`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+7 -7
View File
@@ -4,6 +4,7 @@ import { exec } from 'child_process';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import { CrontabModel, CrontabStatus } from './data/cron'; import { CrontabModel, CrontabStatus } from './data/cron';
import config from './config'; import config from './config';
import { QL_PREFIX, TASK_PREFIX } from './config/const';
const app = express(); const app = express();
@@ -23,8 +24,11 @@ const run = async () => {
) { ) {
schedule.scheduleJob(task.schedule, function () { schedule.scheduleJob(task.schedule, function () {
let command = task.command as string; let command = task.command as string;
if (!command.includes('task ') && !command.includes('ql ')) { if (
command = `task ${command}`; !command.startsWith(TASK_PREFIX) &&
!command.startsWith(QL_PREFIX)
) {
command = `${TASK_PREFIX}${command}`;
} }
exec(`ID=${task.id} ${command}`); exec(`ID=${task.id} ${command}`);
}); });
@@ -44,11 +48,7 @@ app
await require('./loaders/db').default(); await require('./loaders/db').default();
await run(); await run();
Logger.info(` Logger.debug('定时任务服务启动成功!');
################################################
🛡 Schedule listening on port: ${config.cronPort} 🛡
################################################
`);
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
+55 -152
View File
@@ -5,16 +5,20 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync, spawn } from 'child_process'; import { exec, execSync, spawn } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName, concurrentRun, fileExist } from '../config/util'; import {
getFileContentByName,
concurrentRun,
fileExist,
killTask,
} from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util'; import { Op, where, col as colFn } from 'sequelize';
import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import dayjs from 'dayjs'; import { TASK_PREFIX, QL_PREFIX } from '../config/const';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@@ -28,7 +32,7 @@ export default class CronService {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.set_crontab(this.isSixCron(doc)); await this.set_crontab();
return doc; return doc;
} }
@@ -39,7 +43,7 @@ export default class CronService {
public async update(payload: Crontab): Promise<Crontab> { public async update(payload: Crontab): Promise<Crontab> {
payload.saved = false; payload.saved = false;
const newDoc = await this.updateDb(payload); const newDoc = await this.updateDb(payload);
await this.set_crontab(this.isSixCron(newDoc)); await this.set_crontab();
return newDoc; return newDoc;
} }
@@ -78,7 +82,7 @@ export default class CronService {
public async remove(ids: number[]) { public async remove(ids: number[]) {
await CrontabModel.destroy({ where: { id: ids } }); await CrontabModel.destroy({ where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async pin(ids: number[]) { public async pin(ids: number[]) {
@@ -115,8 +119,9 @@ export default class CronService {
private formatViewQuery(query: any, viewQuery: any) { private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) { if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) { const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
query[Op.and] = []; if (!query[primaryOperate]) {
query[primaryOperate] = [];
} }
for (const col of viewQuery.filters) { for (const col of viewQuery.filters) {
const { property, value, operation } = col; const { property, value, operation } = col;
@@ -159,13 +164,27 @@ export default class CronService {
} }
if (operate && operate2) { if (operate && operate2) {
q[property] = { q[property] = {
[operate2]: [ [Op.or]: [
{ [operate]: `%${value}%` }, {
{ [operate]: `%${encodeURIComponent(value)}%` }, [operate2]: [
{ [operate]: `%${value}%` },
{ [operate]: `%${encodeURIComponent(value)}%` },
],
},
{
[operate2]: [
where(colFn(property), operate, `%${value}%`),
where(
colFn(property),
operate,
`%${encodeURIComponent(value)}%`,
),
],
},
], ],
}; };
} }
query[Op.and].push(q); query[primaryOperate].push(q);
} }
} }
} }
@@ -313,31 +332,11 @@ export default class CronService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
process.kill(-doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const err = await this.killTask(doc.command);
const absolutePath = path.resolve(config.logPath, `${doc.log_path}`);
const logFileExist = doc.log_path && (await fileExist(absolutePath));
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? `,耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`,
);
}
} }
await CrontabModel.update( await CrontabModel.update(
@@ -346,42 +345,6 @@ export default class CronService {
); );
} }
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
pids = pids.slice(0, 3);
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number> {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
@@ -399,8 +362,8 @@ export default class CronService {
this.logger.silly('Original command: ' + command); this.logger.silly('Original command: ' + command);
let cmdStr = command; let cmdStr = command;
if (!cmdStr.includes('task ') && !cmdStr.includes('ql ')) { if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `task ${cmdStr}`; cmdStr = `${TASK_PREFIX}${cmdStr}`;
} }
if ( if (
cmdStr.endsWith('.js') || cmdStr.endsWith('.js') ||
@@ -420,7 +383,7 @@ export default class CronService {
); );
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
if (logFileExist) { if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`); fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
} }
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
@@ -446,12 +409,12 @@ export default class CronService {
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 1 }, { where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async enabled(ids: number[]) { public async enabled(ids: number[]) {
await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabModel.update({ isDisabled: 0 }, { where: { id: ids } });
await this.set_crontab(true); await this.set_crontab();
} }
public async log(id: number) { public async log(id: number) {
@@ -464,104 +427,46 @@ export default class CronService {
const logFileExist = doc.log_path && (await fileExist(absolutePath)); const logFileExist = doc.log_path && (await fileExist(absolutePath));
if (logFileExist) { if (logFileExist) {
return getFileContentByName(`${absolutePath}`); return getFileContentByName(`${absolutePath}`);
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return getFileContentByName(`${logDir}/${files[files.length - 1]}`);
} else { } else {
return ''; return '任务未运行或运行失败,请尝试手动运行';
} }
} }
public async logs(id: number) { public async logs(id: number) {
const doc = await this.getDb({ id }); const doc = await this.getDb({ id });
if (!doc) { if (!doc || !doc.log_path) {
return []; return [];
} }
if (doc.log_path) { const relativeDir = path.dirname(`${doc.log_path}`);
const relativeDir = path.dirname(`${doc.log_path}`); const dir = path.resolve(config.logPath, relativeDir);
const dir = path.resolve(config.logPath, relativeDir); if (existsSync(dir)) {
if (existsSync(dir)) { let files = await promises.readdir(dir);
let files = await promises.readdir(dir);
return files
.map((x) => ({
filename: x,
directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
}))
.sort((a, b) => b.time - a.time);
}
}
const [, commandStr, url] = doc.command.split(/ +/);
let logPath = this.getKey(commandStr);
const isQlCommand = doc.command.startsWith('ql ');
const key =
(url && ['repo', 'raw'].includes(commandStr) && this.getKey(url)) ||
logPath;
if (isQlCommand) {
logPath = 'update';
}
let logDir = `${config.logPath}${logPath}`;
if (existsSync(logDir)) {
let files = await promises.readdir(logDir);
if (isQlCommand) {
files = files.filter((x) => x.includes(key));
}
return files return files
.map((x) => ({ .map((x) => ({
filename: x, filename: x,
directory: logPath, directory: relativeDir.replace(config.logPath, ''),
time: fs.statSync(`${logDir}/${x}`).mtime.getTime(), time: fs.statSync(`${dir}/${x}`).mtime.getTime(),
})) }))
.sort((a, b) => b.time - a.time); .sort((a, b) => b.time - a.time);
} else { } else {
return []; return [];
} }
}
private getKey(command: string): string {
const start =
command.lastIndexOf('/') !== -1 ? command.lastIndexOf('/') + 1 : 0;
const end =
command.lastIndexOf('.') !== -1
? command.lastIndexOf('.')
: command.length;
const tmpStr = command.substring(0, start - 1);
let index = 0;
if (tmpStr.lastIndexOf('/') !== -1 && tmpStr.startsWith('http')) {
index = tmpStr.lastIndexOf('/');
} else if (tmpStr.lastIndexOf(':') !== -1 && tmpStr.startsWith('git@')) {
index = tmpStr.lastIndexOf(':');
}
if (index) {
return `${tmpStr.substring(index + 1)}_${command.substring(start, end)}`;
} else {
return command.substring(start, end);
}
} }
private make_command(tab: Crontab) { private make_command(tab: Crontab) {
if (
!tab.command.startsWith(TASK_PREFIX) &&
!tab.command.startsWith(QL_PREFIX)
) {
tab.command = `${TASK_PREFIX}${tab.command}`;
}
const crontab_job_string = `ID=${tab.id} ${tab.command}`; const crontab_job_string = `ID=${tab.id} ${tab.command}`;
return crontab_job_string; return crontab_job_string;
} }
private async set_crontab(needReloadSchedule: boolean = false) { private async set_crontab() {
const tabs = await this.crontabs(); const tabs = await this.crontabs();
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
@@ -584,9 +489,7 @@ export default class CronService {
fs.writeFileSync(config.crontabFile, crontab_string); fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
if (needReloadSchedule) { exec(`pm2 reload schedule`);
exec(`pm2 reload schedule`);
}
await CrontabModel.update({ saved: true }, { where: {} }); await CrontabModel.update({ saved: true }, { where: {} });
} }
+32 -7
View File
@@ -1,14 +1,19 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView'; import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env'; import {
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
@Service() @Service()
export default class CronViewService { export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> { public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition; let position = initPosition;
const views = await this.list(); const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) { if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number; position = views[views.length - 1].position as number;
@@ -16,6 +21,8 @@ export default class CronViewService {
position = position / 2; position = position / 2;
const tab = new CrontabView({ ...payload, position }); const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab); const doc = await this.insert(tab);
await this.checkPosition(tab.position!);
return doc; return doc;
} }
@@ -62,6 +69,22 @@ export default class CronViewService {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } }); await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
} }
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.list();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async move({ public async move({
id, id,
fromIndex, fromIndex,
@@ -76,17 +99,19 @@ export default class CronViewService {
const views = await this.list(); const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) { if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward targetPosition = isUpward
? views[0].position * 2 ? views[0].position! * 2
: views[toIndex].position / 2; : views[toIndex].position! / 2;
} else { } else {
targetPosition = isUpward targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2 ? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2; : (views[toIndex].position! + views[toIndex + 1].position!) / 2;
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
position: targetPosition, position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition);
return newDoc; return newDoc;
} }
} }
-1
View File
@@ -9,7 +9,6 @@ import {
unInstallDependenceCommandTypes, unInstallDependenceCommandTypes,
DependenceModel, DependenceModel,
} from '../data/dependence'; } from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
+53 -28
View File
@@ -1,10 +1,17 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env'; import {
import _ from 'lodash'; Env,
EnvModel,
EnvStatus,
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
@Service() @Service()
@@ -13,17 +20,22 @@ export default class EnvService {
public async create(payloads: Env[]): Promise<Env[]> { public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs(); const envs = await this.envs();
let position = initEnvPosition; let position = initPosition;
if (envs && envs.length > 0 && envs[envs.length - 1].position) { if (
position = envs[envs.length - 1].position as number; envs &&
envs.length > 0 &&
typeof envs[envs.length - 1].position === 'number'
) {
position = envs[envs.length - 1].position!;
} }
const tabs = payloads.map((x) => { const tabs = payloads.map((x) => {
position = position / 2; position = position - stepPosition;
const tab = new Env({ ...x, position }); const tab = new Env({ ...x, position });
return tab; return tab;
}); });
const docs = await this.insert(tabs); const docs = await this.insert(tabs);
await this.set_envs(); await this.set_envs();
await this.checkPosition(tabs[tabs.length - 1].position!);
return docs; return docs;
} }
@@ -67,25 +79,40 @@ export default class EnvService {
const envs = await this.envs(); const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) { if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward targetPosition = isUpward
? envs[0].position * 2 ? envs[0].position! + stepPosition
: envs[toIndex].position / 2; : envs[toIndex].position! - stepPosition;
} else { } else {
targetPosition = isUpward targetPosition = isUpward
? (envs[toIndex].position + envs[toIndex - 1].position) / 2 ? (envs[toIndex].position! + envs[toIndex - 1].position!) / 2
: (envs[toIndex].position + envs[toIndex + 1].position) / 2; : (envs[toIndex].position! + envs[toIndex + 1].position!) / 2;
} }
const newDoc = await this.update({ const newDoc = await this.update({
id, id,
position: targetPosition, position: this.getPrecisionPosition(targetPosition),
}); });
await this.checkPosition(targetPosition);
return newDoc; return newDoc;
} }
public async envs( private async checkPosition(position: number) {
searchText: string = '', const precisionPosition = parseFloat(position.toPrecision(16));
sort: any = { position: -1 }, if (precisionPosition < minPosition || precisionPosition > maxPosition) {
query: any = {}, const envs = await this.envs();
): Promise<Env[]> { let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
const encodeText = encodeURIComponent(searchText); const encodeText = encodeURIComponent(searchText);
@@ -115,7 +142,7 @@ export default class EnvService {
const result = await this.find(condition, [ const result = await this.find(condition, [
['status', 'ASC'], ['status', 'ASC'],
['position', 'DESC'], ['position', 'DESC'],
['createdAt', 'DESC'], ['createdAt', 'ASC'],
]); ]);
return result as any; return result as any;
} catch (error) { } catch (error) {
@@ -155,22 +182,20 @@ export default class EnvService {
} }
public async set_envs() { public async set_envs() {
const envs = await this.envs( const envs = await this.envs('', {
'', name: { [Op.not]: null },
{ position: -1 }, status: EnvStatus.normal,
{ name: { [Op.not]: null } }, });
); const groups = groupBy(envs, 'name');
const groups = _.groupBy(envs, 'name');
let env_string = ''; let env_string = '';
for (const key in groups) { for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) { if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key]; const group = groups[key];
// 忽略不符合bash要求的环境变量名称 // 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) { if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
let value = _(group) let value = group
.filter((x) => x.status !== EnvStatus.disabled) .map((x) => x.value)
.map('value')
.join('&') .join('&')
.replace(/(\\)[^n]/g, '\\\\') .replace(/(\\)[^n]/g, '\\\\')
.replace(/(\\$)/, '\\\\') .replace(/(\\$)/, '\\\\')
+102 -42
View File
@@ -24,18 +24,23 @@ export default class NotificationService {
['dingtalkBot', this.dingtalkBot], ['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot], ['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp], ['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot], ['iGot', this.iGot],
['pushPlus', this.pushPlus], ['pushPlus', this.pushPlus],
['email', this.email], ['email', this.email],
['webhook', this.webhook], ['webhook', this.webhook],
['lark', this.lark],
]); ]);
private timeout = 10000;
private title = ''; private title = '';
private content = ''; private content = '';
private params!: Omit<NotificationInfo, 'type'>; private params!: Omit<NotificationInfo, 'type'>;
private gotOption = {
timeout: 30000,
retry: 1,
};
constructor(@Inject('logger') private logger: winston.Logger) { } constructor(@Inject('logger') private logger: winston.Logger) {}
public async notify( public async notify(
title: string, title: string,
@@ -76,8 +81,7 @@ export default class NotificationService {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params; const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, { .post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `title=${encodeURIComponent( body: `title=${encodeURIComponent(
this.title, this.title,
)}&message=${encodeURIComponent( )}&message=${encodeURIComponent(
@@ -95,8 +99,7 @@ export default class NotificationService {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params; const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, { .post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { message: `${this.title}\n${this.content}` }, json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken }, headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
}) })
@@ -111,8 +114,7 @@ export default class NotificationService {
: `https://sc.ftqq.com/${serverChanKey}.send`; : `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `title=${this.title}&desp=${this.content}`, body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
@@ -121,12 +123,11 @@ export default class NotificationService {
} }
private async pushDeer() { private async pushDeer() {
const { pushDeerKey } = this.params; const { pushDeerKey, pushDeerUrl } = this.params;
const url = `https://api2.pushdeer.com/message/push`; const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent( body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title, this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`, )}&desp=${encodeURIComponent(this.content)}&type=markdown`,
@@ -143,8 +144,7 @@ export default class NotificationService {
const url = `${chatUrl}${chatToken}`; const url = `${chatUrl}${chatToken}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `payload={"text":"${this.title}\n${this.content}"}`, body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
@@ -164,8 +164,7 @@ export default class NotificationService {
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got const res: any = await got
.get(url, { .get(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
.json(); .json();
@@ -182,8 +181,9 @@ export default class NotificationService {
telegramBotUserId, telegramBotUserId,
} = this.params; } = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : ''; const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org' const url = `https://${
}/bot${telegramBotToken}/sendMessage`; telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent; let agent;
if (telegramBotProxyHost && telegramBotProxyPort) { if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = { const options: any = {
@@ -202,8 +202,7 @@ export default class NotificationService {
} }
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`, body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent, agent,
@@ -225,8 +224,7 @@ export default class NotificationService {
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`; const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -243,8 +241,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`; const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
@@ -263,8 +260,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`; const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const tokenRes: any = await got const tokenRes: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
corpid, corpid,
corpsecret, corpsecret,
@@ -314,8 +310,7 @@ export default class NotificationService {
.post( .post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`, `https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{ {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
touser, touser,
agentid, agentid,
@@ -329,13 +324,53 @@ export default class NotificationService {
return res.errcode === 0; return res.errcode === 0;
} }
private async aibotk() {
const { aibotkKey, aibotkType, aibotkName } = this.params;
let url = '';
let json = {};
switch (aibotkType) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${aibotkKey}`,
roomName: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${aibotkKey}`,
name: `${aibotkName}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${this.title}\n${this.content}`,
},
};
break;
}
const res: any = await got
.post(url, {
...this.gotOption,
json: {
...json,
},
})
.json();
return res.code === 0;
}
private async iGot() { private async iGot() {
const { iGotPushKey } = this.params; const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`; const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
body: `title=${this.title}&content=${this.content}`, body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
}) })
@@ -349,8 +384,7 @@ export default class NotificationService {
const url = `https://www.pushplus.plus/send`; const url = `https://www.pushplus.plus/send`;
const res: any = await got const res: any = await got
.post(url, { .post(url, {
timeout: this.timeout, ...this.gotOption,
retry: 0,
json: { json: {
token: `${pushPlusToken}`, token: `${pushPlusToken}`,
title: `${this.title}`, title: `${this.title}`,
@@ -363,6 +397,21 @@ export default class NotificationService {
return res.code === 200; return res.code === 200;
} }
private async lark() {
const { larkKey } = this.params;
const res: any = await got
.post(`https://open.feishu.cn/open-apis/bot/v2/hook/${larkKey}`, {
...this.gotOption,
json: {
msg_type: 'text',
content: { text: `${this.title}\n\n${this.content}` },
},
headers: { 'Content-Type': 'application/json' },
})
.json();
return res.StatusCode === 0;
}
private async email() { private async email() {
const { emailPass, emailService, emailUser } = this.params; const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({ const transporter = nodemailer.createTransport({
@@ -386,10 +435,18 @@ export default class NotificationService {
} }
private async webhook() { private async webhook() {
const { webhookUrl, webhookBody, webhookHeaders, webhookMethod, webhookContentType } = const {
this.params; webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(webhookUrl, webhookBody); const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) { if (!formatUrl && !formatBody) {
return false; return false;
} }
@@ -399,11 +456,10 @@ export default class NotificationService {
const options = { const options = {
method: webhookMethod, method: webhookMethod,
headers, headers,
timeout: this.timeout, ...this.gotOption,
retry: 0,
allowGetBody: true, allowGetBody: true,
...bodyParam ...bodyParam,
} };
const res = await got(formatUrl, options); const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20'); return String(res.statusCode).startsWith('20');
} }
@@ -427,8 +483,12 @@ export default class NotificationService {
} }
return { return {
formatUrl: url.replaceAll('$title', encodeURIComponent(this.title)).replaceAll('$content', encodeURIComponent(this.content)), formatUrl: url
formatBody: body.replaceAll('$title', this.title).replaceAll('$content', this.content), .replaceAll('$title', encodeURIComponent(this.title))
} .replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body
.replaceAll('$title', this.title)
.replaceAll('$content', this.content),
};
} }
} }
+1 -1
View File
@@ -119,7 +119,7 @@ export default class OpenService {
(x) => x.expiration >= timestamp, (x) => x.expiration >= timestamp,
); );
let tokens = invalidTokens; let tokens = invalidTokens;
if (invalidTokens.length > 5) { if (invalidTokens.length >= 5) {
tokens = [ tokens = [
...invalidTokens.slice(0, 4), ...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration }, { ...invalidTokens[4], expiration },
+6 -2
View File
@@ -42,7 +42,11 @@ export default class ScheduleService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
async runTask(command: string, callbacks: TaskCallbacks = {}) { async runTask(
command: string,
callbacks: TaskCallbacks = {},
completionTime: 'start' | 'end' = 'end',
) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
const startTime = dayjs(); const startTime = dayjs();
@@ -52,6 +56,7 @@ export default class ScheduleService {
// TODO: // TODO:
callbacks.onStart?.(cp, startTime); callbacks.onStart?.(cp, startTime);
completionTime === 'start' && resolve(cp.pid);
cp.stdout.on('data', async (data) => { cp.stdout.on('data', async (data) => {
await callbacks.onLog?.(data.toString()); await callbacks.onLog?.(data.toString());
@@ -100,7 +105,6 @@ export default class ScheduleService {
error, error,
); );
await callbacks.onError?.(JSON.stringify(error)); await callbacks.onError?.(JSON.stringify(error));
resolve(null);
} }
}); });
} }
+18 -14
View File
@@ -6,6 +6,8 @@ import SockService from './sock';
import CronService from './cron'; import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config'; import config from '../config';
import { TASK_COMMAND } from '../config/const';
import { getPid, killTask } from '../config/util';
@Service() @Service()
export default class ScriptService { export default class ScriptService {
@@ -40,23 +42,25 @@ export default class ScriptService {
public async runScript(filePath: string) { public async runScript(filePath: string) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
const command = `task -l ${relativePath} now`; const command = `${TASK_COMMAND} -l ${relativePath} now`;
this.scheduleService.runTask(command, this.taskCallbacks(filePath)); const pid = await this.scheduleService.runTask(
command,
this.taskCallbacks(filePath),
'start',
);
return { code: 200 }; return { code: 200, data: pid };
} }
public async stopScript(filePath: string) { public async stopScript(filePath: string, pid: number) {
const relativePath = path.relative(config.scriptPath, filePath); let str = '';
const err = await this.cronService.killTask(`task -l ${relativePath} now`); if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath);
const str = err ? `\n${err}` : ''; pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`);
this.sockService.sendMessage({ }
type: 'manuallyRunScript', try {
message: `${str}\n## 执行结束... ${new Date() await killTask(pid);
.toLocaleString('zh', { hour12: false }) } catch (error) {}
.replace(' 24:', ' 00:')} `,
});
return { code: 200 }; return { code: 200 };
} }
+20 -7
View File
@@ -14,7 +14,7 @@ export default class SshKeyService {
private generatePrivateKeyFile(alias: string, key: string): void { private generatePrivateKeyFile(alias: string, key: string): void {
try { try {
fs.writeFileSync(`${this.sshPath}/${alias}`, key, { fs.writeFileSync(`${this.sshPath}/${alias}`, `${key}${os.EOL}`, {
encoding: 'utf8', encoding: 'utf8',
mode: '400', mode: '400',
}); });
@@ -38,8 +38,16 @@ export default class SshKeyService {
} }
} }
private generateSingleSshConfig(alias: string, host: string): string { private generateSingleSshConfig(
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`; alias: string,
host: string,
proxy?: string,
): string {
if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`;
}
const proxyStr = proxy ? ` ProxyCommand nc -v -x ${proxy} %h %p\n` : '';
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no\n${proxyStr}`;
} }
private generateSshConfig(configs: string[]) { private generateSshConfig(configs: string[]) {
@@ -69,16 +77,21 @@ export default class SshKeyService {
} }
} }
public addSSHKey(key: string, alias: string, host: string): void { public addSSHKey(
key: string,
alias: string,
host: string,
proxy?: string,
): void {
this.generatePrivateKeyFile(alias, key); this.generatePrivateKeyFile(alias, key);
const config = this.generateSingleSshConfig(alias, host); const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias); this.removeSshConfig(alias);
this.generateSshConfig([config]); this.generateSshConfig([config]);
} }
public removeSSHKey(alias: string, host: string): void { public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias); this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host); const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config); this.removeSshConfig(config);
} }
} }
+19 -45
View File
@@ -19,9 +19,9 @@ import {
concurrentRun, concurrentRun,
fileExist, fileExist,
createFile, createFile,
killTask,
} from '../config/util'; } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { promisify } from 'util';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import path from 'path'; import path from 'path';
import ScheduleService, { TaskCallbacks } from './schedule'; import ScheduleService, { TaskCallbacks } from './schedule';
@@ -29,6 +29,7 @@ import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock'; import SockService from './sock';
import SshKeyService from './sshKey'; import SshKeyService from './sshKey';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service() @Service()
export default class SubscriptionService { export default class SubscriptionService {
@@ -76,13 +77,21 @@ export default class SubscriptionService {
private formatCommand(doc: Subscription, url?: string) { private formatCommand(doc: Subscription, url?: string) {
let command = 'ql '; let command = 'ql ';
let _url = url || this.formatUrl(doc).url; let _url = url || this.formatUrl(doc).url;
const { type, whitelist, blacklist, dependences, branch, extensions } = doc; const {
type,
whitelist,
blacklist,
dependences,
branch,
extensions,
proxy,
} = doc;
if (type === 'file') { if (type === 'file') {
command += `raw "${_url}"`; command += `raw "${_url}"`;
} else { } else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${ command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || '' dependences || ''
}" "${branch || ''}" "${extensions || ''}"`; }" "${branch || ''}" "${extensions || ''}" "${proxy || ''}"`;
} }
return command; return command;
} }
@@ -116,9 +125,10 @@ export default class SubscriptionService {
(doc.pull_option as any).private_key, (doc.pull_option as any).private_key,
doc.alias, doc.alias,
host, host,
doc.proxy,
); );
} else { } else {
this.sshKeyService.removeSSHKey(doc.alias, host); this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
} }
} }
@@ -232,7 +242,7 @@ export default class SubscriptionService {
absolutePath, absolutePath,
`\n## 执行结束... ${endTime.format( `\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm:ss',
)} ${diff} `, )} ${diff} ${LOG_END_SYMBOL}`,
); );
await SubscriptionModel.update( await SubscriptionModel.update(
@@ -341,19 +351,18 @@ export default class SubscriptionService {
for (const doc of docs) { for (const doc of docs) {
if (doc.pid) { if (doc.pid) {
try { try {
process.kill(-doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.silly(error);
} }
} }
const command = this.formatCommand(doc);
const err = await this.killTask(command);
const absolutePath = await this.handleLogPath(doc.log_path as string); const absolutePath = await this.handleLogPath(doc.log_path as string);
const str = err ? `\n${err}` : '';
fs.appendFileSync( fs.appendFileSync(
`${absolutePath}`, `${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `, `\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
); );
} }
@@ -363,41 +372,6 @@ export default class SubscriptionService {
); );
} }
public async killTask(name: string) {
let taskCommand = `ps -ef | grep "${name}" | grep -v grep | awk '{print $1}'`;
const execAsync = promisify(exec);
try {
let pid = (await execAsync(taskCommand)).stdout;
if (pid) {
pid = (await execAsync(`pstree -p ${pid}`)).stdout;
} else {
return;
}
let pids = pid.match(/\(\d+/g);
const killLogs = [];
if (pids && pids.length > 0) {
// node 执行脚本时还会有10个子进程,但是ps -ef中不存在,所以截取前三个
for (const id of pids) {
const c = `kill -9 ${id.slice(1)}`;
try {
const { stdout, stderr } = await execAsync(c);
if (stderr) {
killLogs.push(stderr);
}
if (stdout) {
killLogs.push(stdout);
}
} catch (error: any) {
killLogs.push(error.message);
}
}
}
return killLogs.length > 0 ? JSON.stringify(killLogs) : '';
} catch (e) {
return JSON.stringify(e);
}
}
private async runSingle(subscriptionId: number) { private async runSingle(subscriptionId: number) {
const subscription = await this.getDb({ id: subscriptionId }); const subscription = await this.getDb({ id: subscriptionId });
if (subscription.status !== SubscriptionStatus.queued) { if (subscription.status !== SubscriptionStatus.queued) {
+22 -26
View File
@@ -2,7 +2,6 @@ import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@@ -10,6 +9,7 @@ import ScheduleService from './schedule';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import SockService from './sock'; import SockService from './sock';
import got from 'got'; import got from 'got';
import { parseContentVersion, parseVersion } from '../config/util';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -70,9 +70,8 @@ export default class SystemService {
}; };
await this.scheduleService.cancelIntervalTask(cron); await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) { if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, { this.scheduleService.createIntervalTask(cron, {
days: frequency, days: frequency,
runImmediately: true,
}); });
} }
return { code: 200, data: { ...cron } }; return { code: 200, data: { ...cron } };
@@ -80,32 +79,33 @@ export default class SystemService {
public async checkUpdate() { public async checkUpdate() {
try { try {
const versionRegx = /.*export const version = \'(.*)\'\;/; const currentVersionContent = await parseVersion(config.versionFile);
const logRegx = /.*export const changeLog = \`((.*\n.*)+)\`;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8'); let lastVersionContent;
const currentVersion = currentVersionFile.match(versionRegx)![1];
let lastVersion = '';
let lastLog = '';
try { try {
const result = await got.get(config.lastVersionFile, { const result = await got.get(
timeout: 6000, `${config.lastVersionFile}?t=${Date.now()}`,
retry: 0, {
}); timeout: 30000,
const lastVersionFileContent = result.body; },
lastVersion = lastVersionFileContent.match(versionRegx)![1]; );
lastLog = lastVersionFileContent.match(logRegx) lastVersionContent = await parseContentVersion(result.body);
? lastVersionFileContent.match(logRegx)![1]
: '';
} catch (error) {} } catch (error) {}
if (!lastVersionContent) {
lastVersionContent = currentVersionContent;
}
return { return {
code: 200, code: 200,
data: { data: {
hasNewVersion: this.checkHasNewVersion(currentVersion, lastVersion), hasNewVersion: this.checkHasNewVersion(
lastVersion, currentVersionContent.version,
lastLog, lastVersionContent.version,
),
lastVersion: lastVersionContent.version,
lastLog: lastVersionContent.changeLog,
lastLogLink: lastVersionContent.changeLogLink,
}, },
}; };
} catch (error: any) { } catch (error: any) {
@@ -138,10 +138,6 @@ export default class SystemService {
public async updateSystem() { public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' }); const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => { cp.stdout.on('data', (data) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
type: 'updateSystemVersion', type: 'updateSystemVersion',
+33 -20
View File
@@ -3,7 +3,6 @@ import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util'; import { createRandomString, getNetIp, getPlatform } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default'; import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth'; import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
@@ -65,32 +64,37 @@ export default class UserService {
return this.initAuthInfo(); return this.initAuthInfo();
} }
if (retries > 2 && Date.now() - lastlogon < Math.pow(3, retries) * 1000) { const retriesTime = Math.pow(3, retries) * 1000;
if (retries > 2 && timestamp - lastlogon < retriesTime) {
const waitTime = Math.ceil(
(retriesTime - (timestamp - lastlogon)) / 1000,
);
return { return {
code: 410, code: 410,
message: `失败次数过多,请${Math.round( message: `失败次数过多,请${waitTime}秒后重试`,
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000, data: waitTime,
)}`, };
data: Math.round( }
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
), if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
}; };
} }
const { ip, address } = await getNetIp(req); const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) { if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100); const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3; const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, { let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration, expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384', algorithm: 'HS384',
@@ -155,7 +159,16 @@ export default class UserService {
status: LoginStatus.fail, status: LoginStatus.fail,
}, },
}); });
return { code: 400, message: config.authError }; if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
} }
} else { } else {
return this.initAuthInfo(); return this.initAuthInfo();
+9 -3
View File
@@ -5,6 +5,7 @@ import LoggerInstance from './loaders/logger';
import fs from 'fs'; import fs from 'fs';
import config from './config'; import config from './config';
import path from 'path'; import path from 'path';
import os from 'os';
const tokenFile = path.join(config.configPath, 'token.json'); const tokenFile = path.join(config.configPath, 'token.json');
@@ -25,9 +26,14 @@ async function getToken() {
async function writeFile(data: any) { async function writeFile(data: any) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
fs.writeFile(tokenFile, JSON.stringify(data), { encoding: 'utf8' }, () => { fs.writeFile(
resolve(); tokenFile,
}); `${JSON.stringify(data)}${os.EOL}`,
{ encoding: 'utf8' },
() => {
resolve();
},
);
}); });
} }
+21 -9
View File
@@ -1,4 +1,13 @@
FROM python:alpine FROM python:3.10-alpine as builder
COPY package.json .npmrc pnpm-lock.yaml /tmp/build/
RUN set -x \
&& apk update \
&& apk add nodejs npm \
&& npm i -g pnpm \
&& cd /tmp/build \
&& pnpm install --prod
FROM python:3.10-alpine
ARG QL_MAINTAINER="whyour" ARG QL_MAINTAINER="whyour"
LABEL maintainer="${QL_MAINTAINER}" LABEL maintainer="${QL_MAINTAINER}"
@@ -13,8 +22,6 @@ ENV PNPM_HOME=/root/.local/share/pnpm \
QL_DIR=/ql \ QL_DIR=/ql \
QL_BRANCH=${QL_BRANCH} QL_BRANCH=${QL_BRANCH}
WORKDIR ${QL_DIR}
RUN set -x \ RUN set -x \
&& sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \ && sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories \
&& apk update -f \ && apk update -f \
@@ -42,19 +49,24 @@ RUN set -x \
&& git config --global http.postBuffer 524288000 \ && git config --global http.postBuffer 524288000 \
&& npm install -g pnpm \ && npm install -g pnpm \
&& pnpm add -g pm2 ts-node typescript tslib \ && pnpm add -g pm2 ts-node typescript tslib \
&& git clone -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \ && 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 -b ${QL_BRANCH} ${QL_URL} ${QL_DIR} \
&& cd ${QL_DIR} \ && cd ${QL_DIR} \
&& cp -f .env.example .env \ && cp -f .env.example .env \
&& chmod 777 ${QL_DIR}/shell/*.sh \ && chmod 777 ${QL_DIR}/shell/*.sh \
&& chmod 777 ${QL_DIR}/docker/*.sh \ && chmod 777 ${QL_DIR}/docker/*.sh \
&& pnpm install --prod \
&& rm -rf /root/.pnpm-store \
&& rm -rf /root/.local/share/pnpm/store \
&& rm -rf /root/.cache \
&& rm -rf /root/.npm \
&& git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \ && git clone -b ${QL_BRANCH} https://github.com/${QL_MAINTAINER}/qinglong-static.git /static \
&& mkdir -p ${QL_DIR}/static \ && mkdir -p ${QL_DIR}/static \
&& cp -rf /static/* ${QL_DIR}/static \ && cp -rf /static/* ${QL_DIR}/static \
&& rm -rf /static && rm -rf /static
COPY --from=builder /tmp/build/node_modules/. /ql/node_modules/
WORKDIR ${QL_DIR}
ENTRYPOINT ["./docker/docker-entrypoint.sh"] ENTRYPOINT ["./docker/docker-entrypoint.sh"]
+3 -8
View File
@@ -14,12 +14,7 @@ cp -fv $nginx_app_conf /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf sed -i "s,QL_BASE_URL,${qlBaseUrl},g" /etc/nginx/conf.d/front.conf
pm2 l &>/dev/null pm2 l &>/dev/null
if [[ $PipMirror ]]; then patch_version &>/dev/null
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
echo echo
echo -e "======================2. 安装依赖========================\n" echo -e "======================2. 安装依赖========================\n"
@@ -47,13 +42,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then if [[ $AutoStartBot == true ]]; then
echo -e "======================7. 启动bot========================\n" echo -e "======================7. 启动bot========================\n"
nohup ql bot >>$dir_log/start.log 2>&1 & nohup ql bot >$dir_log/bot.log 2>&1 &
echo -e "bot后台启动中...\n" echo -e "bot后台启动中...\n"
fi fi
if [[ $EnableExtraShell == true ]]; then if [[ $EnableExtraShell == true ]]; then
echo -e "======================8. 执行自定义脚本========================\n" echo -e "======================8. 执行自定义脚本========================\n"
nohup ql extra >>$dir_log/start.log 2>&1 & nohup ql extra >$dir_log/extra.log 2>&1 &
echo -e "自定义脚本后台执行中...\n" echo -e "自定义脚本后台执行中...\n"
fi fi
+22 -14
View File
@@ -4,7 +4,7 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node back/public.ts", "start:public": "ts-node --transpile-only ./back/public.ts",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
@@ -42,7 +42,8 @@
"monaco-editor", "monaco-editor",
"rc-field-form", "rc-field-form",
"@types/lodash.merge", "@types/lodash.merge",
"rollup" "rollup",
"styled-components"
], ],
"allowedVersions": { "allowedVersions": {
"react": "18", "react": "18",
@@ -69,18 +70,19 @@
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^0.1.2",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"multer": "^1.4.4", "multer": "^1.4.4",
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-queue": "7.2.0", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"sequelize": "^6.20.1", "sequelize": "^6.25.5",
"serve-handler": "^6.1.3", "serve-handler": "^6.1.3",
"sockjs": "^0.3.24", "sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6", "sqlite3": "npm:@louislam/sqlite3@15.1.2",
"toad-scheduler": "^1.6.0", "toad-scheduler": "^1.6.0",
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
@@ -89,15 +91,17 @@
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1", "@ant-design/pro-layout": "6.38.22",
"@monaco-editor/react": "4.2.1", "@monaco-editor/react": "4.4.6",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1", "@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2", "@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4", "@types/express-jwt": "^6.0.4",
"@types/js-yaml": "^4.0.5",
"@types/jsonwebtoken": "^8.5.8", "@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.179", "@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7", "@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12", "@types/nedb": "^1.8.12",
"@types/node": "^17.0.21", "@types/node": "^17.0.21",
@@ -105,39 +109,43 @@
"@types/nodemailer": "^6.4.4", "@types/nodemailer": "^6.4.4",
"@types/qrcode.react": "^1.0.2", "@types/qrcode.react": "^1.0.2",
"@types/react": "^18.0.20", "@types/react": "^18.0.20",
"@types/react-copy-to-clipboard": "^5.0.4",
"@types/react-dom": "^18.0.6", "@types/react-dom": "^18.0.6",
"@types/serve-handler": "^6.1.1", "@types/serve-handler": "^6.1.1",
"@types/sockjs": "^0.3.33", "@types/sockjs": "^0.3.33",
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@umijs/max": "^4.0.21", "@umijs/max": "^4.0.42",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ansi-to-react": "^6.1.6", "ansi-to-react": "^6.1.6",
"antd": "^4.23.0", "antd": "^4.24.7",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"codemirror": "^5.65.2", "codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"lint-staged": "^12.3.4", "lint-staged": "^13.0.3",
"monaco-editor": "0.33.0",
"nodemon": "^2.0.15", "nodemon": "^2.0.15",
"prettier": "^2.5.1", "prettier": "^2.5.1",
"qiniu": "^7.4.0", "qiniu": "^7.4.0",
"qrcode.react": "^1.0.1", "qrcode.react": "^1.0.1",
"query-string": "^7.1.1", "query-string": "^7.1.1",
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"react": "18.x", "react": "18.2.0",
"react-codemirror2": "^7.2.1", "react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0", "react-dnd-html5-backend": "^14.0.0",
"react-dom": "18.x", "react-dom": "18.2.0",
"react-split-pane": "^0.1.92", "react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0", "sockjs-client": "^1.6.0",
"ts-node": "^10.6.0", "ts-node": "^10.6.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"typescript": "^4.6.2", "typescript": "4.8.4",
"umi-request": "^1.4.0", "umi-request": "^1.4.0",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtuallist-antd": "^0.7.6",
"webpack": "^5.70.0", "webpack": "^5.70.0",
"yorkie": "^2.0.0" "yorkie": "^2.0.0"
} }
+15237
View File
File diff suppressed because it is too large Load Diff
+29 -2
View File
@@ -16,9 +16,14 @@ DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可 ## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py" RepoFileExtensions="js py"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890 ## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
ProxyUrl="" ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天 ## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h" CommandTimeoutTime="1h"
@@ -62,7 +67,7 @@ export PUSH_KEY=""
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123 ## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH="" export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上) ## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="http://qn.whyour.cn/logo.png" export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声 ## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND="" export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong" ## 下方填写推送消息分组,默认为"QingLong"
@@ -146,4 +151,26 @@ export DEER_KEY=""
export CHAT_URL="" export CHAT_URL=""
export CHAT_TOKEN="" export CHAT_TOKEN=""
## 13. aibotk
## 官方说明文档:http://wechat.aibotk.com/oapi/oapi?from=ql
## aibotk_key (必填)填写智能微秘书个人中心的apikey
export AIBOTK_KEY=""
## aibotk_type (必填)填写发送的目标 room 或 contact, 填其他的不生效
export AIBOTK_TYPE=""
## aibotk_name (必填)填写群名或用户昵称,和上面的type类型要对应
export AIBOTK_NAME=""
## 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"
## smtp_email 填写 SMTP 收发件邮箱,通知将会由自己发给自己
export SMTP_EMAIL=""
## smtp_password 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
export SMTP_PASSWORD=""
## smtp_name 填写 SMTP 收发件人姓名,可随意填写
export SMTP_NAME=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+269 -155
View File
@@ -38,6 +38,7 @@ let SCKEY = '';
//此处填你申请的PushDeer KEY. //此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY) //(环境变量名 DEER_KEY)
let PUSHDEER_KEY = ''; let PUSHDEER_KEY = '';
let PUSHDEER_URL = '';
// =======================================Synology Chat通知设置区域=========================================== // =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN //此处填你申请的CHAT_URL与CHAT_TOKEN
@@ -49,7 +50,7 @@ let CHAT_TOKEN = '';
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX) //此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = ''; let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上) //BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'http://qn.whyour.cn/logo.png'; let BARK_ICON = 'https://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写 //BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
@@ -110,6 +111,32 @@ let PUSH_PLUS_USER = '';
let QQ_SKEY = ''; let QQ_SKEY = '';
let QQ_MODE = ''; let QQ_MODE = '';
// =======================================智能微秘书设置区域=======================================
//官方文档:http://wechat.aibotk.com/docs/about
//AIBOTK_KEY 填写智能微秘书个人中心的apikey
//AIBOTK_TYPE:填写发送的目标 room 或 contact, 填其他的不生效
//AIBOTK_NAME: 填写群名或用户昵称,和上面的type类型要对应
let AIBOTK_KEY = '';
let AIBOTK_TYPE = '';
let AIBOTK_NAME = '';
// =======================================飞书机器人设置区域=======================================
//官方文档:https://www.feishu.cn/hc/zh-CN/articles/360024984973
//FSKEY 飞书机器人的 FSKEY
let FSKEY = '';
// =======================================SMTP 邮件设置区域=======================================
// SMTP_SERVER: 填写 SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
// SMTP_SSL: 填写 SMTP 发送邮件服务器是否使用 SSL,内容应为 true 或 false
// SMTP_EMAIL: 填写 SMTP 收发件邮箱,通知将会由自己发给自己
// SMTP_PASSWORD: 填写 SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
// SMTP_NAME: 填写 SMTP 收发件人姓名,可随意填写
let SMTP_SERVER = '';
let SMTP_SSL = 'false';
let SMTP_EMAIL = '';
let SMTP_PASSWORD = '';
let SMTP_NAME = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@@ -137,6 +164,7 @@ if (process.env.PUSH_KEY) {
if (process.env.DEER_KEY) { if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY; PUSHDEER_KEY = process.env.DEER_KEY;
PUSHDEER_URL = process.env.DEER_URL;
} }
if (process.env.CHAT_URL) { if (process.env.CHAT_URL) {
@@ -220,6 +248,36 @@ if (process.env.PUSH_PLUS_TOKEN) {
if (process.env.PUSH_PLUS_USER) { if (process.env.PUSH_PLUS_USER) {
PUSH_PLUS_USER = process.env.PUSH_PLUS_USER; PUSH_PLUS_USER = process.env.PUSH_PLUS_USER;
} }
if (process.env.AIBOTK_KEY) {
AIBOTK_KEY = process.env.AIBOTK_KEY;
}
if (process.env.AIBOTK_TYPE) {
AIBOTK_TYPE = process.env.AIBOTK_TYPE;
}
if (process.env.AIBOTK_NAME) {
AIBOTK_NAME = process.env.AIBOTK_NAME;
}
if (process.env.FSKEY) {
FSKEY = process.env.FSKEY;
}
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_EMAIL) {
SMTP_EMAIL = process.env.SMTP_EMAIL;
}
if (process.env.SMTP_PASSWORD) {
SMTP_PASSWORD = process.env.SMTP_PASSWORD;
}
if (process.env.SMTP_NAME) {
SMTP_NAME = process.env.SMTP_NAME;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@@ -255,6 +313,9 @@ async function sendNotify(
gotifyNotify(text, desp), //gotify gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件
]); ]);
} }
@@ -295,7 +356,7 @@ function gotifyNotify(text, desp) {
}); });
} }
function gobotNotify(text, desp, time = 2100) { function gobotNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (GOBOT_URL) { if (GOBOT_URL) {
const options = { const options = {
@@ -306,38 +367,34 @@ function gobotNotify(text, desp, time = 2100) {
}, },
timeout, timeout,
}; };
setTimeout(() => { $.post(options, (err, resp, data) => {
$.post(options, (err, resp, data) => { try {
try { if (err) {
if (err) { console.log('发送go-cqhttp通知调用API失败!!\n');
console.log('发送go-cqhttp通知调用API失败!!\n'); console.log(err);
console.log(err); } else {
data = JSON.parse(data);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
data = JSON.parse(data); console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
if (data.retcode === 0) {
console.log('go-cqhttp发送通知消息成功🎉\n');
} else if (data.retcode === 100) {
console.log(`go-cqhttp发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
}); } catch (e) {
}, time); $.logErr(e, resp);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
}); });
} }
function serverNotify(text, desp, time = 2100) { function serverNotify(text, desp) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (SCKEY) { if (SCKEY) {
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换 //微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
@@ -352,33 +409,29 @@ function serverNotify(text, desp, time = 2100) {
}, },
timeout, timeout,
}; };
setTimeout(() => { $.post(options, (err, resp, data) => {
$.post(options, (err, resp, data) => { try {
try { if (err) {
if (err) { console.log('发送通知调用API失败!!\n');
console.log('发送通知调用API失败!!\n'); console.log(err);
console.log(err); } else {
data = JSON.parse(data);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else { } else {
data = JSON.parse(data); console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
//server酱和Server酱·Turbo版的返回json格式不太一样
if (data.errno === 0 || data.data.errno === 0) {
console.log('server酱发送通知消息成功🎉\n');
} else if (data.errno === 1024) {
// 一分钟内发送相同的内容会触发
console.log(`server酱发送通知消息异常: ${data.errmsg}\n`);
} else {
console.log(
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
}); } catch (e) {
}, time); $.logErr(e, resp);
} finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
@@ -391,42 +444,36 @@ function PushDeerNotify(text, desp) {
// PushDeer 建议对消息内容进行 urlencode // PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp); desp = encodeURI(desp);
const options = { const options = {
url: `https://api2.pushdeer.com/message/push`, url: PUSHDEER_URL || `https://api2.pushdeer.com/message/push`,
body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`, body: `pushkey=${PUSHDEER_KEY}&text=${text}&desp=${desp}&type=markdown`,
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded', 'Content-Type': 'application/x-www-form-urlencoded',
}, },
timeout, timeout,
}; };
$.post( $.post(options, (err, resp, data) => {
options, try {
(err, resp, data) => { if (err) {
try { console.log('发送通知调用API失败!!\n');
if (err) { console.log(err);
console.log('发送通知调用API失败!!\n'); } else {
console.log(err); data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else { } else {
data = JSON.parse(data); console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
console.log(
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
}
} }
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
} }
}, } catch (e) {
time, $.logErr(e, resp);
); } finally {
resolve(data);
}
});
} else { } else {
resolve(); resolve();
} }
@@ -470,83 +517,6 @@ function ChatNotify(text, desp) {
}); });
} }
function CoolPush(text, desp) {
return new Promise((resolve) => {
if (QQ_SKEY) {
let options = {
url: `https://push.xuthus.cc/${QQ_MODE}/${QQ_SKEY}`,
headers: {
'Content-Type': 'application/json',
},
};
// 已知敏感词
text = text.replace(/京豆/g, '豆豆');
desp = desp.replace(/京豆/g, '');
desp = desp.replace(/🐶/g, '');
desp = desp.replace(/红包/g, 'H包');
switch (QQ_MODE) {
case 'email':
options.json = {
t: text,
c: desp,
};
break;
default:
options.body = `${text}\n\n${desp}`;
}
let pushMode = function (t) {
switch (t) {
case 'send':
return '个人';
case 'group':
return 'QQ群';
case 'wx':
return '微信';
case 'ww':
return '企业微信';
case 'email':
return '邮件';
default:
return '未知方式';
}
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log(`发送${pushMode(QQ_MODE)}通知调用API失败!!\n`);
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 200) {
console.log(`酷推发送${pushMode(QQ_MODE)}通知消息成功🎉\n`);
} else if (data.code === 400) {
console.log(
`QQ酷推(Cool Push)发送${pushMode(QQ_MODE)}推送失败:${
data.msg
}\n`,
);
} else if (data.code === 503) {
console.log(`QQ酷推出错,${data.message}${data.data}\n`);
} else {
console.log(`酷推推送异常: ${JSON.stringify(data)}`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function BarkNotify(text, desp, params = {}) { function BarkNotify(text, desp, params = {}) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (BARK_PUSH) { if (BARK_PUSH) {
@@ -649,7 +619,7 @@ function ddBotNotify(text, desp) {
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
content: ` ${text}\n\n${desp}`, content: `${text}\n\n${desp}`,
}, },
}, },
headers: { headers: {
@@ -716,7 +686,7 @@ function qywxBotNotify(text, desp) {
json: { json: {
msgtype: 'text', msgtype: 'text',
text: { text: {
content: ` ${text}\n\n${desp}`, content: `${text}\n\n${desp}`,
}, },
}, },
headers: { headers: {
@@ -983,6 +953,150 @@ function pushPlusNotify(text, desp) {
}); });
} }
function aibotkNotify(text, desp) {
return new Promise((resolve) => {
if (AIBOTK_KEY && AIBOTK_TYPE && AIBOTK_NAME) {
let json = {};
let url = '';
switch (AIBOTK_TYPE) {
case 'room':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/room';
json = {
apiKey: `${AIBOTK_KEY}`,
roomName: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
case 'contact':
url = 'https://api-bot.aibotk.com/openapi/v1/chat/contact';
json = {
apiKey: `${AIBOTK_KEY}`,
name: `${AIBOTK_NAME}`,
message: {
type: 1,
content: `【青龙快讯】\n\n${text}\n${desp}`,
},
};
break;
}
const options = {
url: url,
json,
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('智能微秘书发送通知消息失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.code === 0) {
console.log('智能微秘书发送通知消息成功🎉。\n');
} else {
console.log(`${data.error}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function fsBotNotify(text, desp) {
return new Promise((resolve) => {
if (FSKEY) {
const options = {
url: `https://open.feishu.cn/open-apis/bot/v2/hook/${FSKEY}`,
json: { msg_type: 'text', content: { text: `${text}\n\n${desp}` } },
headers: {
'Content-Type': 'application/json',
},
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (data.StatusCode === 0) {
console.log('飞书发送通知消息成功🎉\n');
} else {
console.log(`${data.msg}\n`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
async function smtpNotify(text, desp) {
if (![SMTP_SERVER, SMTP_EMAIL, SMTP_PASSWORD].every(Boolean)) {
return;
}
try {
const nodemailer = require('nodemailer');
const transporter = nodemailer.createTransport(
`${SMTP_SSL === 'true' ? 'smtps:' : 'smtp:'}//${SMTP_SERVER}`,
{
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,
});
if (!!info.messageId) {
console.log('SMTP发送通知消息成功🎉\n');
return true;
}
console.log('SMTP发送通知消息失败!!\n');
} catch (e) {
console.log('SMTP发送通知消息出现错误!!\n');
console.log(e);
}
}
function smtpNotify(text, desp) {
return new Promise((resolve) => {
if (SMTP_SERVER && SMTP_SSL && SMTP_EMAIL && SMTP_PASSWORD && SMTP_NAME) {
// todo: Node.js并没有内置的 smtp 实现,需要调用外部库,因为不清楚这个文件的模块依赖情况,所以留给有缘人实现
} else {
resolve();
}
});
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,
+79
View File
@@ -9,6 +9,10 @@ import re
import threading import threading
import time import time
import urllib.parse import urllib.parse
import smtplib
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
import requests import requests
@@ -61,6 +65,7 @@ push_config = {
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版 'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY 'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url 'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token 'CHAT_TOKEN': '', # synology chat token
@@ -81,6 +86,16 @@ push_config = {
'TG_PROXY_AUTH': '', # tg 代理认证参数 'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST 'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT 'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
'SMTP_SERVER': '', # SMTP 发送邮件服务器,形如 smtp.exmail.qq.com:465
'SMTP_SSL': 'false', # SMTP 发送邮件服务器是否使用 SSL,填写 true 或 false
'SMTP_EMAIL': '', # SMTP 收发件邮箱,通知将会由自己发给自己
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -276,6 +291,9 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务启动") print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")} data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push' url = 'https://api2.pushdeer.com/message/push'
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json() response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0: if len(response.get("content").get("result")) > 0:
@@ -521,6 +539,63 @@ def telegram_bot(title: str, content: str) -> None:
print("tg 推送失败!") print("tg 推送失败!")
def aibotk(title: str, content: str) -> None:
"""
使用 智能微秘书 推送消息
"""
if not push_config.get("AIBOTK_KEY") or not push_config.get("AIBOTK_TYPE") or not push_config.get("AIBOTK_NAME"):
print("智能微秘书 的 AIBOTK_KEY 或者 AIBOTK_TYPE 或者 AIBOTK_NAME 未设置!!\n取消推送")
return
print("智能微秘书 服务启动")
if push_config.get("AIBOTK_TYPE") == 'room':
url = "https://api-bot.aibotk.com/openapi/v1/chat/room"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"roomName": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
else:
url = "https://api-bot.aibotk.com/openapi/v1/chat/contact"
data = {
"apiKey": push_config.get("AIBOTK_KEY"),
"name": push_config.get("AIBOTK_NAME"),
"message": {"type": 1, "content": f'【青龙快讯】\n\n${title}\n${content}' }
}
body = json.dumps(data).encode(encoding="utf-8")
headers = {"Content-Type": "application/json"}
response = requests.post(url=url, data=body, headers=headers).json()
print(response)
if response["code"] == 0:
print("智能微秘书 推送成功!")
else:
print(f'智能微秘书 推送失败!{response["error"]}')
def 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取消推送")
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')
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.close()
print("SMTP 邮件 推送成功!")
except Exception as e:
print(f'SMTP 邮件 推送失败!{e}')
def one() -> str: def one() -> str:
""" """
获取一条一言 获取一条一言
@@ -561,6 +636,10 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot) notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"): if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot) notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk)
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)
def send(title: str, content: str) -> None: def send(title: str, content: str) -> None:
+2 -1
View File
@@ -5,8 +5,9 @@
"dependencies": { "dependencies": {
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"download": "^8.0.0", "download": "^8.0.0",
"http-server": "^0.12.3",
"got": "^11.5.1", "got": "^11.5.1",
"http-server": "^0.12.3",
"nodemailer": "^6.8.0",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",
"request": "^2.88.2", "request": "^2.88.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
+2 -2
View File
@@ -5,14 +5,14 @@ const envFound = dotenv.config();
const accessKey = process.env.QINIU_AK; const accessKey = process.env.QINIU_AK;
const secretKey = process.env.QINIU_SK; const secretKey = process.env.QINIU_SK;
const mac = new qiniu.auth.digest.Mac(accessKey, secretKey); const mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
const key = 'version.ts'; const key = 'version.yaml';
const options = { const options = {
scope: `${process.env.QINIU_SCOPE}:${key}`, scope: `${process.env.QINIU_SCOPE}:${key}`,
}; };
const putPolicy = new qiniu.rs.PutPolicy(options); const putPolicy = new qiniu.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(mac); const uploadToken = putPolicy.uploadToken(mac);
const localFile = 'src/version.ts'; const localFile = 'version.yaml';
const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 }); const config = new qiniu.conf.Config({ zone: qiniu.zone.Zone_z1 });
const formUploader = new qiniu.form_up.FormUploader(config); const formUploader = new qiniu.form_up.FormUploader(config);
const putExtra = new qiniu.form_up.PutExtra( const putExtra = new qiniu.form_up.PutExtra(
+168 -159
View File
@@ -1,187 +1,196 @@
#!/usr/bin/env bash #!/usr/bin/env bash
get_token() { get_token() {
if [[ -f $file_auth_token ]]; then
token=$(cat $file_auth_token | jq -r .value) token=$(cat $file_auth_token | jq -r .value)
else
local token_command="ts-node-transpile-only ${dir_root}/back/token.ts"
local token_file="${dir_root}static/build/token.js"
if [[ -f $token_file ]]; then
token_command="node ${token_file}"
fi
token=$(eval "$token_command")
fi
} }
add_cron_api() { add_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功" echo -e "$name -> 添加成功"
else else
echo -e "$name -> 添加失败(${message})" echo -e "$name -> 添加失败(${message})"
fi fi
} }
update_cron_api() { update_cron_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}') local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}') local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}') local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}') local id=$(echo "$1" | awk -F ":" '{print $4}')
else else
local schedule=$1 local schedule=$1
local command=$2 local command=$2
local name=$3 local name=$3
local id=$4 local id=$4
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \ --data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功" echo -e "$name -> 更新成功"
else else
echo -e "$name -> 更新失败(${message})" echo -e "$name -> 更新失败(${message})"
fi fi
} }
update_cron_command_api() { update_cron_command_api() {
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}') local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}') local id=$(echo "$1" | awk -F ":" '{print $2}')
else else
local command=$1 local command=$1
local id=$2 local id=$2
fi fi
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \ --data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功" echo -e "$command -> 更新成功"
else else
echo -e "$command -> 更新失败(${message})" echo -e "$command -> 更新失败(${message})"
fi fi
} }
del_cron_api() { del_cron_api() {
local ids=$1 local ids=$1
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \ -X 'DELETE' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \ --data-raw "[$ids]" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "成功" echo -e "成功"
else else
echo -e "失败(${message})" echo -e "失败(${message})"
fi fi
} }
update_cron() { update_cron() {
local ids="$1" local ids="$1"
local status="$2" local status="$2"
local pid="${3:-''}" local pid="${3:-''}"
local logPath="$4" local logPath="$4"
local lastExecutingTime="${5:-0}" local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}" local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \ --data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi fi
} }
notify_api() { notify_api() {
local title=$1 local title=$1
local content=$2 local content=$2
local currentTimeStamp=$(date +%s) local currentTimeStamp=$(date +%s)
local api=$( local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \ curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \ -X 'PUT' \
-H "Accept: application/json" \ -H "Accept: application/json" \
-H "Authorization: Bearer $token" \ -H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \ -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \ -H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \ -H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \ -H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \ -H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \ --data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed --compressed
) )
code=$(echo $api | jq -r .code) code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message) message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then if [[ $code == 200 ]]; then
echo -e "通知发送成功" echo -e "通知发送成功"
else else
echo -e "通知失败(${message})" echo -e "通知失败(${message})"
fi fi
} }
get_token get_token
+4 -5
View File
@@ -35,11 +35,10 @@ fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data" cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data cd $dir_data
cat requirements.txt | while read LREAD cat requirements.txt | while read LREAD; do
do if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then pip3 --default-timeout=100 install ${LREAD}
pip3 --default-timeout=100 install ${LREAD} fi
fi
done done
echo -e "\npython3依赖安装成功...\n" echo -e "\npython3依赖安装成功...\n"
+8 -3
View File
@@ -87,13 +87,18 @@ check_pm2() {
fi fi
} }
start_public() {
echo -e "=====> 启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
}
main() { main() {
echo -e "=====> 开始检测" echo -e "=====> 开始检测"
npm i -g pnpm npm i -g pnpm
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
pnpm add -g pm2 pnpm add -g pm2
patch_version
start_public
copy_dep copy_dep
check_ql check_ql
check_nginx check_nginx
+282
View File
@@ -0,0 +1,282 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
trap "single_hanle" 2 20 15 14
single_hanle() {
handle_task_after "$@"
exit 1
}
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后执行\n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
}
check_server() {
if [[ $cpu_warn ]] && [[ $mem_warn ]] && [[ $disk_warn ]]; then
local top_result=$(top -b -n 1)
cpu_use=$(echo "$top_result" | grep CPU | grep -v -E 'grep|PID' | awk '{print $2}' | cut -f 1 -d "%" | head -n 1)
mem_free=$(free -m | grep "Mem" | awk '{print $3}' | head -n 1)
mem_total=$(free -m | grep "Mem" | awk '{print $2}' | head -n 1)
mem_use=$(printf "%d%%" $((mem_free * 100 / mem_total)) | cut -f 1 -d "%" | head -n 1)
disk_use=$(df -P | grep /dev | grep -v -E '(tmp|boot|shm)' | awk '{print $5}' | cut -f 1 -d "%" | head -n 1)
if [[ $cpu_use -gt $cpu_warn ]] && [[ $cpu_warn ]] || [[ $mem_free -lt $mem_warn ]] || [[ $disk_use -gt $disk_warn ]]; then
local resource=$(echo "$top_result" | grep -v -E 'grep|Mem|idle|Load|tr' | awk '{$2="";$3="";$4="";$5="";$7="";print $0}' | head -n 10 | tr '\n' '|' | sed s/\|/\\\\n/g)
notify_api "服务器资源异常警告" "当前CPU占用 $cpu_use% 内存占用 $mem_use% 磁盘占用 $disk_use% \n资源占用详情 \n\n $resource"
fi
fi
}
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n"
[[ $is_macos -eq 0 ]] && check_server
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
. $file_task_before "$@"
}
handle_task_after() {
. $file_task_after "$@"
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
}
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ${file_param} != /* ]] && [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
cat $single_log_path
[[ -f $single_log_path ]] && rm -f $single_log_path
done
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
$timeoutCmd $which_program $file_param
}
## 运行其他命令
run_else() {
local file_param="$1"
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
$timeoutCmd $which_program $file_param "$@"
}
## 命令检测
main() {
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
elif [[ $# -eq 0 ]]; then
echo
usage
else
run_else "$@"
fi
}
handle_task_before "$@"
main "$@"
handle_task_after "$@"
+1 -1
View File
@@ -13,7 +13,7 @@ git push
echo -e "更新cdn文件" echo -e "更新cdn文件"
ts-node sample/tool.ts ts-node sample/tool.ts
string=$(cat src/version.ts | grep "version" | egrep "[^\']*" -o | egrep "\d\.*") string=$(cat version.yaml | grep "version" | egrep "[^ ]*" -o | egrep "\d\.*")
version="v$string" version="v$string"
echo -e "当前版本$version" echo -e "当前版本$version"
+23 -23
View File
@@ -8,35 +8,35 @@ days=$1
## 删除运行js脚本的旧日志 ## 删除运行js脚本的旧日志
remove_js_log() { remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log") local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time local diff_time
for log in $log_full_path_list; do for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠 local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s))) diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date"))) diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log [[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi fi
done done
} }
## 删除空文件夹 ## 删除空文件夹
remove_empty_dir() { remove_empty_dir() {
cd $dir_log cd $dir_log
for dir in $(ls); do for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir rm -rf $dir
fi fi
done done
} }
## 运行 ## 运行
if [[ ${days} ]]; then if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n" echo -e "查找旧日志文件中...\n"
remove_js_log remove_js_log
remove_empty_dir remove_empty_dir
echo -e "删除旧日志执行完毕\n" echo -e "删除旧日志执行完毕\n"
fi fi
+319 -259
View File
@@ -50,377 +50,437 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系 ## 软连接及其原始文件对应关系
link_name=( link_name=(
task task
ql ql
) )
original_name=( original_name=(
task.sh task.sh
update.sh update.sh
) )
init_env() { init_env() {
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1 export PYTHONUNBUFFERED=1
} }
import_config() { import_config() {
[[ -f $file_config_user ]] && . $file_config_user [[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""} ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"} command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH} current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}" default_cron="${DefaultCronRule}"
else else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *" default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi fi
cpu_warn=${CpuWarn}
mem_warn=${MemoryWarn}
disk_warn=${DiskWarn}
} }
set_proxy() { set_proxy() {
if [[ $proxy_url ]]; then local proxy="$1"
export http_proxy="${proxy_url}" if [[ $proxy ]]; then
export https_proxy="${proxy_url}" proxy_url="$proxy"
fi fi
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
} }
unset_proxy() { unset_proxy() {
unset http_proxy unset http_proxy
unset https_proxy unset https_proxy
} }
make_dir() { make_dir() {
local dir=$1 local dir=$1
if [[ ! -d $dir ]]; then if [[ ! -d $dir ]]; then
mkdir -p $dir mkdir -p $dir
fi fi
} }
detect_termux() { detect_termux() {
if [[ $PATH == *com.termux* ]]; then if [[ $PATH == *com.termux* ]]; then
is_termux=1 is_termux=1
else else
is_termux=0 is_termux=0
fi fi
} }
detect_macos() { detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0 [[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
} }
gen_random_num() { gen_random_num() {
local divi=$1 local divi=$1
echo $((${RANDOM} % $divi)) echo $((${RANDOM} % $divi))
} }
link_shell_sub() { link_shell_sub() {
local link_path="$1" local link_path="$1"
local original_path="$2" local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path ln -sf $original_path $link_path
fi fi
} }
link_shell() { link_shell() {
if [[ $is_termux -eq 1 ]]; then if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/" local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/" local path="/usr/local/bin/"
else else
local path="" local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n" echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi fi
if [[ $path ]]; then if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}" link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done done
fi fi
} }
define_cmd() { define_cmd() {
local cmd_prefix cmd_suffix local cmd_prefix cmd_suffix
if type task &>/dev/null; then if type task &>/dev/null; then
cmd_suffix="" cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="" cmd_prefix=""
else
cmd_prefix="bash "
fi
else else
cmd_suffix=".sh" cmd_prefix="bash "
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi fi
for ((i = 0; i < ${#link_name[*]}; i++)); do else
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}" cmd_suffix=".sh"
done if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
} }
fix_config() { fix_config() {
make_dir $dir_static make_dir $dir_static
make_dir $dir_data make_dir $dir_data
make_dir $dir_config make_dir $dir_config
make_dir $dir_log make_dir $dir_log
make_dir $dir_db make_dir $dir_db
make_dir $dir_scripts make_dir $dir_scripts
make_dir $dir_list_tmp make_dir $dir_list_tmp
make_dir $dir_repo make_dir $dir_repo
make_dir $dir_raw make_dir $dir_raw
make_dir $dir_update_log make_dir $dir_update_log
make_dir $dir_dep make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n" echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user cp -fv $file_config_sample $file_config_user
echo echo
fi fi
if [[ ! -f $file_env ]]; then if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n" echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env touch $file_env
echo echo
fi fi
if [[ ! -f $file_task_before ]]; then if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n" echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before cp -fv $file_task_sample $file_task_before
echo echo
fi fi
if [[ ! -f $file_task_after ]]; then if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n" echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after cp -fv $file_task_sample $file_task_after
echo echo
fi fi
if [[ ! -f $file_extra_shell ]]; then if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n" echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell cp -fv $file_extra_sample $file_extra_shell
echo echo
fi fi
if [[ ! -s $file_auth_user ]]; then if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n" echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user cp -fv $file_auth_sample $file_auth_user
echo echo
fi fi
if [[ ! -s $file_notify_py ]]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py
echo echo
fi fi
if [[ ! -s $file_notify_js ]]; then if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n" echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js cp -fv $file_notify_js_sample $file_notify_js
echo echo
fi fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n" echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf cat /dev/null >/etc/nginx/conf.d/default.conf
echo echo
fi fi
if [[ ! -s $dep_notify_js ]]; then if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n" echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js cp -fv $file_notify_js_sample $dep_notify_js
echo echo
fi fi
if [[ ! -s $dep_notify_py ]]; then if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n" echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py cp -fv $file_notify_py_sample $dep_notify_py
echo echo
fi fi
} }
npm_install_sub() { npm_install_sub() {
if [ $is_termux -eq 1 ]; then if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then elif ! type pnpm &>/dev/null; then
npm install --production npm install --production
else else
pnpm install --loglevel error --production pnpm install --loglevel error --production
fi fi
} }
npm_install_1() { npm_install_1() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "运行 npm install...\n" echo -e "运行 npm install...\n"
npm_install_sub npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n" [[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current cd $dir_current
} }
npm_install_2() { npm_install_2() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
cd $dir_work cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n" echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub npm_install_sub
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n" echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work npm_install_1 $dir_work
fi fi
cd $dir_current cd $dir_current
} }
diff_and_copy() { diff_and_copy() {
local copy_source=$1 local copy_source=$1
local copy_to=$2 local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to cp -f $copy_source $copy_to
fi fi
} }
update_depend() { update_depend() {
local dir_current=$(pwd) local dir_current=$(pwd)
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
cd $dir_current cd $dir_current
} }
git_clone_scripts() { git_clone_scripts() {
local url=$1 local url="$1"
local dir=$2 local dir="$2"
local branch=$3 local branch="$3"
[[ $branch ]] && local part_cmd="-b $branch " local proxy="$4"
echo -e "开始克隆仓库 $url$dir\n" [[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
set_proxy set_proxy "$proxy"
git clone $part_cmd $url $dir git clone $part_cmd $url $dir
exit_status=$? exit_status=$?
unset_proxy unset_proxy
} }
git_pull_scripts() { git_pull_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work="$1" local dir_work="$1"
local branch="$2" local branch="$2"
cd $dir_work local proxy="$3"
echo -e "开始更新仓库:$dir_work\n" cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
set_proxy set_proxy "$proxy"
git fetch --all git fetch --all
exit_status=$? exit_status=$?
git pull &>/dev/null git pull &>/dev/null
unset_proxy unset_proxy
reset_branch "$branch"
cd $dir_current cd $dir_current
} }
reset_romote_url() { reset_romote_url() {
local dir_current=$(pwd) local dir_current=$(pwd)
local dir_work=$1 local dir_work=$1
local url=$2 local url=$2
local branch="$3" local branch="$3"
if [[ -d "$dir_work/.git" ]]; then cd $dir_work
cd $dir_work if [[ -d "$dir_work/.git" ]]; then
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null [[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null git remote set-url origin $url &>/dev/null
else
local part_cmd="" git init
reset_branch "$branch" git remote add origin $url &>/dev/null
cd $dir_current fi
fi reset_branch "$branch"
cd $dir_current
} }
reset_branch() { reset_branch() {
local branch="$1" local branch="$1"
if [[ $branch ]]; then local part_cmd=""
part_cmd="origin/${branch}" if [[ $branch ]]; then
git checkout -B "$branch" part_cmd="origin/${branch}"
git branch --set-upstream-to=$part_cmd $branch git checkout -B "$branch" &>/dev/null
fi git branch --set-upstream-to=$part_cmd $branch &>/dev/null
git reset --hard $part_cmd &>/dev/null fi
git reset --hard $part_cmd &>/dev/null
} }
random_range() { random_range() {
local beg=$1 local beg=$1
local end=$2 local end=$2
echo $((RANDOM % ($end - $beg) + $beg)) echo $((RANDOM % ($end - $beg) + $beg))
} }
reload_pm2() { reload_pm2() {
pm2 l &>/dev/null pm2 l &>/dev/null
echo -e "启动面板服务\n" echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n" echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
} }
diff_time() { diff_time() {
local format="$1" local format="$1"
local begin_time="$2" local begin_time="$2"
local end_time="$3" local end_time="$3"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s))) diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time"))) diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi fi
echo "$diff_time" echo "$diff_time"
} }
format_time() { format_time() {
local format="$1" local format="$1"
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S") echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi fi
} }
format_log_time() { format_log_time() {
local format="$1" local format="$1"
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S") echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi fi
} }
format_timestamp() { format_timestamp() {
local format="$1" local format="$1"
local time="$2" local time="$2"
if [[ $is_macos -eq 1 ]]; then if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s") echo $(date -j -f "$format" "$time" "+%s")
else else
echo $(date -d "$time" "+%s") echo $(date -d "$time" "+%s")
fi fi
}
patch_version() {
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
if [[ -f "$dir_root/db/cookie.db" ]]; then
echo -e "检测到旧的db文件,拷贝为新db...\n"
mv $dir_root/db/cookie.db $dir_root/db/env.db
rm -rf $dir_root/db/cookie.db
echo
fi
if ! type ts-node &>/dev/null; then
pnpm add -g ts-node typescript tslib
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
} }
init_env init_env
@@ -429,4 +489,4 @@ detect_macos
define_cmd define_cmd
fix_config fix_config
import_config $1 >$task_error_log_path 2>&1 import_config $1 2>$task_error_log_path
+67 -343
View File
@@ -7,357 +7,81 @@ dir_shell=$QL_DIR/shell
## 选择python3还是node ## 选择python3还是node
define_program() { define_program() {
local file_param=$1 local file_param=$1
if [[ $file_param == *.js ]]; then if [[ $file_param == *.js ]]; then
which_program="node" which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3" which_program="python3"
elif [[ $file_param == *.sh ]]; then elif [[ $file_param == *.sh ]]; then
which_program="bash" which_program="bash"
elif [[ $file_param == *.ts ]]; then elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only" which_program="ts-node-transpile-only"
else else
which_program="" which_program=""
fi fi
}
random_delay() {
local random_delay_max=$RandomDelay
if [[ $random_delay_max ]] && [[ $random_delay_max -gt 0 ]]; then
local file_param=$1
local file_extensions=${RandomDelayFileExtensions-"js"}
local ignored_minutes=${RandomDelayIgnoredMinutes-"0 30"}
if [[ -n $file_extensions ]]; then
if ! echo "$file_param" | grep -qE "\.${file_extensions// /$|\\.}$"; then
# echo -e "\n当前文件需要准点运行, 放弃随机延迟\n"
return
fi
fi
local current_min
current_min=$(date "+%-M")
for minute in $ignored_minutes; do
if [[ $current_min -eq $minute ]]; then
# echo -e "\n当前时间需要准点运行, 放弃随机延迟\n"
return
fi
done
local delay_second=$(($(gen_random_num "$random_delay_max") + 1))
echo -e "\n命令未添加 \"now\",随机延迟 $delay_second 秒后再执行任务,如需立即终止,请按 CTRL+C...\n"
sleep $delay_second
fi
}
## scripts目录下所有可运行脚本数组
gen_array_scripts() {
local dir_current=$(pwd)
local i="-1"
cd $dir_scripts
for file in $(ls); do
if [[ -f $file ]] && [[ $file == *.js && $file != sendNotify.js ]]; then
let i++
array_scripts[i]=$(echo "$file" | perl -pe "s|$dir_scripts/||g")
array_scripts_name[i]=$(grep "new Env" $file | awk -F "'|\"" '{print $2}' | head -1)
[[ -z ${array_scripts_name[i]} ]] && array_scripts_name[i]="<未识别出活动名称>"
fi
done
cd $dir_current
}
## 使用说明
usage() {
define_cmd
gen_array_scripts
echo -e "task命令运行本程序自动添加进crontab的脚本,需要输入脚本的绝对路径或去掉 “$dir_scripts/” 目录后的相对路径(定时任务中请写作相对路径),用法为:"
echo -e "1.$cmd_task <file_name> # 依次执行,如果设置了随机延迟,将随机延迟一定秒数"
echo -e "2.$cmd_task <file_name> now # 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日志,同时记录在日志文件中"
echo -e "3.$cmd_task <file_name> conc <环境变量名称> <账号编号,空格分隔>(可选的) # 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日志,直接记录在日志文件中,且可指定账号执行"
echo -e "4.$cmd_task <file_name> desi <环境变量名称> <账号编号,空格分隔> # 指定账号执行,无论是否设置了随机延迟,均立即运行"
if [[ ${#array_scripts[*]} -gt 0 ]]; then
echo -e "\n当前有以下脚本可以运行:"
for ((i = 0; i < ${#array_scripts[*]}; i++)); do
echo -e "$(($i + 1)). ${array_scripts_name[i]}${array_scripts[i]}"
done
else
echo -e "\n暂无脚本可以执行"
fi
}
## run nohup,$1:文件名,不含路径,带后缀
run_nohup() {
local file_name=$1
nohup node $file_name &>$log_path &
} }
handle_log_path() { handle_log_path() {
define_program "$file_param" local file_param=$1
local suffix="" if [[ -z $file_param ]];then
if [[ ! -z $ID ]]; then file_param="task"
suffix="_${ID}" fi
fi
time=$(date "+$time_format")
log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
log_dir_tmp_path="${file_param:1}"
else
log_dir_tmp_path="${file_param}"
fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
}
## 正常运行单个脚本,$1:传入参数 local suffix=""
run_normal() { if [[ ! -z $ID ]]; then
local file_param=$1 suffix="_${ID}"
if [[ $# -eq 1 ]]; then fi
random_delay "$file_param" time=$(date "+$time_format")
fi log_time=$(format_log_time "$time_format" "$time")
log_dir_tmp="${file_param##*/}"
handle_log_path if [[ $file_param =~ "/" ]]; then
if [[ $file_param == /* ]]; then
local begin_time=$(format_time "$time_format" "$time") log_dir_tmp_path="${file_param:1}"
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval $timeoutCmd $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(expr $end_timestamp - $begin_timestamp)
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
run_concurrent() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]]; then
echo -e "\n 缺少并发运行的环境变量参数"
exit 1
fi
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
handle_log_path
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%N")
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
for i in "${!array[@]}"; do
export ${env_param}=${array[i]}
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path &
done
wait
for i in "${!array[@]}"; do
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval cat $single_log_path $cmd
[[ -f $single_log_path ]] && rm -f $single_log_path
done
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
run_designated() {
local file_param="$1"
local env_param="$2"
local num_param=$(echo "$3" | perl -pe "s|.*$2(.*)|\1|")
if [[ ! $env_param ]] || [[ ! $num_param ]]; then
echo -e "\n 缺少单独运行的参数 task xxx.js desi Test 1 3"
exit 1
fi
handle_log_path
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
local envs=$(eval echo "\$${env_param}")
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<< ${runArr[@]}))
local n=0
for i in ${runArr[@]}; do
array_run[n]=${array[$i - 1]}
let n++
done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g')
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr}
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $file_param" | perl -pe "s|.*ID=(.*) $cmd_task $file_param\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
eval $timeoutCmd $which_program $file_param $cmd
eval . $file_task_after "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 运行其他命令
run_else() {
local file_param="$1"
handle_log_path
local begin_time=$(format_time "$time_format" "$time")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
local id=$(cat $list_crontab_user | grep -E "$cmd_task $@" | perl -pe "s|.*ID=(.*) $cmd_task $@\.*|\1|" | head -1 | awk -F " " '{print $1}')
[[ $id ]] && update_cron "\"$id\"" "0" "$$" "$log_path" "$begin_timestamp"
eval . $file_task_before "$@" $cmd
cd $dir_scripts
local relative_path="${file_param%/*}"
if [[ ! -z ${relative_path} ]] && [[ ${file_param} =~ "/" ]]; then
cd ${relative_path}
file_param=${file_param/$relative_path\//}
fi
shift
eval $timeoutCmd $which_program "$file_param" "$@" $cmd
eval . $file_task_after "$file_param" "$@" $cmd
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local end_timestamp=$(date "+%s")
local diff_time=$(( $end_timestamp - $begin_timestamp ))
[[ $id ]] && update_cron "\"$id\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
}
## 命令检测
main() {
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
show_log="true"
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout -k 10s $command_timeout_time "
fi
time_format="%Y-%m-%d %H:%M:%S"
if [[ $1 == *.js ]] || [[ $1 == *.py ]] || [[ $1 == *.pyc ]] || [[ $1 == *.sh ]] || [[ $1 == *.ts ]]; then
case $# in
1)
run_normal "$1"
;;
*)
case $2 in
now)
run_normal "$1" "$2"
;;
conc)
run_concurrent "$1" "$3" "$*"
;;
desi)
run_designated "$1" "$3" "$*"
;;
*)
run_else "$@"
;;
esac
;;
esac
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
elif [[ $# -eq 0 ]]; then
echo
usage
else else
run_else "$@" log_dir_tmp_path="${file_param}"
fi fi
fi
log_dir_tmp_path="${log_dir_tmp_path%/*}"
log_dir_tmp_path="${log_dir_tmp_path##*/}"
[[ $log_dir_tmp_path ]] && log_dir_tmp="${log_dir_tmp_path}_${log_dir_tmp}"
log_dir="${log_dir_tmp%.*}${suffix}"
log_path="$log_dir/$log_time.log"
cmd=">> $dir_log/$log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
make_dir "$dir_log/$log_dir"
} }
main "$@" format_params() {
time_format="%Y-%m-%d %H:%M:%S"
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout --foreground -s 14 -k 10s $command_timeout_time "
fi
params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
}
while getopts ":lm:" opt; do
case $opt in
l)
show_log="true"
;;
m)
max_time="$OPTARG"
;;
esac
done
[[ $show_log ]] && shift $(($OPTIND - 1))
if [[ $max_time ]]; then
shift $(($OPTIND - 1))
command_timeout_time="$max_time"
fi
format_params "$@"
define_program "$@"
handle_log_path "$@"
eval . $dir_shell/otask.sh "$params" "$cmd"
[[ -f "$dir_log/$log_path" ]] && cat "$dir_log/$log_path"
exit 0 exit 0
+404 -454
View File
@@ -8,554 +8,504 @@ send_mark=$dir_shell/send_mark
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径 ## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() { diff_cron() {
local list_scripts="$1" local list_scripts="$1"
local list_task="$2" local list_task="$2"
local list_add="$3" local list_add="$3"
local list_drop="$4" local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
grep -vwf $list_task $list_scripts >$list_add grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop grep -vwf $list_scripts $list_task >$list_drop
fi fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add cp -f $list_scripts $list_add
fi fi
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
cp -f $list_task $list_drop cp -f $list_task $list_drop
fi fi
}
## 检测配置文件版本
detect_config_version() {
## 识别出两个文件的版本号
ver_config_sample=$(grep " Version: " $file_config_sample | perl -pe "s|.+v((\d+\.?){3})|\1|")
[[ -f $file_config_user ]] && ver_config_user=$(grep " Version: " $file_config_user | perl -pe "s|.+v((\d+\.?){3})|\1|")
## 删除旧的发送记录文件
[[ -f $send_mark ]] && [[ $(cat $send_mark) != $ver_config_sample ]] && rm -f $send_mark
## 识别出更新日期和更新内容
update_date=$(grep " Date: " $file_config_sample | awk -F ": " '{print $2}')
update_content=$(grep " Update Content: " $file_config_sample | awk -F ": " '{print $2}')
## 如果是今天,并且版本号不一致,则发送通知
if [[ -f $file_config_user ]] && [[ $ver_config_user != $ver_config_sample ]] && [[ $update_date == $(date "+%Y-%m-%d") ]]; then
if [[ ! -f $send_mark ]]; then
local notify_title="配置文件更新通知"
local notify_content="更新日期: $update_date\n用户版本: $ver_config_user\n新的版本: $ver_config_sample\n更新内容: $update_content\n更新说明: 如需使用新功能请对照config.sample.sh,将相关新参数手动增加到你自己的config.sh中,否则请无视本消息。本消息只在该新版本配置文件更新当天发送一次。\n"
echo -e $notify_content
notify_api "$notify_title" "$notify_content"
[[ $? -eq 0 ]] && echo $ver_config_sample >$send_mark
fi
else
[[ -f $send_mark ]] && rm -f $send_mark
fi
} }
## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效 ## 输出是否有新的或失效的定时任务,$1:新的或失效的任务清单文件路径,$2:新/失效
output_list_add_drop() { output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n" echo -e "检测到有$type的定时任务\n"
cat $list cat $list
echo echo
fi fi
} }
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务 ## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务
## $1:失效任务清单文件路径 ## $1:失效任务清单文件路径
del_cron() { del_cron() {
local list_drop=$1 local list_drop=$1
local path=$2 local path=$2
local detail="" local detail=""
local ids="" local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n" echo -e "开始尝试自动删除失效的定时任务...\n"
for cron in $(cat $list_drop); do for cron in $(cat $list_drop); do
local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local id=$(cat $list_crontab_user | grep -E "$cmd_task $cron" | perl -pe "s|.*ID=(.*) $cmd_task $cron\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
if [[ $ids ]]; then
ids="$ids,\"$id\""
else
ids="\"$id\""
fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then if [[ $ids ]]; then
result=$(del_cron_api "$ids") ids="$ids,\"$id\""
notify_api "$path 删除任务${result}" "$detail" else
ids="\"$id\""
fi fi
cron_file="$dir_scripts/${cron}"
if [[ -f $cron_file ]]; then
cron_name=$(grep "new Env" $cron_file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
rm -f $cron_file
fi
[[ -z $cron_name ]] && cron_name="$cron"
if [[ $detail ]]; then
detail="${detail}\n${cron_name}"
else
detail="${cron_name}"
fi
done
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
fi
} }
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空 ## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
## $1:新任务清单文件路径 ## $1:新任务清单文件路径
add_cron() { add_cron() {
local list_add=$1 local list_add=$1
local path=$2 local path=$2
echo -e "开始尝试自动添加定时任务...\n" echo -e "开始尝试自动添加定时任务...\n"
local detail="" local detail=""
cd $dir_scripts cd $dir_scripts
for file in $(cat $list_add); do for file in $(cat $list_add); do
local file_name=${file/${path}\//} local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/} file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then if [[ -f $file ]]; then
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file | }" $file |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$file_name" [[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result" echo -e "$result"
if [[ $detail ]]; then if [[ $detail ]]; then
detail="${detail}${result}\n" detail="${detail}${result}\n"
else else
detail="${result}\n" detail="${result}\n"
fi fi
fi fi
done done
notify_api "$path 新增任务" "$detail" notify_api "$path 新增任务" "$detail"
} }
## 更新仓库 ## 更新仓库
update_repo() { update_repo() {
local url="$1" local url="$1"
local path="$2" local path="$2"
local blackword="$3" local blackword="$3"
local dependence="$4" local dependence="$4"
local branch="$5" local branch="$5"
local extensions="$6" local extensions="$6"
local tmp="${url%/*}" local proxy="$7"
local authorTmp1="${tmp##*/}" local tmp="${url%/*}"
local authorTmp2="${authorTmp1##*:}" local authorTmp1="${tmp##*/}"
local author="${authorTmp2##*.}" local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local repo_path="${dir_repo}/${uniq_path}" local repo_path="${dir_repo}/${uniq_path}"
make_dir "${dir_scripts}/${uniq_path}" make_dir "${dir_scripts}/${uniq_path}"
local formatUrl="$url" local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}" reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" git_pull_scripts ${repo_path} "${branch}" "${proxy}"
else else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" rm -rf ${repo_path} &>/dev/null
fi git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
if [[ $exit_status -eq 0 ]]; then fi
echo -e "\n更新${repo_path}成功...\n" if [[ $exit_status -eq 0 ]]; then
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" echo -e "\n更新${repo_path}成功...\n"
else diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
echo -e "\n更新${repo_path}失败,请检查网络...\n" else
fi echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
} }
## 更新所有 raw 文件 ## 更新所有 raw 文件
update_raw() { update_raw() {
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
local url="$1" local url="$1"
local raw_url="$url" local raw_url="$url"
local suffix="${raw_url##*.}" local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}" local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n" echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url} wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy unset_proxy
if [[ $? -eq 0 ]]; then if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}" mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n" echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw cd $dir_raw
local filename="raw_${raw_file_name}" local filename="raw_${raw_file_name}"
local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task $filename" | perl -pe "s|.*ID=(.*) $cmd_task $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}') local cron_id=$(cat $list_crontab_user | grep -E "$cmd_task.* $filename" | perl -pe "s|.*ID=(.*) $cmd_task.* $filename\.*|\1|" | head -1 | head -1 | awk -F " " '{print $1}')
cp -f $raw_file_name $dir_scripts/${filename} cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$( cron_line=$(
perl -ne "{ perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/ print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$raw_file_name/
}" $raw_file_name | }" $raw_file_name |
perl -pe "{ perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g; s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$raw_file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g; s|\*([\d\*])(.*)|\1\2|g;
s| | |g; s| | |g;
}" | sort -u | head -1 }" | sort -u | head -1
) )
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1) cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name" [[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs) [[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron" [[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name") result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n" echo -e "$result\n"
notify_api "新增任务通知" "\n$result" notify_api "新增任务通知" "\n$result"
# update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id" # update_cron_api "$cron_line:$cmd_task $filename:$cron_name:$cron_id"
fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi fi
else
echo -e "下载 ${raw_file_name} 失败,保留之前正常下载的版本...\n"
[[ -f "$dir_raw/${raw_file_name}.new" ]] && rm -f "$dir_raw/${raw_file_name}.new"
fi
} }
## 调用用户自定义的extra.sh ## 调用用户自定义的extra.sh
run_extra_shell() { run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n" echo -e "--------------------------------------------------------------\n"
. $file_extra_shell . $file_extra_shell
else else
echo -e "$file_extra_shell文件不存在,跳过执行...\n" echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi fi
fi
} }
## 脚本用法 ## 脚本用法
usage() { usage() {
echo -e "本脚本用法:" echo -e "ql命令使用方法:"
echo -e "1. $cmd_update update # 更新并重启青龙" echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本" echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件" echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本" echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志" echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot" echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复" echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数" echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录" echo -e "9. $cmd_update resettfa # 禁用两步登录"
} }
## 更新qinglong ## 更新qinglong
update_qinglong() { update_qinglong() {
patch_version local mirror="github"
local githubStatus=$(curl -s -m 2 -IL "https://github.com" | grep 200)
if [ "$githubStatus" == "" ]; then
mirror="gitee"
fi
echo -e "使用 ${mirror} 源更新...\n"
export isFirstStartServer=false
export isFirstStartServer=false local all_branch=$(cd ${dir_root} && git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
local no_restart="$1" if [[ $exit_status -eq 0 ]]; then
local all_branch=$(git branch -a) echo -e "\n更新青龙源文件成功...\n"
local primary_branch="master" reset_romote_url ${dir_root} "https://${mirror}.com/whyour/qinglong.git" ${primary_branch}
if [[ "${all_branch}" =~ "${current_branch}" ]]; then cp -f $file_config_sample $dir_config/config.sample.sh
primary_branch="${current_branch}" update_depend
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
reset_romote_url ${dir_root} "https://github.com/whyour/qinglong.git" ${primary_branch}
git_pull_scripts $dir_root ${primary_branch}
if [[ $exit_status -eq 0 ]]; then [[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
echo -e "\n更新青龙源文件成功...\n" [[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
else
echo -e "\n更新青龙源文件失败,请检查原因...\n"
fi
local url="https://github.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
rm -rf $dir_static/*
cp -rf $ql_static_repo/* $dir_static
if [[ $no_restart != "no-restart" ]]; then
nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo -e "重启面板中..."
sleep 3
reload_pm2
fi
else
echo -e "\n更新青龙静态资源失败,请检查原因...\n"
fi
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
} }
patch_version() { update_qinglong_static() {
# 兼容pnpm@7 local no_restart="$1"
pnpm setup &>/dev/null local primary_branch="$2"
source ~/.bashrc local url="https://${mirror}.com/whyour/qinglong-static.git"
pnpm install -g &>/dev/null 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"
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
if [[ -f "$dir_root/db/cookie.db" ]]; then rm -rf $dir_static/*
echo -e "检测到旧的db文件,拷贝为新db...\n" cp -rf $ql_static_repo/* $dir_static
mv $dir_root/db/cookie.db $dir_root/db/env.db if [[ $no_restart != "no-restart" ]]; then
rm -rf $dir_root/db/cookie.db nginx -s reload 2>/dev/null || nginx -c /etc/nginx/nginx.conf
echo echo -e "重启面板中..."
sleep 3
reload_pm2
fi fi
else
if ! type ts-node &>/dev/null; then echo -e "\n更新青龙静态资源失败,请检查网络...\n"
pnpm add -g ts-node typescript tslib fi
fi
git config --global pull.rebase false
cp -f $dir_root/.env.example $dir_root/.env
if [[ -d "$dir_root/db" ]]; then
echo -e "检测到旧的db目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ -d "$dir_root/scripts" ]]; then
echo -e "检测到旧的scripts目录,拷贝到data目录...\n"
cp -rf $dir_root/scripts $dir_root/data
echo
fi
if [[ -d "$dir_root/log" ]]; then
echo -e "检测到旧的log目录,拷贝到data目录...\n"
cp -rf $dir_root/log $dir_root/data
echo
fi
if [[ -d "$dir_root/config" ]]; then
echo -e "检测到旧的config目录,拷贝到data目录...\n"
cp -rf $dir_root/config $dir_root/data
echo
fi
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
} }
## 对比脚本 ## 对比脚本
diff_scripts() { diff_scripts() {
local dir_current=$(pwd) local dir_current=$(pwd)
local repo_path="$1" local repo_path="$1"
local author="$2" local author="$2"
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
local extensions="$6" local extensions="$6"
gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" gen_list_repo "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
local list_add="$dir_list_tmp/${uniq_path}_add.list" local list_add="$dir_list_tmp/${uniq_path}_add.list"
local list_drop="$dir_list_tmp/${uniq_path}_drop.list" local list_drop="$dir_list_tmp/${uniq_path}_drop.list"
diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop diff_cron "$dir_list_tmp/${uniq_path}_scripts.list" "$dir_list_tmp/${uniq_path}_user.list" $list_add $list_drop
if [[ -s $list_drop ]]; then if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效" output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path del_cron $list_drop $uniq_path
fi
fi fi
if [[ -s $list_add ]]; then fi
output_list_add_drop $list_add "新" if [[ -s $list_add ]]; then
if [[ ${AutoAddCron} == true ]]; then output_list_add_drop $list_add "新"
add_cron $list_add $uniq_path if [[ ${AutoAddCron} == true ]]; then
fi add_cron $list_add $uniq_path
fi fi
cd $dir_current fi
cd $dir_current
} }
## 生成脚本的路径清单文件 ## 生成脚本的路径清单文件
gen_list_repo() { gen_list_repo() {
local dir_current=$(pwd) local dir_current=$(pwd)
local repo_path="$1" local repo_path="$1"
local author="$2" local author="$2"
local path="$3" local path="$3"
local blackword="$4" local blackword="$4"
local dependence="$5" local dependence="$5"
rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null rm -f $dir_list_tmp/${uniq_path}*.list &>/dev/null
cd ${repo_path}
local cmd="find ."
local index=0
if [[ $6 ]]; then
file_extensions="$6"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done
files=$(eval $cmd | sed 's/^..//')
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ $dependence ]]; then
cd ${repo_path} cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
local cmd="find ." for _file in ${results}; do
local index=0 file_path=$(dirname $_file)
if [[ $6 ]]; then make_dir "${dir_scripts}/${uniq_path}/${file_path}"
file_extensions="$6" cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
if [[ $file_extensions =~ "|" ]]; then
file_extensions=$(echo $file_extensions | sed 's/|/ /g')
fi
fi
for extension in $file_extensions; do
if [[ $index -eq 0 ]]; then
cmd="${cmd} -name \"*.${extension}\""
else
cmd="${cmd} -o -name \"*.${extension}\""
fi
let index+=1
done done
files=$(eval $cmd | sed 's/^..//') fi
if [[ $path ]]; then
files=$(echo "$files" | egrep $path)
fi
if [[ $blackword ]]; then
files=$(echo "$files" | egrep -v $blackword)
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}" if [[ -d $dir_dep ]]; then
cp -f $file_notify_py "${dir_scripts}/${uniq_path}" cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
if [[ $dependence ]]; then for file in ${files}; do
cd ${repo_path} filename=$(basename $file)
results=$(eval $cmd | sed 's/^..//' | egrep $dependence) cp -f $file "$dir_scripts/${uniq_path}/${filename}"
for _file in ${results}; do echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
file_path=$(dirname $_file) # 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}')
make_dir "${dir_scripts}/${uniq_path}/${file_path}" # if [[ $cron_id ]]; then
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}" # result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
done # fi
fi done
grep -E "${cmd_task}.* ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task}.* (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
if [[ -d $dir_dep ]]; then cd $dir_current
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
for file in ${files}; do
filename=$(basename $file)
cp -f $file "$dir_scripts/${uniq_path}/${filename}"
echo "${uniq_path}/${filename}" >>"$dir_list_tmp/${uniq_path}_scripts.list"
cron_id=$(cat $list_crontab_user | grep -E "$cmd_task ${uniq_path}_${filename}" | perl -pe "s|.*ID=(.*) $cmd_task ${uniq_path}_${filename}\.*|\1|" | head -1 | awk -F " " '{print $1}')
if [[ $cron_id ]]; then
result=$(update_cron_command_api "$cmd_task ${uniq_path}/${filename}:$cron_id")
fi
done
grep -E "${cmd_task} ${uniq_path}" ${list_crontab_user} | perl -pe "s|.*ID=(.*) ${cmd_task} (${uniq_path}.*)\.*|\2|" | awk -F " " '{print $1}' | sort -u >"$dir_list_tmp/${uniq_path}_user.list"
cd $dir_current
} }
get_uniq_path() { get_uniq_path() {
local url="$1" local url="$1"
local branch="$2" local branch="$2"
local urlTmp="${url%*/}" local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}" local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}" local repo="${repoTmp%.*}"
local tmp="${url%/*}" local tmp="${url%/*}"
local authorTmp1="${tmp##*/}" local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}" local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}" local author="${authorTmp2##*.}"
uniq_path="${author}_${repo}" uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}" [[ $branch ]] && uniq_path="${uniq_path}_${branch}"
} }
main() { main() {
## for ql update ## for ql update
show_log="false" show_log="false"
while getopts ":l" opt while getopts ":l" opt; do
do case $opt in
case $opt in l)
l) show_log="true"
show_log="true" ;;
;;
esac
done
[[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
local p1=$1
local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="$dir_log/update/${log_time}_$p1.log"
local begin_time=$(date '+%Y-%m-%d %H:%M:%S')
case $p1 in
update)
cmd=">> $log_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
eval echo -e "## 开始执行... $begin_time\n" $cmd
[[ -f $task_error_log_path ]] && eval cat $task_error_log_path $cmd
eval update_qinglong "$2" $cmd
;;
extra)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
run_extra_shell >>$log_path
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7"
else
echo -e "命令输入错误...\n"
usage
fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
echo -e "命令输入错误...\n"
usage
fi
;;
rmlog)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/rmlog.sh "$p2" >>$log_path
;;
bot)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/bot.sh >>$log_path
;;
check)
echo -e "## 开始执行... $begin_time\n" >>$log_path
[[ -f $task_error_log_path ]] && cat $task_error_log_path >>$log_path
. $dir_shell/check.sh >>$log_path
;;
resetlet)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo -e "重置登录错误次数成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
resettfa)
echo -e "## 开始执行... $begin_time\n" >>$log_path
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo -e "禁用两步验证成功 \n $auth_value" >>$log_path
echo "$auth_value" >$file_auth_user
;;
*)
echo -e "命令输入错误...\n"
usage
;;
esac esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S') done
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time") [[ "$show_log" == "true" ]] && shift $(($OPTIND - 1))
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path local p1=$1
cat $log_path local p2=$2
local p3=$3
local p4=$4
local p5=$5
local p6=$6
local p7=$7
local log_dir="${p1}"
make_dir "$dir_log/$log_dir"
local log_time=$(date "+%Y-%m-%d-%H-%M-%S")
local log_path="${log_dir}/${log_time}.log"
local file_path="$dir_log/$log_path"
cmd=">> $file_path 2>&1"
[[ "$show_log" == "true" ]] && cmd=""
local time_format="%Y-%m-%d %H:%M:%S"
local time=$(date "+$time_format")
local begin_timestamp=$(format_timestamp "$time_format" "$time")
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
local begin_time=$(format_time "$time_format" "$time")
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\#\# 开始执行... $begin_time\\\n" $cmd
fi
if [[ -s $task_error_log_path ]]; then
eval cat $task_error_log_path $cmd
eval echo -e "加载 config.sh 出错,请手动检查" $cmd
eval echo $cmd
fi
if [[ "$show_log" == "true" ]] && [[ $ID ]]; then
eval echo -e "请移除 -l 参数" $cmd
exit 1
fi
case $p1 in
update)
eval update_qinglong "$2" $cmd
;;
extra)
eval run_extra_shell $cmd
;;
repo)
get_uniq_path "$p2" "$p6"
if [[ -n $p2 ]]; then
update_repo "$p2" "$p3" "$p4" "$p5" "$p6" "$p7" "$p8"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi fi
;;
raw)
get_uniq_path "$p2"
if [[ -n $p2 ]]; then
update_raw "$p2"
else
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
fi
;;
rmlog)
eval . $dir_shell/rmlog.sh "$p2" $cmd
;;
bot)
eval . $dir_shell/bot.sh $cmd
;;
check)
eval . $dir_shell/check.sh $cmd
;;
resetlet)
auth_value=$(cat $file_auth_user | jq '.retries =0' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "重置登录错误次数成功" $cmd
;;
resettfa)
auth_value=$(cat $file_auth_user | jq '.twoFactorActivated =false' | jq '.twoFactorActived =false' -c)
echo "$auth_value" >$file_auth_user
eval echo -e "禁用两步验证成功" $cmd
;;
*)
eval echo -e "命令输入错误...\\\n" $cmd
eval usage $cmd
;;
esac
local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime")
local diff_time=$(($end_timestamp - $begin_timestamp))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
if [[ "$p1" != "repo" ]] && [[ "$p1" != "raw" ]]; then
eval echo -e "\\\n\#\# 执行结束... $end_time 耗时 $diff_time" $cmd
eval echo -e "\\\n          " $cmd
fi
if [[ -f $file_path ]]; then
cat $file_path
fi
} }
main "$@" main "$@"
+39
View File
@@ -0,0 +1,39 @@
import React, { useRef, useState, useEffect } from 'react';
import { Tooltip, Typography } from 'antd';
import { CopyOutlined, CheckOutlined } from '@ant-design/icons';
import { CopyToClipboard } from 'react-copy-to-clipboard';
const { Link } = Typography;
const Copy = ({ text }: { text: string }) => {
const [copied, setCopied] = useState(false);
const copyIdRef = useRef<number>();
const copyText = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
setCopied(true);
cleanCopyId();
copyIdRef.current = window.setTimeout(() => {
setCopied(false);
}, 3000);
};
const cleanCopyId = () => {
window.clearTimeout(copyIdRef.current!);
};
return (
<Link onClick={copyText} style={{ marginLeft: 1 }}>
<CopyToClipboard text={text}>
<Tooltip key="copy" title={copied ? '复制成功' : '复制'}>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</Tooltip>
</CopyToClipboard>
</Link>
);
};
export default Copy;
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({ const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'], scriptUrl: ['//at.alicdn.com/t/c/font_3354854_ob5y15ewlyq.js'],
}); });
export default IconFont; export default IconFont;
+48
View File
@@ -0,0 +1,48 @@
import { useMemo, useState } from 'react';
export default (
treeData: any[],
searchValue: string,
{
treeNodeFilterProp,
}: {
treeNodeFilterProp: string;
},
) => {
return useMemo(() => {
const keys: string[] = [];
if (!searchValue) {
return { treeData, keys };
}
const upperStr = searchValue.toUpperCase();
function filterOptionFunc(_: string, dataNode: any[]) {
const value = dataNode[treeNodeFilterProp as any];
return String(value).toUpperCase().includes(upperStr);
}
function dig(list: any[], keepAll: boolean = false): any[] {
return list
.map((dataNode) => {
const children = dataNode.children;
const match = keepAll || filterOptionFunc!(searchValue, dataNode);
const childList = dig(children || [], match);
if (match || childList.length) {
childList.length && keys.push(dataNode.key);
return {
...dataNode,
children: childList,
};
}
return null;
})
.filter((node) => node);
}
return { treeData: dig(treeData), keys };
}, [treeData, searchValue, treeNodeFilterProp]);
};
+16
View File
@@ -0,0 +1,16 @@
import { MutableRefObject, useLayoutEffect, useState } from 'react';
import useResizeObserver from '@react-hook/resize-observer'
import { getTableScroll } from '@/utils';
export default <T extends HTMLElement>(target: MutableRefObject<T>, extraHeight?: number) => {
const [height, setHeight] = useState<number>()
useResizeObserver(target, (entry) => {
let _targe = entry.target as any
if (!_targe.classList.contains('ant-table-wrapper')) {
_targe = entry.target.querySelector('.ant-table-wrapper')
}
setHeight(getTableScroll({ extraHeight, target: _targe as HTMLElement }))
})
return height
}
+8 -7
View File
@@ -10,6 +10,7 @@ import {
ContainerOutlined, ContainerOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { BasicLayoutProps } from '@ant-design/pro-layout';
export default { export default {
route: { route: {
@@ -68,18 +69,18 @@ export default {
icon: <IconFont type="ql-icon-dependence" />, icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index', component: '@/pages/dependence/index',
}, },
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/diff', path: '/diff',
name: '对比工具', name: '对比工具',
icon: <IconFont type="ql-icon-diff" />, icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index', component: '@/pages/diff/index',
}, },
{
path: '/log',
name: '任务日志',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{ {
path: '/setting', path: '/setting',
name: '系统设置', name: '系统设置',
@@ -93,4 +94,4 @@ export default {
contentWidth: 'Fixed', contentWidth: 'Fixed',
splitMenus: false, splitMenus: false,
siderWidth: 180, siderWidth: 180,
} as any; } as BasicLayoutProps;
+32 -28
View File
@@ -10,8 +10,8 @@
} }
.ant-modal-body { .ant-modal-body {
max-height: calc(85vh - 110px); max-height: calc(80vh - 110px);
max-height: calc(85vh - var(--vh-offset, 110px)); max-height: calc(80vh - var(--vh-offset, 110px));
overflow-y: auto; overflow-y: auto;
} }
@@ -161,7 +161,7 @@
&.env-wrapper, &.env-wrapper,
&.config-wrapper { &.config-wrapper {
.CodeMirror { .CodeMirror {
width: calc(100vw - 80px); width: calc(100vw - 24px);
} }
} }
} }
@@ -179,8 +179,8 @@
.Resizer { .Resizer {
background: @component-background; background: @component-background;
opacity: 0.2; opacity: 0.8;
z-index: 1; z-index: 100;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
@@ -243,31 +243,29 @@
font-size: 13px; font-size: 13px;
} }
[data-dark='true'] { /* Change autocomplete styles in WebKit */
/* Change autocomplete styles in WebKit */ input:-webkit-autofill,
input:-webkit-autofill, input:-webkit-autofill:hover,
input:-webkit-autofill:hover, input:-webkit-autofill:focus,
input:-webkit-autofill:focus, textarea:-webkit-autofill,
textarea:-webkit-autofill, textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:hover, textarea:-webkit-autofill:focus,
textarea:-webkit-autofill:focus, select:-webkit-autofill,
select:-webkit-autofill, select:-webkit-autofill:hover,
select:-webkit-autofill:hover, select:-webkit-autofill:focus {
select:-webkit-autofill:focus { box-shadow: none;
border: 1px solid @border-color-base; transition: background-color 5000s ease-in-out 0s;
box-shadow: none; -webkit-text-fill-color: @text-color;
transition: background-color 5000s ease-in-out 0s; caret-color: @text-color;
-webkit-text-fill-color: @text-color; color: @text-color;
caret-color: #e8e6f3 !important; }
}
::placeholder { ::placeholder {
opacity: 0.5 !important; opacity: 0.5 !important;
} }
.ant-select-selection-placeholder { .ant-select-selection-placeholder {
opacity: 0.5 !important; opacity: 0.5 !important;
}
} }
.ant-pro-basicLayout-content { .ant-pro-basicLayout-content {
@@ -345,3 +343,9 @@
width: auto !important; width: auto !important;
} }
} }
pre {
word-break: break-all !important;
white-space: break-spaces !important;
padding: 0 !important;
}
+72 -38
View File
@@ -13,13 +13,27 @@ import config from '@/utils/config';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import './index.less'; import './index.less';
import vhCheck from 'vh-check'; import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
import { useCtx, useTheme } from '@/utils/hooks'; import { useCtx, useTheme } from '@/utils/hooks';
import { message, Badge, Modal, Avatar, Dropdown, Menu, Image } from 'antd'; import {
message,
Badge,
Modal,
Avatar,
Dropdown,
Menu,
Image,
Popover,
Descriptions,
Tooltip,
MenuProps,
} from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client'; import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export interface SharedContext { export interface SharedContext {
headerStyle: React.CSSProperties; headerStyle: React.CSSProperties;
@@ -29,6 +43,17 @@ export interface SharedContext {
reloadUser: (needLoading?: boolean) => void; reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void; reloadTheme: () => void;
socketMessage: any; socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: number;
version: string;
changeLog: string;
changeLogLink: string;
} }
export default function () { export default function () {
@@ -37,10 +62,11 @@ export default function () {
const { theme, reloadTheme } = useTheme(); const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>(); const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null); const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>(); const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const { const {
enable: enableDarkMode, enable: enableDarkMode,
disable: disableDarkMode, disable: disableDarkMode,
@@ -64,17 +90,16 @@ export default function () {
setSystemInfo(data); setSystemInfo(data);
if (!data.isInitialized) { if (!data.isInitialized) {
history.push('/initialization'); history.push('/initialization');
setLoading(false);
} else { } else {
init(data.version);
getUser(); getUser();
} }
} else {
message.error(data);
} }
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
}); })
.finally(() => setInitLoading(false));
}; };
const getUser = (needLoading = true) => { const getUser = (needLoading = true) => {
@@ -87,8 +112,6 @@ export default function () {
if (location.pathname === '/') { if (location.pathname === '/') {
history.push('/crontab'); history.push('/crontab');
} }
} else {
message.error(data);
} }
needLoading && setLoading(false); needLoading && setLoading(false);
}) })
@@ -123,7 +146,6 @@ export default function () {
useEffect(() => { useEffect(() => {
vhCheck(); vhCheck();
init();
const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const _theme = localStorage.getItem('qinglong_dark_theme') || 'auto';
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
@@ -196,10 +218,11 @@ export default function () {
}; };
}, []); }, []);
if (initLoading) {
return <PageLoading />;
}
if (['/login', '/initialization', '/error'].includes(location.pathname)) { if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
} - `;
if (systemInfo?.isInitialized && location.pathname === '/initialization') { if (systemInfo?.isInitialized && location.pathname === '/initialization') {
history.push('/crontab'); history.push('/crontab');
} }
@@ -226,13 +249,17 @@ export default function () {
!navigator.userAgent.includes('Chrome'); !navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser'); const isQQBrowser = navigator.userAgent.includes('QQBrowser');
const menu = ( const menu: MenuProps = {
<Menu items: [
className="side-menu-user-drop-menu" {
items={[{ label: '退出登录', key: 'logout', icon: <LogoutOutlined /> }]} label: '退出登录',
onClick={logout} className: 'side-menu-user-drop-menu',
/> onClick: logout,
); key: 'logout',
icon: <LogoutOutlined />,
},
],
};
return loading ? ( return loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
@@ -240,29 +267,36 @@ export default function () {
selectedKeys={[location.pathname]} selectedKeys={[location.pathname]}
loading={loading} loading={loading}
ErrorBoundary={Sentry.ErrorBoundary} ErrorBoundary={Sentry.ErrorBoundary}
logo={<Image preview={false} src="http://qn.whyour.cn/logo.png" />} logo={<Image preview={false} src="https://qn.whyour.cn/logo.png" />}
// @ts-ignore
title={ title={
<> <>
<span style={{ fontSize: 16 }}></span> <span style={{ fontSize: 16 }}></span>
<a <a
href={changeLogLink} href={systemInfo?.changeLogLink}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
> >
<span <Tooltip
style={{ title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
> >
v{version} <Badge size="small" dot={systemInfo?.branch === 'develop'}>
</span> <span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{systemInfo?.version}
</span>
</Badge>
</Tooltip>
</a> </a>
</> </>
} }
@@ -277,16 +311,15 @@ export default function () {
return <Link to={menuItemProps.path}>{defaultDom}</Link>; return <Link to={menuItemProps.path}>{defaultDom}</Link>;
}} }}
pageTitleRender={(props, pageName, info) => { pageTitleRender={(props, pageName, info) => {
if (info && typeof info.pageName === 'string') { const title =
return `${info.pageName} - 控制面板`; (config.documentTitleMap as any)[location.pathname] || '未找到';
} return `${title} - 控制面板`;
return '控制面板';
}} }}
onCollapse={setCollapsed} onCollapse={setCollapsed}
collapsed={collapsed} collapsed={collapsed}
rightContentRender={() => rightContentRender={() =>
ctx.isPhone && ( ctx.isPhone && (
<Dropdown overlay={menu} placement="bottomRight" trigger={['click']}> <Dropdown menu={menu} placement="bottomRight" trigger={['click']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
@@ -308,7 +341,7 @@ export default function () {
}} }}
> >
{!collapsed && !ctx.isPhone && ( {!collapsed && !ctx.isPhone && (
<Dropdown overlay={menu} placement="topLeft" trigger={['hover']}> <Dropdown menu={menu} placement="topLeft" trigger={['hover']}>
<span className="side-menu-user-wrapper"> <span className="side-menu-user-wrapper">
<Avatar <Avatar
shape="square" shape="square"
@@ -338,6 +371,7 @@ export default function () {
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage, socketMessage,
systemInfo,
}} }}
/> />
</ProLayout> </ProLayout>
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { Button, Result, Typography } from 'antd';
const { Link } = Typography;
const NotFound: React.FC = () => (
<Result
status="404"
title="404"
extra={
<Button type="primary">
<Link href="/"></Link>
</Button>
}
/>
);
export default NotFound;
+13 -6
View File
@@ -25,8 +25,10 @@ const Config = () => {
const [confirmLoading, setConfirmLoading] = useState(false); const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => { const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => { request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -34,8 +36,10 @@ const Config = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then((data: any) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -50,8 +54,10 @@ const Config = () => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: select }, data: { content, name: select },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success(data.message); if (code === 200) {
message.success('保存成功');
}
setConfirmLoading(false); setConfirmLoading(false);
}); });
}; };
@@ -74,6 +80,7 @@ const Config = () => {
loading={loading} loading={loading}
extra={[ extra={[
<TreeSelect <TreeSelect
treeExpandAction="click"
className="config-select" className="config-select"
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+27 -45
View File
@@ -29,6 +29,7 @@ import config from '@/utils/config';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import { getCommandScript } from '@/utils';
const { Text } = Typography; const { Text } = Typography;
@@ -103,7 +104,6 @@ const CronDetailModal = ({
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false, glyphMargin: false,
wordWrap: 'on', wordWrap: 'on',
}} }}
@@ -123,9 +123,11 @@ const CronDetailModal = ({
.get( .get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`, `${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
) )
.then((data) => { .then(({ code, data }) => {
setLog(data.data); if (code === 200) {
setIsLogModalVisible(true); setLog(data);
setIsLogModalVisible(true);
}
}); });
}; };
@@ -137,38 +139,28 @@ const CronDetailModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons/${cron.id}/logs`) .get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setLogs(data.data); setLogs(data);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
const getScript = () => { const getScript = () => {
const cmd = cron.command.split(' ') as string[]; const result = getCommandScript(cron.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
setValidTabs(validTabs); setValidTabs(validTabs);
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
setScriptInfo({ parent: p, filename: s }); setScriptInfo({ parent: p, filename: s });
request request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`) .get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
} else { } else if (result) {
setValidTabs([validTabs[0]]); setValidTabs([validTabs[0]]);
} }
}; };
@@ -198,12 +190,10 @@ const CronDetailModal = ({
content, content,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data.code === 200) { if (code === 200) {
setValue(content); setValue(content);
message.success(`保存成功`); message.success(`保存成功`);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -231,14 +221,12 @@ const CronDetailModal = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running }); setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => { setTimeout(() => {
getLogs(); getLogs();
}, 1000); }, 1000);
} else {
message.error(data);
} }
}); });
}, },
@@ -263,11 +251,9 @@ const CronDetailModal = ({
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle }); setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
} else {
message.error(data);
} }
}); });
}, },
@@ -300,14 +286,12 @@ const CronDetailModal = ({
data: [currentCron.id], data: [currentCron.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ setCurrentCron({
...currentCron, ...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1, isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
}); });
} else {
message.error(data);
} }
}); });
}, },
@@ -340,14 +324,12 @@ const CronDetailModal = ({
data: [currentCron.id], data: [currentCron.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setCurrentCron({ setCurrentCron({
...currentCron, ...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1, isPinned: currentCron.isPinned === 1 ? 0 : 1,
}); });
} else {
message.error(data);
} }
}); });
}, },
+8 -5
View File
@@ -4,13 +4,10 @@
.crontab-detail { .crontab-detail {
.card-wrapper { .card-wrapper {
height: 80vh;
height: calc(80vh - var(--vh-offset, 0px));
.ant-card:last-child { .ant-card:last-child {
.ant-card-body { .ant-card-body {
height: calc(80vh - 238px); height: calc(80vh - 367px);
height: calc(80vh - var(--vh-offset, 0px) - 238px); height: calc(80vh - var(--vh-offset, 0px) - 367px);
overflow-y: auto; overflow-y: auto;
} }
} }
@@ -176,3 +173,9 @@ tr.drop-over-upward td {
padding-top: 10px; padding-top: 10px;
} }
} }
.view-filters-container.active {
.filter-item > div > .ant-form-item-control {
margin-left: 40px;
}
}
+231 -270
View File
@@ -1,4 +1,4 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react'; import React, { useState, useEffect, useRef, useMemo } from 'react';
import { import {
Button, Button,
message, message,
@@ -14,6 +14,7 @@ import {
Popover, Popover,
Tabs, Tabs,
TablePaginationConfig, TablePaginationConfig,
MenuProps,
} from 'antd'; } from 'antd';
import { import {
ClockCircleOutlined, ClockCircleOutlined,
@@ -43,14 +44,16 @@ import CronLogModal from './logModal';
import CronDetailModal from './detail'; import CronDetailModal from './detail';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history, useOutletContext } from '@umijs/max'; import { history, useOutletContext } from '@umijs/max';
import './index.less'; import './index.less';
import ViewCreateModal from './viewCreateModal'; import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal'; import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript } from '@/utils';
import { ColumnProps } from 'antd/lib/table';
import { VList } from 'virtuallist-antd';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -82,9 +85,23 @@ enum OperationPath {
'unpin', 'unpin',
} }
export interface ICrontab {
name: string;
command: string;
schedule: string;
id: number;
status: number;
isDisabled?: 1 | 0;
isPinned?: 1 | 0;
labels?: string[];
last_running_time?: number;
last_execution_time?: number;
nextRunTime: Date;
}
const Crontab = () => { const Crontab = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: ColumnProps<ICrontab>[] = [
{ {
title: '名称', title: '名称',
dataIndex: 'name', dataIndex: 'name',
@@ -138,16 +155,16 @@ const Crontab = () => {
</> </>
), ),
sorter: { sorter: {
compare: (a: any, b: any) => a?.name?.localeCompare(b?.name), compare: (a, b) => a?.name?.localeCompare(b?.name),
}, },
}, },
{ {
title: '命令', title: '命令/脚本',
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 300, width: 300,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => { render: (text, record) => {
return ( return (
<Paragraph <Paragraph
style={{ style={{
@@ -178,7 +195,7 @@ const Crontab = () => {
width: 110, width: 110,
align: 'center' as const, align: 'center' as const,
sorter: { sorter: {
compare: (a: any, b: any) => a.schedule.localeCompare(b.schedule), compare: (a, b) => a.schedule.localeCompare(b.schedule),
}, },
}, },
{ {
@@ -188,11 +205,11 @@ const Crontab = () => {
key: 'last_execution_time', key: 'last_execution_time',
width: 150, width: 150,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a, b) => {
return a.last_execution_time - b.last_execution_time; return (a.last_execution_time || 0) - (b.last_execution_time || 0);
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return ( return (
<span <span
@@ -222,7 +239,7 @@ const Crontab = () => {
return a.last_running_time - b.last_running_time; return a.last_running_time - b.last_running_time;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
return record.last_running_time return record.last_running_time
? diffTime(record.last_running_time) ? diffTime(record.last_running_time)
: '-'; : '-';
@@ -237,7 +254,7 @@ const Crontab = () => {
return a.nextRunTime - b.nextRunTime; return a.nextRunTime - b.nextRunTime;
}, },
}, },
render: (text: string, record: any) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; const language = navigator.language || navigator.languages[0];
return record.nextRunTime return record.nextRunTime
.toLocaleString(language, { .toLocaleString(language, {
@@ -271,14 +288,14 @@ const Crontab = () => {
value: 3, value: 3,
}, },
], ],
onFilter: (value: number, record: any) => { onFilter: (value, record) => {
if (record.isDisabled && record.status !== 0) { if (record.isDisabled && record.status !== 0) {
return value === 2; return value === 2;
} else { } else {
return record.status === value; return record.status === value;
} }
}, },
render: (text: string, record: any) => ( render: (text, record) => (
<> <>
{(!record.isDisabled || record.status !== CrontabStatus.idle) && ( {(!record.isDisabled || record.status !== CrontabStatus.idle) && (
<> <>
@@ -315,7 +332,7 @@ const Crontab = () => {
key: 'action', key: 'action',
align: 'center' as const, align: 'center' as const,
width: 100, width: 100,
render: (text: string, record: any, index: number) => { render: (text, record, index) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
@@ -376,7 +393,6 @@ const Crontab = () => {
filters: any; filters: any;
}>({} as any); }>({} as any);
const [viewConf, setViewConf] = useState<any>(); const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false); const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>(); const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
@@ -388,26 +404,16 @@ const Crontab = () => {
const [cronViews, setCronViews] = useState<any[]>([]); const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]); const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false); const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const goToScriptManager = (record: any) => { const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[]; const result = getCommandScript(record.command);
if (cmd[0] === 'task') { if (Array.isArray(result)) {
if (cmd[1].startsWith('/ql/data/scripts')) { const [s, p] = result;
cmd[1] = cmd[1].replace('/ql/data/scripts/', '');
}
let p: string, s: string;
let index = cmd[1].lastIndexOf('/');
if (index >= 0) {
s = cmd[1].slice(index + 1);
p = cmd[1].slice(0, index);
} else {
s = cmd[1];
p = '';
}
history.push(`/script?p=${p}&s=${s}`); history.push(`/script?p=${p}&s=${s}`);
} else if (cmd[1] === 'repo') { } else if (result) {
location.href = cmd[2]; location.href = result;
} }
}; };
@@ -429,24 +435,27 @@ const Crontab = () => {
url += `&queryString=${JSON.stringify({ url += `&queryString=${JSON.stringify({
filters: viewConf.filters, filters: viewConf.filters,
sorts: viewConf.sorts, sorts: viewConf.sorts,
filterRelation: viewConf.filterRelation || 'and',
})}`; })}`;
} }
request request
.get(url) .get(url)
.then((_data: any) => { .then(({ code, data: _data }) => {
const { data, total } = _data.data; if (code === 200) {
setValue( const { data, total } = _data;
data.map((x) => { setValue(
return { data.map((x) => {
...x, return {
nextRunTime: cron_parser ...x,
.parseExpression(x.schedule) nextRunTime: cron_parser
.next() .parseExpression(x.schedule)
.toDate(), .next()
}; .toDate(),
}), };
); }),
setTotal(total); );
setTotal(total);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -476,8 +485,8 @@ const Crontab = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: [record.id] }) .delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -485,8 +494,6 @@ const Crontab = () => {
result.splice(i, 1); result.splice(i, 1);
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -511,8 +518,8 @@ const Crontab = () => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] }) .put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -522,8 +529,6 @@ const Crontab = () => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -548,8 +553,8 @@ const Crontab = () => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] }) .put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) { if (i !== -1) {
@@ -560,8 +565,6 @@ const Crontab = () => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -594,8 +597,8 @@ const Crontab = () => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1; const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -606,8 +609,6 @@ const Crontab = () => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -640,8 +641,8 @@ const Crontab = () => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1; const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value]; const result = [...value];
const i = result.findIndex((x) => x.id === record.id); const i = result.findIndex((x) => x.id === record.id);
@@ -652,8 +653,6 @@ const Crontab = () => {
}); });
setValue(result); setValue(result);
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -689,15 +688,13 @@ const Crontab = () => {
arrow={{ pointAtCenter: true }} arrow={{ pointAtCenter: true }}
placement="bottomRight" placement="bottomRight"
trigger={['click']} trigger={['click']}
overlay={ menu={{
<Menu items: getMenuItems(record),
items={getMenuItems(record)} onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); action(key, record, index);
action(key, record, index); },
}} }}
/>
}
> >
<a onClick={(e) => e.stopPropagation()}> <a onClick={(e) => e.stopPropagation()}>
<EllipsisOutlined /> <EllipsisOutlined />
@@ -724,50 +721,33 @@ const Crontab = () => {
} }
}; };
const handleCancel = (cron?: any) => { const handleCancel = () => {
setIsModalVisible(false); setIsModalVisible(false);
if (cron) { getCrons();
handleCrons(cron);
}
}; };
const onSearch = (value: string) => { const onSearch = (value: string) => {
setSearchText(value.trim()); setSearchText(value.trim());
}; };
const handleCrons = (cron: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
cron.nextRunTime = cron_parser
.parseExpression(cron.schedule)
.next()
.toDate();
if (index === -1) {
result.unshift(cron);
} else {
result.splice(index, 1, {
...cron,
});
}
setValue(result);
};
const getCronDetail = (cron: any) => { const getCronDetail = (cron: any) => {
request request
.get(`${config.apiPrefix}crons/${cron.id}`) .get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => { .then(({ code, data }) => {
const index = value.findIndex((x) => x.id === cron.id); if (code === 200) {
const result = [...value]; const index = value.findIndex((x) => x.id === cron.id);
data.data.nextRunTime = cron_parser const result = [...value];
.parseExpression(data.data.schedule) data.nextRunTime = cron_parser
.next() .parseExpression(data.schedule)
.toDate(); .next()
if (index !== -1) { .toDate();
result.splice(index, 1, { if (index !== -1) {
...cron, result.splice(index, 1, {
...data.data, ...cron,
}); ...data,
setValue(result); });
setValue(result);
}
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -775,16 +755,10 @@ const Crontab = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -795,13 +769,11 @@ const Crontab = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds }) .delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -820,11 +792,9 @@ const Crontab = () => {
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
getCrons(); getCrons();
} else {
message.error(data);
} }
}); });
}, },
@@ -872,7 +842,7 @@ const Crontab = () => {
useEffect(() => { useEffect(() => {
if (viewConf && enabledCronViews && enabledCronViews.length > 0) { if (viewConf && enabledCronViews && enabledCronViews.length > 0) {
const view = enabledCronViews.slice(2).find((x) => x.id === viewConf.id); const view = enabledCronViews.slice(4).find((x) => x.id === viewConf.id);
setMoreMenuActive(!!view); setMoreMenuActive(!!view);
} }
}, [viewConf, enabledCronViews]); }, [viewConf, enabledCronViews]);
@@ -884,96 +854,9 @@ const Crontab = () => {
sorter: {}, sorter: {},
filters: {}, filters: {},
}); });
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
getCronViews(); getCronViews();
}, []); }, []);
const panelContent = (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button type="primary" style={{ marginBottom: 5 }} onClick={delCrons}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
/>
</>
);
const viewAction = (key: string) => { const viewAction = (key: string) => {
switch (key) { switch (key) {
case 'new': case 'new':
@@ -989,49 +872,49 @@ const Crontab = () => {
} }
}; };
const menu = ( const menu: MenuProps = {
<Menu onClick: ({ key, domEvent }) => {
onClick={({ key, domEvent }) => { domEvent.stopPropagation();
domEvent.stopPropagation(); viewAction(key);
viewAction(key); },
}} items: [
items={[ ...[...enabledCronViews].slice(4).map((x) => ({
...[...enabledCronViews].slice(2).map((x) => ({ label: (
label: ( <Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<Space style={{ display: 'flex', justifyContent: 'space-between' }}> <span>{x.name}</span>
<span>{x.name}</span> {viewConf?.id === x.id && (
{viewConf?.id === x.id && ( <CheckOutlined style={{ color: '#1890ff' }} />
<CheckOutlined style={{ color: '#1890ff' }} /> )}
)} </Space>
</Space> ),
), key: x.id,
key: x.id, icon: <UnorderedListOutlined />,
icon: <UnorderedListOutlined />, })),
})), {
{ type: 'divider' as 'group',
type: 'divider', },
}, {
{ label: '新建视图',
label: '新建视图', key: 'new',
key: 'new', icon: <PlusOutlined />,
icon: <PlusOutlined />, },
}, {
{ label: '视图管理',
label: '视图管理', key: 'manage',
key: 'manage', icon: <SettingOutlined />,
icon: <SettingOutlined />, },
}, ],
]} };
/>
);
const getCronViews = () => { const getCronViews = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}crons/views`) .get(`${config.apiPrefix}crons/views`)
.then((data: any) => { .then(({ code, data }) => {
setCronViews(data.data); if (code === 200) {
setEnabledCronViews(data.data.filter((x) => !x.isDisabled)); setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
}
}) })
.finally(() => { .finally(() => {
setLoading(false); setLoading(false);
@@ -1040,10 +923,17 @@ const Crontab = () => {
const tabClick = (key: string) => { const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key); const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]);
setPageConf({ ...pageConf, page: 1 }); setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null); setViewConf(view ? view : null);
}; };
const vComponents = useMemo(() => {
return VList({
height: tableScrollHeight!,
});
}, [tableScrollHeight]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper crontab-wrapper ql-container-wrapper-has-tab"
@@ -1074,7 +964,7 @@ const Crontab = () => {
className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`} className={`crontab-view ${moreMenuActive ? 'more-active' : ''}`}
tabBarExtraContent={ tabBarExtraContent={
<Dropdown <Dropdown
overlay={menu} menu={menu}
trigger={['click']} trigger={['click']}
overlayStyle={{ minWidth: 200 }} overlayStyle={{ minWidth: 200 }}
> >
@@ -1089,27 +979,98 @@ const Crontab = () => {
} }
onTabClick={tabClick} onTabClick={tabClick}
items={[ items={[
{ ...[...enabledCronViews].slice(0, 4).map((x) => ({
key: 'all',
label: '全部任务',
children: panelContent,
},
...[...enabledCronViews].slice(0, 2).map((x) => ({
key: x.id, key: x.id,
label: x.name, label: x.name,
children: panelContent,
})), })),
]} ]}
> />
<Tabs.TabPane tab="全部任务" key="all"> <div ref={tableRef}>
{panelContent} {selectedRowIds.length > 0 && (
</Tabs.TabPane> <div style={{ marginBottom: 16 }}>
{[...enabledCronViews].slice(0, 2).map((x) => ( <Button
<Tabs.TabPane tab={x.name} key={x.id}> type="primary"
{panelContent} style={{ marginBottom: 5 }}
</Tabs.TabPane> onClick={delCrons}
))} >
</Tabs>
</Button>
<Button
type="primary"
onClick={() => operateCrons(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
style={{ marginRight: 8 }}
onClick={() => operateCrons(2)}
>
</Button>
<Button type="primary" onClick={() => operateCrons(3)}>
</Button>
<Button
type="primary"
onClick={() => operateCrons(4)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => operateCrons(5)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<Button
type="primary"
onClick={() => setIsLabelModalVisible(true)}
style={{ marginLeft: 8, marginRight: 8 }}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<Table
columns={columns}
pagination={{
current: pageConf.page,
pageSize: pageConf.size,
showSizeChanger: true,
simple: isPhone,
total,
showTotal: (total: number, range: number[]) =>
`${range[0]}-${range[1]} 条/总共 ${total}`,
pageSizeOptions: [10, 20, 50, 100, 200, 500, total || 10000].sort(
(a, b) => a - b,
),
}}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 1000, y: tableScrollHeight }}
loading={loading}
rowSelection={rowSelection}
rowClassName={getRowClassName}
onChange={onPageChange}
components={vComponents}
/>
</div>
<CronLogModal <CronLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={() => { handleCancel={() => {
+17 -9
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd'; import { Modal, message, Input, Form, Statistic, Button } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined, CheckCircleOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
enum CrontabStatus { enum CrontabStatus {
'running', 'running',
@@ -33,7 +34,6 @@ const CronLogModal = ({
const [loading, setLoading] = useState<any>(true); const [loading, setLoading] = useState<any>(true);
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const [theme, setTheme] = useState<string>('');
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -41,14 +41,21 @@ const CronLogModal = ({
} }
request request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`) .get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => { .then(({ code, data }) => {
if (localStorage.getItem('logCron') === String(cron.id)) { if (
const log = data.data as string; code === 200 &&
localStorage.getItem('logCron') === String(cron.id)
) {
const log = data as string;
setValue(log || '暂无日志'); setValue(log || '暂无日志');
setExecuting( const hasNext = log && !logEnded(log) && !log.includes('重启面板') && !log.includes('任务未运行或运行失败,请尝试手动运行');
log && !log.includes('执行结束') && !log.includes('重启面板'), setExecuting(hasNext);
); setTimeout(() => {
if (log && !log.includes('执行结束') && !log.includes('重启面板')) { document
.querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' });
}, 1000);
if (hasNext) {
setTimeout(() => { setTimeout(() => {
getCronLog(); getCronLog();
}, 2000); }, 2000);
@@ -151,6 +158,7 @@ const CronLogModal = ({
{value} {value}
</pre> </pre>
)} )}
<div id="log-flag"></div>
</Modal> </Modal>
); );
}; };
+2 -6
View File
@@ -32,8 +32,6 @@ const CronModal = ({
if (code === 200) { if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功'); message.success(cron ? '更新Cron成功' : '新建Cron成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
} catch (error: any) { } catch (error: any) {
@@ -76,13 +74,13 @@ const CronModal = ({
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="command" name="command"
label="命令" label="命令/脚本"
rules={[{ required: true, whitespace: true }]} rules={[{ required: true, whitespace: true }]}
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={true}
placeholder="请输入要执行命令" placeholder="支持输入脚本路径/任意系统可执行命令/task 脚本路径"
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -142,8 +140,6 @@ const CronLabelModal = ({
action === 'post' ? '添加Labels成功' : '删除Labels成功', action === 'post' ? '添加Labels成功' : '删除Labels成功',
); );
handleCancel(true); handleCancel(true);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
+173 -73
View File
@@ -12,19 +12,28 @@ import {
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
import get from 'lodash/get';
const PROPERTIES = [ const PROPERTIES = [
{ name: '命令', value: 'command' }, { name: '命令', value: 'command' },
{ name: '名称', value: 'name' }, { name: '名称', value: 'name' },
{ name: '定时规则', value: 'schedule' }, { name: '定时规则', value: 'schedule' },
{ name: '状态', value: 'status' }, { name: '状态', value: 'status' },
{ name: '标签', value: 'labels' },
]; ];
const EOperation: any = {
Reg: '',
NotReg: '',
In: 'select',
Nin: 'select',
};
const OPERATIONS = [ const OPERATIONS = [
{ name: '包含', value: 'Reg' }, { name: '包含', value: 'Reg' },
{ name: '不包含', value: 'NotReg' }, { name: '不包含', value: 'NotReg' },
{ name: '属于', value: 'In' }, { name: '属于', value: 'In', type: 'select' },
{ name: '不属于', value: 'Nin' }, { name: '不属于', value: 'Nin', type: 'select' },
// { name: '等于', value: 'Eq' }, // { name: '等于', value: 'Eq' },
// { name: '不等于', value: 'Ne' }, // { name: '不等于', value: 'Ne' },
// { name: '为空', value: 'IsNull' }, // { name: '为空', value: 'IsNull' },
@@ -36,11 +45,18 @@ const SORTTYPES = [
{ name: '倒序', value: 'DESC' }, { name: '倒序', value: 'DESC' },
]; ];
const STATUS = [ const STATUS_MAP = {
{ name: '运行中', value: 0 }, status: [
{ name: '空闲中', value: 1 }, { name: '运行中', value: 0 },
{ name: '已禁用', value: 2 }, { name: '空闲中', value: 1 },
]; { name: '已禁用', value: 2 },
],
};
enum ViewFilterRelation {
'and' = '且',
'or' = '或',
}
const ViewCreateModal = ({ const ViewCreateModal = ({
view, view,
@@ -53,10 +69,11 @@ const ViewCreateModal = ({
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [operationMap, setOperationMap] = useState<any>(); const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
values.filterRelation = filterRelation;
const method = view ? 'put' : 'post'; const method = view ? 'put' : 'post';
try { try {
const { code, data } = await request[method]( const { code, data } = await request[method](
@@ -66,9 +83,7 @@ const ViewCreateModal = ({
}, },
); );
if (code !== 200) { if (code === 200) {
message.error(data);
} else {
handleCancel(data); handleCancel(data);
} }
setLoading(false); setLoading(false);
@@ -89,12 +104,7 @@ const ViewCreateModal = ({
}, [view, visible]); }, [view, visible]);
const operationElement = ( const operationElement = (
<Select <Select style={{ width: 80 }}>
style={{ width: 100 }}
onChange={() => {
setOperationMap({});
}}
>
{OPERATIONS.map((x) => ( {OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}> <Select.Option key={x.name} value={x.value}>
{x.name} {x.name}
@@ -125,15 +135,17 @@ const ViewCreateModal = ({
</Select> </Select>
); );
const statusElement = ( const statusElement = (property: keyof typeof STATUS_MAP) => {
<Select mode="multiple" allowClear placeholder="请选择状态"> return (
{STATUS.map((x) => ( <Select mode="tags" allowClear placeholder="输入后回车增加自定义选项">
<Select.Option key={x.name} value={x.value}> {STATUS_MAP[property]?.map((x) => (
{x.name} <Select.Option key={x.name} value={x.value}>
</Select.Option> {x.name}
))} </Select.Option>
</Select> ))}
); </Select>
);
};
return ( return (
<Modal <Modal
@@ -166,57 +178,145 @@ const ViewCreateModal = ({
</Form.Item> </Form.Item>
<Form.List name="filters"> <Form.List name="filters">
{(fields, { add, remove }) => ( {(fields, { add, remove }) => (
<> <div
{fields.map(({ key, name, ...restField }, index) => ( style={{ position: 'relative' }}
<Form.Item className={`view-filters-container ${
label={index === 0 ? '筛选条件' : ''} fields.length > 1 ? 'active' : ''
key={key} }`}
style={{ marginBottom: 0 }} >
required {fields.length > 1 && (
<div
style={{
position: 'absolute',
width: 50,
borderRadius: 10,
border: '1px solid rgb(190, 220, 255)',
borderRight: 'none',
height: 56 * (fields.length - 1),
top: 46,
left: 15,
}}
> >
<Space className="view-create-modal-filters" align="baseline"> <Button
<Form.Item type="primary"
{...restField} size="small"
name={[name, 'property']} style={{
rules={[{ required: true }]} position: 'absolute',
top: '50%',
translate: '-50% -50%',
padding: '0 0 0 3px',
cursor: 'pointer',
}}
onClick={() => {
setFilterRelation(
filterRelation === 'and' ? 'or' : 'and',
);
}}
>
<>
<span>{ViewFilterRelation[filterRelation]}</span>
<IconFont type="ql-icon-d-caret" />
</>
</Button>
</div>
)}
<div>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
className="filter-item"
>
<Space
className="view-create-modal-filters"
align="baseline"
style={
fields.length > 1 ? { width: 'calc(100% - 40px)' } : {}
}
> >
{propertyElement(PROPERTIES, { width: 120 })} <Form.Item
</Form.Item> {...restField}
<Form.Item name={[name, 'property']}
{...restField} rules={[{ required: true }]}
name={[name, 'operation']} >
rules={[{ required: true }]} {propertyElement(PROPERTIES, { width: 90 })}
> </Form.Item>
{operationElement} <Form.Item
</Form.Item> {...restField}
<Form.Item name={[name, 'operation']}
{...restField} rules={[{ required: true }]}
name={[name, 'value']} >
rules={[{ required: true, message: '请输入内容' }]} {operationElement}
> </Form.Item>
{['In', 'Nin'].includes( <Form.Item
form.getFieldValue(['filters', index, 'operation']), noStyle
) ? ( shouldUpdate={(prevValues, nextValues) => {
statusElement const preOperation =
) : ( EOperation[
<Input placeholder="请输入内容" /> get(prevValues, ['filters', name, 'operation'])
];
const nextOperation =
EOperation[
get(nextValues, ['filters', name, 'operation'])
];
const flag = preOperation !== nextOperation;
if (flag) {
form.setFieldValue(
['filters', name, 'value'],
nextOperation === 'select' ? [] : '',
);
}
return flag;
}}
>
{() => {
const property = form.getFieldValue([
'filters',
index,
'property',
]) as 'status';
const operate = form.getFieldValue([
'filters',
name,
'operation',
]);
return (
<Form.Item
{...restField}
name={[name, 'value']}
rules={[
{ required: true, message: '请输入内容' },
]}
>
{EOperation[operate] === 'select' ? (
statusElement(property)
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
);
}}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)} )}
</Form.Item> </Space>
{index !== 0 && ( </Form.Item>
<MinusCircleOutlined onClick={() => remove(name)} /> ))}
)} <Form.Item>
</Space> <a
onClick={() =>
add({ property: 'command', operation: 'Reg' })
}
>
<PlusOutlined />
</a>
</Form.Item> </Form.Item>
))} </div>
<Form.Item> </div>
<a
onClick={() => add({ property: 'command', operation: 'Reg' })}
>
<PlusOutlined />
</a>
</Form.Item>
</>
)} )}
</Form.List> </Form.List>
<Form.List name="sorts"> <Form.List name="sorts">
+21 -15
View File
@@ -81,6 +81,16 @@ const ViewManageModal = ({
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
align: 'center' as const, align: 'center' as const,
render: (text) => (
<div style={{ textAlign: 'left', paddingLeft: 30 }}>{text}</div>
),
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center' as const,
render: (v) => (v === 1 ? '系统' : '个人'),
}, },
{ {
title: '显示', title: '显示',
@@ -100,10 +110,10 @@ const ViewManageModal = ({
{ {
title: '操作', title: '操作',
key: 'action', key: 'action',
width: 140, width: 100,
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return ( return record.type !== 1 ? (
<Space size="middle"> <Space size="middle">
<a onClick={() => editView(record, index)}> <a onClick={() => editView(record, index)}>
<EditOutlined /> <EditOutlined />
@@ -112,6 +122,8 @@ const ViewManageModal = ({
<DeleteOutlined /> <DeleteOutlined />
</a> </a>
</Space> </Space>
) : (
'-'
); );
}, },
}, },
@@ -141,12 +153,10 @@ const ViewManageModal = ({
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] }) .delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
cronViewChange(); cronViewChange();
} else {
message.error(data);
} }
}); });
}, },
@@ -162,14 +172,12 @@ const ViewManageModal = ({
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, { .put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const _list = [...list]; const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked }); _list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list); setList(_list);
cronViewChange(); cronViewChange();
} else {
message.error(data);
} }
}); });
}; };
@@ -190,15 +198,13 @@ const ViewManageModal = ({
.put(`${config.apiPrefix}crons/views/move`, { .put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id }, data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newData = [...list]; const newData = [...list];
newData.splice(dragIndex, 1); newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data }); newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setList(newData); setList(newData);
cronViewChange(); cronViewChange();
} else {
message.error(data);
} }
}); });
}, },
+78 -99
View File
@@ -27,10 +27,11 @@ import DependenceModal from './modal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal'; import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -164,10 +165,11 @@ const Dependence = () => {
const [editedDependence, setEditedDependence] = useState(); const [editedDependence, setEditedDependence] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [logDependence, setLogDependence] = useState<any>(); const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false); const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs'); const [type, setType] = useState('nodejs');
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const getDependencies = () => { const getDependencies = () => {
setLoading(true); setLoading(true);
@@ -175,8 +177,10 @@ const Dependence = () => {
.get( .get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`, `${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
) )
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -212,18 +216,14 @@ const Dependence = () => {
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, { .delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200 && force) {
if (force) { const i = value.findIndex((x) => x.id === data.data[0].id);
const i = value.findIndex((x) => x.id === data.data[0].id); if (i !== -1) {
if (i !== -1) { const result = [...value];
const result = [...value]; result.splice(i, 1);
result.splice(i, 1); setValue(result);
setValue(result);
}
} }
} else {
message.error(data);
} }
}); });
}, },
@@ -250,11 +250,9 @@ const Dependence = () => {
.put(`${config.apiPrefix}dependencies/reinstall`, { .put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id], data: [record.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
handleDependence(data.data[0]); handleDependence(data[0]);
} else {
message.error(data);
} }
}); });
}, },
@@ -286,16 +284,10 @@ const Dependence = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -309,12 +301,10 @@ const Dependence = () => {
.delete(`${config.apiPrefix}dependencies${forceUrl}`, { .delete(`${config.apiPrefix}dependencies${forceUrl}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setSelectedRowIds([]); setSelectedRowIds([]);
getDependencies(); getDependencies();
} else {
message.error(data);
} }
}); });
}, },
@@ -333,12 +323,10 @@ const Dependence = () => {
.put(`${config.apiPrefix}dependencies/reinstall`, { .put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
setSelectedRowIds([]); setSelectedRowIds([]);
getDependencies(); getDependencies();
} else {
message.error(data);
} }
}); });
}, },
@@ -351,15 +339,17 @@ const Dependence = () => {
const getDependenceDetail = (dependence: any) => { const getDependenceDetail = (dependence: any) => {
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then(({ code, data }) => {
const index = value.findIndex((x) => x.id === dependence.id); if (code === 200) {
const result = [...value]; const index = value.findIndex((x) => x.id === dependence.id);
if (index !== -1) { const result = [...value];
result.splice(index, 1, { if (index !== -1) {
...dependence, result.splice(index, 1, {
...data.data, ...dependence,
}); ...data,
setValue(result); });
setValue(result);
}
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -373,12 +363,6 @@ const Dependence = () => {
getDependencies(); getDependencies();
}, [searchText, type]); }, [searchText, type]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
useEffect(() => { useEffect(() => {
if (logDependence) { if (logDependence) {
localStorage.setItem('logDependence', logDependence.id); localStorage.setItem('logDependence', logDependence.id);
@@ -427,53 +411,8 @@ const Dependence = () => {
} }
}, [socketMessage]); }, [socketMessage]);
const panelContent = () => (
<>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</>
);
const onTabChange = (activeKey: string) => { const onTabChange = (activeKey: string) => {
setSelectedRowIds([]);
setType(activeKey); setType(activeKey);
}; };
@@ -506,20 +445,60 @@ const Dependence = () => {
{ {
key: 'nodejs', key: 'nodejs',
label: 'NodeJs', label: 'NodeJs',
children: panelContent(),
}, },
{ {
key: 'python3', key: 'python3',
label: 'Python3', label: 'Python3',
children: panelContent(),
}, },
{ {
key: 'linux', key: 'linux',
label: 'Linux', label: 'Linux',
children: panelContent(),
}, },
]} ]}
/> />
<div ref={tableRef}>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => handlereInstallDependencies()}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(false)}
>
</Button>
<Button
type="primary"
style={{ marginBottom: 5, marginLeft: 8 }}
onClick={() => delDependencies(true)}
>
</Button>
<span style={{ marginLeft: 8 }}>
<a>{selectedRowIds?.length}</a>
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="id"
size="middle"
scroll={{ x: 768, y: tableScrollHeight }}
loading={loading}
/>
</DndProvider>
</div>
<DependenceModal <DependenceModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+10 -5
View File
@@ -48,9 +48,12 @@ const DependenceLogModal = ({
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}dependencies/${dependence.id}`) .get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => { .then(({ code, data }) => {
if (localStorage.getItem('logDependence') === String(dependence.id)) { if (
const log = (data.data.log || []).join('') as string; code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id)
) {
const log = (data.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes('结束时间')); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败')); setIsRemoveFailed(log.includes('删除失败'));
@@ -67,8 +70,10 @@ const DependenceLogModal = ({
.delete(`${config.apiPrefix}dependencies/force`, { .delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence.id], data: [dependence.id],
}) })
.then((data: any) => { .then(({ code, data }) => {
cancel(true); if (code === 200) {
cancel(true);
}
}) })
.finally(() => { .finally(() => {
setRemoveLoading(false); setRemoveLoading(false);
+1 -3
View File
@@ -53,9 +53,7 @@ const DependenceModal = ({
}, },
); );
if (code !== 200) { if (code === 200) {
message.error(data);
} else {
handleCancel(data); handleCancel(data);
} }
setLoading(false); setLoading(false);
+22 -10
View File
@@ -22,15 +22,23 @@ const Diff = () => {
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const getConfig = () => { const getConfig = () => {
request.get(`${config.apiPrefix}configs/${current}`).then((data) => { request
setCurrentValue(data.data); .get(`${config.apiPrefix}configs/${current}`)
}); .then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
}
});
}; };
const getSample = () => { const getSample = () => {
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => { request
setOriginValue(data.data); .get(`${config.apiPrefix}configs/${origin}`)
}); .then(({ code, data }) => {
if (code === 200) {
setOriginValue(data);
}
});
}; };
const updateConfig = () => { const updateConfig = () => {
@@ -42,8 +50,10 @@ const Diff = () => {
.post(`${config.apiPrefix}configs/save`, { .post(`${config.apiPrefix}configs/save`, {
data: { content, name: current }, data: { content, name: current },
}) })
.then((data: any) => { .then(({ code, data }) => {
message.success(data.message); if (code === 200) {
message.success('保存成功');
}
}); });
}; };
@@ -51,8 +61,10 @@ const Diff = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}configs/files`) .get(`${config.apiPrefix}configs/files`)
.then((data: any) => { .then(({ code, data }) => {
setFiles(data.data); if (code === 200) {
setFiles(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
-2
View File
@@ -28,8 +28,6 @@ const EditNameModal = ({
if (code === 200) { if (code === 200) {
message.success('更新环境变量名称成功'); message.success('更新环境变量名称成功');
handleCancel(); handleCancel();
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
+6
View File
@@ -5,3 +5,9 @@ tr.drop-over-downward td {
tr.drop-over-upward td { tr.drop-over-upward td {
border-top: 2px dashed #1890ff; border-top: 2px dashed #1890ff;
} }
.text-ellipsis {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
+146 -113
View File
@@ -1,4 +1,10 @@
import React, { useCallback, useRef, useState, useEffect } from 'react'; import React, {
useCallback,
useRef,
useState,
useEffect,
useMemo,
} from 'react';
import { import {
Button, Button,
message, message,
@@ -9,6 +15,8 @@ import {
Typography, Typography,
Tooltip, Tooltip,
Input, Input,
UploadProps,
Upload,
} from 'antd'; } from 'antd';
import { import {
EditOutlined, EditOutlined,
@@ -16,6 +24,7 @@ import {
SyncOutlined, SyncOutlined,
CheckCircleOutlined, CheckCircleOutlined,
StopOutlined, StopOutlined,
UploadOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -25,11 +34,14 @@ import EditNameModal from './editNameModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend'; import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less'; import './index.less';
import { exportJson, getTableScroll } from '@/utils/index'; import { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy';
import { VList } from 'virtuallist-antd';
const { Text, Paragraph } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status { enum Status {
@@ -54,50 +66,6 @@ enum OperationPath {
const type = 'DragableBodyRow'; const type = 'DragableBodyRow';
const DragableBodyRow = ({
index,
moveRow,
className,
style,
...restProps
}: any) => {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
return (
<tr
ref={ref}
className={`${className}${isOver ? dropClassName : ''}`}
style={{ cursor: 'move', ...style }}
{...restProps}
/>
);
};
const Env = () => { const Env = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
@@ -124,16 +92,12 @@ const Env = () => {
width: '35%', width: '35%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return ( return (
<Paragraph <div style={{ display: 'flex', alignItems: 'center' }}>
style={{ <Tooltip title={text} placement="topLeft">
wordBreak: 'break-all', <div className="text-ellipsis">{text}</div>
marginBottom: 0, </Tooltip>
textAlign: 'left', <Copy text={text} />
}} </div>
ellipsis={{ tooltip: text, rows: 2 }}
>
{text}
</Paragraph>
); );
}, },
}, },
@@ -142,6 +106,13 @@ const Env = () => {
dataIndex: 'remarks', dataIndex: 'remarks',
key: 'remarks', key: 'remarks',
align: 'center' as const, align: 'center' as const,
render: (text: string, record: any) => {
return (
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
);
},
}, },
{ {
title: '更新时间', title: '更新时间',
@@ -249,14 +220,18 @@ const Env = () => {
const [editedEnv, setEditedEnv] = useState(); const [editedEnv, setEditedEnv] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]); const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>(); const [importLoading, setImportLoading] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59);
const getEnvs = () => { const getEnvs = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`) .get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -284,8 +259,8 @@ const Env = () => {
data: [record.id], data: [record.id],
}, },
) )
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success( message.success(
`${record.status === Status. ? '启用' : '禁用'}成功`, `${record.status === Status. ? '启用' : '禁用'}成功`,
); );
@@ -297,8 +272,6 @@ const Env = () => {
status: newStatus, status: newStatus,
}); });
setValue(result); setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -333,14 +306,12 @@ const Env = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: [record.id] }) .delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...value]; const result = [...value];
result.splice(index, 1); result.splice(index, 1);
setValue(result); setValue(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -352,7 +323,7 @@ const Env = () => {
const handleCancel = (env?: any[]) => { const handleCancel = (env?: any[]) => {
setIsModalVisible(false); setIsModalVisible(false);
env && handleEnv(env); getEnvs();
}; };
const handleEditNameCancel = (env?: any[]) => { const handleEditNameCancel = (env?: any[]) => {
@@ -360,28 +331,71 @@ const Env = () => {
getEnvs(); getEnvs();
}; };
const handleEnv = (env: any) => { const vComponents = useMemo(() => {
const result = [...value]; return VList({
const index = value.findIndex((x) => x.id === env.id); height: tableScrollHeight!,
if (index === -1) { resetTopWhenDataChange: false,
env = Array.isArray(env) ? env : [env]; });
result.push(...env); }, [tableScrollHeight]);
} else {
result.splice(index, 1, { const DragableBodyRow = (props: any) => {
...env, const { index, moveRow, className, style, ...restProps } = props;
}); const ref = useRef();
} const [{ isOver, dropClassName }, drop] = useDrop({
setValue(result); accept: type,
collect: (monitor) => {
const { index: dragIndex } = (monitor.getItem() as any) || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index ? ' drop-over-downward' : ' drop-over-upward',
};
},
drop: (item: any) => {
moveRow(item.index, index);
},
});
const [, drag] = useDrag({
type,
item: { index },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
useEffect(() => {
drop(drag(ref));
}, [drag, drop]);
const components = useMemo(() => vComponents.body.row, []);
const tempProps = useMemo(() => {
return {
ref: ref,
className: `${className}${isOver ? dropClassName : ''}`,
style: { cursor: 'move', ...style },
...restProps,
};
}, [className, dropClassName, restProps, style, isOver]);
return <> {components(tempProps, ref)} </>;
}; };
const components = { const components = useMemo(() => {
body: { return {
row: DragableBodyRow, ...vComponents,
}, body: {
}; ...vComponents.body,
row: DragableBodyRow,
},
};
}, [vComponents]);
const moveRow = useCallback( const moveRow = useCallback(
(dragIndex, hoverIndex) => { (dragIndex: number, hoverIndex: number) => {
if (dragIndex === hoverIndex) { if (dragIndex === hoverIndex) {
return; return;
} }
@@ -390,14 +404,12 @@ const Env = () => {
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, { .put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex }, data: { fromIndex: dragIndex, toIndex: hoverIndex },
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
const newData = [...value]; const newData = [...value];
newData.splice(dragIndex, 1); newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data }); newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]); setValue([...newData]);
} else {
message.error(data);
} }
}); });
}, },
@@ -406,16 +418,10 @@ const Env = () => {
const onSelectChange = (selectedIds: any[]) => { const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds); setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
}; };
const rowSelection = { const rowSelection = {
selectedRowIds, selectedRowKeys: selectedRowIds,
onChange: onSelectChange, onChange: onSelectChange,
}; };
@@ -426,13 +432,11 @@ const Env = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds }) .delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('批量删除成功'); message.success('批量删除成功');
setSelectedRowIds([]); setSelectedRowIds([]);
getEnvs(); getEnvs();
} else {
message.error(data);
} }
}); });
}, },
@@ -451,11 +455,9 @@ const Env = () => {
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, { .put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds, data: selectedRowIds,
}) })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
getEnvs(); getEnvs();
} else {
message.error(data);
} }
}); });
}, },
@@ -480,16 +482,37 @@ const Env = () => {
setSearchText(value.trim()); setSearchText(value.trim());
}; };
const uploadProps: UploadProps = {
accept: 'application/json',
beforeUpload: async (file) => {
const formData = new FormData();
formData.append('env', file);
setImportLoading(true);
try {
const { code, data } = await request.post(
`${config.apiPrefix}envs/upload`,
{
data: formData,
},
);
if (code === 200) {
message.success(`成功上传${data.length}个环境变量`);
getEnvs();
}
setImportLoading(false);
} catch (error: any) {
setImportLoading(false);
}
return false;
},
fileList: [],
};
useEffect(() => { useEffect(() => {
getEnvs(); getEnvs();
}, [searchText]); }, [searchText]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper env-wrapper" className="ql-container-wrapper env-wrapper"
@@ -502,6 +525,15 @@ const Env = () => {
loading={loading} loading={loading}
onSearch={onSearch} onSearch={onSearch}
/>, />,
<Upload {...uploadProps}>
<Button
type="primary"
icon={<UploadOutlined />}
loading={importLoading}
>
</Button>
</Upload>,
<Button key="2" type="primary" onClick={() => addEnv()}> <Button key="2" type="primary" onClick={() => addEnv()}>
</Button>, </Button>,
@@ -555,6 +587,7 @@ const Env = () => {
)} )}
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
<Table <Table
ref={tableRef}
columns={columns} columns={columns}
rowSelection={rowSelection} rowSelection={rowSelection}
pagination={false} pagination={false}
@@ -564,7 +597,7 @@ const Env = () => {
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1000, y: tableScrollHeight }}
components={components} components={components}
loading={loading} loading={loading}
onRow={(record: any, index: number) => { onRow={(record: any, index: number | undefined) => {
return { return {
index, index,
moveRow, moveRow,
-2
View File
@@ -44,8 +44,6 @@ const EnvModal = ({
if (code === 200) { if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功'); message.success(env ? '更新变量成功' : '新建变量成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
} catch (error: any) { } catch (error: any) {
+30 -7
View File
@@ -9,18 +9,37 @@ import './index.less';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
const Error = () => { const Error = () => {
const { user, theme } = useOutletContext<SharedContext>(); const { user, theme, reloadUser } = useOutletContext<SharedContext>();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState('暂无日志'); const [data, setData] = useState('暂无日志');
const getLog = () => { const getTimes = () => {
setLoading(true); return parseInt(localStorage.getItem('error_retry_times') || '0', 10);
};
let times = getTimes();
const getLog = (needLoading: boolean = true) => {
needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}public/panel/log`) .get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setData(data);
if (!data) {
times = getTimes();
if (times > 5) {
return;
}
localStorage.setItem('error_retry_times', `${times + 1}`);
setTimeout(() => {
reloadUser();
getLog(false);
}, 3000);
}
}
}) })
.finally(() => setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
useEffect(() => { useEffect(() => {
@@ -37,7 +56,7 @@ const Error = () => {
<div className="error-wrapper"> <div className="error-wrapper">
{loading ? ( {loading ? (
<PageLoading /> <PageLoading />
) : ( ) : data ? (
<Terminal <Terminal
name="服务错误" name="服务错误"
colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light} colorMode={theme === 'vs-dark' ? ColorMode.Dark : ColorMode.Light}
@@ -53,6 +72,10 @@ const Error = () => {
}, },
]} ]}
/> />
) : times > 5 ? (
<> ql -l check </>
) : (
<PageLoading tip="启动中,请稍后..." />
)} )}
</div> </div>
); );
+5 -9
View File
@@ -41,11 +41,9 @@ const Initialization = () => {
password: values.password, password: values.password,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
next(); next();
} else {
message.error(data.message);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -59,11 +57,9 @@ const Initialization = () => {
...values, ...values,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data && _data.code === 200) { if (code === 200) {
next(); next();
} else {
message.error(_data.message);
} }
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
@@ -236,7 +232,7 @@ const Initialization = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="http://qn.whyour.cn/logo.png" src="https://qn.whyour.cn/logo.png"
/> />
<span className={styles.title}></span> <span className={styles.title}></span>
</div> </div>
+152 -60
View File
@@ -1,5 +1,15 @@
import { useState, useEffect, useCallback, Key, useRef } from 'react'; import { useState, useEffect, useCallback, Key, useRef } from 'react';
import { TreeSelect, Tree, Input, Empty } from 'antd'; import {
TreeSelect,
Tree,
Input,
Empty,
Button,
message,
Modal,
Tooltip,
Typography,
} from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
@@ -9,54 +19,34 @@ import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import { DeleteOutlined } from '@ant-design/icons';
import { depthFirstSearch } from '@/utils';
import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq';
import useFilterTreeData from '@/hooks/useFilterTreeData';
function getFilterData(keyword: string, data: any) { const { Text } = Typography;
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const Log = () => { const Log = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件'); const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
const treeDom = useRef<any>(); const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [currentNode, setCurrentNode] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const getLogs = () => { const getLogs = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}logs`) .get(`${config.apiPrefix}logs`)
.then((data) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setFilterData(data.data); setData(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -64,18 +54,27 @@ const Log = () => {
const getLog = (node: any) => { const getLog = (node: any) => {
request request
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
if (node.type === 'directory') {
setValue('请选择日志文件');
return;
}
setValue('加载中...'); setValue('加载中...');
setSelect(value);
setTitle(node.key);
getLog(node); getLog(node);
}; };
@@ -86,41 +85,130 @@ const Log = () => {
const onSearch = useCallback( const onSearch = useCallback(
(e) => { (e) => {
const keyword = e.target.value; const keyword = e.target.value;
const { tree, expandedKeys } = getFilterData( debounceSearch(keyword);
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree);
setExpandedKeys(expandedKeys);
}, },
[data, setFilterData], [data],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
}, 300),
[data],
);
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
); );
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const deleteFile = () => {
Modal.confirm({
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>
{currentNode.type === 'directory' ? '夹下所以日志' : ''}
</>
),
onOk() {
request
.delete(`${config.apiPrefix}logs`, {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
type: currentNode.type,
},
})
.then(({ code }) => {
if (code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
newData = depthFirstSearch(
newData,
(c) => c.key === currentNode.key,
);
} else {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
);
if (index !== -1) {
newData.splice(index, 1);
}
}
setData(newData);
initState();
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const initState = () => {
setSelect('');
setCurrentNode(null);
setValue('请选择脚本文件');
};
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
useEffect(() => { useEffect(() => {
getLogs(); getLogs();
if (treeDom && treeDom.current) { }, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight); setHeight(treeDom.current.clientHeight);
} }
}, []); }, [treeDom.current, data]);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={select}
loading={loading} loading={loading}
extra={ extra={
isPhone && [ isPhone
<TreeSelect ? [
className="log-select" <TreeSelect
value={select} treeExpandAction="click"
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} className="log-select"
treeData={data} value={select}
placeholder="请选择日志" dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
fieldNames={{ value: 'key', label: 'title' }} treeData={data}
showSearch placeholder="请选择日志"
onSelect={onSelect} fieldNames={{ value: 'key' }}
/>, treeNodeFilterProp="title"
] showSearch
allowClear
onSelect={onSelect}
/>,
]
: [
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
]
} }
header={{ header={{
style: headerStyle, style: headerStyle,
@@ -128,6 +216,7 @@ const Log = () => {
> >
<div className={`${styles['log-container']} log-container`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}> <SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
{data.length > 0 ? ( {data.length > 0 ? (
@@ -140,6 +229,7 @@ const Log = () => {
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
expandAction="click"
className={styles['left-tree']} className={styles['left-tree']}
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
@@ -147,6 +237,8 @@ const Log = () => {
selectedKeys={[select]} selectedKeys={[select]}
showLine={{ showLeafIcon: true }} showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect} onSelect={onTreeSelect}
expandedKeys={expandedKeys}
onExpand={onExpand}
></Tree> ></Tree>
</div> </div>
</> </>
+17 -25
View File
@@ -40,15 +40,7 @@ const Login = () => {
}, },
}) })
.then((data) => { .then((data) => {
if (data.code === 420) { checkResponse(data, values);
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
} else {
checkResponse(data);
}
setLoading(false); setLoading(false);
}) })
.catch(function (error) { .catch(function (error) {
@@ -64,11 +56,7 @@ const Login = () => {
data: { ...loginInfo, code: values.code }, data: { ...loginInfo, code: values.code },
}) })
.then((data: any) => { .then((data: any) => {
if (data.code === 430) { checkResponse(data);
message.error(data.message);
} else {
checkResponse(data);
}
setVerifying(false); setVerifying(false);
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -77,8 +65,11 @@ const Login = () => {
}); });
}; };
const checkResponse = (data: any) => { const checkResponse = (
if (data.code === 200) { { code, data, message: _message }: any,
values?: any,
) => {
if (code === 200) {
const { const {
token, token,
lastip, lastip,
@@ -86,7 +77,7 @@ const Login = () => {
lastlogon, lastlogon,
retries = 0, retries = 0,
platform, platform,
} = data.data; } = data;
localStorage.setItem(config.authKey, token); localStorage.setItem(config.authKey, token);
notification.success({ notification.success({
message: '登录成功!', message: '登录成功!',
@@ -105,13 +96,14 @@ const Login = () => {
}); });
reloadUser(true); reloadUser(true);
history.push('/crontab'); history.push('/crontab');
} else if (data.code === 100) { } else if (code === 410) {
message.warn(data.message); setWaitTime(data);
} else if (data.code === 410) { } else if (code === 420) {
message.error(data.message); setLoginInfo({
setWaitTime(data.data); username: values.username,
} else { password: values.password,
message.error(data.message); });
setTwoFactor(true);
} }
}; };
@@ -137,7 +129,7 @@ const Login = () => {
<img <img
alt="logo" alt="logo"
className={styles.logo} className={styles.logo}
src="http://qn.whyour.cn/logo.png" src="https://qn.whyour.cn/logo.png"
/> />
<span className={styles.title}> <span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName} {twoFactor ? '两步验证' : config.siteName}
+37 -12
View File
@@ -7,6 +7,7 @@ import Editor from '@monaco-editor/react';
import SaveModal from './saveModal'; import SaveModal from './saveModal';
import SettingModal from './setting'; import SettingModal from './setting';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { logEnded } from '@/utils';
const { Option } = Select; const { Option } = Select;
const LangMap: any = { const LangMap: any = {
@@ -48,6 +49,7 @@ const EditModal = ({
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [currentPid, setCurrentPid] = useState(null);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
@@ -57,6 +59,11 @@ const EditModal = ({
if (node.key === selectedKey || !value) { if (node.key === selectedKey || !value) {
return; return;
} }
if (node.type === 'directory') {
return;
}
const newMode = LangMap[value.slice(-3)] || ''; const newMode = LangMap[value.slice(-3)] || '';
setCNode(node); setCNode(node);
setLanguage(newMode); setLanguage(newMode);
@@ -67,8 +74,10 @@ const EditModal = ({
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => { .then(({ code, data }) => {
setValue(data.data); if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -83,26 +92,30 @@ const EditModal = ({
content, content,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
setIsRunning(true); if (code === 200) {
setIsRunning(true);
setCurrentPid(data);
}
}); });
}; };
const stop = () => { const stop = () => {
if (!cNode || !cNode.title) { if (!cNode || !cNode.title || !currentPid) {
return; return;
} }
const content = editorRef.current.getValue().replace(/\r\n/g, '\n');
request request
.put(`${config.apiPrefix}scripts/stop`, { .put(`${config.apiPrefix}scripts/stop`, {
data: { data: {
filename: cNode.title, filename: cNode.title,
path: cNode.parent || '', path: cNode.parent || '',
content, pid: currentPid,
}, },
}) })
.then((data) => { .then(({ code, data }) => {
setIsRunning(false); if (code === 200) {
setIsRunning(false);
}
}); });
}; };
@@ -117,7 +130,7 @@ const EditModal = ({
return; return;
} }
if (_message.includes('执行结束')) { if (logEnded(_message)) {
setTimeout(() => { setTimeout(() => {
setIsRunning(false); setIsRunning(false);
}, 300); }, 300);
@@ -130,10 +143,13 @@ const EditModal = ({
}, [socketMessage]); }, [socketMessage]);
useEffect(() => { useEffect(() => {
setLog('');
if (currentNode) { if (currentNode) {
setCNode(currentNode); setCNode(currentNode);
setValue(content as string); setValue(content as string);
setSelectedKey(currentNode.key); setSelectedKey(currentNode.key);
const newMode = LangMap[currentNode.title.slice(-3)] || '';
setLanguage(newMode);
} }
}, [content, currentNode]); }, [content, currentNode]);
@@ -144,6 +160,7 @@ const EditModal = ({
title={ title={
<> <>
<TreeSelect <TreeSelect
treeExpandAction="click"
style={{ marginRight: 8, width: 150 }} style={{ marginRight: 8, width: 150 }}
value={selectedKey} value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
@@ -216,6 +233,7 @@ const EditModal = ({
onClose={cancel} onClose={cancel}
open={visible} open={visible}
> >
{/* @ts-ignore */}
<SplitPane <SplitPane
split="vertical" split="vertical"
minSize={200} minSize={200}
@@ -236,7 +254,14 @@ const EditModal = ({
editorRef.current = editor; editorRef.current = editor;
}} }}
/> />
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre> <pre
style={{
height: '100%',
padding: '0 15px',
}}
>
{log}
</pre>
</SplitPane> </SplitPane>
<SaveModal <SaveModal
visible={saveModalVisible} visible={saveModalVisible}
@@ -247,7 +272,7 @@ const EditModal = ({
content: content:
editorRef.current && editorRef.current &&
editorRef.current.getValue().replace(/\r\n/g, '\n'), editorRef.current.getValue().replace(/\r\n/g, '\n'),
filename: cNode?.title, ...cNode,
}} }}
/> />
<SettingModal <SettingModal
-2
View File
@@ -57,8 +57,6 @@ const EditScriptNameModal = ({
path, path,
key: `${key}${filename}`, key: `${key}${filename}`,
}); });
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}) })
+154 -129
View File
@@ -11,6 +11,7 @@ import {
Dropdown, Dropdown,
Menu, Menu,
Empty, Empty,
MenuProps,
} from 'antd'; } from 'antd';
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
@@ -37,37 +38,13 @@ import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string'; import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils'; import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq';
import IconFont from '@/components/iconfont';
import RenameModal from './renameModal';
const { Text } = Typography; const { Text } = Typography;
function getFilterData(keyword: string, data: any) {
const expandedKeys: string[] = [];
if (keyword) {
const tree: any = [];
data.forEach((item: any) => {
if (item.title.toLocaleLowerCase().includes(keyword)) {
tree.push(item);
} else {
const children: any[] = [];
(item.children || []).forEach((subItem: any) => {
if (subItem.title.toLocaleLowerCase().includes(keyword)) {
children.push(subItem);
}
});
if (children.length > 0) {
tree.push({
...item,
children,
});
expandedKeys.push(item.key);
}
}
});
return { tree, expandedKeys };
}
return { tree: data, expandedKeys };
}
const LangMap: any = { const LangMap: any = {
'.py': 'python', '.py': 'python',
'.js': 'javascript', '.js': 'javascript',
@@ -78,11 +55,9 @@ const LangMap: any = {
const Script = () => { const Script = () => {
const { headerStyle, isPhone, theme, socketMessage } = const { headerStyle, isPhone, theme, socketMessage } =
useOutletContext<SharedContext>(); useOutletContext<SharedContext>();
const [title, setTitle] = useState('请选择脚本文件');
const [value, setValue] = useState('请选择脚本文件'); const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>(); const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]); const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [mode, setMode] = useState(''); const [mode, setMode] = useState('');
const [height, setHeight] = useState<number>(); const [height, setHeight] = useState<number>();
@@ -92,26 +67,36 @@ const Script = () => {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false); const [isAddFileModalVisible, setIsAddFileModalVisible] = useState(false);
const [isRenameFileModalVisible, setIsRenameFileModalVisible] =
useState(false);
const [currentNode, setCurrentNode] = useState<any>(); const [currentNode, setCurrentNode] = useState<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]); const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const getScripts = () => { const getScripts = (needLoading: boolean = true) => {
setLoading(true); needLoading && setLoading(true);
request request
.get(`${config.apiPrefix}scripts`) .get(`${config.apiPrefix}scripts`)
.then((data) => { .then(({ code, data }) => {
setData(data.data); if (code === 200) {
setFilterData(data.data); setData(data);
initGetScript(); initState();
initGetScript();
}
}) })
.finally(() => setLoading(false)); .finally(() => needLoading && setLoading(false));
}; };
const getDetail = (node: any) => { const getDetail = (node: any) => {
request request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`) .get(
.then((data) => { `${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
setValue(data.data); node.parent || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
}); });
}; };
@@ -132,22 +117,24 @@ const Script = () => {
}; };
const onSelect = (value: any, node: any) => { const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) { if (node.key === select || !value) {
return; return;
} }
setValue('加载中...');
if (node.type === 'directory') {
setValue('请选择脚本文件');
return;
}
const newMode = value ? LangMap[value.slice(-3)] : ''; const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode); setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(node.key); setValue('加载中...');
setTitle(node.key);
setCurrentNode(node);
getDetail(node); getDetail(node);
}; };
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback( const onTreeSelect = useCallback(
(keys: Key[], e: any) => { (keys: Key[], e: any) => {
const content = editorRef.current const content = editorRef.current
@@ -178,22 +165,30 @@ const Script = () => {
const keyword = e.target.value; const keyword = e.target.value;
debounceSearch(keyword); debounceSearch(keyword);
}, },
[data, setFilterData], [data],
); );
const debounceSearch = useCallback( const debounceSearch = useCallback(
debounce((keyword) => { debounce((keyword) => {
setSearchValue(keyword); setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 300), }, 300),
[data, setFilterData], [data],
); );
const { treeData: filterData, keys: searchExpandedKeys } = useFilterTreeData(
data,
searchValue,
{ treeNodeFilterProp: 'title' },
);
useEffect(() => {
setExpandedKeys(uniq([...expandedKeys, ...searchExpandedKeys]));
}, [searchExpandedKeys]);
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const editFile = () => { const editFile = () => {
setTimeout(() => { setTimeout(() => {
setIsEditing(true); setIsEditing(true);
@@ -231,13 +226,11 @@ const Script = () => {
content, content,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data.code === 200) { if (code === 200) {
message.success(`保存成功`); message.success(`保存成功`);
setValue(content); setValue(content);
setIsEditing(false); setIsEditing(false);
} else {
message.error(_data);
} }
resolve(null); resolve(null);
}) })
@@ -255,10 +248,11 @@ const Script = () => {
title: `确认删除`, title: `确认删除`,
content: ( content: (
<> <>
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Text style={{ wordBreak: 'break-all' }} type="warning">
{select} {select}
</Text>{' '} </Text>
{currentNode.type === 'directory' ? '夹及其子文件' : ''}
</> </>
), ),
@@ -268,24 +262,18 @@ const Script = () => {
data: { data: {
filename: currentNode.title, filename: currentNode.title,
path: currentNode.parent || '', path: currentNode.parent || '',
type: currentNode.type,
}, },
}) })
.then((_data: any) => { .then(({ code }) => {
if (_data.code === 200) { if (code === 200) {
message.success(`删除成功`); message.success(`删除成功`);
let newData = [...data]; let newData = [...data];
if (currentNode.parent) { if (currentNode.parent) {
const parentNodeIndex = newData.findIndex( newData = depthFirstSearch(
(x) => x.key === currentNode.parent, newData,
(c) => c.key === currentNode.key,
); );
const parentNode = newData[parentNodeIndex];
const index = parentNode.children.findIndex(
(y) => y.key === currentNode.key,
);
if (index !== -1 && parentNodeIndex !== -1) {
parentNode.children.splice(index, 1);
newData.splice(parentNodeIndex, 1, { ...parentNode });
}
} else { } else {
const index = newData.findIndex( const index = newData.findIndex(
(x) => x.key === currentNode.key, (x) => x.key === currentNode.key,
@@ -295,8 +283,7 @@ const Script = () => {
} }
} }
setData(newData); setData(newData);
} else { initState();
message.error(_data);
} }
}); });
}, },
@@ -306,6 +293,15 @@ const Script = () => {
}); });
}; };
const renameFile = () => {
setIsRenameFileModalVisible(true);
};
const handleRenameFileCancel = () => {
setIsRenameFileModalVisible(false);
getScripts(false);
};
const addFile = () => { const addFile = () => {
setIsAddFileModalVisible(true); setIsAddFileModalVisible(true);
}; };
@@ -346,30 +342,35 @@ const Script = () => {
filename: currentNode.title, filename: currentNode.title,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
const blob = new Blob([_data], { type: 'application/json' }); if (code === 200) {
const url = URL.createObjectURL(blob); const blob = new Blob([data], { type: 'application/json' });
const a = document.createElement('a'); const url = URL.createObjectURL(blob);
a.href = url; const a = document.createElement('a');
a.download = currentNode.title; a.href = url;
document.documentElement.appendChild(a); a.download = currentNode.title;
a.click(); document.documentElement.appendChild(a);
document.documentElement.removeChild(a); a.click();
document.documentElement.removeChild(a);
}
}); });
}; };
useEffect(() => { const initState = () => {
const word = searchValue || ''; setSelect('');
const { tree } = getFilterData(word.toLocaleLowerCase(), data); setCurrentNode(null);
setFilterData(tree); setValue('请选择脚本文件');
}, [data]); };
useEffect(() => { useEffect(() => {
getScripts(); getScripts();
if (treeDom && treeDom.current) { }, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight); setHeight(treeDom.current.clientHeight);
} }
}, []); }, [treeDom.current, data]);
const action = (key: string | number) => { const action = (key: string | number) => {
switch (key) { switch (key) {
@@ -395,65 +396,75 @@ const Script = () => {
case 'delete': case 'delete':
deleteFile(); deleteFile();
break; break;
case 'rename':
renameFile();
break;
default: default:
break; break;
} }
}; };
const menu = isEditing ? ( const menu: MenuProps = isEditing
<Menu ? {
items={[ items: [
{ label: '保存', key: 'save', icon: <PlusOutlined /> }, { label: '保存', key: 'save', icon: <PlusOutlined /> },
{ label: '退出编辑', key: 'exit', icon: <EditOutlined /> }, { label: '退出编辑', key: 'exit', icon: <EditOutlined /> },
]} ],
onClick={({ key, domEvent }) => { onClick: ({ key, domEvent }) => {
domEvent.stopPropagation(); domEvent.stopPropagation();
action(key); action(key);
}}
/>
) : (
<Menu
items={[
{ label: '新建', key: 'add', icon: <PlusOutlined /> },
{
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
}, },
{ }
label: '删除', : {
key: 'delete', items: [
icon: <DeleteOutlined />, { label: '新建', key: 'add', icon: <PlusOutlined /> },
disabled: !select, {
label: '编辑',
key: 'edit',
icon: <EditOutlined />,
disabled: !select,
},
{
label: '重命名',
key: 'rename',
icon: <IconFont type="ql-icon-rename" />,
disabled: !select,
},
{
label: '删除',
key: 'delete',
icon: <DeleteOutlined />,
disabled: !select,
},
],
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}, },
]} };
onClick={({ key, domEvent }) => {
domEvent.stopPropagation();
menuAction(key);
}}
/>
);
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper log-wrapper" className="ql-container-wrapper log-wrapper"
title={title} title={select}
loading={loading} loading={loading}
extra={ extra={
isPhone isPhone
? [ ? [
<TreeSelect <TreeSelect
treeExpandAction="click"
className="log-select" className="log-select"
value={select} value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data} treeData={data}
placeholder="请选择脚本" placeholder="请选择脚本"
fieldNames={{ value: 'key', label: 'title' }} fieldNames={{ value: 'key' }}
treeNodeFilterProp="title"
showSearch showSearch
allowClear
onSelect={onSelect} onSelect={onSelect}
/>, />,
<Dropdown overlay={menu} trigger={['click']}> <Dropdown menu={menu} trigger={['click']}>
<Button type="primary" icon={<EllipsisOutlined />} /> <Button type="primary" icon={<EllipsisOutlined />} />
</Dropdown>, </Dropdown>,
] ]
@@ -482,6 +493,14 @@ const Script = () => {
icon={<EditOutlined />} icon={<EditOutlined />}
/> />
</Tooltip>, </Tooltip>,
<Tooltip title="重命名">
<Button
disabled={!select}
type="primary"
onClick={renameFile}
icon={<IconFont type="ql-icon-rename" />}
/>
</Tooltip>,
<Tooltip title="删除"> <Tooltip title="删除">
<Button <Button
type="primary" type="primary"
@@ -506,6 +525,7 @@ const Script = () => {
> >
<div className={`${styles['log-container']} log-container`}> <div className={`${styles['log-container']} log-container`}>
{!isPhone && ( {!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}> <SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}> <div className={styles['left-tree-container']}>
{data.length > 0 ? ( {data.length > 0 ? (
@@ -518,6 +538,7 @@ const Script = () => {
></Input.Search> ></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}> <div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree <Tree
expandAction="click"
className={styles['left-tree']} className={styles['left-tree']}
treeData={filterData} treeData={filterData}
showIcon={true} showIcon={true}
@@ -554,7 +575,6 @@ const Script = () => {
readOnly: !isEditing, readOnly: !isEditing,
fontSize: 12, fontSize: 12,
lineNumbersMinChars: 3, lineNumbersMinChars: 3,
folding: false,
glyphMargin: false, glyphMargin: false,
}} }}
onMount={(editor) => { onMount={(editor) => {
@@ -595,6 +615,11 @@ const Script = () => {
treeData={data} treeData={data}
handleCancel={addFileModalClose} handleCancel={addFileModalClose}
/> />
<RenameModal
visible={isRenameFileModalVisible}
handleCancel={handleRenameFileCancel}
currentNode={currentNode}
/>
</div> </div>
</PageContainer> </PageContainer>
); );
+79
View File
@@ -0,0 +1,79 @@
import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form } from 'antd';
import { request } from '@/utils/http';
import config from '@/utils/config';
const RenameModal = ({
currentNode,
handleCancel,
visible,
}: {
currentNode?: any;
visible: boolean;
handleCancel: () => void;
}) => {
console.log(currentNode);
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const handleOk = async (values: any) => {
setLoading(true);
try {
const { code, data } = await request.put(
`${config.apiPrefix}scripts/rename`,
{
data: {
filename: currentNode.title,
path: currentNode.parent || '',
newFilename: values.name,
},
},
);
if (code === 200) {
message.success('更新名称成功');
handleCancel();
}
setLoading(false);
} catch (error) {
setLoading(false);
}
};
useEffect(() => {
form.resetFields();
}, [currentNode, visible]);
return (
<Modal
title="重命名"
open={visible}
forceRender
centered
maskClosable={false}
onOk={() => {
form
.validateFields()
.then((values) => {
handleOk(values);
})
.catch((info) => {
console.log('Validate Failed:', info);
});
}}
onCancel={() => handleCancel()}
confirmLoading={loading}
>
<Form form={form} layout="vertical" name="edit_name_modal">
<Form.Item
name="name"
rules={[{ required: true, message: '请输入新名称' }]}
>
<Input placeholder="请输入新名称" />
</Form.Item>
</Form>
</Modal>
);
};
export default RenameModal;
+2 -4
View File
@@ -17,7 +17,7 @@ const SaveModal = ({
const handleOk = async (values: any) => { const handleOk = async (values: any) => {
setLoading(true); setLoading(true);
const payload = { ...file, ...values, originFilename: file.filename }; const payload = { ...file, ...values, originFilename: file.title };
request request
.post(`${config.apiPrefix}scripts`, { .post(`${config.apiPrefix}scripts`, {
data: payload, data: payload,
@@ -26,8 +26,6 @@ const SaveModal = ({
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}); });
@@ -62,7 +60,7 @@ const SaveModal = ({
form={form} form={form}
layout="vertical" layout="vertical"
name="script_modal" name="script_modal"
initialValues={file} initialValues={{ filename: file?.title, path: file?.parent || '' }}
> >
<Form.Item <Form.Item
name="filename" name="filename"
-2
View File
@@ -26,8 +26,6 @@ const SettingModal = ({
if (code === 200) { if (code === 200) {
message.success('保存文件成功'); message.success('保存文件成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
}); });
+31 -3
View File
@@ -1,16 +1,23 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Typography, Input, Form, Button, message } from 'antd'; import { Typography, Input, Form, Button, message, Descriptions } from 'antd';
import styles from './index.less'; import styles from './index.less';
import { SharedContext } from '@/layouts';
import dayjs from 'dayjs';
const { Link } = Typography; const { Link } = Typography;
const About = () => { enum TVersion {
'develop' = '开发版',
'master' = '正式版',
}
const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<img <img
alt="logo" alt="logo"
style={{ width: 140, marginRight: 20 }} style={{ width: 140, marginRight: 20 }}
src="http://qn.whyour.cn/logo.png" src="https://qn.whyour.cn/logo.png"
/> />
<div className={styles.right}> <div className={styles.right}>
<span className={styles.title}></span> <span className={styles.title}></span>
@@ -19,6 +26,27 @@ const About = () => {
task management panel that supports typescript, javaScript, python3, task management panel that supports typescript, javaScript, python3,
and shell. and shell.
</span> </span>
<Descriptions>
<Descriptions.Item label="版本" span={3}>
{TVersion[systemInfo.branch]} v{systemInfo.version}
</Descriptions.Item>
<Descriptions.Item label="更新时间" span={3}>
{dayjs(systemInfo.lastCommitTime * 1000).format(
'YYYY-MM-DD HH:mm:ss',
)}
</Descriptions.Item>
<Descriptions.Item label="更新ID" span={3}>
{systemInfo.lastCommitId}
</Descriptions.Item>
<Descriptions.Item label="更新日志" span={3}>
<Link
href={`https://qn.whyour.cn/version.yaml?t=${Date.now()}`}
target="_blank"
>
</Link>
</Descriptions.Item>
</Descriptions>
<div> <div>
<Link <Link
href="https://github.com/whyour/qinglong" href="https://github.com/whyour/qinglong"
-2
View File
@@ -30,8 +30,6 @@ const AppModal = ({
if (code === 200) { if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功'); message.success(app ? '更新应用成功' : '新建应用成功');
handleCancel(data); handleCancel(data);
} else {
message.error(data);
} }
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
+28 -30
View File
@@ -2,11 +2,10 @@ import React, { useEffect, useState, useRef } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd'; import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { version } from '../../version';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ socketMessage }: any) => { const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
@@ -17,17 +16,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
message.loading('检查更新中...', 0); message.loading('检查更新中...', 0);
request request
.put(`${config.apiPrefix}system/update-check`) .put(`${config.apiPrefix}system/update-check`)
.then((_data: any) => { .then(({ code, data }) => {
message.destroy(); message.destroy();
const { code, data } = _data;
if (code === 200) { if (code === 200) {
if (data.hasNewVersion) { if (data.hasNewVersion) {
showConfirmUpdateModal(data); showConfirmUpdateModal(data);
} else { } else {
showForceUpdateModal(); showForceUpdateModal(data);
} }
} else {
message.error(data);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -39,7 +35,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
}); });
}; };
const showForceUpdateModal = () => { const showForceUpdateModal = (data: any) => {
Modal.confirm({ Modal.confirm({
width: 500, width: 500,
title: '更新', title: '更新',
@@ -47,7 +43,7 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<div></div> <div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{version} {data.lastVersion}
</div> </div>
</> </>
), ),
@@ -73,16 +69,13 @@ const CheckUpdate = ({ socketMessage }: any) => {
<> <>
<div></div> <div></div>
<div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}> <div style={{ fontSize: 12, fontWeight: 400, marginTop: 5 }}>
{lastVersion}使{version} {lastVersion} 使 {systemInfo.version}
</div> </div>
</> </>
), ),
content: ( content: (
<pre <pre
style={{ style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15,
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
}} }}
@@ -105,26 +98,23 @@ const CheckUpdate = ({ socketMessage }: any) => {
}; };
const showUpdatingModal = () => { const showUpdatingModal = () => {
setValue('');
modalRef.current = Modal.info({ modalRef.current = Modal.info({
width: 600, width: 600,
maskClosable: false, maskClosable: false,
closable: false, closable: false,
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: '更新日志', title: '更新中...',
centered: true, centered: true,
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <pre
<pre style={{
style={{ fontSize: 12,
wordBreak: 'break-all', fontWeight: 400,
whiteSpace: 'pre-wrap', }}
fontSize: 12, >
fontWeight: 400, {value}
}} </pre>
>
{value}
</pre>
</div>
), ),
}); });
}; };
@@ -140,13 +130,16 @@ const CheckUpdate = ({ socketMessage }: any) => {
} }
const newMessage = `${value}${_message}`; const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes('失败,请检查');
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed,
closable: updateFailed,
okButtonProps: { disabled: !updateFailed },
content: ( content: (
<div style={{ height: '60vh', overflowY: 'auto' }}> <>
<pre <pre
style={{ style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: 12, fontSize: 12,
fontWeight: 400, fontWeight: 400,
}} }}
@@ -154,9 +147,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
{newMessage} {newMessage}
</pre> </pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div> <div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</div> </>
), ),
}); });
if (updateFailed && !value.includes('失败,请检查')) {
message.error('更新失败,请检查网络及日志或稍后再试');
}
setValue(newMessage); setValue(newMessage);
document.getElementById('log-identifier') && document.getElementById('log-identifier') &&
+15
View File
@@ -18,5 +18,20 @@
.desc { .desc {
margin-bottom: 10px; margin-bottom: 10px;
} }
:global {
.ant-descriptions-row > th,
.ant-descriptions-row > td {
padding-bottom: 10px;
}
}
}
}
.ql-setting-container {
.ant-tabs-content-holder {
max-height: calc(100vh - 114px);
max-height: calc(100vh - var(--vh-offset, 114px));
overflow-y: auto;
} }
} }
+42 -121
View File
@@ -17,7 +17,6 @@ import {
import config from '@/utils/config'; import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import * as DarkReader from '@umijs/ssr-darkreader';
import AppModal from './appModal'; import AppModal from './appModal';
import { import {
EditOutlined, EditOutlined,
@@ -27,21 +26,24 @@ import {
import SecuritySettings from './security'; import SecuritySettings from './security';
import LoginLog from './loginLog'; import LoginLog from './loginLog';
import NotificationSetting from './notification'; import NotificationSetting from './notification';
import CheckUpdate from './checkUpdate'; import Other from './other';
import About from './about'; import About from './about';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import './index.less';
const { Text } = Typography; const { Text } = Typography;
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Setting = () => { const Setting = () => {
const { headerStyle, isPhone, user, reloadUser, reloadTheme, socketMessage } = const {
useOutletContext<SharedContext>(); headerStyle,
isPhone,
user,
reloadUser,
reloadTheme,
socketMessage,
systemInfo,
} = useOutletContext<SharedContext>();
const columns = [ const columns = [
{ {
title: '名称', title: '名称',
@@ -72,10 +74,15 @@ const Setting = () => {
dataIndex: 'scopes', dataIndex: 'scopes',
key: 'scopes', key: 'scopes',
align: 'center' as const, align: 'center' as const,
width: '40%',
render: (text: string, record: any) => { render: (text: string, record: any) => {
return record.scopes.map((scope: any) => { return (
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>; <div style={{ textAlign: 'left' }}>
}); {record.scopes.map((scope: any) => {
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
})}
</div>
);
}, },
}, },
{ {
@@ -108,44 +115,21 @@ const Setting = () => {
]; ];
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [dataSource, setDataSource] = useState<any[]>([]); const [dataSource, setDataSource] = useState<any[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [editedApp, setEditedApp] = useState<any>(); const [editedApp, setEditedApp] = useState<any>();
const [tabActiveKey, setTabActiveKey] = useState('security'); const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]); const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [notificationInfo, setNotificationInfo] = useState<any>(); const [notificationInfo, setNotificationInfo] = useState<any>();
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getApps = () => { const getApps = () => {
setLoading(true); setLoading(true);
request request
.get(`${config.apiPrefix}apps`) .get(`${config.apiPrefix}apps`)
.then((data: any) => { .then(({ code, data }) => {
setDataSource(data.data); if (code === 200) {
setDataSource(data);
}
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}; };
@@ -175,14 +159,12 @@ const Setting = () => {
onOk() { onOk() {
request request
.delete(`${config.apiPrefix}apps`, { data: [record.id] }) .delete(`${config.apiPrefix}apps`, { data: [record.id] })
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('删除成功'); message.success('删除成功');
const result = [...dataSource]; const result = [...dataSource];
result.splice(index, 1); result.splice(index, 1);
setDataSource(result); setDataSource(result);
} else {
message.error(data);
} }
}); });
}, },
@@ -209,12 +191,10 @@ const Setting = () => {
onOk() { onOk() {
request request
.put(`${config.apiPrefix}apps/${record.id}/reset-secret`) .put(`${config.apiPrefix}apps/${record.id}/reset-secret`)
.then((data: any) => { .then(({ code, data }) => {
if (data.code === 200) { if (code === 200) {
message.success('重置成功'); message.success('重置成功');
handleApp(data.data); handleApp(data);
} else {
message.error(data);
} }
}); });
}, },
@@ -247,8 +227,10 @@ const Setting = () => {
const getLoginLog = () => { const getLoginLog = () => {
request request
.get(`${config.apiPrefix}user/login-log`) .get(`${config.apiPrefix}user/login-log`)
.then((data: any) => { .then(({ code, data }) => {
setLoginLogData(data.data); if (code === 200) {
setLoginLogData(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -263,29 +245,15 @@ const Setting = () => {
getLoginLog(); getLoginLog();
} else if (activeKey === 'notification') { } else if (activeKey === 'notification') {
getNotification(); getNotification();
} else if (activeKey === 'other') {
getLogRemoveFrequency();
} }
}; };
const getNotification = () => { const getNotification = () => {
request request
.get(`${config.apiPrefix}user/notification`) .get(`${config.apiPrefix}user/notification`)
.then((data: any) => { .then(({ code, data }) => {
setNotificationInfo(data.data); if (code === 200) {
}) setNotificationInfo(data);
.catch((error: any) => {
console.log(error);
});
};
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then((data: any) => {
if (data.data.info) {
const { frequency } = data.data.info;
setLogRemoveFrequency(frequency);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -293,24 +261,9 @@ const Setting = () => {
}); });
}; };
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then((data: any) => {
message.success('更新成功');
})
.catch((error: any) => {
console.log(error);
});
});
};
return ( return (
<PageContainer <PageContainer
className="ql-container-wrapper ql-container-wrapper-has-tab" className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
title="系统设置" title="系统设置"
header={{ header={{
style: headerStyle, style: headerStyle,
@@ -365,49 +318,17 @@ const Setting = () => {
key: 'other', key: 'other',
label: '其他设置', label: '其他设置',
children: ( children: (
<Form layout="vertical" form={form}> <Other
<Form.Item reloadTheme={reloadTheme}
label="主题设置" socketMessage={socketMessage}
name="theme" systemInfo={systemInfo}
initialValue={defaultTheme} />
>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate socketMessage={socketMessage} />
</Form.Item>
</Form>
), ),
}, },
{ {
key: 'about', key: 'about',
label: '关于', label: '关于',
children: <About />, children: <About systemInfo={systemInfo} />,
}, },
]} ]}
></Tabs> ></Tabs>
+16 -20
View File
@@ -23,11 +23,9 @@ const NotificationSetting = ({ data }: any) => {
...values, ...values,
}, },
}) })
.then((_data: any) => { .then(({ code, data }) => {
if (_data && _data.code === 200) { if (code === 200) {
message.success(values.type ? '通知发送成功' : '通知关闭成功'); message.success(values.type ? '通知发送成功' : '通知关闭成功');
} else {
message.error(_data.message);
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -75,22 +73,20 @@ const NotificationSetting = ({ data }: any) => {
rules={[{ required: x.required }]} rules={[{ required: x.required }]}
style={{ maxWidth: 400 }} style={{ maxWidth: 400 }}
> >
{ {x.items ? (
x.items ? ( <Select placeholder={x.placeholder || `请选择${x.label}`}>
<Select placeholder={x.placeholder || `请选择${x.label}`}> {x.items.map((y) => (
{x.items.map((y) => ( <Option key={y.value} value={y.value}>
<Option key={y.value} value={y.value}> {y.label || y.value}
{y.label || y.value} </Option>
</Option> ))}
))} </Select>
</Select> ) : (
) : ( <Input.TextArea
<Input.TextArea autoSize={true}
autoSize={true} placeholder={x.placeholder || `请输入${x.label}`}
placeholder={x.placeholder || `请输入${x.label}`} />
/> )}
)
}
</Form.Item> </Form.Item>
))} ))}
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
+120
View File
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { Button, InputNumber, Form, Radio, message, Input } from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config';
import { request } from '@/utils/http';
import CheckUpdate from './checkUpdate';
import { SharedContext } from '@/layouts';
import './index.less';
const optionsWithDisabled = [
{ label: '亮色', value: 'light' },
{ label: '暗色', value: 'dark' },
{ label: '跟随系统', value: 'auto' },
];
const Other = ({
systemInfo,
socketMessage,
reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [logRemoveFrequency, setLogRemoveFrequency] = useState<number | null>();
const [form] = Form.useForm();
const {
enable: enableDarkMode,
disable: disableDarkMode,
exportGeneratedCSS: collectCSS,
setFetchMethod,
auto: followSystemColorScheme,
} = DarkReader || {};
const themeChange = (e: any) => {
const _theme = e.target.value;
localStorage.setItem('qinglong_dark_theme', e.target.value);
setFetchMethod(fetch);
if (_theme === 'dark') {
enableDarkMode({});
} else if (_theme === 'light') {
disableDarkMode();
} else {
followSystemColorScheme({});
}
reloadTheme();
};
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
.catch((error: any) => {
console.log(error);
});
};
const updateRemoveLogFrequency = () => {
setTimeout(() => {
request
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
});
});
};
useEffect(() => {
getLogRemoveFrequency();
}, []);
return (
<Form layout="vertical" form={form}>
<Form.Item label="主题设置" name="theme" initialValue={defaultTheme}>
<Radio.Group
options={optionsWithDisabled}
onChange={themeChange}
value={defaultTheme}
optionType="button"
buttonStyle="solid"
/>
</Form.Item>
<Form.Item
label="日志删除频率"
name="frequency"
tooltip="每x天自动删除x天以前的日志"
>
<Input.Group compact>
<InputNumber
addonBefore="每"
addonAfter="天"
style={{ width: 150 }}
min={0}
value={logRemoveFrequency}
onChange={(value) => setLogRemoveFrequency(value)}
/>
<Button type="primary" onClick={updateRemoveLogFrequency}>
</Button>
</Input.Group>
</Form.Item>
<Form.Item label="检查更新" name="update">
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} />
</Form.Item>
</Form>
);
};
export default Other;
+21 -15
View File
@@ -27,9 +27,11 @@ const SecuritySettings = ({ user, userChange }: any) => {
password: values.password, password: values.password,
}, },
}) })
.then((data: any) => { .then(({ code, data }) => {
localStorage.removeItem(config.authKey); if (code === 200) {
history.push('/login'); localStorage.removeItem(config.authKey);
history.push('/login');
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);
@@ -48,8 +50,8 @@ const SecuritySettings = ({ user, userChange }: any) => {
const deactiveTowFactor = () => { const deactiveTowFactor = () => {
request request
.put(`${config.apiPrefix}user/two-factor/deactive`) .put(`${config.apiPrefix}user/two-factor/deactive`)
.then((data: any) => { .then(({ code, data }) => {
if (data.data) { if (code === 200 && data) {
setTwoFactorActivated(false); setTwoFactorActivated(false);
userChange(); userChange();
} }
@@ -63,14 +65,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
setLoading(true); setLoading(true);
request request
.put(`${config.apiPrefix}user/two-factor/active`, { data: { code } }) .put(`${config.apiPrefix}user/two-factor/active`, { data: { code } })
.then((data: any) => { .then(({ code, data }) => {
if (data.data) { if (code === 200) {
message.success('激活成功'); if (data) {
setTwoFactoring(false); message.success('激活成功');
setTwoFactorActivated(true); setTwoFactoring(false);
userChange(); setTwoFactorActivated(true);
} else { userChange();
message.success('验证失败'); } else {
message.success('验证失败');
}
} }
}) })
.catch((error: any) => { .catch((error: any) => {
@@ -82,8 +86,10 @@ const SecuritySettings = ({ user, userChange }: any) => {
const getTwoFactorInfo = () => { const getTwoFactorInfo = () => {
request request
.get(`${config.apiPrefix}user/two-factor/init`) .get(`${config.apiPrefix}user/two-factor/init`)
.then((data: any) => { .then(({ code, data }) => {
setTwoFactorInfo(data.data); if (code === 200) {
setTwoFactorInfo(data);
}
}) })
.catch((error: any) => { .catch((error: any) => {
console.log(error); console.log(error);

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