Compare commits

...
133 Commits
Author SHA1 Message Date
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
97 changed files with 17122 additions and 2525 deletions
+60 -10
View File
@@ -17,16 +17,47 @@ on:
workflow_dispatch:
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:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: latest
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: build front and back
run: |
npm i -g pnpm
pnpm install
pnpm install --frozen-lockfile
pnpm build:front
pnpm build:back
@@ -34,6 +65,9 @@ jobs:
env:
GITHUB_REPO: github.com/${{ github.repository_owner }}/qinglong-static
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: |
mkdir -p tmp
cd ./tmp
@@ -44,6 +78,16 @@ jobs:
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}
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:
needs: build-static
@@ -55,14 +99,17 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Set time zone
uses: szenius/set-timezone@v1.0
- uses: pnpm/action-setup@v2
with:
timezoneLinux: "Asia/Shanghai"
timezoneMacos: "Asia/Shanghai"
timezoneWindows: "China Standard Time"
version: latest
- uses: actions/setup-node@v3
with:
cache: 'pnpm'
- name: Setup timezone
uses: zcong1993/setup-timezone@master
with:
timezone: Asia/Shanghai
- name: Login to DockerHub
uses: docker/login-action@v2
@@ -100,7 +147,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
@@ -114,3 +161,6 @@ jobs:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
-1
View File
@@ -5,7 +5,6 @@
/npm-debug.log*
/yarn-error.log
/yarn.lock
/pnpm-lock.yaml
/package-lock.json
# production
+10 -6
View File
@@ -1,6 +1,6 @@
<p align="center">
<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>
</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
</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
@@ -41,7 +41,7 @@ Timed task management panel with python3, javaScript, shell, typescript support
### Local Deployment
```bash
# To be completed
# To be refined, see the development steps first (not supported on windows yet)
```
### Podman Deployment
@@ -61,7 +61,6 @@ podman run -dit \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
@@ -161,7 +160,11 @@ task <file_path> now
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
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
@@ -176,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Links
@@ -183,7 +187,7 @@ task <file_path> desi <env_name> <account_number>
- [crontab-ui](https://github.com/alseambusher/crontab-ui)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
+10 -6
View File
@@ -1,6 +1,6 @@
<p align="center">
<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>
</p>
@@ -22,7 +22,7 @@
[docker-image-size-url]: https://hub.docker.com/r/whyour/qinglong
</div>
[![](https://user-images.githubusercontent.com/22700758/161788855-c4e51cb8-d4e9-44fe-bb17-ee1a56c8549b.png)](https://whyour.cn)
[![](https://user-images.githubusercontent.com/22700758/203243067-1a8a570d-b1b4-4837-9f12-d78d83e31f35.jpg)](https://whyour.cn)
简体中文 | [English](./README-en.md)
@@ -41,7 +41,7 @@
### 本机部署
```bash
# 待完善
# 待完善,可先参考开发步骤 (windows暂时不支持)
```
### podman 部署
@@ -61,7 +61,6 @@ podman run -dit \
-p 5700:5700 \
--name qinglong \
--hostname qinglong \
--restart unless-stopped \
docker.io/whyour/qinglong:latest
```
@@ -160,7 +159,11 @@ task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
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. 参数说明
@@ -176,6 +179,7 @@ task <file_path> desi <env_name> <account_number>
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* 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)
- [Ant Design](https://ant.design)
- [Ant Design Pro](https://pro.ant.design/)
- [Umijs3.0](https://umijs.org)
- [Umijs](https://umijs.org)
- [darkreader](https://github.com/darkreader/darkreader)
- [admin-server](https://github.com/sunpu007/admin-server)
+8 -2
View File
@@ -30,6 +30,7 @@ export default (app: Router) => {
name: Joi.string().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -51,13 +52,18 @@ export default (app: Router) => {
id: Joi.number().required(),
sorts: Joi.array().optional().allow(null),
filters: Joi.array().optional(),
filterRelation: Joi.string().optional(),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
try {
const cronViewService = Container.get(CronViewService);
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
if (req.body.type === 1) {
return res.send({ code: 400, message: '参数错误' });
} else {
const data = await cronViewService.update(req.body);
return res.send({ code: 200, data });
}
} catch (e) {
return next(e);
}
+49 -2
View File
@@ -3,8 +3,21 @@ import { Container } from 'typedi';
import EnvService from '../services/env';
import { Logger } from 'winston';
import { celebrate, Joi } from 'celebrate';
import multer from 'multer';
import config from '../config';
import fs from 'fs';
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) => {
app.use('/envs', route);
@@ -26,7 +39,9 @@ export default (app: Router) => {
body: Joi.array().items(
Joi.object({
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(''),
}),
),
@@ -94,7 +109,6 @@ export default (app: Router) => {
}),
}),
async (req: Request<{ id: number }>, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
const envService = Container.get(EnvService);
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 * as fs from 'fs';
import config from '../config';
import { getFileContentByName, readDirs } from '../config/util';
import { emptyDir, getFileContentByName, readDirs } from '../config/util';
import { join } from 'path';
import { celebrate, Joi } from 'celebrate';
const route = Router();
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);
}
},
);
};
+9 -2
View File
@@ -4,6 +4,7 @@ import {
readDirs,
getLastModifyFilePath,
readDir,
emptyDir,
} from '../config/util';
import { Router, Request, Response, NextFunction } from 'express';
import { Container } from 'typedi';
@@ -169,17 +170,23 @@ export default (app: Router) => {
body: Joi.object({
filename: Joi.string().required(),
path: Joi.string().allow(''),
type: Joi.string().optional()
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try {
let { filename, path } = req.body as {
let { filename, path, type } = req.body as {
filename: string;
path: string;
type: string;
};
const filePath = join(config.scriptPath, path, filename);
fs.unlinkSync(filePath);
if (type === 'directory') {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
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),
schedule_type: Joi.string().required(),
alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
}),
}),
async (req: Request, res: Response, next: NextFunction) => {
@@ -177,6 +178,7 @@ export default (app: Router) => {
sub_before: Joi.string().optional().allow('').allow(null),
sub_after: Joi.string().optional().allow('').allow(null),
alias: Joi.string().required(),
proxy: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(),
}),
}),
+19
View File
@@ -7,6 +7,9 @@ import SystemService from '../services/system';
import { celebrate, Joi } from 'celebrate';
import UserService from '../services/user';
import { EnvModel } from '../data/env';
import { promiseExec } from '../config/util';
import dayjs from 'dayjs';
const route = Router();
export default (app: Router) => {
@@ -22,6 +25,19 @@ export default (app: Router) => {
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;
if (
@@ -37,6 +53,9 @@ export default (app: Router) => {
data: {
isInitialized,
version,
lastCommitTime: dayjs(lastCommitTime).unix(),
lastCommitId,
branch,
},
});
} catch (e) {
+1 -1
View File
@@ -17,7 +17,7 @@ async function startServer() {
const server = app
.listen(config.port, () => {
Logger.debug(`✌️ Back server launched on port ${config.port}`);
Logger.debug(`✌️ 后端服务启动成功!`);
})
.on('error', (err) => {
Logger.error(err);
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+2 -2
View File
@@ -11,10 +11,10 @@ if (!process.env.QL_DIR) {
if (qlHomePath.endsWith('/static/')) {
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.ts`;
const rootPath = process.env.QL_DIR as string;
const envFound = dotenv.config({ path: path.join(rootPath, '.env') });
+68 -30
View File
@@ -284,6 +284,20 @@ enum FileType {
'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(
dir: string,
baseDir: string = '',
@@ -302,24 +316,21 @@ export function readDirs(
title: file,
key,
type: 'directory',
disabled: true,
parent: relativePath,
children: readDirs(subPath, baseDir).sort(
(a: any, b: any) =>
(FileType as any)[a.type] - (FileType as any)[b.type],
),
mtime: stats.mtime.getTime(),
children: readDirs(subPath, baseDir).sort(dirSort),
};
}
return {
title: file,
type: 'file',
isLeaf: true,
key,
parent: relativePath,
mtime: stats.mtime.getTime(),
};
});
return result.sort(
(a: any, b: any) => (FileType as any)[a.type] - (FileType as any)[b.type],
);
return result.sort(dirSort);
}
export function readDir(
@@ -345,6 +356,20 @@ export function readDir(
return result;
}
export function emptyDir(path: string) {
const files = fs.readdirSync(path);
files.forEach((file) => {
const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isDirectory()) {
emptyDir(filePath);
} else {
fs.unlinkSync(filePath);
}
});
fs.rmdirSync(path);
}
export function promiseExec(command: string): Promise<string> {
return new Promise((resolve, reject) => {
exec(
@@ -365,22 +390,29 @@ export function parseHeaders(headers: string) {
let val;
let i;
headers && headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
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 '';
const parsed: any = {};
@@ -388,22 +420,28 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
let val;
let i;
body && body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
if (!key || parsed[key]) {
return;
}
parsed[key] = val;
});
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c])
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
@@ -413,4 +451,4 @@ export function parseBody(body: string, contentType: 'application/json' | 'multi
}
return parsed;
};
}
+16 -1
View File
@@ -1,13 +1,19 @@
import { sequelize } from '.';
import { DataTypes, Model } from 'sequelize';
export enum CronViewType {
'系统' = 1,
'个人',
}
interface SortType {
type: 'ASC' | 'DESC';
value: string;
}
interface FilterType {
type: 'or' | 'and';
property: string;
operation: string;
value: string;
}
@@ -18,6 +24,8 @@ export class CrontabView {
isDisabled?: 1 | 0;
filters?: FilterType[];
sorts?: SortType[];
filterRelation?: 'and' | 'or';
type?: CronViewType;
constructor(options: CrontabView) {
this.name = options.name;
@@ -26,6 +34,8 @@ export class CrontabView {
this.isDisabled = options.isDisabled || 0;
this.filters = options.filters;
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,
filters: 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',
}
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 {}
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';
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: `${config.dbPath}database.sqlite`,
logging: false,
pool: {
max: 6,
min: 0,
idle: 30000,
retry: {
max: 10,
match: ['SQLITE_BUSY: database is locked'],
},
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 };
+21 -3
View File
@@ -11,9 +11,11 @@ export enum NotificationMode {
'dingtalkBot' = 'dingtalkBot',
'weWorkBot' = 'weWorkBot',
'weWorkApp' = 'weWorkApp',
'aibotk' = 'aibotk',
'iGot' = 'iGot',
'pushPlus' = 'pushPlus',
'email' = 'email',
'feishu' = 'feishu',
'webhook' = 'webhook',
}
@@ -39,6 +41,7 @@ export class ServerChanNotification extends NotificationBaseInfo {
export class PushDeerNotification extends NotificationBaseInfo {
public pushDeerKey = '';
public pushDeerUrl = '';
}
export class ChatNotification extends NotificationBaseInfo {
@@ -48,7 +51,7 @@ export class ChatNotification extends NotificationBaseInfo {
export class BarkNotification extends NotificationBaseInfo {
public barkPush = '';
public barkIcon = 'http://qn.whyour.cn/logo.png';
public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = '';
public barkGroup = 'qinglong';
}
@@ -75,6 +78,12 @@ export class WeWorkAppNotification extends NotificationBaseInfo {
public weWorkAppKey = '';
}
export class AibotkNotification extends NotificationBaseInfo {
public aibotkKey: string = '';
public aibotkType: 'room' | 'contact' = 'room';
public aibotkName: string = '';
}
export class IGotNotification extends NotificationBaseInfo {
public iGotPushKey = '';
}
@@ -95,7 +104,14 @@ export class WebhookNotification extends NotificationBaseInfo {
public webhookBody: string = '';
public webhookUrl: string = '';
public webhookMethod: 'GET' | 'POST' | 'PUT' = 'GET';
public webhookContentType: 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' = 'application/json';
public webhookContentType:
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded' = 'application/json';
}
export class LarkNotification extends NotificationBaseInfo {
public larkKey = '';
}
export interface NotificationInfo
@@ -109,7 +125,9 @@ export interface NotificationInfo
DingtalkBotNotification,
WeWorkBotNotification,
WeWorkAppNotification,
AibotkNotification,
IGotNotification,
PushPlusNotification,
EmailNotification,
WebhookNotification {}
WebhookNotification,
LarkNotification {}
+3
View File
@@ -28,6 +28,7 @@ export class Subscription {
extensions?: string;
sub_before?: string;
sub_after?: string;
proxy?: string;
constructor(options: Subscription) {
this.id = options.id;
@@ -54,6 +55,7 @@ export class Subscription {
this.extensions = options.extensions;
this.sub_before = options.sub_before;
this.sub_after = options.sub_after;
this.proxy = options.proxy;
}
}
@@ -102,5 +104,6 @@ export const SubscriptionModel = sequelize.define<SubscriptionInstance>(
log_path: DataTypes.STRING,
schedule_type: DataTypes.STRING,
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 { CrontabViewModel } from '../data/cronView';
import config from '../config';
import { sequelize } from '../data';
export default async () => {
try {
@@ -21,12 +22,20 @@ export default async () => {
await SubscriptionModel.sync();
await CrontabViewModel.sync();
// try {
// const queryInterface = sequelize.getQueryInterface();
// await queryInterface.addIndex('Crontabs', ['command'], { unique: true });
// await queryInterface.addIndex('Envs', ['name', 'value'], { unique: true });
// await queryInterface.addIndex('Apps', ['name'], { unique: true });
// } catch (error) { }
// 初始化新增字段
try {
await sequelize.query(
'alter table CrontabViews add column filterRelation VARCHAR(255)',
);
} 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 升级
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 CronService from '../services/cron';
import EnvService from '../services/env';
import _ from 'lodash';
import groupBy from 'lodash/groupBy';
import { DependenceModel } from '../data/dependence';
import { Op } from 'sequelize';
import config from '../config';
import { CrontabViewModel, CronViewType } from '../data/cronView';
import { initPosition } from '../data/env';
export default async () => {
const cronService = Container.get(CronService);
const envService = Container.get(EnvService);
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(
{ status: CrontabStatus.idle },
@@ -26,7 +42,7 @@ export default async () => {
order: [['type', 'DESC']],
raw: true,
}).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));
for (const key of keys) {
const group = groups[key];
+2 -5
View File
@@ -1,5 +1,4 @@
import { Container } from 'typedi';
import _ from 'lodash';
import SystemService from '../services/system';
import ScheduleService from '../services/schedule';
import SubscriptionService from '../services/subscription';
@@ -18,13 +17,12 @@ export default async () => {
tokenCommand = `node ${tokenFile}`;
}
const cron = {
id: 'token',
id: NaN,
name: '生成token',
command: tokenCommand,
};
scheduleService.createIntervalTask(cron as any, {
scheduleService.createIntervalTask(cron, {
days: 28,
runImmediately: true,
});
// 运行删除日志任务
@@ -37,7 +35,6 @@ export default async () => {
};
scheduleService.createIntervalTask(cron, {
days: data.info.frequency,
runImmediately: true,
});
}
+9 -2
View File
@@ -2,16 +2,23 @@ import { Application } from 'express';
import * as Sentry from '@sentry/node';
import * as Tracing from '@sentry/tracing';
import Logger from './logger';
import config from '../config';
import fs from 'fs';
export default ({ expressApp }: { expressApp: Application }) => {
const versionRegx = /.*export const version = \'(.*)\'\;/;
const currentVersionFile = fs.readFileSync(config.versionFile, 'utf8');
const currentVersion = currentVersionFile.match(versionRegx)![1];
Sentry.init({
dsn: 'https://f4b5b55fb3c645b29a5dc2d70a1a4ef4@o1098464.ingest.sentry.io/6122819',
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app: expressApp }),
],
tracesSampleRate: 1.0,
tracesSampleRate: 0.1,
release: currentVersion,
});
expressApp.use(Sentry.Handlers.requestHandler());
+1 -1
View File
@@ -22,7 +22,7 @@ app
await require('./loaders/sentry').default({ expressApp: app });
await require('./loaders/db').default();
Logger.debug(`✌️ Back server launched on port ${config.publicPort}`);
Logger.debug(`✌️ 公共服务启动成功!`);
})
.on('error', (err) => {
Logger.error(err);
+1 -5
View File
@@ -44,11 +44,7 @@ app
await require('./loaders/db').default();
await run();
Logger.info(`
################################################
🛡️ Schedule listening on port: ${config.cronPort} 🛡️
################################################
`);
Logger.debug('定时任务服务启动成功!');
})
.on('error', (err) => {
Logger.error(err);
+9 -7
View File
@@ -11,6 +11,7 @@ import { promisify } from 'util';
import { Op } from 'sequelize';
import path from 'path';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class CronService {
@@ -115,8 +116,9 @@ export default class CronService {
private formatViewQuery(query: any, viewQuery: any) {
if (viewQuery.filters && viewQuery.filters.length > 0) {
if (!query[Op.and]) {
query[Op.and] = [];
const primaryOperate = viewQuery.filterRelation === 'or' ? Op.or : Op.and;
if (!query[primaryOperate]) {
query[primaryOperate] = [];
}
for (const col of viewQuery.filters) {
const { property, value, operation } = col;
@@ -165,7 +167,7 @@ export default class CronService {
],
};
}
query[Op.and].push(q);
query[primaryOperate].push(q);
}
}
}
@@ -324,10 +326,10 @@ export default class CronService {
const endTime = dayjs();
const diffTimeStr = doc.last_execution_time
? `耗时 ${endTime.diff(
? ` 耗时 ${endTime.diff(
dayjs(doc.last_execution_time * 1000),
'second',
)}`
)}`
: '';
if (logFileExist) {
const str = err ? `\n${err}` : '';
@@ -335,7 +337,7 @@ export default class CronService {
`${absolutePath}`,
`${str}\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)}${diffTimeStr}`,
)}${diffTimeStr}${LOG_END_SYMBOL}`,
);
}
}
@@ -420,7 +422,7 @@ export default class CronService {
);
cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data}`);
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
}
});
cp.on('error', (err) => {
+32 -7
View File
@@ -1,14 +1,19 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { CrontabView, CrontabViewModel } from '../data/cronView';
import { initEnvPosition } from '../data/env';
import {
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
@Service()
export default class CronViewService {
constructor(@Inject('logger') private logger: winston.Logger) {}
public async create(payload: CrontabView): Promise<CrontabView> {
let position = initEnvPosition;
let position = initPosition;
const views = await this.list();
if (views && views.length > 0 && views[views.length - 1].position) {
position = views[views.length - 1].position as number;
@@ -16,6 +21,8 @@ export default class CronViewService {
position = position / 2;
const tab = new CrontabView({ ...payload, position });
const doc = await this.insert(tab);
await this.checkPosition(tab.position!);
return doc;
}
@@ -62,6 +69,22 @@ export default class CronViewService {
await CrontabViewModel.update({ isDisabled: 0 }, { where: { id: ids } });
}
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.list();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async move({
id,
fromIndex,
@@ -76,17 +99,19 @@ export default class CronViewService {
const views = await this.list();
if (toIndex === 0 || toIndex === views.length - 1) {
targetPosition = isUpward
? views[0].position * 2
: views[toIndex].position / 2;
? views[0].position! * 2
: views[toIndex].position! / 2;
} else {
targetPosition = isUpward
? (views[toIndex].position + views[toIndex - 1].position) / 2
: (views[toIndex].position + views[toIndex + 1].position) / 2;
? (views[toIndex].position! + views[toIndex - 1].position!) / 2
: (views[toIndex].position! + views[toIndex + 1].position!) / 2;
}
const newDoc = await this.update({
id,
position: targetPosition,
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition);
return newDoc;
}
}
-1
View File
@@ -9,7 +9,6 @@ import {
unInstallDependenceCommandTypes,
DependenceModel,
} from '../data/dependence';
import _ from 'lodash';
import { spawn } from 'child_process';
import SockService from './sock';
import { Op } from 'sequelize';
+53 -28
View File
@@ -1,10 +1,17 @@
import { Service, Inject } from 'typedi';
import winston from 'winston';
import { getFileContentByName } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import { Env, EnvModel, EnvStatus, initEnvPosition } from '../data/env';
import _ from 'lodash';
import {
Env,
EnvModel,
EnvStatus,
initPosition,
maxPosition,
minPosition,
stepPosition,
} from '../data/env';
import groupBy from 'lodash/groupBy';
import { Op } from 'sequelize';
@Service()
@@ -13,17 +20,22 @@ export default class EnvService {
public async create(payloads: Env[]): Promise<Env[]> {
const envs = await this.envs();
let position = initEnvPosition;
if (envs && envs.length > 0 && envs[envs.length - 1].position) {
position = envs[envs.length - 1].position as number;
let position = initPosition;
if (
envs &&
envs.length > 0 &&
typeof envs[envs.length - 1].position === 'number'
) {
position = envs[envs.length - 1].position!;
}
const tabs = payloads.map((x) => {
position = position / 2;
position = position - stepPosition;
const tab = new Env({ ...x, position });
return tab;
});
const docs = await this.insert(tabs);
await this.set_envs();
await this.checkPosition(tabs[tabs.length - 1].position!);
return docs;
}
@@ -67,25 +79,40 @@ export default class EnvService {
const envs = await this.envs();
if (toIndex === 0 || toIndex === envs.length - 1) {
targetPosition = isUpward
? envs[0].position * 2
: envs[toIndex].position / 2;
? envs[0].position! + stepPosition
: envs[toIndex].position! - stepPosition;
} else {
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({
id,
position: targetPosition,
position: this.getPrecisionPosition(targetPosition),
});
await this.checkPosition(targetPosition);
return newDoc;
}
public async envs(
searchText: string = '',
sort: any = { position: -1 },
query: any = {},
): Promise<Env[]> {
private async checkPosition(position: number) {
const precisionPosition = parseFloat(position.toPrecision(16));
if (precisionPosition < minPosition || precisionPosition > maxPosition) {
const envs = await this.envs();
let position = initPosition;
for (const env of envs) {
position = position - stepPosition;
await this.updateDb({ id: env.id, position });
}
}
}
private getPrecisionPosition(position: number): number {
return parseFloat(position.toPrecision(16));
}
public async envs(searchText: string = '', query: any = {}): Promise<Env[]> {
let condition = { ...query };
if (searchText) {
const encodeText = encodeURIComponent(searchText);
@@ -115,7 +142,7 @@ export default class EnvService {
const result = await this.find(condition, [
['status', 'ASC'],
['position', 'DESC'],
['createdAt', 'DESC'],
['createdAt', 'ASC'],
]);
return result as any;
} catch (error) {
@@ -155,22 +182,20 @@ export default class EnvService {
}
public async set_envs() {
const envs = await this.envs(
'',
{ position: -1 },
{ name: { [Op.not]: null } },
);
const groups = _.groupBy(envs, 'name');
const envs = await this.envs('', {
name: { [Op.not]: null },
status: EnvStatus.normal,
});
const groups = groupBy(envs, 'name');
let env_string = '';
for (const key in groups) {
if (Object.prototype.hasOwnProperty.call(groups, key)) {
const group = groups[key];
// 忽略不符合bash要求的环境变量名称
if (/^[a-zA-Z_][0-9a-zA-Z_]+$/.test(key)) {
let value = _(group)
.filter((x) => x.status !== EnvStatus.disabled)
.map('value')
if (/^[a-zA-Z_][0-9a-zA-Z_]*$/.test(key)) {
let value = group
.map((x) => x.value)
.join('&')
.replace(/(\\)[^n]/g, '\\\\')
.replace(/(\\$)/, '\\\\')
+103 -43
View File
@@ -24,18 +24,23 @@ export default class NotificationService {
['dingtalkBot', this.dingtalkBot],
['weWorkBot', this.weWorkBot],
['weWorkApp', this.weWorkApp],
['aibotk', this.aibotk],
['iGot', this.iGot],
['pushPlus', this.pushPlus],
['email', this.email],
['webhook', this.webhook],
['lark', this.lark],
]);
private timeout = 10000;
private title = '';
private content = '';
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(
title: string,
@@ -76,8 +81,7 @@ export default class NotificationService {
const { gotifyUrl, gotifyToken, gotifyPriority } = this.params;
const res: any = await got
.post(`${gotifyUrl}/message?token=${gotifyToken}`, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `title=${encodeURIComponent(
this.title,
)}&message=${encodeURIComponent(
@@ -95,8 +99,7 @@ export default class NotificationService {
const { goCqHttpBotQq, goCqHttpBotToken, goCqHttpBotUrl } = this.params;
const res: any = await got
.post(`${goCqHttpBotUrl}?${goCqHttpBotQq}`, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: { message: `${this.title}\n${this.content}` },
headers: { Authorization: 'Bearer ' + goCqHttpBotToken },
})
@@ -111,8 +114,7 @@ export default class NotificationService {
: `https://sc.ftqq.com/${serverChanKey}.send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `title=${this.title}&desp=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
@@ -121,12 +123,11 @@ export default class NotificationService {
}
private async pushDeer() {
const { pushDeerKey } = this.params;
const url = `https://api2.pushdeer.com/message/push`;
const { pushDeerKey, pushDeerUrl } = this.params;
const url = pushDeerUrl || `https://api2.pushdeer.com/message/push`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `pushkey=${pushDeerKey}&text=${encodeURIComponent(
this.title,
)}&desp=${encodeURIComponent(this.content)}&type=markdown`,
@@ -143,8 +144,7 @@ export default class NotificationService {
const url = `${chatUrl}${chatToken}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `payload={"text":"${this.title}\n${this.content}"}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
@@ -164,8 +164,7 @@ export default class NotificationService {
)}?icon=${barkIcon}?sound=${barkSound}&group=${barkGroup}`;
const res: any = await got
.get(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
.json();
@@ -182,8 +181,9 @@ export default class NotificationService {
telegramBotUserId,
} = this.params;
const authStr = telegramBotProxyAuth ? `${telegramBotProxyAuth}@` : '';
const url = `https://${telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
const url = `https://${
telegramBotApiHost ? telegramBotApiHost : 'api.telegram.org'
}/bot${telegramBotToken}/sendMessage`;
let agent;
if (telegramBotProxyHost && telegramBotProxyPort) {
const options: any = {
@@ -202,8 +202,7 @@ export default class NotificationService {
}
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `chat_id=${telegramBotUserId}&text=${this.title}\n\n${this.content}&disable_web_page_preview=true`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
agent,
@@ -225,8 +224,7 @@ export default class NotificationService {
const url = `https://oapi.dingtalk.com/robot/send?access_token=${dingtalkBotToken}${secretParam}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
msgtype: 'text',
text: {
@@ -243,8 +241,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${weWorkBotKey}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
msgtype: 'text',
text: {
@@ -263,8 +260,7 @@ export default class NotificationService {
const url = `https://qyapi.weixin.qq.com/cgi-bin/gettoken`;
const tokenRes: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
corpid,
corpsecret,
@@ -314,8 +310,7 @@ export default class NotificationService {
.post(
`https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=${tokenRes.access_token}`,
{
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
touser,
agentid,
@@ -329,13 +324,53 @@ export default class NotificationService {
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() {
const { iGotPushKey } = this.params;
const url = `https://push.hellyw.com/${iGotPushKey.toLowerCase()}`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
body: `title=${this.title}&content=${this.content}`,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})
@@ -349,8 +384,7 @@ export default class NotificationService {
const url = `https://www.pushplus.plus/send`;
const res: any = await got
.post(url, {
timeout: this.timeout,
retry: 0,
...this.gotOption,
json: {
token: `${pushPlusToken}`,
title: `${this.title}`,
@@ -363,6 +397,21 @@ export default class NotificationService {
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() {
const { emailPass, emailService, emailUser } = this.params;
const transporter = nodemailer.createTransport({
@@ -386,11 +435,19 @@ export default class NotificationService {
}
private async webhook() {
const { webhookUrl, webhookBody, webhookHeaders, webhookMethod, webhookContentType } =
this.params;
const {
webhookUrl,
webhookBody,
webhookHeaders,
webhookMethod,
webhookContentType,
} = this.params;
const { formatBody, formatUrl } = this.formatNotifyContent(webhookUrl, webhookBody);
if (!formatUrl && !formatBody) {
const { formatBody, formatUrl } = this.formatNotifyContent(
webhookUrl,
webhookBody,
);
if (!formatUrl && !formatBody) {
return false;
}
const headers = parseHeaders(webhookHeaders);
@@ -399,11 +456,10 @@ export default class NotificationService {
const options = {
method: webhookMethod,
headers,
timeout: this.timeout,
retry: 0,
...this.gotOption,
allowGetBody: true,
...bodyParam
}
...bodyParam,
};
const res = await got(formatUrl, options);
return String(res.statusCode).startsWith('20');
}
@@ -427,8 +483,12 @@ export default class NotificationService {
}
return {
formatUrl: url.replaceAll('$title', encodeURIComponent(this.title)).replaceAll('$content', encodeURIComponent(this.content)),
formatBody: body.replaceAll('$title', this.title).replaceAll('$content', this.content),
}
formatUrl: url
.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,
);
let tokens = invalidTokens;
if (invalidTokens.length > 5) {
if (invalidTokens.length >= 5) {
tokens = [
...invalidTokens.slice(0, 4),
{ ...invalidTokens[4], expiration },
+2 -1
View File
@@ -6,6 +6,7 @@ import SockService from './sock';
import CronService from './cron';
import ScheduleService, { TaskCallbacks } from './schedule';
import config from '../config';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class ScriptService {
@@ -55,7 +56,7 @@ export default class ScriptService {
type: 'manuallyRunScript',
message: `${str}\n## 执行结束... ${new Date()
.toLocaleString('zh', { hour12: false })
.replace(' 24:', ' 00:')} `,
.replace(' 24:', ' 00:')}${LOG_END_SYMBOL}`,
});
return { code: 200 };
+19 -6
View File
@@ -38,8 +38,16 @@ export default class SshKeyService {
}
}
private generateSingleSshConfig(alias: string, host: string): string {
return `\nHost ${alias}\n Hostname ${host}\n IdentityFile ${this.sshPath}/${alias}\n StrictHostKeyChecking no`;
private generateSingleSshConfig(
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[]) {
@@ -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);
const config = this.generateSingleSshConfig(alias, host);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(alias);
this.generateSshConfig([config]);
}
public removeSSHKey(alias: string, host: string): void {
public removeSSHKey(alias: string, host: string, proxy?: string): void {
this.removePrivateKeyFile(alias);
const config = this.generateSingleSshConfig(alias, host);
const config = this.generateSingleSshConfig(alias, host, proxy);
this.removeSshConfig(config);
}
}
+17 -5
View File
@@ -29,6 +29,7 @@ import { SimpleIntervalSchedule } from 'toad-scheduler';
import SockService from './sock';
import SshKeyService from './sshKey';
import dayjs from 'dayjs';
import { LOG_END_SYMBOL } from '../config/const';
@Service()
export default class SubscriptionService {
@@ -76,13 +77,21 @@ export default class SubscriptionService {
private formatCommand(doc: Subscription, url?: string) {
let command = 'ql ';
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') {
command += `raw "${_url}"`;
} else {
command += `repo "${_url}" "${whitelist || ''}" "${blacklist || ''}" "${
dependences || ''
}" "${branch || ''}" "${extensions || ''}"`;
}" "${branch || ''}" "${extensions || ''}" "${proxy || ''}"`;
}
return command;
}
@@ -116,9 +125,10 @@ export default class SubscriptionService {
(doc.pull_option as any).private_key,
doc.alias,
host,
doc.proxy,
);
} else {
this.sshKeyService.removeSSHKey(doc.alias, host);
this.sshKeyService.removeSSHKey(doc.alias, host, doc.proxy);
}
}
@@ -232,7 +242,7 @@ export default class SubscriptionService {
absolutePath,
`\n## 执行结束... ${endTime.format(
'YYYY-MM-DD HH:mm:ss',
)} 耗时 ${diff}`,
)} 耗时 ${diff}${LOG_END_SYMBOL}`,
);
await SubscriptionModel.update(
@@ -353,7 +363,9 @@ export default class SubscriptionService {
fs.appendFileSync(
`${absolutePath}`,
`${str}\n## 执行结束... ${dayjs().format('YYYY-MM-DD HH:mm:ss')} `,
`${str}\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
);
}
+7 -11
View File
@@ -2,7 +2,6 @@ import { Service, Inject } from 'typedi';
import winston from 'winston';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
import { NotificationInfo } from '../data/notify';
import NotificationService from './notify';
@@ -70,9 +69,8 @@ export default class SystemService {
};
await this.scheduleService.cancelIntervalTask(cron);
if (frequency > 0) {
await this.scheduleService.createIntervalTask(cron, {
this.scheduleService.createIntervalTask(cron, {
days: frequency,
runImmediately: true,
});
}
return { code: 200, data: { ...cron } };
@@ -89,10 +87,12 @@ export default class SystemService {
let lastVersion = '';
let lastLog = '';
try {
const result = await got.get(config.lastVersionFile, {
timeout: 6000,
retry: 0,
});
const result = await got.get(
`${config.lastVersionFile}?t=${Date.now()}`,
{
timeout: 30000,
},
);
const lastVersionFileContent = result.body;
lastVersion = lastVersionFileContent.match(versionRegx)![1];
lastLog = lastVersionFileContent.match(logRegx)
@@ -138,10 +138,6 @@ export default class SystemService {
public async updateSystem() {
const cp = spawn('ql -l update', { shell: '/bin/bash' });
this.sockService.sendMessage({
type: 'updateSystemVersion',
message: `开始更新系统`,
});
cp.stdout.on('data', (data) => {
this.sockService.sendMessage({
type: 'updateSystemVersion',
+33 -20
View File
@@ -3,7 +3,6 @@ import winston from 'winston';
import { createRandomString, getNetIp, getPlatform } from '../config/util';
import config from '../config';
import * as fs from 'fs';
import _ from 'lodash';
import jwt from 'jsonwebtoken';
import { authenticator } from '@otplib/preset-default';
import { AuthDataType, AuthInfo, AuthModel, LoginStatus } from '../data/auth';
@@ -65,32 +64,37 @@ export default class UserService {
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 {
code: 410,
message: `失败次数过多,请${Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
)}秒后重试`,
data: Math.round(
(Math.pow(3, retries) * 1000 - Date.now() + lastlogon) / 1000,
),
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
}
if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const { ip, address } = await getNetIp(req);
if (username === cUsername && password === cPassword) {
if (twoFactorActivated && needTwoFactor) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '请输入两步验证token',
};
}
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 30 : 3;
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
@@ -155,7 +159,16 @@ export default class UserService {
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 {
return this.initAuthInfo();
+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
pm2 l &>/dev/null
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
patch_version &>/dev/null
echo
echo -e "======================2. 安装依赖========================\n"
@@ -47,13 +42,13 @@ echo -e "定时任务启动成功...\n"
if [[ $AutoStartBot == true ]]; then
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"
fi
if [[ $EnableExtraShell == true ]]; then
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"
fi
+7 -7
View File
@@ -75,9 +75,8 @@
"nedb": "^1.8.0",
"node-schedule": "^2.1.0",
"nodemailer": "^6.7.2",
"p-queue": "7.2.0",
"reflect-metadata": "^0.1.13",
"sequelize": "^6.20.1",
"sequelize": "^6.25.5",
"serve-handler": "^6.1.3",
"sockjs": "^0.3.24",
"sqlite3": "npm:@louislam/sqlite3@^15.0.6",
@@ -91,13 +90,14 @@
"@ant-design/icons": "^4.7.0",
"@ant-design/pro-layout": "^6.33.1",
"@monaco-editor/react": "4.2.1",
"@react-hook/resize-observer": "^1.2.6",
"@sentry/react": "^7.12.1",
"@types/body-parser": "^1.19.2",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.13",
"@types/express-jwt": "^6.0.4",
"@types/jsonwebtoken": "^8.5.8",
"@types/lodash": "^4.14.179",
"@types/lodash": "^4.14.185",
"@types/multer": "^1.4.7",
"@types/nedb": "^1.8.12",
"@types/node": "^17.0.21",
@@ -118,24 +118,24 @@
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0",
"lint-staged": "^12.3.4",
"lint-staged": "^13.0.3",
"nodemon": "^2.0.15",
"prettier": "^2.5.1",
"qiniu": "^7.4.0",
"qrcode.react": "^1.0.1",
"query-string": "^7.1.1",
"rc-tween-one": "^3.0.6",
"react": "18.x",
"react": "18.2.0",
"react-codemirror2": "^7.2.1",
"react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2",
"react-dnd-html5-backend": "^14.0.0",
"react-dom": "18.x",
"react-dom": "18.2.0",
"react-split-pane": "^0.1.92",
"sockjs-client": "^1.6.0",
"ts-node": "^10.6.0",
"tslib": "^2.4.0",
"typescript": "^4.6.2",
"typescript": "4.8.4",
"umi-request": "^1.4.0",
"vh-check": "^2.0.5",
"webpack": "^5.70.0",
+13702
View File
File diff suppressed because it is too large Load Diff
+16 -2
View File
@@ -16,9 +16,14 @@ DefaultCronRule=""
## ql repo命令拉取脚本时需要拉取的文件后缀,直接写文件后缀名即可
RepoFileExtensions="js py"
## 代理地址,支持http/https/socks,例如 http://127.0.0.1:7890
## 代理地址,支持HTTP/SOCK5,例如 http://127.0.0.1:7890
ProxyUrl=""
## 资源告警阙值,默认CPU 80%、内存80%、磁盘90%
CpuWarn=80
MemoryWarn=80
DiskWarn=90
## 设置定时任务执行的超时时间,默认1h,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
CommandTimeoutTime="1h"
@@ -62,7 +67,7 @@ export PUSH_KEY=""
## 下方填写app提供的设备码,例如:https://api.day.app/123 那么此处的设备码就是123
export BARK_PUSH=""
## 下方填写推送图标设置,自定义推送图标(需iOS15或以上)
export BARK_ICON="http://qn.whyour.cn/logo.png"
export BARK_ICON="https://qn.whyour.cn/logo.png"
## 下方填写推送声音设置,例如choo,具体值请在bark-推送铃声-查看所有铃声
export BARK_SOUND=""
## 下方填写推送消息分组,默认为"QingLong"
@@ -146,4 +151,13 @@ export DEER_KEY=""
export CHAT_URL=""
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=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+192 -155
View File
@@ -38,6 +38,7 @@ let SCKEY = '';
//此处填你申请的PushDeer KEY.
//(环境变量名 DEER_KEY)
let PUSHDEER_KEY = '';
let PUSHDEER_URL = '';
// =======================================Synology Chat通知设置区域===========================================
//此处填你申请的CHAT_URL与CHAT_TOKEN
@@ -49,7 +50,7 @@ let CHAT_TOKEN = '';
//此处填你BarkAPP的信息(IP/设备码,例如:https://api.day.app/XXXXXXXX)
let BARK_PUSH = '';
//BARK app推送图标,自定义推送图标(需iOS15或以上)
let BARK_ICON = 'http://qn.whyour.cn/logo.png';
let BARK_ICON = 'https://qn.whyour.cn/logo.png';
//BARK app推送铃声,铃声列表去APP查看复制填写
let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong"
@@ -110,6 +111,20 @@ let PUSH_PLUS_USER = '';
let QQ_SKEY = '';
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 = '';
//==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL;
@@ -137,6 +152,7 @@ if (process.env.PUSH_KEY) {
if (process.env.DEER_KEY) {
PUSHDEER_KEY = process.env.DEER_KEY;
PUSHDEER_URL = process.env.DEER_URL;
}
if (process.env.CHAT_URL) {
@@ -220,6 +236,20 @@ if (process.env.PUSH_PLUS_TOKEN) {
if (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;
}
//==========================云端环境变量的判断与接收=========================
/**
@@ -255,6 +285,8 @@ async function sendNotify(
gotifyNotify(text, desp), //gotify
ChatNotify(text, desp), //synolog chat
PushDeerNotify(text, desp), //PushDeer
aibotkNotify(text, desp), //智能微秘书
fsBotNotify(text, desp), //飞书机器人
]);
}
@@ -295,7 +327,7 @@ function gotifyNotify(text, desp) {
});
}
function gobotNotify(text, desp, time = 2100) {
function gobotNotify(text, desp) {
return new Promise((resolve) => {
if (GOBOT_URL) {
const options = {
@@ -306,38 +338,34 @@ function gobotNotify(text, desp, time = 2100) {
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送go-cqhttp通知调用API失败!!\n');
console.log(err);
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送go-cqhttp通知调用API失败!!\n');
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 {
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 {
console.log(
`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`,
);
}
console.log(`go-cqhttp发送通知消息异常\n${JSON.stringify(data)}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}, time);
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
});
}
function serverNotify(text, desp, time = 2100) {
function serverNotify(text, desp) {
return new Promise((resolve) => {
if (SCKEY) {
//微信server酱推送通知一个\n不会换行,需要两个\n才能换行,故做此替换
@@ -352,33 +380,29 @@ function serverNotify(text, desp, time = 2100) {
},
timeout,
};
setTimeout(() => {
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
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 {
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 {
console.log(
`server酱发送通知消息异常\n${JSON.stringify(data)}`,
);
}
console.log(`server酱发送通知消息异常\n${JSON.stringify(data)}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}, time);
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
@@ -391,42 +415,36 @@ function PushDeerNotify(text, desp) {
// PushDeer 建议对消息内容进行 urlencode
desp = encodeURI(desp);
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`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout,
};
$.post(
options,
(err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('发送通知调用API失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
data = JSON.parse(data);
// 通过返回的result的长度来判断是否成功
if (
data.content.result.length !== undefined &&
data.content.result.length > 0
) {
console.log('PushDeer发送通知消息成功🎉\n');
} else {
console.log(
`PushDeer发送通知消息异常\n${JSON.stringify(data)}`,
);
}
console.log(`PushDeer发送通知消息异常\n${JSON.stringify(data)}`);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
},
time,
);
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
} else {
resolve();
}
@@ -470,83 +488,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 = {}) {
return new Promise((resolve) => {
if (BARK_PUSH) {
@@ -649,7 +590,7 @@ function ddBotNotify(text, desp) {
json: {
msgtype: 'text',
text: {
content: ` ${text}\n\n${desp}`,
content: `${text}\n\n${desp}`,
},
},
headers: {
@@ -716,7 +657,7 @@ function qywxBotNotify(text, desp) {
json: {
msgtype: 'text',
text: {
content: ` ${text}\n\n${desp}`,
content: `${text}\n\n${desp}`,
},
},
headers: {
@@ -983,6 +924,102 @@ 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);
}
});
}
});
}
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();
}
});
}
module.exports = {
sendNotify,
BARK_PUSH,
+43
View File
@@ -61,6 +61,7 @@ push_config = {
'PUSH_KEY': '', # server 酱的 PUSH_KEY,兼容旧版与 Turbo 版
'DEER_KEY': '', # PushDeer 的 PUSHDEER_KEY
'DEER_URL': '', # PushDeer 的 PUSHDEER_URL
'CHAT_URL': '', # synology chat url
'CHAT_TOKEN': '', # synology chat token
@@ -81,6 +82,10 @@ push_config = {
'TG_PROXY_AUTH': '', # tg 代理认证参数
'TG_PROXY_HOST': '', # tg 机器人的 TG_PROXY_HOST
'TG_PROXY_PORT': '', # tg 机器人的 TG_PROXY_PORT
'AIBOTK_KEY': '', # 智能微秘书 个人中心的apikey 文档地址:http://wechat.aibotk.com/docs/about
'AIBOTK_TYPE': '', # 智能微秘书 发送目标 room 或 contact
'AIBOTK_NAME': '', # 智能微秘书 发送群名 或者好友昵称和type要对应好
}
notify_function = []
# fmt: on
@@ -276,6 +281,9 @@ def pushdeer(title: str, content: str) -> None:
print("PushDeer 服务启动")
data = {"text": title, "desp": content, "type": "markdown", "pushkey": push_config.get("DEER_KEY")}
url = 'https://api2.pushdeer.com/message/push'
if push_config.get("DEER_URL"):
url = push_config.get("DEER_URL")
response = requests.post(url, data=data).json()
if len(response.get("content").get("result")) > 0:
@@ -521,6 +529,39 @@ def telegram_bot(title: str, content: str) -> None:
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 one() -> str:
"""
获取一条一言。
@@ -561,6 +602,8 @@ if push_config.get("QYWX_KEY"):
notify_function.append(wecom_bot)
if push_config.get("TG_BOT_TOKEN") and push_config.get("TG_USER_ID"):
notify_function.append(telegram_bot)
if push_config.get("AIBOTK_KEY") and push_config.get("AIBOTK_TYPE") and push_config.get("AIBOTK_NAME"):
notify_function.append(aibotk)
def send(title: str, content: str) -> None:
+160 -160
View File
@@ -1,187 +1,187 @@
#!/usr/bin/env bash
get_token() {
token=$(cat $file_auth_token | jq -r .value)
token=$(cat $file_auth_token | jq -r .value)
}
add_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
else
local schedule=$1
local command=$2
local name=$3
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 添加成功"
else
echo -e "$name -> 添加失败(${message})"
fi
}
update_cron_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local schedule=$(echo "$1" | awk -F ":" '{print $1}')
local command=$(echo "$1" | awk -F ":" '{print $2}')
local name=$(echo "$1" | awk -F ":" '{print $3}')
local id=$(echo "$1" | awk -F ":" '{print $4}')
else
local schedule=$1
local command=$2
local name=$3
local id=$4
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"name\":\"$name\",\"command\":\"$command\",\"schedule\":\"$schedule\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$name -> 更新成功"
else
echo -e "$name -> 更新失败(${message})"
fi
}
update_cron_command_api() {
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
fi
local currentTimeStamp=$(date +%s)
if [[ $# -eq 1 ]]; then
local command=$(echo "$1" | awk -F ":" '{print $1}')
local id=$(echo "$1" | awk -F ":" '{print $2}')
else
local command=$1
local id=$2
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"command\":\"$command\",\"id\":\"$id\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "$command -> 更新成功"
else
echo -e "$command -> 更新失败(${message})"
fi
}
del_cron_api() {
local ids=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
local ids=$1
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons?t=$currentTimeStamp" \
-X 'DELETE' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "[$ids]" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "成功"
else
echo -e "失败(${message})"
fi
}
update_cron() {
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >> $dir_log/$log_path
fi
local ids="$1"
local status="$2"
local pid="${3:-''}"
local logPath="$4"
local lastExecutingTime="${5:-0}"
local runningTime="${6:-0}"
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/crons/status?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"ids\":[$ids],\"status\":\"$status\",\"pid\":\"$pid\",\"log_path\":\"$logPath\",\"last_execution_time\":$lastExecutingTime,\"last_running_time\":$runningTime}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code != 200 ]]; then
echo -e "\n## 更新任务状态失败(${message})\n" >>$dir_log/$log_path
fi
}
notify_api() {
local title=$1
local content=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功"
else
echo -e "通知失败(${message})"
fi
local title=$1
local content=$2
local currentTimeStamp=$(date +%s)
local api=$(
curl -s --noproxy "*" "http://0.0.0.0:5600/open/system/notify?t=$currentTimeStamp" \
-X 'PUT' \
-H "Accept: application/json" \
-H "Authorization: Bearer $token" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.90 Safari/537.36" \
-H "Content-Type: application/json;charset=UTF-8" \
-H "Origin: http://0.0.0.0:5700" \
-H "Referer: http://0.0.0.0:5700/crontab" \
-H "Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" \
--data-raw "{\"title\":\"$title\",\"content\":\"$content\"}" \
--compressed
)
code=$(echo $api | jq -r .code)
message=$(echo $api | jq -r .message)
if [[ $code == 200 ]]; then
echo -e "通知发送成功"
else
echo -e "通知失败(${message})"
fi
}
get_token
+4 -5
View File
@@ -35,11 +35,10 @@ fi
cp -f "$repo_path/jbot/requirements.txt" "$dir_data"
cd $dir_data
cat requirements.txt | while read LREAD
do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
cat requirements.txt | while read LREAD; do
if [[ ! $(pip3 show "${LREAD%%=*}" 2>/dev/null) ]]; then
pip3 --default-timeout=100 install ${LREAD}
fi
done
echo -e "\npython3依赖安装成功...\n"
+8 -3
View File
@@ -87,13 +87,18 @@ check_pm2() {
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() {
echo -e "=====> 开始检测"
npm i -g pnpm
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
pnpm add -g pm2
patch_version
start_public
copy_dep
check_ql
check_nginx
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env bash
## 导入通用变量与函数
dir_shell=$QL_DIR/shell
. $dir_shell/share.sh
. $dir_shell/api.sh
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() {
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() {
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 ]] || [[ $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
}
handle_task_before() {
begin_time=$(format_time "$time_format" "$time")
begin_timestamp=$(format_timestamp "$time_format" "$time")
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
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
. $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))
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time"
echo -e "\n     "
}
## 正常运行单个脚本,$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
@@ -29,4 +29,4 @@ git tag -a "$version" -m "release $version"
echo -e "提交tag"
git push --tags
echo -e "完成发布"
echo -e "完成发布"
+23 -23
View File
@@ -8,35 +8,35 @@ days=$1
## 删除运行js脚本的旧日志
remove_js_log() {
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi
done
local log_full_path_list=$(find $dir_log/ -name "*.log")
local diff_time
for log in $log_full_path_list; do
local log_date=$(echo $log | awk -F "/" '{print $NF}' | cut -c1-10) #文件名比文件属性获得的日期要可靠
if [[ $(date +%s -d $log_date 2>/dev/null) ]]; then
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date +%s) - $(date -j -f "%Y-%m-%d" "$log_date" +%s)))
else
diff_time=$(($(date +%s) - $(date +%s -d "$log_date")))
fi
[[ $diff_time -gt $((${days} * 86400)) ]] && rm -vf $log
fi
done
}
## 删除空文件夹
remove_empty_dir() {
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
cd $dir_log
for dir in $(ls); do
if [[ -d $dir ]] && [[ -z $(ls $dir) ]]; then
rm -rf $dir
fi
done
}
## 运行
if [[ ${days} ]]; then
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
echo -e "查找旧日志文件中...\n"
remove_js_log
remove_empty_dir
echo -e "删除旧日志执行完毕\n"
fi
+319 -259
View File
@@ -50,377 +50,437 @@ list_own_drop=$dir_list_tmp/own_drop.list
## 软连接及其原始文件对应关系
link_name=(
task
ql
task
ql
)
original_name=(
task.sh
update.sh
task.sh
update.sh
)
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 PYTHONUNBUFFERED=1
export NODE_PATH=/usr/local/bin:/usr/local/pnpm-global/5/node_modules:/usr/local/lib/node_modules:/root/.local/share/pnpm/global/5/node_modules
export PYTHONUNBUFFERED=1
}
import_config() {
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
[[ -f $file_config_user ]] && . $file_config_user
[[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
ql_base_url=${QlBaseUrl:-""}
command_timeout_time=${CommandTimeoutTime:-"1h"}
proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"}
current_branch=${QL_BRANCH}
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
if [[ -n "${DefaultCronRule}" ]]; then
default_cron="${DefaultCronRule}"
else
default_cron="$(random_range 0 59) $(random_range 0 23) * * *"
fi
cpu_warn=${CpuWarn:-80}
mem_warn=${MemoryWarn:-80}
disk_warn=${DiskWarn:-90}
}
set_proxy() {
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
local proxy="$1"
if [[ $proxy ]]; then
proxy_url="$proxy"
fi
if [[ $proxy_url ]]; then
export http_proxy="${proxy_url}"
export https_proxy="${proxy_url}"
fi
}
unset_proxy() {
unset http_proxy
unset https_proxy
unset http_proxy
unset https_proxy
}
make_dir() {
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
local dir=$1
if [[ ! -d $dir ]]; then
mkdir -p $dir
fi
}
detect_termux() {
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
if [[ $PATH == *com.termux* ]]; then
is_termux=1
else
is_termux=0
fi
}
detect_macos() {
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
[[ $(uname -s) == Darwin ]] && is_macos=1 || is_macos=0
}
gen_random_num() {
local divi=$1
echo $((${RANDOM} % $divi))
local divi=$1
echo $((${RANDOM} % $divi))
}
link_shell_sub() {
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
local link_path="$1"
local original_path="$2"
if [[ ! -L $link_path ]] || [[ $(readlink -f $link_path) != $original_path ]]; then
rm -f $link_path 2>/dev/null
ln -sf $original_path $link_path
fi
}
link_shell() {
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
if [[ $is_termux -eq 1 ]]; then
local path="/data/data/com.termux/files/usr/bin/"
elif [[ $PATH == */usr/local/bin* ]] && [[ -d /usr/local/bin ]]; then
local path="/usr/local/bin/"
else
local path=""
echo -e "脚本功能受限,请自行添加命令的软连接...\n"
fi
if [[ $path ]]; then
for ((i = 0; i < ${#link_name[*]}; i++)); do
link_shell_sub "$path${link_name[i]}" "$dir_shell/${original_name[i]}"
done
fi
}
define_cmd() {
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_prefix="bash "
fi
local cmd_prefix cmd_suffix
if type task &>/dev/null; then
cmd_suffix=""
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix=""
else
cmd_suffix=".sh"
if [[ -f "$dir_shell/task.sh" ]]; then
cmd_prefix="$dir_shell/"
else
cmd_prefix="bash $dir_shell/"
fi
cmd_prefix="bash "
fi
for ((i = 0; i < ${#link_name[*]}; i++)); do
export cmd_${link_name[i]}="${cmd_prefix}${link_name[i]}${cmd_suffix}"
done
else
cmd_suffix=".sh"
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() {
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
make_dir $dir_static
make_dir $dir_data
make_dir $dir_config
make_dir $dir_log
make_dir $dir_db
make_dir $dir_scripts
make_dir $dir_list_tmp
make_dir $dir_repo
make_dir $dir_raw
make_dir $dir_update_log
make_dir $dir_dep
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -s $file_config_user ]]; then
echo -e "复制一份 $file_config_sample$file_config_user,随后请按注释编辑你的配置文件:$file_config_user\n"
cp -fv $file_config_sample $file_config_user
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_env ]]; then
echo -e "检测到config配置目录下不存在env.sh,创建一个空文件用于初始化...\n"
touch $file_env
echo
fi
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_before ]]; then
echo -e "复制一份 $file_task_sample$file_task_before\n"
cp -fv $file_task_sample $file_task_before
echo
fi
if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_task_after ]]; then
echo -e "复制一份 $file_task_sample$file_task_after\n"
cp -fv $file_task_sample $file_task_after
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -f $file_extra_shell ]]; then
echo -e "复制一份 $file_extra_sample$file_extra_shell\n"
cp -fv $file_extra_sample $file_extra_shell
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ ! -s $file_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$file_notify_js\n"
cp -fv $file_notify_js_sample $file_notify_js
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ -s /etc/nginx/conf.d/default.conf ]]; then
echo -e "检测到默认nginx配置文件,清空...\n"
cat /dev/null >/etc/nginx/conf.d/default.conf
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_js ]]; then
echo -e "复制一份 $file_notify_js_sample$dep_notify_js\n"
cp -fv $file_notify_js_sample $dep_notify_js
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
if [[ ! -s $dep_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$dep_notify_py\n"
cp -fv $file_notify_py_sample $dep_notify_py
echo
fi
}
npm_install_sub() {
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production
else
pnpm install --loglevel error --production
fi
if [ $is_termux -eq 1 ]; then
npm install --production --no-bin-links
elif ! type pnpm &>/dev/null; then
npm install --production
else
pnpm install --loglevel error --production
fi
}
npm_install_1() {
local dir_current=$(pwd)
local dir_work=$1
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
cd $dir_work
echo -e "运行 npm install...\n"
npm_install_sub
[[ $? -ne 0 ]] && echo -e "\nnpm install 运行不成功,请进入 $dir_work 目录后手动运行 npm install...\n"
cd $dir_current
}
npm_install_2() {
local dir_current=$(pwd)
local dir_work=$1
local dir_current=$(pwd)
local dir_work=$1
cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
cd $dir_work
echo -e "检测到 $dir_work 的依赖包有变化,运行 npm install...\n"
npm_install_sub
if [[ $? -ne 0 ]]; then
echo -e "\n安装 $dir_work 的依赖包运行不成功,再次尝试一遍...\n"
npm_install_1 $dir_work
fi
cd $dir_current
}
diff_and_copy() {
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
local copy_source=$1
local copy_to=$2
if [[ ! -s $copy_to ]] || [[ $(diff $copy_source $copy_to) ]]; then
cp -f $copy_source $copy_to
fi
}
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
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts
fi
cd $dir_current
cd $dir_current
}
git_clone_scripts() {
local url=$1
local dir=$2
local branch=$3
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
local url="$1"
local dir="$2"
local branch="$3"
local proxy="$4"
[[ $branch ]] && local part_cmd="-b $branch "
echo -e "开始克隆仓库 $url$dir\n"
set_proxy
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
set_proxy "$proxy"
git clone $part_cmd $url $dir
exit_status=$?
unset_proxy
}
git_pull_scripts() {
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
local dir_current=$(pwd)
local dir_work="$1"
local branch="$2"
local proxy="$3"
cd $dir_work
echo -e "开始更新仓库:$dir_work\n"
set_proxy
git fetch --all
exit_status=$?
git pull &>/dev/null
unset_proxy
reset_branch "$branch"
set_proxy "$proxy"
git fetch --all
exit_status=$?
git pull &>/dev/null
unset_proxy
cd $dir_current
cd $dir_current
}
reset_romote_url() {
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
local dir_current=$(pwd)
local dir_work=$1
local url=$2
local branch="$3"
if [[ -d "$dir_work/.git" ]]; then
cd $dir_work
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
local part_cmd=""
reset_branch "$branch"
cd $dir_current
fi
cd $dir_work
if [[ -d "$dir_work/.git" ]]; then
[[ -f ".git/index.lock" ]] && rm -f .git/index.lock >/dev/null
git remote set-url origin $url &>/dev/null
else
git init
git remote add origin $url &>/dev/null
fi
reset_branch "$branch"
cd $dir_current
}
reset_branch() {
local branch="$1"
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch"
git branch --set-upstream-to=$part_cmd $branch
fi
git reset --hard $part_cmd &>/dev/null
local branch="$1"
local part_cmd=""
if [[ $branch ]]; then
part_cmd="origin/${branch}"
git checkout -B "$branch" &>/dev/null
git branch --set-upstream-to=$part_cmd $branch &>/dev/null
fi
git reset --hard $part_cmd &>/dev/null
}
random_range() {
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
local beg=$1
local end=$2
echo $((RANDOM % ($end - $beg) + $beg))
}
reload_pm2() {
pm2 l &>/dev/null
pm2 l &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动面板服务\n"
pm2 delete panel --source-map-support --time &>/dev/null
pm2 start $dir_static/build/app.js -n panel --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
echo -e "启动公开服务\n"
pm2 delete public --source-map-support --time &>/dev/null
pm2 start $dir_static/build/public.js -n public --source-map-support --time &>/dev/null
echo -e "启动定时任务服务\n"
pm2 delete schedule --source-map-support --time &>/dev/null
pm2 start $dir_static/build/schedule.js -n schedule --source-map-support --time &>/dev/null
}
diff_time() {
local format="$1"
local begin_time="$2"
local end_time="$3"
local format="$1"
local begin_time="$2"
local end_time="$3"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
if [[ $is_macos -eq 1 ]]; then
diff_time=$(($(date -j -f "$format" "$end_time" +%s) - $(date -j -f "$format" "$begin_time" +%s)))
else
diff_time=$(($(date +%s -d "$end_time") - $(date +%s -d "$begin_time")))
fi
echo "$diff_time"
}
format_time() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d %H:%M:%S")
else
echo $(date -d "$time" "+%Y-%m-%d %H:%M:%S")
fi
}
format_log_time() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%Y-%m-%d-%H-%M-%S")
else
echo $(date -d "$time" "+%Y-%m-%d-%H-%M-%S")
fi
}
format_timestamp() {
local format="$1"
local time="$2"
local format="$1"
local time="$2"
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
fi
if [[ $is_macos -eq 1 ]]; then
echo $(date -j -f "$format" "$time" "+%s")
else
echo $(date -d "$time" "+%s")
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
@@ -429,4 +489,4 @@ detect_macos
define_cmd
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
define_program() {
local file_param=$1
if [[ $file_param == *.js ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="bash"
elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only"
else
which_program=""
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 &
local file_param=$1
if [[ $file_param == *.js ]]; then
which_program="node"
elif [[ $file_param == *.py ]] || [[ $file_param == *.pyc ]]; then
which_program="python3"
elif [[ $file_param == *.sh ]]; then
which_program="bash"
elif [[ $file_param == *.ts ]]; then
which_program="ts-node-transpile-only"
else
which_program=""
fi
}
handle_log_path() {
define_program "$file_param"
local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
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"
}
local file_param=$1
## 正常运行单个脚本,$1:传入参数
run_normal() {
local file_param=$1
if [[ $# -eq 1 ]]; then
random_delay "$file_param"
fi
if [[ -z $file_param ]];then
file_param="task"
fi
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
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
local suffix=""
if [[ ! -z $ID ]]; then
suffix="_${ID}"
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
run_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"
}
main "$@"
format_params() {
time_format="%Y-%m-%d %H:%M:%S"
timeoutCmd=""
if type timeout &>/dev/null; then
timeoutCmd="timeout -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
+423 -445
View File
@@ -8,554 +8,532 @@ send_mark=$dir_shell/send_mark
## 检测cron的差异,$1:脚本清单文件路径,$2:cron任务清单文件路径,$3:增加任务清单文件路径,$4:删除任务清单文件路径
diff_cron() {
local list_scripts="$1"
local list_task="$2"
local list_add="$3"
local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop
fi
local list_scripts="$1"
local list_task="$2"
local list_add="$3"
local list_drop="$4"
if [[ -s $list_task ]] && [[ -s $list_scripts ]]; then
grep -vwf $list_task $list_scripts >$list_add
grep -vwf $list_scripts $list_task >$list_drop
fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add
fi
if [[ ! -s $list_task ]] && [[ -s $list_scripts ]]; then
cp -f $list_scripts $list_add
fi
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
cp -f $list_task $list_drop
fi
if [[ ! -s $list_scripts ]] && [[ -s $list_task ]]; then
cp -f $list_task $list_drop
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|")
## 识别出两个文件的版本号
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
## 删除旧的发送记录文件
[[ -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}')
## 识别出更新日期和更新内容
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
## 如果是今天,并且版本号不一致,则发送通知
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:新/失效
output_list_add_drop() {
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
cat $list
echo
fi
local list=$1
local type=$2
if [[ -s $list ]]; then
echo -e "检测到有$type的定时任务\n"
cat $list
echo
fi
}
## 自动删除失效的脚本与定时任务,需要:1.AutoDelCron 设置为 true2.正常更新js脚本,没有报错;3.存在失效任务
## $1:失效任务清单文件路径
del_cron() {
local list_drop=$1
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
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}')
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
local list_drop=$1
local path=$2
local detail=""
local ids=""
echo -e "开始尝试自动删除失效的定时任务...\n"
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}')
if [[ $ids ]]; then
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
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
result=$(del_cron_api "$ids")
notify_api "$path 删除任务${result}" "$detail"
fi
}
## 自动增加定时任务,需要:1.AutoAddCron 设置为 true2.正常更新js脚本,没有报错;3.存在新任务;4.crontab.list存在并且不为空
## $1:新任务清单文件路径
add_cron() {
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$(
perl -ne "{
local list_add=$1
local path=$2
echo -e "开始尝试自动添加定时任务...\n"
local detail=""
cd $dir_scripts
for file in $(cat $list_add); do
local file_name=${file/${path}\//}
file_name=${file_name/${path}\_/}
if [[ -f $file ]]; then
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$file_name/
}" $file |
perl -pe "{
perl -pe "{
s|[^\d\*]*(([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*])( \|,\|\").*/?$file_name.*|\1|g;
s|\*([\d\*])(.*)|\1\2|g;
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $file | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $file | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $file | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result"
if [[ $detail ]]; then
detail="${detail}${result}\n"
else
detail="${result}\n"
fi
fi
done
notify_api "$path 新增任务" "$detail"
)
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_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="$default_cron"
result=$(add_cron_api "$cron_line:$cmd_task $file:$cron_name")
echo -e "$result"
if [[ $detail ]]; then
detail="${detail}${result}\n"
else
detail="${result}\n"
fi
fi
done
notify_api "$path 新增任务" "$detail"
}
## 更新仓库
update_repo() {
local url="$1"
local path="$2"
local blackword="$3"
local dependence="$4"
local branch="$5"
local extensions="$6"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local url="$1"
local path="$2"
local blackword="$3"
local dependence="$4"
local branch="$5"
local extensions="$6"
local proxy="$7"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
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"
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}"
else
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
local formatUrl="$url"
if [[ -d ${repo_path}/.git ]]; then
reset_romote_url ${repo_path} "${formatUrl}" "${branch}"
git_pull_scripts ${repo_path} "${branch}" "${proxy}"
else
rm -rf ${repo_path} &>/dev/null
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions"
else
echo -e "\n更新${repo_path}失败,请检查网络...\n"
fi
}
## 更新所有 raw 文件
update_raw() {
echo -e "--------------------------------------------------------------\n"
local url="$1"
local raw_url="$url"
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
echo -e "--------------------------------------------------------------\n"
local url="$1"
local raw_url="$url"
local suffix="${raw_url##*.}"
local raw_file_name="${uniq_path}.${suffix}"
echo -e "开始下载:${raw_url} \n\n保存路径:$dir_raw/${raw_file_name}\n"
set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
set_proxy
wget -q --no-check-certificate -O "$dir_raw/${raw_file_name}.new" ${raw_url}
unset_proxy
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw
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}')
cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$(
perl -ne "{
if [[ $? -eq 0 ]]; then
mv "$dir_raw/${raw_file_name}.new" "$dir_raw/${raw_file_name}"
echo -e "下载 ${raw_file_name} 成功...\n"
cd $dir_raw
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}')
cp -f $raw_file_name $dir_scripts/${filename}
cron_line=$(
perl -ne "{
print if /.*([\d\*]*[\*-\/,\d]*[\d\*] ){4,5}[\d\*]*[\*-\/,\d]*[\d\*]( |,|\").*$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\*])(.*)|\1\2|g;
s| | |g;
}" | sort -u | head -1
)
cron_name=$(grep "new Env" $raw_file_name | awk -F "\(" '{print $2}' | awk -F "\)" '{print $1}' | sed 's:^.\(.*\).$:\1:' | head -1)
[[ -z $cron_name ]] && cron_name="$raw_file_name"
[[ -z $cron_line ]] && cron_line=$(grep "cron:" $raw_file_name | awk -F ":" '{print $2}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line=$(grep "cron " $raw_file_name | awk -F "cron \"" '{print $2}' | awk -F "\" " '{print $1}' | head -1 | xargs)
[[ -z $cron_line ]] && cron_line="$default_cron"
if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n"
notify_api "新增任务通知" "\n$result"
# 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"
)
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_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="$default_cron"
if [[ -z $cron_id ]]; then
result=$(add_cron_api "$cron_line:$cmd_task $filename:$cron_name")
echo -e "$result\n"
notify_api "新增任务通知" "\n$result"
# 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
}
## 调用用户自定义的extra.sh
run_extra_shell() {
if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n"
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
if [[ ${EnableExtraShell} == true ]]; then
if [[ -f $file_extra_shell ]]; then
echo -e "--------------------------------------------------------------\n"
. $file_extra_shell
else
echo -e "$file_extra_shell文件不存在,跳过执行...\n"
fi
fi
}
## 脚本用法
usage() {
echo -e "本脚本用法:"
echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
echo -e "ql命令使用方法:"
echo -e "1. $cmd_update update # 更新并重启青龙"
echo -e "2. $cmd_update extra # 运行自定义脚本"
echo -e "3. $cmd_update raw <fileurl> # 更新单个脚本文件"
echo -e "4. $cmd_update repo <repourl> <path> <blacklist> <dependence> <branch> <extensions> # 更新单个仓库的脚本"
echo -e "5. $cmd_update rmlog <days> # 删除旧日志"
echo -e "6. $cmd_update bot # 启动tg-bot"
echo -e "7. $cmd_update check # 检测青龙环境并修复"
echo -e "8. $cmd_update resetlet # 重置登录错误次数"
echo -e "9. $cmd_update resettfa # 禁用两步登录"
}
## 更新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 "\n使用 ${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"
local all_branch=$(git branch -a)
local primary_branch="master"
if [[ "${all_branch}" =~ "${current_branch}" ]]; then
primary_branch="${current_branch}"
fi
[[ -f $dir_root/package.json ]] && ql_depend_old=$(cat $dir_root/package.json)
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
echo -e "\n更新青龙源文件成功...\n"
cp -f $file_config_sample $dir_config/config.sample.sh
detect_config_version
update_depend
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n"
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
[[ -f $dir_root/package.json ]] && ql_depend_new=$(cat $dir_root/package.json)
[[ "$ql_depend_old" != "$ql_depend_new" ]] && npm_install_2 $dir_root
update_qinglong_static "$1" "$primary_branch"
else
echo -e "\n更新青龙源文件失败,请检查网络...\n"
fi
}
patch_version() {
# 兼容pnpm@7
pnpm setup &>/dev/null
source ~/.bashrc
pnpm install -g &>/dev/null
update_qinglong_static() {
local no_restart="$1"
local primary_branch="$2"
local url="https://${mirror}.com/whyour/qinglong-static.git"
if [[ -d ${ql_static_repo}/.git ]]; then
reset_romote_url ${ql_static_repo} ${url} ${primary_branch}
git_pull_scripts ${ql_static_repo} ${primary_branch}
else
rm -rf ${ql_static_repo} &>/dev/null
git_clone_scripts ${url} ${ql_static_repo} ${primary_branch}
fi
if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n"
local static_version=$(cat $dir_root/src/version.ts | perl -pe "s|.*\'(.*)\';\.*|\1|" | head -1)
echo -e "\n当前版本 $static_version...\n"
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
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
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
if [[ $PipMirror ]]; then
pip3 config set global.index-url $PipMirror
fi
if [[ $NpmMirror ]]; then
npm config set registry $NpmMirror
fi
else
echo -e "\n更新青龙静态资源失败,请检查网络...\n"
fi
}
## 对比脚本
diff_scripts() {
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
local extensions="$6"
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
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_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
local list_add="$dir_list_tmp/${uniq_path}_add.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
if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path
fi
if [[ -s $list_drop ]]; then
output_list_add_drop $list_drop "失效"
if [[ ${AutoDelCron} == true ]]; then
del_cron $list_drop $uniq_path
fi
if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then
add_cron $list_add $uniq_path
fi
fi
if [[ -s $list_add ]]; then
output_list_add_drop $list_add "新"
if [[ ${AutoAddCron} == true ]]; then
add_cron $list_add $uniq_path
fi
cd $dir_current
fi
cd $dir_current
}
## 生成脚本的路径清单文件
gen_list_repo() {
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
local dependence="$5"
local dir_current=$(pwd)
local repo_path="$1"
local author="$2"
local path="$3"
local blackword="$4"
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}
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
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
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
fi
cp -f $file_notify_js "${dir_scripts}/${uniq_path}"
cp -f $file_notify_py "${dir_scripts}/${uniq_path}"
if [[ -d $dir_dep ]]; then
cp -rf $dir_dep/* "${dir_scripts}/${uniq_path}" &>/dev/null
fi
if [[ $dependence ]]; then
cd ${repo_path}
results=$(eval $cmd | sed 's/^..//' | egrep $dependence)
for _file in ${results}; do
file_path=$(dirname $_file)
make_dir "${dir_scripts}/${uniq_path}/${file_path}"
cp -f $_file "${dir_scripts}/${uniq_path}/${file_path}"
done
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
if [[ -d $dir_dep ]]; then
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
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() {
local url="$1"
local branch="$2"
local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
local url="$1"
local branch="$2"
local urlTmp="${url%*/}"
local repoTmp="${urlTmp##*/}"
local repo="${repoTmp%.*}"
local tmp="${url%/*}"
local authorTmp1="${tmp##*/}"
local authorTmp2="${authorTmp1##*:}"
local author="${authorTmp2##*.}"
uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
uniq_path="${author}_${repo}"
[[ $branch ]] && uniq_path="${uniq_path}_${branch}"
}
main() {
## for ql update
show_log="false"
while getopts ":l" opt
do
case $opt in
l)
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
;;
## for ql update
show_log="false"
while getopts ":l" opt; do
case $opt in
l)
show_log="true"
;;
esac
local end_time=$(date '+%Y-%m-%d %H:%M:%S')
local diff_time=$(diff_time "%Y-%m-%d %H:%M:%S" "$begin_time" "$end_time")
if [[ $p1 != "repo" ]] && [[ $p1 != "raw" ]]; then
echo -e "\n## 执行结束... $end_time 耗时 $diff_time" >>$log_path
cat $log_path
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_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
;;
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 "$@"
+1 -1
View File
@@ -1,7 +1,7 @@
import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
scriptUrl: ['//at.alicdn.com/t/font_3354854_ds8pa06q1qa.js'],
scriptUrl: ['//at.alicdn.com/t/c/font_3354854_z0d9rbri1ci.js'],
});
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
}
+6 -6
View File
@@ -68,18 +68,18 @@ export default {
icon: <IconFont type="ql-icon-dependence" />,
component: '@/pages/dependence/index',
},
{
path: '/log',
name: '日志管理',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/diff',
name: '对比工具',
icon: <IconFont type="ql-icon-diff" />,
component: '@/pages/diff/index',
},
{
path: '/log',
name: '任务日志',
icon: <IconFont type="ql-icon-log" />,
component: '@/pages/log/index',
},
{
path: '/setting',
name: '系统设置',
+31 -28
View File
@@ -10,8 +10,8 @@
}
.ant-modal-body {
max-height: calc(85vh - 110px);
max-height: calc(85vh - var(--vh-offset, 110px));
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
overflow-y: auto;
}
@@ -161,7 +161,7 @@
&.env-wrapper,
&.config-wrapper {
.CodeMirror {
width: calc(100vw - 80px);
width: calc(100vw - 24px);
}
}
}
@@ -179,8 +179,8 @@
.Resizer {
background: @component-background;
opacity: 0.2;
z-index: 1;
opacity: 0.8;
z-index: 100;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
@@ -243,31 +243,29 @@
font-size: 13px;
}
[data-dark='true'] {
/* Change autocomplete styles in WebKit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
border: 1px solid @border-color-base;
box-shadow: none;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color;
caret-color: #e8e6f3 !important;
}
/* Change autocomplete styles in WebKit */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill,
textarea:-webkit-autofill:hover,
textarea:-webkit-autofill:focus,
select:-webkit-autofill,
select:-webkit-autofill:hover,
select:-webkit-autofill:focus {
box-shadow: none;
transition: background-color 5000s ease-in-out 0s;
-webkit-text-fill-color: @text-color;
caret-color: @text-color;
color: @text-color;
}
::placeholder {
opacity: 0.5 !important;
}
::placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
.ant-select-selection-placeholder {
opacity: 0.5 !important;
}
.ant-pro-basicLayout-content {
@@ -345,3 +343,8 @@
width: auto !important;
}
}
pre {
word-break: break-all !important;
white-space: break-spaces !important;
}
+50 -19
View File
@@ -15,11 +15,25 @@ import './index.less';
import vhCheck from 'vh-check';
import { version, changeLogLink, changeLog } from '../version';
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,
} from 'antd';
// @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react';
import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export interface SharedContext {
headerStyle: React.CSSProperties;
@@ -29,6 +43,15 @@ export interface SharedContext {
reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo;
}
interface TSystemInfo {
branch: 'develop' | 'master';
isInitialized: boolean;
lastCommitId: string;
lastCommitTime: number;
version: string;
}
export default function () {
@@ -37,10 +60,11 @@ export default function () {
const { theme, reloadTheme } = useTheme();
const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<{ isInitialized: boolean }>();
const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true);
const {
enable: enableDarkMode,
disable: disableDarkMode,
@@ -64,17 +88,15 @@ export default function () {
setSystemInfo(data);
if (!data.isInitialized) {
history.push('/initialization');
setLoading(false);
} else {
getUser();
}
} else {
message.error(data);
}
})
.catch((error) => {
console.log(error);
});
})
.finally(() => setInitLoading(false));
};
const getUser = (needLoading = true) => {
@@ -87,8 +109,6 @@ export default function () {
if (location.pathname === '/') {
history.push('/crontab');
}
} else {
message.error(data);
}
needLoading && setLoading(false);
})
@@ -196,6 +216,10 @@ export default function () {
};
}, []);
if (initLoading) {
return <PageLoading />;
}
if (['/login', '/initialization', '/error'].includes(location.pathname)) {
document.title = `${
(config.documentTitleMap as any)[location.pathname]
@@ -240,7 +264,7 @@ export default function () {
selectedKeys={[location.pathname]}
loading={loading}
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" />}
title={
<>
<span style={{ fontSize: 16 }}></span>
@@ -252,17 +276,23 @@ export default function () {
e.stopPropagation();
}}
>
<span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
<Tooltip
title={systemInfo?.branch === 'develop' ? '开发版' : '正式版'}
>
v{version}
</span>
<Badge size="small" dot={systemInfo?.branch === 'develop'}>
<span
style={{
fontSize: isFirefox ? 9 : 12,
color: '#666',
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
v{version}
</span>
</Badge>
</Tooltip>
</a>
</>
}
@@ -338,6 +368,7 @@ export default function () {
reloadUser,
reloadTheme,
socketMessage,
systemInfo,
}}
/>
</ProLayout>
+13 -6
View File
@@ -25,8 +25,10 @@ const Config = () => {
const [confirmLoading, setConfirmLoading] = useState(false);
const getConfig = (name: string) => {
request.get(`${config.apiPrefix}configs/${name}`).then((data: any) => {
setValue(data.data);
request.get(`${config.apiPrefix}configs/${name}`).then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
@@ -34,8 +36,10 @@ const Config = () => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.then((data: any) => {
setData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
@@ -50,8 +54,10 @@ const Config = () => {
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: select },
})
.then((data: any) => {
message.success(data.message);
.then(({ code, data }) => {
if (code === 200) {
message.success('保存成功');
}
setConfirmLoading(false);
});
};
@@ -74,6 +80,7 @@ const Config = () => {
loading={loading}
extra={[
<TreeSelect
treeExpandAction="click"
className="config-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+22 -29
View File
@@ -103,7 +103,6 @@ const CronDetailModal = ({
fontSize: 12,
lineNumbersMinChars: 3,
fontFamily: 'Source Code Pro',
folding: false,
glyphMargin: false,
wordWrap: 'on',
}}
@@ -123,9 +122,11 @@ const CronDetailModal = ({
.get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
)
.then((data) => {
setLog(data.data);
setIsLogModalVisible(true);
.then(({ code, data }) => {
if (code === 200) {
setLog(data);
setIsLogModalVisible(true);
}
});
};
@@ -137,9 +138,9 @@ const CronDetailModal = ({
setLoading(true);
request
.get(`${config.apiPrefix}crons/${cron.id}/logs`)
.then((data: any) => {
if (data.code === 200) {
setLogs(data.data);
.then(({ code, data }) => {
if (code === 200) {
setLogs(data);
}
})
.finally(() => setLoading(false));
@@ -165,8 +166,10 @@ const CronDetailModal = ({
setScriptInfo({ parent: p, filename: s });
request
.get(`${config.apiPrefix}scripts/${s}?path=${p || ''}`)
.then((data) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
} else {
setValidTabs([validTabs[0]]);
@@ -198,12 +201,10 @@ const CronDetailModal = ({
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setValue(content);
message.success(`保存成功`);
} else {
message.error(_data);
}
resolve(null);
})
@@ -231,14 +232,12 @@ const CronDetailModal = ({
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [currentCron.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.running });
setTimeout(() => {
getLogs();
}, 1000);
} else {
message.error(data);
}
});
},
@@ -263,11 +262,9 @@ const CronDetailModal = ({
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [currentCron.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({ ...currentCron, status: CrontabStatus.idle });
} else {
message.error(data);
}
});
},
@@ -300,14 +297,12 @@ const CronDetailModal = ({
data: [currentCron.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({
...currentCron,
isDisabled: currentCron.isDisabled === 1 ? 0 : 1,
});
} else {
message.error(data);
}
});
},
@@ -340,14 +335,12 @@ const CronDetailModal = ({
data: [currentCron.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setCurrentCron({
...currentCron,
isPinned: currentCron.isPinned === 1 ? 0 : 1,
});
} else {
message.error(data);
}
});
},
+8 -5
View File
@@ -4,13 +4,10 @@
.crontab-detail {
.card-wrapper {
height: 80vh;
height: calc(80vh - var(--vh-offset, 0px));
.ant-card:last-child {
.ant-card-body {
height: calc(80vh - 238px);
height: calc(80vh - var(--vh-offset, 0px) - 238px);
height: calc(80vh - 367px);
height: calc(80vh - var(--vh-offset, 0px) - 367px);
overflow-y: auto;
}
}
@@ -176,3 +173,9 @@ tr.drop-over-upward td {
padding-top: 10px;
}
}
.view-filters-container.active {
.filter-item > div > .ant-form-item-control {
margin-left: 40px;
}
}
+146 -175
View File
@@ -1,4 +1,4 @@
import React, { PureComponent, Fragment, useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Button,
message,
@@ -43,14 +43,13 @@ import CronLogModal from './logModal';
import CronDetailModal from './detail';
import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date';
import { getTableScroll } from '@/utils/index';
import { history, useOutletContext } from '@umijs/max';
import './index.less';
import ViewCreateModal from './viewCreateModal';
import ViewManageModal from './viewManageModal';
import pagination from 'antd/lib/pagination';
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -376,7 +375,6 @@ const Crontab = () => {
filters: any;
}>({} as any);
const [viewConf, setViewConf] = useState<any>();
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [isDetailModalVisible, setIsDetailModalVisible] = useState(false);
const [detailCron, setDetailCron] = useState<any>();
const [searchValue, setSearchValue] = useState('');
@@ -388,6 +386,8 @@ const Crontab = () => {
const [cronViews, setCronViews] = useState<any[]>([]);
const [enabledCronViews, setEnabledCronViews] = useState<any[]>([]);
const [moreMenuActive, setMoreMenuActive] = useState(false);
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef);
const goToScriptManager = (record: any) => {
const cmd = record.command.split(' ') as string[];
@@ -429,24 +429,27 @@ const Crontab = () => {
url += `&queryString=${JSON.stringify({
filters: viewConf.filters,
sorts: viewConf.sorts,
filterRelation: viewConf.filterRelation || 'and',
})}`;
}
request
.get(url)
.then((_data: any) => {
const { data, total } = _data.data;
setValue(
data.map((x) => {
return {
...x,
nextRunTime: cron_parser
.parseExpression(x.schedule)
.next()
.toDate(),
};
}),
);
setTotal(total);
.then(({ code, data: _data }) => {
if (code === 200) {
const { data, total } = _data;
setValue(
data.map((x) => {
return {
...x,
nextRunTime: cron_parser
.parseExpression(x.schedule)
.next()
.toDate(),
};
}),
);
setTotal(total);
}
})
.finally(() => setLoading(false));
};
@@ -476,8 +479,8 @@ const Crontab = () => {
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -485,8 +488,6 @@ const Crontab = () => {
result.splice(i, 1);
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -511,8 +512,8 @@ const Crontab = () => {
onOk() {
request
.put(`${config.apiPrefix}crons/run`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -522,8 +523,6 @@ const Crontab = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -548,8 +547,8 @@ const Crontab = () => {
onOk() {
request
.put(`${config.apiPrefix}crons/stop`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -560,8 +559,6 @@ const Crontab = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -594,8 +591,8 @@ const Crontab = () => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newStatus = record.isDisabled === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -606,8 +603,6 @@ const Crontab = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -640,8 +635,8 @@ const Crontab = () => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newStatus = record.isPinned === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -652,8 +647,6 @@ const Crontab = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -755,19 +748,21 @@ const Crontab = () => {
const getCronDetail = (cron: any) => {
request
.get(`${config.apiPrefix}crons/${cron.id}`)
.then((data: any) => {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
data.data.nextRunTime = cron_parser
.parseExpression(data.data.schedule)
.next()
.toDate();
if (index !== -1) {
result.splice(index, 1, {
...cron,
...data.data,
});
setValue(result);
.then(({ code, data }) => {
if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id);
const result = [...value];
data.nextRunTime = cron_parser
.parseExpression(data.schedule)
.next()
.toDate();
if (index !== -1) {
result.splice(index, 1, {
...cron,
...data,
});
setValue(result);
}
}
})
.finally(() => setLoading(false));
@@ -775,16 +770,10 @@ const Crontab = () => {
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll());
}
});
};
const rowSelection = {
selectedRowIds,
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
@@ -795,13 +784,11 @@ const Crontab = () => {
onOk() {
request
.delete(`${config.apiPrefix}crons`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getCrons();
} else {
message.error(data);
}
});
},
@@ -820,11 +807,9 @@ const Crontab = () => {
.put(`${config.apiPrefix}crons/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
getCrons();
} else {
message.error(data);
}
});
},
@@ -872,7 +857,7 @@ const Crontab = () => {
useEffect(() => {
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);
}
}, [viewConf, enabledCronViews]);
@@ -884,96 +869,9 @@ const Crontab = () => {
sorter: {},
filters: {},
});
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
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) => {
switch (key) {
case 'new':
@@ -996,7 +894,7 @@ const Crontab = () => {
viewAction(key);
}}
items={[
...[...enabledCronViews].slice(2).map((x) => ({
...[...enabledCronViews].slice(4).map((x) => ({
label: (
<Space style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{x.name}</span>
@@ -1029,9 +927,11 @@ const Crontab = () => {
setLoading(true);
request
.get(`${config.apiPrefix}crons/views`)
.then((data: any) => {
setCronViews(data.data);
setEnabledCronViews(data.data.filter((x) => !x.isDisabled));
.then(({ code, data }) => {
if (code === 200) {
setCronViews(data);
setEnabledCronViews(data.filter((x) => !x.isDisabled));
}
})
.finally(() => {
setLoading(false);
@@ -1040,6 +940,7 @@ const Crontab = () => {
const tabClick = (key: string) => {
const view = enabledCronViews.find((x) => x.id == key);
setSelectedRowIds([]);
setPageConf({ ...pageConf, page: 1 });
setViewConf(view ? view : null);
};
@@ -1089,27 +990,97 @@ const Crontab = () => {
}
onTabClick={tabClick}
items={[
{
key: 'all',
label: '全部任务',
children: panelContent,
},
...[...enabledCronViews].slice(0, 2).map((x) => ({
...[...enabledCronViews].slice(0, 4).map((x) => ({
key: x.id,
label: x.name,
children: panelContent,
})),
]}
>
<Tabs.TabPane tab="全部任务" key="all">
{panelContent}
</Tabs.TabPane>
{[...enabledCronViews].slice(0, 2).map((x) => (
<Tabs.TabPane tab={x.name} key={x.id}>
{panelContent}
</Tabs.TabPane>
))}
</Tabs>
/>
<div ref={tableRef}>
{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}
/>
</div>
<CronLogModal
visible={isLogModalVisible}
handleCancel={() => {
+9 -5
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
enum CrontabStatus {
'running',
@@ -41,14 +42,17 @@ const CronLogModal = ({
}
request
.get(logUrl ? logUrl : `${config.apiPrefix}crons/${cron.id}/log`)
.then((data: any) => {
if (localStorage.getItem('logCron') === String(cron.id)) {
const log = data.data as string;
.then(({ code, data }) => {
if (
code === 200 &&
localStorage.getItem('logCron') === String(cron.id)
) {
const log = data as string;
setValue(log || '暂无日志');
setExecuting(
log && !log.includes('执行结束') && !log.includes('重启面板'),
log && !logEnded(log) && !log.includes('重启面板'),
);
if (log && !log.includes('执行结束') && !log.includes('重启面板')) {
if (log && !logEnded(log) && !log.includes('重启面板')) {
setTimeout(() => {
getCronLog();
}, 2000);
-4
View File
@@ -32,8 +32,6 @@ const CronModal = ({
if (code === 200) {
message.success(cron ? '更新Cron成功' : '新建Cron成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
} catch (error: any) {
@@ -142,8 +140,6 @@ const CronLabelModal = ({
action === 'post' ? '添加Labels成功' : '删除Labels成功',
);
handleCancel(true);
} else {
message.error(data);
}
setLoading(false);
} catch (error) {
+109 -57
View File
@@ -12,6 +12,7 @@ import {
import { request } from '@/utils/http';
import config from '@/utils/config';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import IconFont from '@/components/iconfont';
const PROPERTIES = [
{ name: '命令', value: 'command' },
@@ -42,6 +43,11 @@ const STATUS = [
{ name: '已禁用', value: 2 },
];
enum ViewFilterRelation {
'and' = '且',
'or' = '或',
}
const ViewCreateModal = ({
view,
handleCancel,
@@ -53,10 +59,11 @@ const ViewCreateModal = ({
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [operationMap, setOperationMap] = useState<any>();
const [filterRelation, setFilterRelation] = useState<'and' | 'or'>('and');
const handleOk = async (values: any) => {
setLoading(true);
values.filterRelation = filterRelation;
const method = view ? 'put' : 'post';
try {
const { code, data } = await request[method](
@@ -66,9 +73,7 @@ const ViewCreateModal = ({
},
);
if (code !== 200) {
message.error(data);
} else {
if (code === 200) {
handleCancel(data);
}
setLoading(false);
@@ -89,12 +94,7 @@ const ViewCreateModal = ({
}, [view, visible]);
const operationElement = (
<Select
style={{ width: 100 }}
onChange={() => {
setOperationMap({});
}}
>
<Select style={{ width: 80 }}>
{OPERATIONS.map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
@@ -166,57 +166,109 @@ const ViewCreateModal = ({
</Form.Item>
<Form.List name="filters">
{(fields, { add, remove }) => (
<>
{fields.map(({ key, name, ...restField }, index) => (
<Form.Item
label={index === 0 ? '筛选条件' : ''}
key={key}
style={{ marginBottom: 0 }}
required
<div
style={{ position: 'relative' }}
className={`view-filters-container ${
fields.length > 1 ? 'active' : ''
}`}
>
{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">
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
<Button
type="primary"
size="small"
style={{
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}
name={[name, 'operation']}
rules={[{ required: true }]}
>
{operationElement}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{['In', 'Nin'].includes(
form.getFieldValue(['filters', index, 'operation']),
) ? (
statusElement
) : (
<Input placeholder="请输入内容" />
<Form.Item
{...restField}
name={[name, 'property']}
rules={[{ required: true }]}
>
{propertyElement(PROPERTIES, { width: 90 })}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'operation']}
rules={[{ required: true }]}
>
{operationElement}
</Form.Item>
<Form.Item
{...restField}
name={[name, 'value']}
rules={[{ required: true, message: '请输入内容' }]}
>
{['In', 'Nin'].includes(
form.getFieldValue(['filters', index, 'operation']),
) ? (
statusElement
) : (
<Input placeholder="请输入内容" />
)}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Form.Item>
{index !== 0 && (
<MinusCircleOutlined onClick={() => remove(name)} />
)}
</Space>
</Space>
</Form.Item>
))}
<Form.Item>
<a
onClick={() =>
add({ property: 'command', operation: 'Reg' })
}
>
<PlusOutlined />
</a>
</Form.Item>
))}
<Form.Item>
<a
onClick={() => add({ property: 'command', operation: 'Reg' })}
>
<PlusOutlined />
</a>
</Form.Item>
</>
</div>
</div>
)}
</Form.List>
<Form.List name="sorts">
+21 -15
View File
@@ -81,6 +81,16 @@ const ViewManageModal = ({
dataIndex: 'name',
key: 'name',
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: '显示',
@@ -100,10 +110,10 @@ const ViewManageModal = ({
{
title: '操作',
key: 'action',
width: 140,
width: 100,
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return (
return record.type !== 1 ? (
<Space size="middle">
<a onClick={() => editView(record, index)}>
<EditOutlined />
@@ -112,6 +122,8 @@ const ViewManageModal = ({
<DeleteOutlined />
</a>
</Space>
) : (
'-'
);
},
},
@@ -141,12 +153,10 @@ const ViewManageModal = ({
onOk() {
request
.delete(`${config.apiPrefix}crons/views`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
cronViewChange();
} else {
message.error(data);
}
});
},
@@ -162,14 +172,12 @@ const ViewManageModal = ({
.put(`${config.apiPrefix}crons/views/${checked ? 'enable' : 'disable'}`, {
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const _list = [...list];
_list.splice(index, 1, { ...list[index], isDisabled: !checked });
setList(_list);
cronViewChange();
} else {
message.error(data);
}
});
};
@@ -190,15 +198,13 @@ const ViewManageModal = ({
.put(`${config.apiPrefix}crons/views/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex, id: dragRow.id },
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newData = [...list];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
newData.splice(hoverIndex, 0, { ...dragRow, ...data });
setList(newData);
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 { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { getTableScroll } from '@/utils/index';
import DependenceLogModal from './logModal';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text } = Typography;
const { Search } = Input;
@@ -164,10 +165,11 @@ const Dependence = () => {
const [editedDependence, setEditedDependence] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const [searchText, setSearchText] = useState('');
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [logDependence, setLogDependence] = useState<any>();
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [type, setType] = useState('nodejs');
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef, 59)
const getDependencies = () => {
setLoading(true);
@@ -175,8 +177,10 @@ const Dependence = () => {
.get(
`${config.apiPrefix}dependencies?searchValue=${searchText}&type=${type}`,
)
.then((data: any) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
})
.finally(() => setLoading(false));
};
@@ -212,18 +216,14 @@ const Dependence = () => {
.delete(`${config.apiPrefix}dependencies${force ? '/force' : ''}`, {
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
if (force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
setValue(result);
}
.then(({ code, data }) => {
if (code === 200 && force) {
const i = value.findIndex((x) => x.id === data.data[0].id);
if (i !== -1) {
const result = [...value];
result.splice(i, 1);
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -250,11 +250,9 @@ const Dependence = () => {
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: [record.id],
})
.then((data: any) => {
if (data.code === 200) {
handleDependence(data.data[0]);
} else {
message.error(data);
.then(({ code, data }) => {
if (code === 200) {
handleDependence(data[0]);
}
});
},
@@ -286,16 +284,10 @@ const Dependence = () => {
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
};
const rowSelection = {
selectedRowIds,
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
@@ -309,12 +301,10 @@ const Dependence = () => {
.delete(`${config.apiPrefix}dependencies${forceUrl}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]);
getDependencies();
} else {
message.error(data);
}
});
},
@@ -333,12 +323,10 @@ const Dependence = () => {
.put(`${config.apiPrefix}dependencies/reinstall`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
setSelectedRowIds([]);
getDependencies();
} else {
message.error(data);
}
});
},
@@ -351,15 +339,17 @@ const Dependence = () => {
const getDependenceDetail = (dependence: any) => {
request
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => {
const index = value.findIndex((x) => x.id === dependence.id);
const result = [...value];
if (index !== -1) {
result.splice(index, 1, {
...dependence,
...data.data,
});
setValue(result);
.then(({ code, data }) => {
if (code === 200) {
const index = value.findIndex((x) => x.id === dependence.id);
const result = [...value];
if (index !== -1) {
result.splice(index, 1, {
...dependence,
...data,
});
setValue(result);
}
}
})
.finally(() => setLoading(false));
@@ -373,12 +363,6 @@ const Dependence = () => {
getDependencies();
}, [searchText, type]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
useEffect(() => {
if (logDependence) {
localStorage.setItem('logDependence', logDependence.id);
@@ -427,53 +411,8 @@ const Dependence = () => {
}
}, [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) => {
setSelectedRowIds([]);
setType(activeKey);
};
@@ -506,20 +445,60 @@ const Dependence = () => {
{
key: 'nodejs',
label: 'NodeJs',
children: panelContent(),
},
{
key: 'python3',
label: 'Python3',
children: panelContent(),
},
{
key: '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
visible={isModalVisible}
handleCancel={handleCancel}
+10 -5
View File
@@ -48,9 +48,12 @@ const DependenceLogModal = ({
setLoading(true);
request
.get(`${config.apiPrefix}dependencies/${dependence.id}`)
.then((data: any) => {
if (localStorage.getItem('logDependence') === String(dependence.id)) {
const log = (data.data.log || []).join('') as string;
.then(({ code, data }) => {
if (
code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id)
) {
const log = (data.log || []).join('') as string;
setValue(log);
setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes('删除失败'));
@@ -67,8 +70,10 @@ const DependenceLogModal = ({
.delete(`${config.apiPrefix}dependencies/force`, {
data: [dependence.id],
})
.then((data: any) => {
cancel(true);
.then(({ code, data }) => {
if (code === 200) {
cancel(true);
}
})
.finally(() => {
setRemoveLoading(false);
+1 -3
View File
@@ -53,9 +53,7 @@ const DependenceModal = ({
},
);
if (code !== 200) {
message.error(data);
} else {
if (code === 200) {
handleCancel(data);
}
setLoading(false);
+22 -10
View File
@@ -22,15 +22,23 @@ const Diff = () => {
const editorRef = useRef<any>(null);
const getConfig = () => {
request.get(`${config.apiPrefix}configs/${current}`).then((data) => {
setCurrentValue(data.data);
});
request
.get(`${config.apiPrefix}configs/${current}`)
.then(({ code, data }) => {
if (code === 200) {
setCurrentValue(data);
}
});
};
const getSample = () => {
request.get(`${config.apiPrefix}configs/${origin}`).then((data) => {
setOriginValue(data.data);
});
request
.get(`${config.apiPrefix}configs/${origin}`)
.then(({ code, data }) => {
if (code === 200) {
setOriginValue(data);
}
});
};
const updateConfig = () => {
@@ -42,8 +50,10 @@ const Diff = () => {
.post(`${config.apiPrefix}configs/save`, {
data: { content, name: current },
})
.then((data: any) => {
message.success(data.message);
.then(({ code, data }) => {
if (code === 200) {
message.success('保存成功');
}
});
};
@@ -51,8 +61,10 @@ const Diff = () => {
setLoading(true);
request
.get(`${config.apiPrefix}configs/files`)
.then((data: any) => {
setFiles(data.data);
.then(({ code, data }) => {
if (code === 200) {
setFiles(data);
}
})
.finally(() => setLoading(false));
};
-2
View File
@@ -28,8 +28,6 @@ const EditNameModal = ({
if (code === 200) {
message.success('更新环境变量名称成功');
handleCancel();
} else {
message.error(data);
}
setLoading(false);
} catch (error) {
+63 -40
View File
@@ -9,6 +9,8 @@ import {
Typography,
Tooltip,
Input,
UploadProps,
Upload,
} from 'antd';
import {
EditOutlined,
@@ -16,6 +18,7 @@ import {
SyncOutlined,
CheckCircleOutlined,
StopOutlined,
UploadOutlined,
} from '@ant-design/icons';
import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
@@ -25,9 +28,10 @@ import EditNameModal from './editNameModal';
import { DndProvider, useDrag, useDrop } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import './index.less';
import { exportJson, getTableScroll } from '@/utils/index';
import { exportJson } from '@/utils/index';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -131,6 +135,7 @@ const Env = () => {
textAlign: 'left',
}}
ellipsis={{ tooltip: text, rows: 2 }}
copyable
>
{text}
</Paragraph>
@@ -249,14 +254,18 @@ const Env = () => {
const [editedEnv, setEditedEnv] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
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 = () => {
setLoading(true);
request
.get(`${config.apiPrefix}envs?searchValue=${searchText}`)
.then((data: any) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
})
.finally(() => setLoading(false));
};
@@ -277,15 +286,14 @@ const Env = () => {
onOk() {
request
.put(
`${config.apiPrefix}envs/${
record.status === Status. ? 'enable' : 'disable'
`${config.apiPrefix}envs/${record.status === Status. ? 'enable' : 'disable'
}`,
{
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success(
`${record.status === Status. ? '启用' : '禁用'}成功`,
);
@@ -297,8 +305,6 @@ const Env = () => {
status: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
@@ -333,14 +339,12 @@ const Env = () => {
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
message.error(data);
}
});
},
@@ -390,14 +394,12 @@ const Env = () => {
.put(`${config.apiPrefix}envs/${dragRow.id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, { ...dragRow, ...data.data });
setValue([...newData]);
} else {
message.error(data);
}
});
},
@@ -406,16 +408,10 @@ const Env = () => {
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
setTimeout(() => {
if (selectedRowIds.length === 0 || selectedIds.length === 0) {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
}
});
};
const rowSelection = {
selectedRowIds,
selectedRowKeys: selectedRowIds,
onChange: onSelectChange,
};
@@ -426,13 +422,11 @@ const Env = () => {
onOk() {
request
.delete(`${config.apiPrefix}envs`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getEnvs();
} else {
message.error(data);
}
});
},
@@ -451,11 +445,9 @@ const Env = () => {
.put(`${config.apiPrefix}envs/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
getEnvs();
} else {
message.error(data);
}
});
},
@@ -480,16 +472,37 @@ const Env = () => {
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(() => {
getEnvs();
}, [searchText]);
useEffect(() => {
setTimeout(() => {
setTableScrollHeight(getTableScroll({ extraHeight: 87 }));
});
}, []);
return (
<PageContainer
className="ql-container-wrapper env-wrapper"
@@ -502,6 +515,15 @@ const Env = () => {
loading={loading}
onSearch={onSearch}
/>,
<Upload {...uploadProps}>
<Button
type="primary"
icon={<UploadOutlined />}
loading={importLoading}
>
</Button>
</Upload>,
<Button key="2" type="primary" onClick={() => addEnv()}>
</Button>,
@@ -555,6 +577,7 @@ const Env = () => {
)}
<DndProvider backend={HTML5Backend}>
<Table
ref={tableRef}
columns={columns}
rowSelection={rowSelection}
pagination={false}
@@ -564,7 +587,7 @@ const Env = () => {
scroll={{ x: 1000, y: tableScrollHeight }}
components={components}
loading={loading}
onRow={(record: any, index: number) => {
onRow={(record: any, index: number | undefined) => {
return {
index,
moveRow,
-2
View File
@@ -44,8 +44,6 @@ const EnvModal = ({
if (code === 200) {
message.success(env ? '更新变量成功' : '新建变量成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
} catch (error: any) {
+4 -2
View File
@@ -17,8 +17,10 @@ const Error = () => {
setLoading(true);
request
.get(`${config.apiPrefix}public/panel/log`)
.then((data: any) => {
setData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
+5 -9
View File
@@ -41,11 +41,9 @@ const Initialization = () => {
password: values.password,
},
})
.then((data) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
next();
} else {
message.error(data.message);
}
})
.finally(() => setLoading(false));
@@ -59,11 +57,9 @@ const Initialization = () => {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
next();
} else {
message.error(_data.message);
}
})
.finally(() => setLoading(false));
@@ -236,7 +232,7 @@ const Initialization = () => {
<img
alt="logo"
className={styles.logo}
src="http://qn.whyour.cn/logo.png"
src="https://qn.whyour.cn/logo.png"
/>
<span className={styles.title}></span>
</div>
+152 -60
View File
@@ -1,5 +1,15 @@
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 { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react';
@@ -9,54 +19,34 @@ import { Controlled as CodeMirror } from 'react-codemirror2';
import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max';
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 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 { Text } = Typography;
const Log = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
const [title, setTitle] = useState('请选择日志文件');
const [value, setValue] = useState('请选择日志文件');
const [select, setSelect] = useState<any>();
const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [height, setHeight] = useState<number>();
const treeDom = useRef<any>();
const [expandedKeys, setExpandedKeys] = useState<string[]>([]);
const [currentNode, setCurrentNode] = useState<any>();
const [searchValue, setSearchValue] = useState('');
const getLogs = () => {
setLoading(true);
request
.get(`${config.apiPrefix}logs`)
.then((data) => {
setData(data.data);
setFilterData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setData(data);
}
})
.finally(() => setLoading(false));
};
@@ -64,18 +54,27 @@ const Log = () => {
const getLog = (node: any) => {
request
.get(`${config.apiPrefix}logs/${node.title}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
const onSelect = (value: any, node: any) => {
setCurrentNode(node);
setSelect(value);
if (node.key === select || !value) {
return;
}
if (node.type === 'directory') {
setValue('请选择日志文件');
return;
}
setValue('加载中...');
setSelect(value);
setTitle(node.key);
getLog(node);
};
@@ -86,41 +85,130 @@ const Log = () => {
const onSearch = useCallback(
(e) => {
const keyword = e.target.value;
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setFilterData(tree);
setExpandedKeys(expandedKeys);
debounceSearch(keyword);
},
[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(() => {
getLogs();
if (treeDom && treeDom.current) {
}, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight);
}
}, []);
}, [treeDom.current, data]);
return (
<PageContainer
className="ql-container-wrapper log-wrapper"
title={title}
title={select}
loading={loading}
extra={
isPhone && [
<TreeSelect
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择日志"
fieldNames={{ value: 'key', label: 'title' }}
showSearch
onSelect={onSelect}
/>,
]
isPhone
? [
<TreeSelect
treeExpandAction="click"
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择日志"
fieldNames={{ value: 'key' }}
treeNodeFilterProp="title"
showSearch
allowClear
onSelect={onSelect}
/>,
]
: [
<Tooltip title="删除">
<Button
type="primary"
disabled={!select}
onClick={deleteFile}
icon={<DeleteOutlined />}
/>
</Tooltip>,
]
}
header={{
style: headerStyle,
@@ -128,6 +216,7 @@ const Log = () => {
>
<div className={`${styles['log-container']} log-container`}>
{!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}>
{data.length > 0 ? (
@@ -140,6 +229,7 @@ const Log = () => {
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
expandAction="click"
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
@@ -147,6 +237,8 @@ const Log = () => {
selectedKeys={[select]}
showLine={{ showLeafIcon: true }}
onSelect={onTreeSelect}
expandedKeys={expandedKeys}
onExpand={onExpand}
></Tree>
</div>
</>
+17 -25
View File
@@ -40,15 +40,7 @@ const Login = () => {
},
})
.then((data) => {
if (data.code === 420) {
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
} else {
checkResponse(data);
}
checkResponse(data, values);
setLoading(false);
})
.catch(function (error) {
@@ -64,11 +56,7 @@ const Login = () => {
data: { ...loginInfo, code: values.code },
})
.then((data: any) => {
if (data.code === 430) {
message.error(data.message);
} else {
checkResponse(data);
}
checkResponse(data);
setVerifying(false);
})
.catch((error: any) => {
@@ -77,8 +65,11 @@ const Login = () => {
});
};
const checkResponse = (data: any) => {
if (data.code === 200) {
const checkResponse = (
{ code, data, message: _message }: any,
values?: any,
) => {
if (code === 200) {
const {
token,
lastip,
@@ -86,7 +77,7 @@ const Login = () => {
lastlogon,
retries = 0,
platform,
} = data.data;
} = data;
localStorage.setItem(config.authKey, token);
notification.success({
message: '登录成功!',
@@ -105,13 +96,14 @@ const Login = () => {
});
reloadUser(true);
history.push('/crontab');
} else if (data.code === 100) {
message.warn(data.message);
} else if (data.code === 410) {
message.error(data.message);
setWaitTime(data.data);
} else {
message.error(data.message);
} else if (code === 410) {
setWaitTime(data);
} else if (code === 420) {
setLoginInfo({
username: values.username,
password: values.password,
});
setTwoFactor(true);
}
};
@@ -137,7 +129,7 @@ const Login = () => {
<img
alt="logo"
className={styles.logo}
src="http://qn.whyour.cn/logo.png"
src="https://qn.whyour.cn/logo.png"
/>
<span className={styles.title}>
{twoFactor ? '两步验证' : config.siteName}
+32 -8
View File
@@ -7,6 +7,7 @@ import Editor from '@monaco-editor/react';
import SaveModal from './saveModal';
import SettingModal from './setting';
import { useTheme } from '@/utils/hooks';
import { logEnded } from '@/utils';
const { Option } = Select;
const LangMap: any = {
@@ -57,6 +58,11 @@ const EditModal = ({
if (node.key === selectedKey || !value) {
return;
}
if (node.type === 'directory') {
return;
}
const newMode = LangMap[value.slice(-3)] || '';
setCNode(node);
setLanguage(newMode);
@@ -67,8 +73,10 @@ const EditModal = ({
const getDetail = (node: any) => {
request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
@@ -83,8 +91,10 @@ const EditModal = ({
content,
},
})
.then((data) => {
setIsRunning(true);
.then(({ code, data }) => {
if (code === 200) {
setIsRunning(true);
}
});
};
@@ -101,8 +111,10 @@ const EditModal = ({
content,
},
})
.then((data) => {
setIsRunning(false);
.then(({ code, data }) => {
if (code === 200) {
setIsRunning(false);
}
});
};
@@ -117,7 +129,7 @@ const EditModal = ({
return;
}
if (_message.includes('执行结束')) {
if (logEnded(_message)) {
setTimeout(() => {
setIsRunning(false);
}, 300);
@@ -130,10 +142,13 @@ const EditModal = ({
}, [socketMessage]);
useEffect(() => {
setLog('');
if (currentNode) {
setCNode(currentNode);
setValue(content as string);
setSelectedKey(currentNode.key);
const newMode = LangMap[currentNode.title.slice(-3)] || '';
setLanguage(newMode);
}
}, [content, currentNode]);
@@ -144,6 +159,7 @@ const EditModal = ({
title={
<>
<TreeSelect
treeExpandAction="click"
style={{ marginRight: 8, width: 150 }}
value={selectedKey}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
@@ -216,6 +232,7 @@ const EditModal = ({
onClose={cancel}
open={visible}
>
{/* @ts-ignore */}
<SplitPane
split="vertical"
minSize={200}
@@ -236,7 +253,14 @@ const EditModal = ({
editorRef.current = editor;
}}
/>
<pre style={{ height: '100%', whiteSpace: 'break-spaces' }}>{log}</pre>
<pre
style={{
height: '100%',
padding: '0 15px',
}}
>
{log}
</pre>
</SplitPane>
<SaveModal
visible={saveModalVisible}
-2
View File
@@ -57,8 +57,6 @@ const EditScriptNameModal = ({
path,
key: `${key}${filename}`,
});
} else {
message.error(data);
}
setLoading(false);
})
+83 -93
View File
@@ -37,37 +37,11 @@ import { history, useOutletContext, useLocation } from '@umijs/max';
import { parse } from 'query-string';
import { depthFirstSearch } from '@/utils';
import { SharedContext } from '@/layouts';
import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq';
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 = {
'.py': 'python',
'.js': 'javascript',
@@ -78,11 +52,9 @@ const LangMap: any = {
const Script = () => {
const { headerStyle, isPhone, theme, socketMessage } =
useOutletContext<SharedContext>();
const [title, setTitle] = useState('请选择脚本文件');
const [value, setValue] = useState('请选择脚本文件');
const [select, setSelect] = useState<any>();
const [select, setSelect] = useState<string>('');
const [data, setData] = useState<any[]>([]);
const [filterData, setFilterData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [mode, setMode] = useState('');
const [height, setHeight] = useState<number>();
@@ -99,19 +71,26 @@ const Script = () => {
setLoading(true);
request
.get(`${config.apiPrefix}scripts`)
.then((data) => {
setData(data.data);
setFilterData(data.data);
initGetScript();
.then(({ code, data }) => {
if (code === 200) {
setData(data);
initGetScript();
}
})
.finally(() => setLoading(false));
};
const getDetail = (node: any) => {
request
.get(`${config.apiPrefix}scripts/${node.title}?path=${node.parent || ''}`)
.then((data) => {
setValue(data.data);
.get(
`${config.apiPrefix}scripts/${encodeURIComponent(node.title)}?path=${
node.parent || ''
}`,
)
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
}
});
};
@@ -132,22 +111,24 @@ const Script = () => {
};
const onSelect = (value: any, node: any) => {
setSelect(node.key);
setCurrentNode(node);
if (node.key === select || !value) {
return;
}
setValue('加载中...');
if (node.type === 'directory') {
setValue('请选择脚本文件');
return;
}
const newMode = value ? LangMap[value.slice(-3)] : '';
setMode(isPhone && newMode === 'typescript' ? 'javascript' : newMode);
setSelect(node.key);
setTitle(node.key);
setCurrentNode(node);
setValue('加载中...');
getDetail(node);
};
const onExpand = (expKeys: any) => {
setExpandedKeys(expKeys);
};
const onTreeSelect = useCallback(
(keys: Key[], e: any) => {
const content = editorRef.current
@@ -178,22 +159,30 @@ const Script = () => {
const keyword = e.target.value;
debounceSearch(keyword);
},
[data, setFilterData],
[data],
);
const debounceSearch = useCallback(
debounce((keyword) => {
setSearchValue(keyword);
const { tree, expandedKeys } = getFilterData(
keyword.toLocaleLowerCase(),
data,
);
setExpandedKeys(expandedKeys);
setFilterData(tree);
}, 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 = () => {
setTimeout(() => {
setIsEditing(true);
@@ -231,13 +220,11 @@ const Script = () => {
content,
},
})
.then((_data: any) => {
if (_data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success(`保存成功`);
setValue(content);
setIsEditing(false);
} else {
message.error(_data);
}
resolve(null);
})
@@ -255,10 +242,11 @@ const Script = () => {
title: `确认删除`,
content: (
<>
<Text style={{ wordBreak: 'break-all' }} type="warning">
{select}
</Text>{' '}
</Text>
{currentNode.type === 'directory' ? '夹及其子文件' : ''}
</>
),
@@ -268,24 +256,18 @@ const Script = () => {
data: {
filename: currentNode.title,
path: currentNode.parent || '',
type: currentNode.type,
},
})
.then((_data: any) => {
if (_data.code === 200) {
.then(({ code }) => {
if (code === 200) {
message.success(`删除成功`);
let newData = [...data];
if (currentNode.parent) {
const parentNodeIndex = newData.findIndex(
(x) => x.key === currentNode.parent,
newData = depthFirstSearch(
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 {
const index = newData.findIndex(
(x) => x.key === currentNode.key,
@@ -295,8 +277,7 @@ const Script = () => {
}
}
setData(newData);
} else {
message.error(_data);
initState();
}
});
},
@@ -346,30 +327,35 @@ const Script = () => {
filename: currentNode.title,
},
})
.then((_data: any) => {
const blob = new Blob([_data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentNode.title;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
.then(({ code, data }) => {
if (code === 200) {
const blob = new Blob([data], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = currentNode.title;
document.documentElement.appendChild(a);
a.click();
document.documentElement.removeChild(a);
}
});
};
useEffect(() => {
const word = searchValue || '';
const { tree } = getFilterData(word.toLocaleLowerCase(), data);
setFilterData(tree);
}, [data]);
const initState = () => {
setSelect('');
setCurrentNode(null);
setValue('请选择脚本文件');
};
useEffect(() => {
getScripts();
if (treeDom && treeDom.current) {
}, []);
useEffect(() => {
if (treeDom.current) {
setHeight(treeDom.current.clientHeight);
}
}, []);
}, [treeDom.current, data]);
const action = (key: string | number) => {
switch (key) {
@@ -438,19 +424,22 @@ const Script = () => {
return (
<PageContainer
className="ql-container-wrapper log-wrapper"
title={title}
title={select}
loading={loading}
extra={
isPhone
? [
<TreeSelect
treeExpandAction="click"
className="log-select"
value={select}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
treeData={data}
placeholder="请选择脚本"
fieldNames={{ value: 'key', label: 'title' }}
fieldNames={{ value: 'key' }}
treeNodeFilterProp="title"
showSearch
allowClear
onSelect={onSelect}
/>,
<Dropdown overlay={menu} trigger={['click']}>
@@ -506,6 +495,7 @@ const Script = () => {
>
<div className={`${styles['log-container']} log-container`}>
{!isPhone && (
/*// @ts-ignore*/
<SplitPane split="vertical" size={200} maxSize={-100}>
<div className={styles['left-tree-container']}>
{data.length > 0 ? (
@@ -518,6 +508,7 @@ const Script = () => {
></Input.Search>
<div className={styles['left-tree-scroller']} ref={treeDom}>
<Tree
expandAction="click"
className={styles['left-tree']}
treeData={filterData}
showIcon={true}
@@ -554,7 +545,6 @@ const Script = () => {
readOnly: !isEditing,
fontSize: 12,
lineNumbersMinChars: 3,
folding: false,
glyphMargin: false,
}}
onMount={(editor) => {
-2
View File
@@ -26,8 +26,6 @@ const SaveModal = ({
if (code === 200) {
message.success('保存文件成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
});
-2
View File
@@ -26,8 +26,6 @@ const SettingModal = ({
if (code === 200) {
message.success('保存文件成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
});
+23 -3
View File
@@ -1,16 +1,23 @@
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 { SharedContext } from '@/layouts';
import dayjs from 'dayjs';
const { Link } = Typography;
const About = () => {
enum TVersion {
'develop' = '开发版',
'master' = '正式版',
}
const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
return (
<div className={styles.container}>
<img
alt="logo"
style={{ width: 140, marginRight: 20 }}
src="http://qn.whyour.cn/logo.png"
src="https://qn.whyour.cn/logo.png"
/>
<div className={styles.right}>
<span className={styles.title}></span>
@@ -19,6 +26,19 @@ const About = () => {
task management panel that supports typescript, javaScript, python3,
and shell.
</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>
<div>
<Link
href="https://github.com/whyour/qinglong"
-2
View File
@@ -30,8 +30,6 @@ const AppModal = ({
if (code === 200) {
message.success(app ? '更新应用成功' : '新建应用成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
} catch (error) {
+23 -23
View File
@@ -17,17 +17,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
message.loading('检查更新中...', 0);
request
.put(`${config.apiPrefix}system/update-check`)
.then((_data: any) => {
.then(({ code, data }) => {
message.destroy();
const { code, data } = _data;
if (code === 200) {
if (data.hasNewVersion) {
showConfirmUpdateModal(data);
} else {
showForceUpdateModal();
}
} else {
message.error(data);
}
})
.catch((error: any) => {
@@ -80,8 +77,6 @@ const CheckUpdate = ({ socketMessage }: any) => {
content: (
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
paddingTop: 15,
fontSize: 12,
fontWeight: 400,
@@ -105,26 +100,23 @@ const CheckUpdate = ({ socketMessage }: any) => {
};
const showUpdatingModal = () => {
setValue('');
modalRef.current = Modal.info({
width: 600,
maskClosable: false,
closable: false,
okButtonProps: { disabled: true },
title: '更新日志',
title: '更新中...',
centered: true,
content: (
<div style={{ height: '60vh', overflowY: 'auto' }}>
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: 12,
fontWeight: 400,
}}
>
{value}
</pre>
</div>
<pre
style={{
fontSize: 12,
fontWeight: 400,
}}
>
{value}
</pre>
),
});
};
@@ -140,13 +132,16 @@ const CheckUpdate = ({ socketMessage }: any) => {
}
const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes('失败,请检查');
modalRef.current.update({
maskClosable: updateFailed,
closable: updateFailed,
okButtonProps: { disabled: !updateFailed },
content: (
<div style={{ height: '60vh', overflowY: 'auto' }}>
<>
<pre
style={{
wordBreak: 'break-all',
whiteSpace: 'pre-wrap',
fontSize: 12,
fontWeight: 400,
}}
@@ -154,9 +149,14 @@ const CheckUpdate = ({ socketMessage }: any) => {
{newMessage}
</pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</div>
</>
),
});
if (updateFailed && !value.includes('失败,请检查')) {
message.error('更新失败,请检查网络及日志或稍后再试');
}
setValue(newMessage);
document.getElementById('log-identifier') &&
+15
View File
@@ -18,5 +18,20 @@
.desc {
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;
}
}
+44 -27
View File
@@ -31,6 +31,7 @@ import CheckUpdate from './checkUpdate';
import About from './about';
import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts';
import './index.less';
const { Text } = Typography;
const optionsWithDisabled = [
@@ -40,8 +41,15 @@ const optionsWithDisabled = [
];
const Setting = () => {
const { headerStyle, isPhone, user, reloadUser, reloadTheme, socketMessage } =
useOutletContext<SharedContext>();
const {
headerStyle,
isPhone,
user,
reloadUser,
reloadTheme,
socketMessage,
systemInfo,
} = useOutletContext<SharedContext>();
const columns = [
{
title: '名称',
@@ -72,10 +80,15 @@ const Setting = () => {
dataIndex: 'scopes',
key: 'scopes',
align: 'center' as const,
width: '40%',
render: (text: string, record: any) => {
return record.scopes.map((scope: any) => {
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
});
return (
<div style={{ textAlign: 'left' }}>
{record.scopes.map((scope: any) => {
return <Tag key={scope}>{(config.scopesMap as any)[scope]}</Tag>;
})}
</div>
);
},
},
{
@@ -144,8 +157,10 @@ const Setting = () => {
setLoading(true);
request
.get(`${config.apiPrefix}apps`)
.then((data: any) => {
setDataSource(data.data);
.then(({ code, data }) => {
if (code === 200) {
setDataSource(data);
}
})
.finally(() => setLoading(false));
};
@@ -175,14 +190,12 @@ const Setting = () => {
onOk() {
request
.delete(`${config.apiPrefix}apps`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...dataSource];
result.splice(index, 1);
setDataSource(result);
} else {
message.error(data);
}
});
},
@@ -209,12 +222,10 @@ const Setting = () => {
onOk() {
request
.put(`${config.apiPrefix}apps/${record.id}/reset-secret`)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('重置成功');
handleApp(data.data);
} else {
message.error(data);
handleApp(data);
}
});
},
@@ -247,8 +258,10 @@ const Setting = () => {
const getLoginLog = () => {
request
.get(`${config.apiPrefix}user/login-log`)
.then((data: any) => {
setLoginLogData(data.data);
.then(({ code, data }) => {
if (code === 200) {
setLoginLogData(data);
}
})
.catch((error: any) => {
console.log(error);
@@ -271,8 +284,10 @@ const Setting = () => {
const getNotification = () => {
request
.get(`${config.apiPrefix}user/notification`)
.then((data: any) => {
setNotificationInfo(data.data);
.then(({ code, data }) => {
if (code === 200) {
setNotificationInfo(data);
}
})
.catch((error: any) => {
console.log(error);
@@ -282,9 +297,9 @@ const Setting = () => {
const getLogRemoveFrequency = () => {
request
.get(`${config.apiPrefix}system/log/remove`)
.then((data: any) => {
if (data.data.info) {
const { frequency } = data.data.info;
.then(({ code, data }) => {
if (code === 200 && data.info) {
const { frequency } = data.info;
setLogRemoveFrequency(frequency);
}
})
@@ -299,8 +314,10 @@ const Setting = () => {
.put(`${config.apiPrefix}system/log/remove`, {
data: { frequency: logRemoveFrequency },
})
.then((data: any) => {
message.success('更新成功');
.then(({ code, data }) => {
if (code === 200) {
message.success('更新成功');
}
})
.catch((error: any) => {
console.log(error);
@@ -310,7 +327,7 @@ const Setting = () => {
return (
<PageContainer
className="ql-container-wrapper ql-container-wrapper-has-tab"
className="ql-container-wrapper ql-container-wrapper-has-tab ql-setting-container"
title="系统设置"
header={{
style: headerStyle,
@@ -407,7 +424,7 @@ const Setting = () => {
{
key: 'about',
label: '关于',
children: <About />,
children: <About systemInfo={systemInfo} />,
},
]}
></Tabs>
+16 -20
View File
@@ -23,11 +23,9 @@ const NotificationSetting = ({ data }: any) => {
...values,
},
})
.then((_data: any) => {
if (_data && _data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success(values.type ? '通知发送成功' : '通知关闭成功');
} else {
message.error(_data.message);
}
})
.catch((error: any) => {
@@ -75,22 +73,20 @@ const NotificationSetting = ({ data }: any) => {
rules={[{ required: x.required }]}
style={{ maxWidth: 400 }}
>
{
x.items ? (
<Select placeholder={x.placeholder || `请选择${x.label}`}>
{x.items.map((y) => (
<Option key={y.value} value={y.value}>
{y.label || y.value}
</Option>
))}
</Select>
) : (
<Input.TextArea
autoSize={true}
placeholder={x.placeholder || `请输入${x.label}`}
/>
)
}
{x.items ? (
<Select placeholder={x.placeholder || `请选择${x.label}`}>
{x.items.map((y) => (
<Option key={y.value} value={y.value}>
{y.label || y.value}
</Option>
))}
</Select>
) : (
<Input.TextArea
autoSize={true}
placeholder={x.placeholder || `请输入${x.label}`}
/>
)}
</Form.Item>
))}
<Button type="primary" htmlType="submit">
+21 -15
View File
@@ -27,9 +27,11 @@ const SecuritySettings = ({ user, userChange }: any) => {
password: values.password,
},
})
.then((data: any) => {
localStorage.removeItem(config.authKey);
history.push('/login');
.then(({ code, data }) => {
if (code === 200) {
localStorage.removeItem(config.authKey);
history.push('/login');
}
})
.catch((error: any) => {
console.log(error);
@@ -48,8 +50,8 @@ const SecuritySettings = ({ user, userChange }: any) => {
const deactiveTowFactor = () => {
request
.put(`${config.apiPrefix}user/two-factor/deactive`)
.then((data: any) => {
if (data.data) {
.then(({ code, data }) => {
if (code === 200 && data) {
setTwoFactorActivated(false);
userChange();
}
@@ -63,14 +65,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
setLoading(true);
request
.put(`${config.apiPrefix}user/two-factor/active`, { data: { code } })
.then((data: any) => {
if (data.data) {
message.success('激活成功');
setTwoFactoring(false);
setTwoFactorActivated(true);
userChange();
} else {
message.success('验证失败');
.then(({ code, data }) => {
if (code === 200) {
if (data) {
message.success('激活成功');
setTwoFactoring(false);
setTwoFactorActivated(true);
userChange();
} else {
message.success('验证失败');
}
}
})
.catch((error: any) => {
@@ -82,8 +86,10 @@ const SecuritySettings = ({ user, userChange }: any) => {
const getTwoFactorInfo = () => {
request
.get(`${config.apiPrefix}user/two-factor/init`)
.then((data: any) => {
setTwoFactorInfo(data.data);
.then(({ code, data }) => {
if (code === 200) {
setTwoFactorInfo(data);
}
})
.catch((error: any) => {
console.log(error);
+18 -25
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
Button,
message,
@@ -29,11 +29,11 @@ import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http';
import SubscriptionModal from './modal';
import { getTableScroll } from '@/utils/index';
import { history, useOutletContext } from '@umijs/max';
import './index.less';
import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
const { Text, Paragraph } = Typography;
const { Search } = Input;
@@ -243,10 +243,11 @@ const Subscription = () => {
const [searchText, setSearchText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [tableScrollHeight, setTableScrollHeight] = useState<number>();
const [searchValue, setSearchValue] = useState('');
const [isLogModalVisible, setIsLogModalVisible] = useState(false);
const [logSubscription, setLogSubscription] = useState<any>();
const tableRef = useRef<any>();
const tableScrollHeight = useTableScrollHeight(tableRef)
const runSubscription = (record: any, index: number) => {
Modal.confirm({
@@ -263,8 +264,8 @@ const Subscription = () => {
onOk() {
request
.put(`${config.apiPrefix}subscriptions/run`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -274,8 +275,6 @@ const Subscription = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -300,8 +299,8 @@ const Subscription = () => {
onOk() {
request
.put(`${config.apiPrefix}subscriptions/stop`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
if (i !== -1) {
@@ -312,8 +311,6 @@ const Subscription = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -327,9 +324,11 @@ const Subscription = () => {
setLoading(true);
request
.get(`${config.apiPrefix}subscriptions?searchValue=${searchText}`)
.then((data: any) => {
setValue(data.data);
setCurrentPage(1);
.then(({ code, data }) => {
if (code === 200) {
setValue(data);
setCurrentPage(1);
}
})
.finally(() => setLoading(false));
};
@@ -359,8 +358,8 @@ const Subscription = () => {
onOk() {
request
.delete(`${config.apiPrefix}subscriptions`, { data: [record.id] })
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
message.success('删除成功');
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -368,8 +367,6 @@ const Subscription = () => {
result.splice(i, 1);
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -402,8 +399,8 @@ const Subscription = () => {
data: [record.id],
},
)
.then((data: any) => {
if (data.code === 200) {
.then(({ code, data }) => {
if (code === 200) {
const newStatus = record.is_disabled === 1 ? 0 : 1;
const result = [...value];
const i = result.findIndex((x) => x.id === record.id);
@@ -414,8 +411,6 @@ const Subscription = () => {
});
setValue(result);
}
} else {
message.error(data);
}
});
},
@@ -545,9 +540,6 @@ const Subscription = () => {
useEffect(() => {
setPageSize(parseInt(localStorage.getItem('pageSize') || '20'));
setTimeout(() => {
setTableScrollHeight(getTableScroll());
});
}, []);
return (
@@ -574,6 +566,7 @@ const Subscription = () => {
}}
>
<Table
ref={tableRef}
columns={columns}
pagination={{
current: currentPage,
+6 -4
View File
@@ -7,6 +7,7 @@ import {
CheckCircleOutlined,
} from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils';
const SubscriptionLogModal = ({
subscription,
@@ -36,14 +37,15 @@ const SubscriptionLogModal = ({
? logUrl
: `${config.apiPrefix}subscriptions/${subscription.id}/log`,
)
.then((data: any) => {
.then(({ code, data }) => {
if (
code === 200 &&
localStorage.getItem('logSubscription') === String(subscription.id)
) {
const log = data.data as string;
const log = data as string;
setValue(log || '暂无日志');
setExecuting(log && !log.includes('执行结束'));
if (log && !log.includes('执行结束')) {
setExecuting(log && !logEnded(log));
if (log && !logEnded(log)) {
setTimeout(() => {
getCronLog();
}, 2000);
+27 -32
View File
@@ -40,8 +40,6 @@ const SubscriptionModal = ({
if (code === 200) {
message.success(subscription ? '更新订阅成功' : '新建订阅成功');
handleCancel(data);
} else {
message.error(data);
}
setLoading(false);
} catch (error: any) {
@@ -190,8 +188,8 @@ const SubscriptionModal = ({
};
const onPaste = useCallback((e: any) => {
const text = e.clipboardData.getData('text');
if (!subscription && text.includes('ql ')) {
const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) {
const [
,
type,
@@ -225,6 +223,13 @@ const SubscriptionModal = ({
}
}, []);
const onNamePaste = useCallback((e) => {
const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) {
e.preventDefault();
}
}, []);
useEffect(() => {
if (visible) {
window.addEventListener('paste', onPaste);
@@ -243,35 +248,9 @@ const SubscriptionModal = ({
}
}, [subscription, visible]);
const isFirefox = navigator.userAgent.includes('Firefox');
const isSafari =
navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Chrome');
const isQQBrowser = navigator.userAgent.includes('QQBrowser');
return (
<Modal
title={
subscription ? (
'编辑订阅'
) : (
<span>
<small
style={{
color: '#999',
fontWeight: 400,
fontSize: isFirefox ? 9 : 12,
marginLeft: 2,
zoom: isSafari ? 0.66 : 0.8,
letterSpacing: isQQBrowser ? -2 : 0,
}}
>
repo/raw命令
</small>
</span>
)
}
title={subscription ? '编辑订阅' : '新建订阅'}
open={visible}
forceRender
centered
@@ -291,7 +270,10 @@ const SubscriptionModal = ({
>
<Form form={form} name="form_in_modal" layout="vertical">
<Form.Item name="name" label="名称">
<Input placeholder="请输入订阅名" />
<Input
placeholder="支持拷贝ql repo/raw命令,粘贴导入"
onPaste={onNamePaste}
/>
</Form.Item>
<Form.Item
name="type"
@@ -457,6 +439,19 @@ const SubscriptionModal = ({
</Form.Item>
</>
)}
<Form.Item
name="proxy"
label="代理"
tooltip="公开仓库支持HTTP/SOCK5代理,私有仓库支持SOCK5代理"
>
<Input
placeholder={
type === 'private-repo'
? 'SOCK5代理,例如 IP:PORT'
: 'HTTP/SOCK5代理,例如 http://127.0.0.1:1080'
}
/>
</Form.Item>
</Form>
</Modal>
);
+43 -4
View File
@@ -56,7 +56,7 @@ export default {
value: 'scripts',
},
{
name: '任务日志',
name: '日志管理',
value: 'logs',
},
{
@@ -74,7 +74,7 @@ export default {
subscriptions: '订阅管理',
configs: '配置文件',
scripts: '脚本管理',
logs: '任务日志',
logs: '日志管理',
dependencies: '依赖管理',
system: '系统信息',
},
@@ -88,10 +88,12 @@ export default {
{ value: 'dingtalkBot', label: '钉钉机器人' },
{ value: 'weWorkBot', label: '企业微信机器人' },
{ value: 'weWorkApp', label: '企业微信应用' },
{ value: 'aibotk', label: '智能微秘书' },
{ value: 'iGot', label: 'IGot' },
{ value: 'pushPlus', label: 'PushPlus' },
{ value: 'chat', label: '群辉chat' },
{ value: 'email', label: '邮箱' },
{ value: 'lark', label: '飞书机器人' },
{ value: 'webhook', label: '自定义通知' },
{ value: 'closed', label: '已关闭' },
],
@@ -135,6 +137,10 @@ export default {
tip: 'PushDeer的Keyhttps://github.com/easychen/pushdeer',
required: true,
},
{
label: 'pushDeerUrl',
tip: 'PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push',
},
],
bark: [
{
@@ -196,6 +202,28 @@ export default {
required: true,
},
],
aibotk: [
{
label: 'aibotkKey',
tip: '密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql',
required: true,
},
{
label: 'aibotkType',
tip: '发送的目标,群组或者好友',
required: true,
placeholder: '请输入要发送的目标',
items: [
{ value: 'room', label: '群聊' },
{ value: 'contact', label: '好友' },
],
},
{
label: 'aibotkName',
tip: '要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
required: true,
},
],
iGot: [
{
label: 'iGotPushKey',
@@ -214,6 +242,13 @@ export default {
tip: '一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
},
],
lark: [
{
label: 'larkKey',
tip: '飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973',
required: true,
},
],
email: [
{
label: 'emailService',
@@ -234,7 +269,11 @@ export default {
label: 'webhookContentType',
tip: '请求头Content-Type',
required: true,
items: [{ value: 'application/json' }, { value: 'multipart/form-data' }, { value: 'application/x-www-form-urlencoded' }],
items: [
{ value: 'application/json' },
{ value: 'multipart/form-data' },
{ value: 'application/x-www-form-urlencoded' },
],
},
{
label: 'webhookUrl',
@@ -263,7 +302,7 @@ export default {
'/config': '配置文件',
'/script': '脚本管理',
'/diff': '对比工具',
'/log': '任务日志',
'/log': '日志管理',
'/setting': '系统设置',
'/error': '错误日志',
},
+1
View File
@@ -0,0 +1 @@
export const LOG_END_SYMBOL = '\n          ';
+17 -1
View File
@@ -54,7 +54,23 @@ _request.interceptors.request.use((url, options) => {
});
_request.interceptors.response.use(async (response) => {
const res = await response.clone();
const responseStatus = response.status;
if ([502, 504].includes(responseStatus)) {
message.error('服务异常,请稍后刷新!');
history.push('/error');
} else if (responseStatus === 401) {
if (history.location.pathname !== '/login') {
localStorage.removeItem(config.authKey);
history.push('/login');
}
} else {
const res = await response.clone().json();
if (res.code !== 200) {
const msg = res.message || res.data;
msg && message.error(msg);
}
return res;
}
return response;
});
+21 -13
View File
@@ -1,3 +1,5 @@
import { LOG_END_SYMBOL } from './const';
export default function browserType() {
// 权重:系统 + 系统版本 > 平台 > 内核 + 载体 + 内核版本 + 载体版本 > 外壳 + 外壳版本
const ua = navigator.userAgent.toLowerCase();
@@ -150,9 +152,9 @@ export default function browserType() {
shell === 'none'
? {}
: {
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
shell, // wechat qq uc 360 2345 sougou liebao maxthon
shellVs,
},
);
console.log(
@@ -178,19 +180,15 @@ export default function browserType() {
*/
export function getTableScroll({
extraHeight,
id,
}: { extraHeight?: number; id?: string } = {}) {
target,
}: { extraHeight?: number; target?: HTMLElement } = {}) {
if (typeof extraHeight == 'undefined') {
// 47 + 40 + 12
extraHeight = 99;
}
let tHeader = null;
if (id) {
tHeader = document.getElementById(id)
? document
.getElementById(id)!
.getElementsByClassName('ant-table-thead')[0]
: null;
if (target) {
tHeader = target;
} else {
tHeader = document.querySelector('.ant-table-wrapper');
}
@@ -200,7 +198,7 @@ export function getTableScroll({
if (tHeader) {
mainTop = tHeader.getBoundingClientRect().top;
}
//窗体高度-表格内容顶部的高度-表格内容底部的高度
let height = document.body.clientHeight - mainTop - extraHeight;
return height;
@@ -244,7 +242,7 @@ export function exportJson(name: string, data: string) {
export function depthFirstSearch<
T extends Record<string, any> & { children?: T[] },
>(children: T[], condition: (column: T) => boolean, item: T) {
>(children: T[], condition: (column: T) => boolean, item?: T) {
const c = [...children];
const keys = [];
@@ -252,6 +250,11 @@ export function depthFirstSearch<
if (!cls) return;
for (let i = 0; i < cls?.length; i++) {
if (condition(cls[i])) {
if (!item) {
cls.splice(i, 1);
return;
}
if (cls[i].children) {
cls[i].children!.unshift(item);
} else {
@@ -268,3 +271,8 @@ export function depthFirstSearch<
return c;
}
export function logEnded(log: string): boolean {
const endTips = [LOG_END_SYMBOL, '执行结束'];
return endTips.some((x) => log.includes(x));
}
+7 -7
View File
@@ -1,8 +1,8 @@
export const version = '2.14.4';
export const changeLogLink = 'https://t.me/jiao_long/331';
export const changeLog = `2.14.4 版本说明
1.
2.
3.
4.
export const version = '2.15.2';
export const changeLogLink = 'https://t.me/jiao_long/353';
export const changeLog = `2.15.2 版本说明
1.
2. task -m CommandTimeoutTime task -m 1h xxx.js
3.
4.
`;
+3
View File
@@ -7,6 +7,9 @@
"./src/types",
"./node_modules/celebrate/lib/index.d.ts"
],
"paths": {
"@/*": ["./back/*"],
},
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,