mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-11 19:05:20 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
046239404f | ||
|
|
b90243f55c | ||
|
|
43c0cd8132 | ||
|
|
533e12a796 | ||
|
|
bd004a0489 | ||
|
|
41befcc0a8 | ||
|
|
9fb9b3d121 | ||
|
|
5055045d22 | ||
|
|
9f7beb934d | ||
|
|
26b06c17c5 | ||
|
|
b8a9b26ca3 | ||
|
|
b6376ed2e8 | ||
|
|
99f6073c8e | ||
|
|
5e73f0390f | ||
|
|
c35cfba8b0 | ||
|
|
aac109621a | ||
|
|
a340964c82 | ||
|
|
00818b694a | ||
|
|
ec5b885476 | ||
|
|
9d55cb108c | ||
|
|
4c19054b30 | ||
|
|
2a41f64d1b | ||
|
|
d3023d31e3 | ||
|
|
eddc03e295 |
+64
-57
@@ -36,6 +36,7 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
- Support cell phone operation
|
- Support cell phone operation
|
||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
### docker
|
### docker
|
||||||
|
|
||||||
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||||
@@ -53,6 +54,66 @@ The npm version supports `debian/ubuntu/centos/alpine` systems and requires `nod
|
|||||||
npm i @whyour/qinglong
|
npm i @whyour/qinglong
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Built-in commands
|
||||||
|
|
||||||
|
1. task
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
||||||
|
task <file_path>
|
||||||
|
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
||||||
|
# and the foreground will output the day, while recorded in the log file
|
||||||
|
task <file_path> now
|
||||||
|
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
||||||
|
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
||||||
|
task <file_path> conc <env_name> <account_number>(Optional)
|
||||||
|
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
# Set task timeout
|
||||||
|
task -m <max_time> <file_path>
|
||||||
|
# Print task log in real time, no need to carry this parameter when creating timed tasks
|
||||||
|
task -l <file_path>
|
||||||
|
# Use -- to split, -- followed by a parameter that is passed to the script, as in the following example, the script receives the parameter -u whyour -p password
|
||||||
|
task <file_path> -- -u whyour -p password
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ql
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update and restart Green Dragon
|
||||||
|
ql update
|
||||||
|
# Run custom scripts extra.sh
|
||||||
|
ql extra
|
||||||
|
# Adding a single script file
|
||||||
|
ql raw <file_url>
|
||||||
|
# Add a specific script for a single repository
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
||||||
|
# Delete old logs
|
||||||
|
ql rmlog <days>
|
||||||
|
# Start bot
|
||||||
|
ql bot
|
||||||
|
# Detecting the Green Dragon environment and repairing it
|
||||||
|
ql check
|
||||||
|
# Reset the number of login errors
|
||||||
|
ql resetlet
|
||||||
|
# Disable two-step login
|
||||||
|
ql resettfa
|
||||||
|
```
|
||||||
|
|
||||||
|
1. parameter description
|
||||||
|
|
||||||
|
- file_url: Script address
|
||||||
|
- repo_url: Repository address
|
||||||
|
- whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
|
||||||
|
- blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
|
||||||
|
- dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
|
||||||
|
- branch: Pull the branch of the repository
|
||||||
|
- days: Number of days of logs to be kept
|
||||||
|
- file_path: File path for task execution
|
||||||
|
- env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
|
||||||
|
- account_number: Specify the account number of an environment variable to be executed when the task is executed
|
||||||
|
- max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
### Docker (Recommended)
|
### Docker (Recommended)
|
||||||
@@ -63,7 +124,7 @@ docker run -dit \
|
|||||||
-v $PWD/ql/data:/ql/data \
|
-v $PWD/ql/data:/ql/data \
|
||||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
# Deployment paths are not required and begin and end with a slash, e.g. /test/.
|
# Deployment paths are not required, e.g. /test.
|
||||||
-e QlBaseUrl="/" \
|
-e QlBaseUrl="/" \
|
||||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||||
-e QlPort="5700" \
|
-e QlPort="5700" \
|
||||||
@@ -95,7 +156,7 @@ podman run -dit \
|
|||||||
-v $PWD/ql/data:/ql/data \
|
-v $PWD/ql/data:/ql/data \
|
||||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
# Deployment paths are not required and begin and end with a slash, e.g. /test/.
|
# Deployment paths are not required, e.g. /test.
|
||||||
-e QlBaseUrl="/" \
|
-e QlBaseUrl="/" \
|
||||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||||
-e QlPort="5700" \
|
-e QlPort="5700" \
|
||||||
@@ -118,60 +179,6 @@ export QL_DATA_DIR=""
|
|||||||
qinglong
|
qinglong
|
||||||
```
|
```
|
||||||
|
|
||||||
## Use
|
|
||||||
|
|
||||||
1. built-in commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Update and restart Green Dragon
|
|
||||||
ql update
|
|
||||||
# Run custom scripts extra.sh
|
|
||||||
ql extra
|
|
||||||
# Adding a single script file
|
|
||||||
ql raw <file_url>
|
|
||||||
# Add a specific script for a single repository
|
|
||||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
|
|
||||||
# Delete old logs
|
|
||||||
ql rmlog <days>
|
|
||||||
# Start bot
|
|
||||||
ql bot
|
|
||||||
# Detecting the Green Dragon environment and repairing it
|
|
||||||
ql check
|
|
||||||
# Reset the number of login errors
|
|
||||||
ql resetlet
|
|
||||||
# Disable two-step login
|
|
||||||
ql resettfa
|
|
||||||
|
|
||||||
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
|
|
||||||
task <file_path>
|
|
||||||
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
|
|
||||||
# and the foreground will output the day, while recorded in the log file
|
|
||||||
task <file_path> now
|
|
||||||
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
|
|
||||||
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
|
|
||||||
task <file_path> conc <env_name> <account_number>(Optional)
|
|
||||||
# Specify the account to execute and run immediately regardless of whether a random delay is set
|
|
||||||
task <file_path> desi <env_name> <account_number>
|
|
||||||
# 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
|
|
||||||
|
|
||||||
* file_url: Script address
|
|
||||||
* repo_url: Repository address
|
|
||||||
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
|
|
||||||
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
|
|
||||||
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
|
|
||||||
* branch: Pull the branch of the repository
|
|
||||||
* days: Number of days of logs to be kept
|
|
||||||
* file_path: File path for task execution
|
|
||||||
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
|
|
||||||
* account_number: Specify the account number of an environment variable to be executed when the task is executed
|
|
||||||
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -184,7 +191,7 @@ $ pnpm install
|
|||||||
$ pnpm start
|
$ pnpm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Open your browser and visit http://127.0.0.1:5700
|
Open your browser and visit <http://127.0.0.1:5700>
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
|||||||
- 支持手机端操作
|
- 支持手机端操作
|
||||||
|
|
||||||
## 版本
|
## 版本
|
||||||
|
|
||||||
### docker
|
### docker
|
||||||
|
|
||||||
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||||
@@ -55,6 +56,65 @@ npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node
|
|||||||
npm i @whyour/qinglong
|
npm i @whyour/qinglong
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 内置命令
|
||||||
|
|
||||||
|
1. task
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
||||||
|
task <file_path>
|
||||||
|
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
||||||
|
task <file_path> now
|
||||||
|
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
||||||
|
task <file_path> conc <env_name> <account_number>(可选的)
|
||||||
|
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
||||||
|
task <file_path> desi <env_name> <account_number>
|
||||||
|
# 设置任务超时时间
|
||||||
|
task -m <max_time> <file_path>
|
||||||
|
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
||||||
|
task -l <file_path>
|
||||||
|
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
|
||||||
|
task <file_path> -- -u whyour -p password
|
||||||
|
```
|
||||||
|
|
||||||
|
1. ql
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 更新并重启青龙
|
||||||
|
ql update
|
||||||
|
# 运行自定义脚本extra.sh
|
||||||
|
ql extra
|
||||||
|
# 添加单个脚本文件
|
||||||
|
ql raw <file_url>
|
||||||
|
# 添加单个仓库的指定脚本
|
||||||
|
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
||||||
|
# 删除旧日志
|
||||||
|
ql rmlog <days>
|
||||||
|
# 启动tg-bot
|
||||||
|
ql bot
|
||||||
|
# 检测青龙环境并修复
|
||||||
|
ql check
|
||||||
|
# 重置登录错误次数
|
||||||
|
ql resetlet
|
||||||
|
# 禁用两步登录
|
||||||
|
ql resettfa
|
||||||
|
```
|
||||||
|
|
||||||
|
1. 参数说明
|
||||||
|
|
||||||
|
- file_url: 脚本地址
|
||||||
|
- repo_url: 仓库地址
|
||||||
|
- whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
||||||
|
- blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
||||||
|
- dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
||||||
|
- extensions: 拉取仓库的文件后缀,多个竖线分割
|
||||||
|
- branch: 拉取仓库的分支
|
||||||
|
- days: 需要保留的日志的天数
|
||||||
|
- file_path: 任务执行时的文件路径
|
||||||
|
- env_name: 任务执行时需要并发或者指定时的环境变量名称
|
||||||
|
- account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
||||||
|
- max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
|
|
||||||
### docker (推荐)
|
### docker (推荐)
|
||||||
@@ -65,7 +125,7 @@ docker run -dit \
|
|||||||
-v $PWD/ql/data:/ql/data \
|
-v $PWD/ql/data:/ql/data \
|
||||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
# 部署路径非必须,比如 /test
|
||||||
-e QlBaseUrl="/" \
|
-e QlBaseUrl="/" \
|
||||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||||
-e QlPort="5700" \
|
-e QlPort="5700" \
|
||||||
@@ -97,7 +157,7 @@ podman run -dit \
|
|||||||
-v $PWD/ql/data:/ql/data \
|
-v $PWD/ql/data:/ql/data \
|
||||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||||
-p 5700:5700 \
|
-p 5700:5700 \
|
||||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
# 部署路径非必须,比如 /test
|
||||||
-e QlBaseUrl="/" \
|
-e QlBaseUrl="/" \
|
||||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||||
-e QlPort="5700" \
|
-e QlPort="5700" \
|
||||||
@@ -120,59 +180,6 @@ export QL_DATA_DIR=""
|
|||||||
qinglong
|
qinglong
|
||||||
```
|
```
|
||||||
|
|
||||||
## 使用
|
|
||||||
|
|
||||||
1. 内置命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 更新并重启青龙
|
|
||||||
ql update
|
|
||||||
# 运行自定义脚本extra.sh
|
|
||||||
ql extra
|
|
||||||
# 添加单个脚本文件
|
|
||||||
ql raw <file_url>
|
|
||||||
# 添加单个仓库的指定脚本
|
|
||||||
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
|
|
||||||
# 删除旧日志
|
|
||||||
ql rmlog <days>
|
|
||||||
# 启动tg-bot
|
|
||||||
ql bot
|
|
||||||
# 检测青龙环境并修复
|
|
||||||
ql check
|
|
||||||
# 重置登录错误次数
|
|
||||||
ql resetlet
|
|
||||||
# 禁用两步登录
|
|
||||||
ql resettfa
|
|
||||||
|
|
||||||
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
|
|
||||||
task <file_path>
|
|
||||||
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
|
|
||||||
task <file_path> now
|
|
||||||
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
|
|
||||||
task <file_path> conc <env_name> <account_number>(可选的)
|
|
||||||
# 指定账号执行,无论是否设置了随机延迟,均立即运行
|
|
||||||
task <file_path> desi <env_name> <account_number>
|
|
||||||
# 设置任务超时时间
|
|
||||||
task -m <max_time> <file_path>
|
|
||||||
# 实时打印任务日志,创建定时任务时,不用携带此参数
|
|
||||||
task -l <file_path>
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 参数说明
|
|
||||||
|
|
||||||
* file_url: 脚本地址
|
|
||||||
* repo_url: 仓库地址
|
|
||||||
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
|
|
||||||
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
|
|
||||||
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
|
|
||||||
* extensions: 拉取仓库的文件后缀,多个竖线分割
|
|
||||||
* branch: 拉取仓库的分支
|
|
||||||
* days: 需要保留的日志的天数
|
|
||||||
* file_path: 任务执行时的文件路径
|
|
||||||
* env_name: 任务执行时需要并发或者指定时的环境变量名称
|
|
||||||
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
|
|
||||||
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+2
-3
@@ -458,13 +458,12 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
const cronService = Container.get(CronService);
|
const cronService = Container.get(CronService);
|
||||||
const data = await cronService.status({
|
const data = await cronService.status({
|
||||||
...req.body,
|
...req.body,
|
||||||
status: parseInt(req.body.status),
|
status: req.body.status ? parseInt(req.body.status) : undefined,
|
||||||
pid: parseInt(req.body.pid) || '',
|
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
||||||
});
|
});
|
||||||
return res.send({ code: 200, data });
|
return res.send({ code: 200, data });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+1
-1
@@ -65,7 +65,7 @@ export default (app: Router) => {
|
|||||||
};
|
};
|
||||||
const filePath = join(config.logPath, path, filename);
|
const filePath = join(config.logPath, path, filename);
|
||||||
if (type === 'directory') {
|
if (type === 'directory') {
|
||||||
emptyDir(filePath);
|
await emptyDir(filePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -183,7 +183,7 @@ export default (app: Router) => {
|
|||||||
};
|
};
|
||||||
const filePath = join(config.scriptPath, path, filename);
|
const filePath = join(config.scriptPath, path, filename);
|
||||||
if (type === 'directory') {
|
if (type === 'directory') {
|
||||||
emptyDir(filePath);
|
await emptyDir(filePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
@@ -260,7 +260,6 @@ export default (app: Router) => {
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger: Logger = Container.get('logger');
|
|
||||||
try {
|
try {
|
||||||
let { filename, path, pid } = req.body;
|
let { filename, path, pid } = req.body;
|
||||||
const { name, ext } = parse(filename);
|
const { name, ext } = parse(filename);
|
||||||
@@ -269,7 +268,9 @@ export default (app: Router) => {
|
|||||||
|
|
||||||
const scriptService = Container.get(ScriptService);
|
const scriptService = Container.get(ScriptService);
|
||||||
const result = await scriptService.stopScript(filePath, pid);
|
const result = await scriptService.stopScript(filePath, pid);
|
||||||
|
setTimeout(() => {
|
||||||
emptyDir(logPath);
|
emptyDir(logPath);
|
||||||
|
}, 3000);
|
||||||
res.send(result);
|
res.send(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return next(e);
|
return next(e);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
|
|||||||
import config from './config';
|
import config from './config';
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import Logger from './loaders/logger';
|
import Logger from './loaders/logger';
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
async function startServer() {
|
async function startServer() {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ let pickedEnv: Record<string, string>;
|
|||||||
function getPickedEnv() {
|
function getPickedEnv() {
|
||||||
if (pickedEnv) return pickedEnv;
|
if (pickedEnv) return pickedEnv;
|
||||||
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
||||||
|
if (picked.QlBaseUrl) {
|
||||||
|
if (!picked.QlBaseUrl.startsWith('/')) {
|
||||||
|
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
|
||||||
|
}
|
||||||
|
if (!picked.QlBaseUrl.endsWith('/')) {
|
||||||
|
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
|
||||||
|
}
|
||||||
|
}
|
||||||
pickedEnv = picked as Record<string, string>;
|
pickedEnv = picked as Record<string, string>;
|
||||||
return picked;
|
return picked;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-7
@@ -84,6 +84,11 @@ export async function getNetIp(req: any) {
|
|||||||
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
|
||||||
ip = '';
|
ip = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!ip) {
|
||||||
|
return { address: `获取失败`, ip };
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const baiduApi = got
|
const baiduApi = got
|
||||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||||
@@ -298,17 +303,21 @@ export function readDir(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function emptyDir(path: string) {
|
export async function emptyDir(path: string) {
|
||||||
|
const pathExist = await fileExist(path);
|
||||||
|
if (!pathExist) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const files = fs.readdirSync(path);
|
const files = fs.readdirSync(path);
|
||||||
files.forEach((file) => {
|
for (const file of files) {
|
||||||
const filePath = `${path}/${file}`;
|
const filePath = `${path}/${file}`;
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
emptyDir(filePath);
|
await emptyDir(filePath);
|
||||||
} else {
|
} else {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
fs.rmdirSync(path);
|
fs.rmdirSync(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,11 +423,11 @@ export function psTree(pid: number): Promise<number[]> {
|
|||||||
|
|
||||||
export async function killTask(pid: number) {
|
export async function killTask(pid: number) {
|
||||||
const pids = await psTree(pid);
|
const pids = await psTree(pid);
|
||||||
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
|
|
||||||
if (pids.length) {
|
if (pids.length) {
|
||||||
try {
|
try {
|
||||||
[pid, ...pids].forEach((x) => {
|
[pid, ...pids].reverse().forEach((x) => {
|
||||||
process.kill(x, 2);
|
process.kill(x, 15);
|
||||||
});
|
});
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export enum NotificationMode {
|
|||||||
'pushMe' = 'pushMe',
|
'pushMe' = 'pushMe',
|
||||||
'feishu' = 'feishu',
|
'feishu' = 'feishu',
|
||||||
'webhook' = 'webhook',
|
'webhook' = 'webhook',
|
||||||
|
'chronocat' = 'Chronocat',
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class NotificationBaseInfo {
|
abstract class NotificationBaseInfo {
|
||||||
@@ -108,6 +109,12 @@ export class PushMeNotification extends NotificationBaseInfo {
|
|||||||
public pushMeKey: string = '';
|
public pushMeKey: string = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ChronocatNotification extends NotificationBaseInfo {
|
||||||
|
public chronocatURL: string = '';
|
||||||
|
public chronocatQQ: string = '';
|
||||||
|
public chronocatToekn: string = '';
|
||||||
|
}
|
||||||
|
|
||||||
export class WebhookNotification extends NotificationBaseInfo {
|
export class WebhookNotification extends NotificationBaseInfo {
|
||||||
public webhookHeaders: string = '';
|
public webhookHeaders: string = '';
|
||||||
public webhookBody: string = '';
|
public webhookBody: string = '';
|
||||||
@@ -140,4 +147,6 @@ export interface NotificationInfo
|
|||||||
EmailNotification,
|
EmailNotification,
|
||||||
PushMeNotification,
|
PushMeNotification,
|
||||||
WebhookNotification,
|
WebhookNotification,
|
||||||
|
ChronocatNotification,
|
||||||
LarkNotification {}
|
LarkNotification {}
|
||||||
|
|
||||||
|
|||||||
@@ -30,13 +30,9 @@ export default async ({ server }: { server: Server }) => {
|
|||||||
|
|
||||||
process.on('uncaughtException', (error) => {
|
process.on('uncaughtException', (error) => {
|
||||||
Logger.error('Uncaught exception:', error);
|
Logger.error('Uncaught exception:', error);
|
||||||
console.error('Uncaught exception:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
Logger.error('Unhandled rejection:', reason, promise);
|
Logger.error('Unhandled rejection:', reason, promise);
|
||||||
console.error('Unhandled rejection:', reason, promise);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ message ICron {
|
|||||||
string schedule = 2;
|
string schedule = 2;
|
||||||
string command = 3;
|
string command = 3;
|
||||||
repeated ISchedule extra_schedules = 4;
|
repeated ISchedule extra_schedules = 4;
|
||||||
|
string name = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddCronRequest { repeated ICron crons = 1; }
|
message AddCronRequest { repeated ICron crons = 1; }
|
||||||
|
|||||||
+15
-1
@@ -24,6 +24,7 @@ export interface ICron {
|
|||||||
schedule: string;
|
schedule: string;
|
||||||
command: string;
|
command: string;
|
||||||
extraSchedules: ISchedule[];
|
extraSchedules: ISchedule[];
|
||||||
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddCronRequest {
|
export interface AddCronRequest {
|
||||||
@@ -97,7 +98,7 @@ export const ISchedule = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function createBaseICron(): ICron {
|
function createBaseICron(): ICron {
|
||||||
return { id: "", schedule: "", command: "", extraSchedules: [] };
|
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ICron = {
|
export const ICron = {
|
||||||
@@ -114,6 +115,9 @@ export const ICron = {
|
|||||||
for (const v of message.extraSchedules) {
|
for (const v of message.extraSchedules) {
|
||||||
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
||||||
}
|
}
|
||||||
|
if (message.name !== "") {
|
||||||
|
writer.uint32(42).string(message.name);
|
||||||
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -152,6 +156,13 @@ export const ICron = {
|
|||||||
|
|
||||||
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||||
continue;
|
continue;
|
||||||
|
case 5:
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.name = reader.string();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
@@ -169,6 +180,7 @@ export const ICron = {
|
|||||||
extraSchedules: Array.isArray(object?.extraSchedules)
|
extraSchedules: Array.isArray(object?.extraSchedules)
|
||||||
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||||
: [],
|
: [],
|
||||||
|
name: isSet(object.name) ? String(object.name) : "",
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -182,6 +194,7 @@ export const ICron = {
|
|||||||
} else {
|
} else {
|
||||||
obj.extraSchedules = [];
|
obj.extraSchedules = [];
|
||||||
}
|
}
|
||||||
|
message.name !== undefined && (obj.name = message.name);
|
||||||
return obj;
|
return obj;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -195,6 +208,7 @@ export const ICron = {
|
|||||||
message.schedule = object.schedule ?? "";
|
message.schedule = object.schedule ?? "";
|
||||||
message.command = object.command ?? "";
|
message.command = object.command ?? "";
|
||||||
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||||
|
message.name = object.name ?? "";
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
|||||||
import nodeSchedule from 'node-schedule';
|
import nodeSchedule from 'node-schedule';
|
||||||
import { scheduleStacks } from './data';
|
import { scheduleStacks } from './data';
|
||||||
import { runCron } from '../shared/runCron';
|
import { runCron } from '../shared/runCron';
|
||||||
import { QL_PREFIX, TASK_PREFIX } from '../config/const';
|
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
const addCron = (
|
const addCron = (
|
||||||
@@ -11,14 +10,15 @@ const addCron = (
|
|||||||
callback: sendUnaryData<AddCronResponse>,
|
callback: sendUnaryData<AddCronResponse>,
|
||||||
) => {
|
) => {
|
||||||
for (const item of call.request.crons) {
|
for (const item of call.request.crons) {
|
||||||
const { id, schedule, command, extraSchedules } = item;
|
const { id, schedule, command, extraSchedules, name } = item;
|
||||||
if (scheduleStacks.has(id)) {
|
if (scheduleStacks.has(id)) {
|
||||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][创建定时任务], 任务ID: %s, cron: %s, 执行命令: %s',
|
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
id,
|
id,
|
||||||
|
name,
|
||||||
schedule,
|
schedule,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
@@ -26,8 +26,9 @@ const addCron = (
|
|||||||
if (extraSchedules?.length) {
|
if (extraSchedules?.length) {
|
||||||
extraSchedules.forEach(x => {
|
extraSchedules.forEach(x => {
|
||||||
Logger.info(
|
Logger.info(
|
||||||
'[schedule][创建定时任务], 任务ID: %s, cron: %s, 执行命令: %s',
|
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||||
id,
|
id,
|
||||||
|
name,
|
||||||
x.schedule,
|
x.schedule,
|
||||||
command,
|
command,
|
||||||
);
|
);
|
||||||
@@ -37,13 +38,13 @@ const addCron = (
|
|||||||
scheduleStacks.set(id, [
|
scheduleStacks.set(id, [
|
||||||
nodeSchedule.scheduleJob(id, schedule, async () => {
|
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
runCron(command);
|
runCron(command, { name, schedule, extraSchedules });
|
||||||
}),
|
}),
|
||||||
...(extraSchedules?.length
|
...(extraSchedules?.length
|
||||||
? extraSchedules.map((x) =>
|
? extraSchedules.map((x) =>
|
||||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||||
runCron(command);
|
runCron(command, { name, schedule, extraSchedules });
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
+23
-18
@@ -14,6 +14,8 @@ import cronClient from '../schedule/client';
|
|||||||
import taskLimit from '../shared/pLimit';
|
import taskLimit from '../shared/pLimit';
|
||||||
import { spawn } from 'cross-spawn';
|
import { spawn } from 'cross-spawn';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import pickBy from 'lodash/pickBy';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class CronService {
|
export default class CronService {
|
||||||
@@ -33,7 +35,7 @@ export default class CronService {
|
|||||||
const doc = await this.insert(tab);
|
const doc = await this.insert(tab);
|
||||||
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
|
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
|
||||||
await cronClient.addCron([
|
await cronClient.addCron([
|
||||||
{ id: String(doc.id), schedule: doc.schedule!, command: this.makeCommand(doc), extraSchedules: doc.extra_schedules || [] },
|
{ name: doc.name || '', id: String(doc.id), schedule: doc.schedule!, command: this.makeCommand(doc), extraSchedules: doc.extra_schedules || [] },
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
await this.set_crontab();
|
await this.set_crontab();
|
||||||
@@ -58,6 +60,7 @@ export default class CronService {
|
|||||||
if (this.isSixCron(newDoc) || newDoc.extra_schedules?.length) {
|
if (this.isSixCron(newDoc) || newDoc.extra_schedules?.length) {
|
||||||
await cronClient.addCron([
|
await cronClient.addCron([
|
||||||
{
|
{
|
||||||
|
name: doc.name || '',
|
||||||
id: String(newDoc.id),
|
id: String(newDoc.id),
|
||||||
schedule: newDoc.schedule!,
|
schedule: newDoc.schedule!,
|
||||||
command: this.makeCommand(newDoc),
|
command: this.makeCommand(newDoc),
|
||||||
@@ -89,7 +92,7 @@ export default class CronService {
|
|||||||
last_running_time: number;
|
last_running_time: number;
|
||||||
last_execution_time: number;
|
last_execution_time: number;
|
||||||
}) {
|
}) {
|
||||||
const options: any = {
|
let options: any = {
|
||||||
status,
|
status,
|
||||||
pid,
|
pid,
|
||||||
log_path,
|
log_path,
|
||||||
@@ -99,7 +102,13 @@ export default class CronService {
|
|||||||
options.last_running_time = last_running_time;
|
options.last_running_time = last_running_time;
|
||||||
}
|
}
|
||||||
|
|
||||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
for (const id of ids) {
|
||||||
|
const cron = await this.getDb({ id });
|
||||||
|
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||||
|
options = omit(options, ['status', 'log_path', 'pid']);
|
||||||
|
}
|
||||||
|
await CrontabModel.update({ ...pickBy(options, (v) => v === 0 || !!v) }, { where: { id } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async remove(ids: number[]) {
|
public async remove(ids: number[]) {
|
||||||
@@ -383,15 +392,18 @@ export default class CronService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async runSingle(cronId: number): Promise<number> {
|
private async runSingle(cronId: number): Promise<number | void> {
|
||||||
return taskLimit.runWithCpuLimit(() => {
|
return taskLimit.runWithCronLimit(() => {
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
const cron = await this.getDb({ id: cronId });
|
const cron = await this.getDb({ id: cronId });
|
||||||
|
const params = { name: cron.name, command: cron.command, schedule: cron.schedule, extraSchedules: cron.extra_schedules };
|
||||||
if (cron.status !== CrontabStatus.queued) {
|
if (cron.status !== CrontabStatus.queued) {
|
||||||
resolve();
|
resolve(params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`);
|
||||||
|
|
||||||
let { id, command, log_path } = cron;
|
let { id, command, log_path } = cron;
|
||||||
const uniqPath = await getUniqPath(command, `${id}`);
|
const uniqPath = await getUniqPath(command, `${id}`);
|
||||||
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
|
||||||
@@ -402,10 +414,6 @@ export default class CronService {
|
|||||||
const logPath = `${uniqPath}/${logTime}.log`;
|
const logPath = `${uniqPath}/${logTime}.log`;
|
||||||
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
const absolutePath = path.resolve(config.logPath, `${logPath}`);
|
||||||
|
|
||||||
this.logger.silly('Running job');
|
|
||||||
this.logger.silly('ID: ' + id);
|
|
||||||
this.logger.silly('Original command: ' + command);
|
|
||||||
|
|
||||||
const cp = spawn(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`, { shell: '/bin/bash' });
|
const cp = spawn(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`, { shell: '/bin/bash' });
|
||||||
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
@@ -419,17 +427,12 @@ export default class CronService {
|
|||||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
cp.on('exit', async (code) => {
|
||||||
this.logger.info(
|
|
||||||
`[panel][任务退出] 任务 ${command} 进程id: ${cp.pid}, 退出码 ${code}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cp.on('close', async (code) => {
|
|
||||||
await CrontabModel.update(
|
await CrontabModel.update(
|
||||||
{ status: CrontabStatus.idle, pid: undefined },
|
{ status: CrontabStatus.idle, pid: undefined },
|
||||||
{ where: { id } },
|
{ where: { id } },
|
||||||
);
|
);
|
||||||
resolve();
|
resolve({ ...params, pid: cp.pid, code });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -447,6 +450,7 @@ export default class CronService {
|
|||||||
const sixCron = docs
|
const sixCron = docs
|
||||||
.filter((x) => this.isSixCron(x))
|
.filter((x) => this.isSixCron(x))
|
||||||
.map((doc) => ({
|
.map((doc) => ({
|
||||||
|
name: doc.name || '',
|
||||||
id: String(doc.id),
|
id: String(doc.id),
|
||||||
schedule: doc.schedule!,
|
schedule: doc.schedule!,
|
||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
@@ -498,7 +502,7 @@ export default class CronService {
|
|||||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||||
command = `${TASK_PREFIX}${tab.command}`;
|
command = `${TASK_PREFIX}${tab.command}`;
|
||||||
}
|
}
|
||||||
let commandVariable = `ID=${tab.id} `
|
let commandVariable = `no_tee=true ID=${tab.id} `
|
||||||
if (tab.task_before) {
|
if (tab.task_before) {
|
||||||
commandVariable += `task_before='${tab.task_before.replace(/'/g, "'\\''")
|
commandVariable += `task_before='${tab.task_before.replace(/'/g, "'\\''")
|
||||||
.trim()}' `;
|
.trim()}' `;
|
||||||
@@ -578,6 +582,7 @@ export default class CronService {
|
|||||||
const sixCron = tabs.data
|
const sixCron = tabs.data
|
||||||
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
||||||
.map((doc) => ({
|
.map((doc) => ({
|
||||||
|
name: doc.name || '',
|
||||||
id: String(doc.id),
|
id: String(doc.id),
|
||||||
schedule: doc.schedule!,
|
schedule: doc.schedule!,
|
||||||
command: this.makeCommand(doc),
|
command: this.makeCommand(doc),
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export default class DependenceService {
|
|||||||
this.updateLog(depIds, JSON.stringify(err));
|
this.updateLog(depIds, JSON.stringify(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
const endTime = dayjs();
|
const endTime = dayjs();
|
||||||
const isSucceed = code === 0;
|
const isSucceed = code === 0;
|
||||||
const resultText = isSucceed ? '成功' : '失败';
|
const resultText = isSucceed ? '成功' : '失败';
|
||||||
|
|||||||
+66
-7
@@ -1,12 +1,12 @@
|
|||||||
import { NotificationInfo } from '../data/notify';
|
|
||||||
import { Service, Inject } from 'typedi';
|
|
||||||
import winston from 'winston';
|
|
||||||
import UserService from './user';
|
|
||||||
import got from 'got';
|
|
||||||
import nodemailer from 'nodemailer';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import got from 'got';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
import nodemailer from 'nodemailer';
|
||||||
|
import { Inject, Service } from 'typedi';
|
||||||
|
import winston from 'winston';
|
||||||
import { parseBody, parseHeaders } from '../config/util';
|
import { parseBody, parseHeaders } from '../config/util';
|
||||||
|
import { NotificationInfo } from '../data/notify';
|
||||||
|
import UserService from './user';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class NotificationService {
|
export default class NotificationService {
|
||||||
@@ -31,6 +31,7 @@ export default class NotificationService {
|
|||||||
['pushMe', this.pushMe],
|
['pushMe', this.pushMe],
|
||||||
['webhook', this.webhook],
|
['webhook', this.webhook],
|
||||||
['lark', this.lark],
|
['lark', this.lark],
|
||||||
|
['chronocat', this.chronocat],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private title = '';
|
private title = '';
|
||||||
@@ -195,7 +196,8 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async bark() {
|
private async bark() {
|
||||||
let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } = this.params;
|
let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
|
||||||
|
this.params;
|
||||||
if (!barkPush.startsWith('http')) {
|
if (!barkPush.startsWith('http')) {
|
||||||
barkPush = `https://api.day.app/${barkPush}`;
|
barkPush = `https://api.day.app/${barkPush}`;
|
||||||
}
|
}
|
||||||
@@ -588,6 +590,63 @@ export default class NotificationService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async chronocat() {
|
||||||
|
const { chronocatURL, chronocatQQ, chronocatToekn } = this.params;
|
||||||
|
try {
|
||||||
|
const user_ids = chronocatQQ
|
||||||
|
.match(/user_id=(\d+)/g)
|
||||||
|
?.map((match: any) => match.split('=')[1]);
|
||||||
|
const group_ids = chronocatQQ
|
||||||
|
.match(/group_id=(\d+)/g)
|
||||||
|
?.map((match: any) => match.split('=')[1]);
|
||||||
|
|
||||||
|
const url = `${chronocatURL}/api/message/send`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${chronocatToekn}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [chat_type, ids] of [
|
||||||
|
[1, user_ids],
|
||||||
|
[2, group_ids],
|
||||||
|
]) {
|
||||||
|
if (!ids) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ids: any = ids;
|
||||||
|
for (const chat_id of _ids) {
|
||||||
|
const data = {
|
||||||
|
peer: {
|
||||||
|
chatType: chat_type,
|
||||||
|
peerUin: chat_id,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementType: 1,
|
||||||
|
textElement: {
|
||||||
|
content: `${this.title}\n\n${this.content}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const res: any = await got.post(url, {
|
||||||
|
...this.gotOption,
|
||||||
|
json: data,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
if (res.body === 'success') {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(res.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error: any) {
|
||||||
|
throw new Error(error.response ? error.response.body : error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async webhook() {
|
private async webhook() {
|
||||||
const {
|
const {
|
||||||
webhookUrl,
|
webhookUrl,
|
||||||
|
|||||||
+28
-13
@@ -47,10 +47,17 @@ export default class ScheduleService {
|
|||||||
async runTask(
|
async runTask(
|
||||||
command: string,
|
command: string,
|
||||||
callbacks: TaskCallbacks = {},
|
callbacks: TaskCallbacks = {},
|
||||||
|
params: {
|
||||||
|
schedule?: string;
|
||||||
|
name?: string;
|
||||||
|
command?: string;
|
||||||
|
},
|
||||||
completionTime: 'start' | 'end' = 'end',
|
completionTime: 'start' | 'end' = 'end',
|
||||||
) {
|
) {
|
||||||
return taskLimit.runWithCpuLimit(() => {
|
return taskLimit.runWithCronLimit(() => {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
|
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const startTime = dayjs();
|
const startTime = dayjs();
|
||||||
await callbacks.onBefore?.(startTime);
|
await callbacks.onBefore?.(startTime);
|
||||||
@@ -82,20 +89,14 @@ export default class ScheduleService {
|
|||||||
await callbacks.onError?.(JSON.stringify(err));
|
await callbacks.onError?.(JSON.stringify(err));
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('exit', async (code, signal) => {
|
cp.on('exit', async (code) => {
|
||||||
this.logger.info(
|
|
||||||
`[panel][任务退出] ${command} 进程id: ${cp.pid}, 退出码 ${code}`,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
|
||||||
const endTime = dayjs();
|
const endTime = dayjs();
|
||||||
await callbacks.onEnd?.(
|
await callbacks.onEnd?.(
|
||||||
cp,
|
cp,
|
||||||
endTime,
|
endTime,
|
||||||
endTime.diff(startTime, 'seconds'),
|
endTime.diff(startTime, 'seconds'),
|
||||||
);
|
);
|
||||||
resolve(null);
|
resolve({ ...params, pid: cp.pid, code });
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -126,12 +127,20 @@ export default class ScheduleService {
|
|||||||
this.scheduleStacks.set(
|
this.scheduleStacks.set(
|
||||||
_id,
|
_id,
|
||||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||||
this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
schedule,
|
||||||
|
command,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +169,10 @@ export default class ScheduleService {
|
|||||||
const task = new Task(
|
const task = new Task(
|
||||||
name,
|
name,
|
||||||
() => {
|
() => {
|
||||||
this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
this.logger.error(
|
this.logger.error(
|
||||||
@@ -180,7 +192,10 @@ export default class ScheduleService {
|
|||||||
this.intervalSchedule.addIntervalJob(job);
|
this.intervalSchedule.addIntervalJob(job);
|
||||||
|
|
||||||
if (runImmediately) {
|
if (runImmediately) {
|
||||||
this.runTask(command, callbacks);
|
this.runTask(command, callbacks, {
|
||||||
|
name,
|
||||||
|
command,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export default class ScriptService {
|
|||||||
const pid = await this.scheduleService.runTask(
|
const pid = await this.scheduleService.runTask(
|
||||||
command,
|
command,
|
||||||
this.taskCallbacks(filePath),
|
this.taskCallbacks(filePath),
|
||||||
|
{ command },
|
||||||
'start',
|
'start',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -320,7 +320,11 @@ export default class SubscriptionService {
|
|||||||
|
|
||||||
const command = formatCommand(subscription);
|
const command = formatCommand(subscription);
|
||||||
|
|
||||||
this.scheduleService.runTask(command, this.taskCallbacks(subscription));
|
this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
|
||||||
|
name: subscription.name,
|
||||||
|
schedule: subscription.schedule,
|
||||||
|
command
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async disabled(ids: number[]) {
|
public async disabled(ids: number[]) {
|
||||||
|
|||||||
@@ -232,6 +232,9 @@ export default class SystemService {
|
|||||||
this.scheduleService.runTask(
|
this.scheduleService.runTask(
|
||||||
`real_log_path=${logPath} real_time=true ${command}`,
|
`real_log_path=${logPath} real_time=true ${command}`,
|
||||||
callback,
|
callback,
|
||||||
|
{
|
||||||
|
command,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-20
@@ -1,29 +1,62 @@
|
|||||||
import pLimit from 'p-limit';
|
import PQueue, { QueueAddOptions } from 'p-queue-cjs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import { AuthDataType, AuthModel } from '../data/auth';
|
import { AuthDataType, AuthModel } from '../data/auth';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
class TaskLimit {
|
class TaskLimit {
|
||||||
private oneLimit = pLimit(1);
|
private oneLimit = new PQueue({ concurrency: 1 });
|
||||||
private updateLogLimit = pLimit(1);
|
private updateLogLimit = new PQueue({ concurrency: 1 });
|
||||||
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
|
private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
|
||||||
|
|
||||||
get cpuLimitActiveCount() {
|
get cronLimitActiveCount() {
|
||||||
return this.cpuLimit.activeCount;
|
return this.cronLimit.pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
get cpuLimitPendingCount() {
|
get cronLimitPendingCount() {
|
||||||
return this.cpuLimit.pendingCount;
|
return this.cronLimit.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.setCustomLimit();
|
this.setCustomLimit();
|
||||||
|
this.handleEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleEvents() {
|
||||||
|
this.cronLimit.on('add', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
this.cronLimit.on('active', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
this.cronLimit.on('completed', (param) => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('error', error => {
|
||||||
|
Logger.error(
|
||||||
|
`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('next', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this.cronLimit.on('idle', () => {
|
||||||
|
Logger.info(
|
||||||
|
`[schedule][任务队列] 空闲中...`,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setCustomLimit(limit?: number) {
|
public async setCustomLimit(limit?: number) {
|
||||||
if (limit) {
|
if (limit) {
|
||||||
this.cpuLimit = pLimit(limit);
|
this.cronLimit.concurrency = limit;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await AuthModel.sync();
|
await AuthModel.sync();
|
||||||
@@ -31,23 +64,20 @@ class TaskLimit {
|
|||||||
where: { type: AuthDataType.systemConfig },
|
where: { type: AuthDataType.systemConfig },
|
||||||
});
|
});
|
||||||
if (doc?.info?.cronConcurrency) {
|
if (doc?.info?.cronConcurrency) {
|
||||||
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
|
this.cronLimit.concurrency = doc.info.cronConcurrency;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
|
public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||||
Logger.info(
|
return this.cronLimit.add(fn, options);
|
||||||
`[schedule][任务加入队列] 运行中任务数: ${this.cpuLimitActiveCount}, 等待中任务数: ${this.cpuLimitPendingCount}`,
|
|
||||||
);
|
|
||||||
return this.cpuLimit(fn);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
|
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||||
return this.oneLimit(fn);
|
return this.oneLimit.add(fn, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateDepLog<T>(fn: () => Promise<T>): Promise<T> {
|
public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||||
return this.updateLogLimit(fn);
|
return this.updateLogLimit.add(fn, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { spawn } from 'cross-spawn';
|
|||||||
import taskLimit from './pLimit';
|
import taskLimit from './pLimit';
|
||||||
import Logger from '../loaders/logger';
|
import Logger from '../loaders/logger';
|
||||||
|
|
||||||
export function runCron(cmd: string): Promise<number> {
|
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
|
||||||
return taskLimit.runWithCpuLimit(() => {
|
return taskLimit.runWithCronLimit(() => {
|
||||||
return new Promise(async (resolve: any) => {
|
return new Promise(async (resolve: any) => {
|
||||||
Logger.info(`[schedule][开始执行任务] 运行命令: ${cmd}`);
|
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
|
||||||
|
|
||||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||||
|
|
||||||
cp.stderr.on('data', (data) => {
|
cp.stderr.on('data', (data) => {
|
||||||
@@ -24,9 +23,8 @@ export function runCron(cmd: string): Promise<number> {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
cp.on('close', async (code) => {
|
cp.on('exit', async (code) => {
|
||||||
Logger.info(`[schedule][任务退出] ${cmd} 进程id: ${cp.pid} 退出, 退出码 ${code}`);
|
resolve({ ...options, command: cmd, pid: cp.pid, code });
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@
|
|||||||
"nedb": "^1.8.0",
|
"nedb": "^1.8.0",
|
||||||
"node-schedule": "^2.1.0",
|
"node-schedule": "^2.1.0",
|
||||||
"nodemailer": "^6.7.2",
|
"nodemailer": "^6.7.2",
|
||||||
"p-limit": "3.1.0",
|
"p-queue-cjs": "7.3.4",
|
||||||
"protobufjs": "^7.2.3",
|
"protobufjs": "^7.2.3",
|
||||||
"pstree.remy": "^1.1.8",
|
"pstree.remy": "^1.1.8",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
|
|||||||
Generated
+228
-159
File diff suppressed because it is too large
Load Diff
@@ -173,4 +173,13 @@ export SMTP_NAME=""
|
|||||||
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
|
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
|
||||||
export PUSHME_KEY=""
|
export PUSHME_KEY=""
|
||||||
|
|
||||||
|
## 13. CHRONOCAT
|
||||||
|
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
|
||||||
|
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
|
||||||
|
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
|
||||||
|
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
|
||||||
|
export CHRONOCAT_URL=""
|
||||||
|
export CHRONOCAT_QQ="" #
|
||||||
|
export CHRONOCAT_TOKEN=""
|
||||||
|
|
||||||
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const querystring = require('querystring');
|
const querystring = require('querystring');
|
||||||
|
const got = require('got');
|
||||||
const $ = new Env();
|
const $ = new Env();
|
||||||
const timeout = 15000; //超时时间(单位毫秒)
|
const timeout = 15000; //超时时间(单位毫秒)
|
||||||
// =======================================gotify通知设置区域==============================================
|
// =======================================gotify通知设置区域==============================================
|
||||||
@@ -150,6 +151,23 @@ let SMTP_NAME = '';
|
|||||||
//此处填你的PushMe KEY.
|
//此处填你的PushMe KEY.
|
||||||
let PUSHME_KEY = '';
|
let PUSHME_KEY = '';
|
||||||
|
|
||||||
|
// =======================================CHRONOCAT通知设置区域===========================================
|
||||||
|
// CHRONOCAT_URL Red协议连接地址 例: http://127.0.0.1:16530
|
||||||
|
// CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
|
||||||
|
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
|
||||||
|
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
|
||||||
|
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
|
||||||
|
let CHRONOCAT_TOKEN = ''; //CHRONOCAT 生成的访问密钥
|
||||||
|
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
|
||||||
|
|
||||||
|
// =======================================自定义通知设置区域=======================================
|
||||||
|
// 自定义通知 接收回调的URL
|
||||||
|
let WEBHOOK_URL = '';
|
||||||
|
let WEBHOOK_BODY = '';
|
||||||
|
let WEBHOOK_HEADERS = '';
|
||||||
|
let WEBHOOK_METHOD = '';
|
||||||
|
let WEBHOOK_CONTENT_TYPE = '';
|
||||||
|
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
if (process.env.GOTIFY_URL) {
|
if (process.env.GOTIFY_URL) {
|
||||||
GOTIFY_URL = process.env.GOTIFY_URL;
|
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||||
@@ -306,6 +324,32 @@ if (process.env.SMTP_NAME) {
|
|||||||
if (process.env.PUSHME_KEY) {
|
if (process.env.PUSHME_KEY) {
|
||||||
PUSHME_KEY = process.env.PUSHME_KEY;
|
PUSHME_KEY = process.env.PUSHME_KEY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.env.CHRONOCAT_URL) {
|
||||||
|
CHRONOCAT_URL = process.env.CHRONOCAT_URL;
|
||||||
|
}
|
||||||
|
if (process.env.CHRONOCAT_QQ) {
|
||||||
|
CHRONOCAT_QQ = process.env.CHRONOCAT_QQ;
|
||||||
|
}
|
||||||
|
if (process.env.CHRONOCAT_TOKEN) {
|
||||||
|
CHRONOCAT_TOKEN = process.env.CHRONOCAT_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.WEBHOOK_URL) {
|
||||||
|
WEBHOOK_URL = process.env.WEBHOOK_URL;
|
||||||
|
}
|
||||||
|
if (process.env.WEBHOOK_BODY) {
|
||||||
|
WEBHOOK_BODY = process.env.WEBHOOK_BODY;
|
||||||
|
}
|
||||||
|
if (process.env.WEBHOOK_HEADERS) {
|
||||||
|
WEBHOOK_HEADERS = process.env.WEBHOOK_HEADERS;
|
||||||
|
}
|
||||||
|
if (process.env.WEBHOOK_METHOD) {
|
||||||
|
WEBHOOK_METHOD = process.env.WEBHOOK_METHOD;
|
||||||
|
}
|
||||||
|
if (process.env.WEBHOOK_CONTENT_TYPE) {
|
||||||
|
WEBHOOK_CONTENT_TYPE = process.env.WEBHOOK_CONTENT_TYPE;
|
||||||
|
}
|
||||||
//==========================云端环境变量的判断与接收=========================
|
//==========================云端环境变量的判断与接收=========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,6 +399,8 @@ async function sendNotify(
|
|||||||
fsBotNotify(text, desp), //飞书机器人
|
fsBotNotify(text, desp), //飞书机器人
|
||||||
smtpNotify(text, desp), //SMTP 邮件
|
smtpNotify(text, desp), //SMTP 邮件
|
||||||
PushMeNotify(text, desp, params), //PushMe
|
PushMeNotify(text, desp, params), //PushMe
|
||||||
|
ChronocatNotify(text, desp), // Chronocat
|
||||||
|
webhookNotify(text, desp), //自定义通知
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,6 +1217,220 @@ function PushMeNotify(text, desp, params = {}) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ChronocatNotify(title, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {
|
||||||
|
console.log(
|
||||||
|
'CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('CHRONOCAT 服务启动');
|
||||||
|
const user_ids = CHRONOCAT_QQ.match(/user_id=(\d+)/g)?.map(
|
||||||
|
(match) => match.split('=')[1],
|
||||||
|
);
|
||||||
|
const group_ids = CHRONOCAT_QQ.match(/group_id=(\d+)/g)?.map(
|
||||||
|
(match) => match.split('=')[1],
|
||||||
|
);
|
||||||
|
|
||||||
|
const url = `${CHRONOCAT_URL}/api/message/send`;
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: `Bearer ${CHRONOCAT_TOKEN}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [chat_type, ids] of [
|
||||||
|
[1, user_ids],
|
||||||
|
[2, group_ids],
|
||||||
|
]) {
|
||||||
|
if (!ids) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const chat_id of ids) {
|
||||||
|
const data = {
|
||||||
|
peer: {
|
||||||
|
chatType: chat_type,
|
||||||
|
peerUin: chat_id,
|
||||||
|
},
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementType: 1,
|
||||||
|
textElement: {
|
||||||
|
content: `${title}\n\n${desp}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const options = {
|
||||||
|
url: url,
|
||||||
|
json: data,
|
||||||
|
headers,
|
||||||
|
timeout,
|
||||||
|
};
|
||||||
|
$.post(options, (err, resp, data) => {
|
||||||
|
try {
|
||||||
|
if (err) {
|
||||||
|
console.log('Chronocat发送QQ通知消息失败!!\n');
|
||||||
|
console.log(err);
|
||||||
|
} else {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (chat_type === 1) {
|
||||||
|
console.log(`QQ个人消息:${ids}推送成功!`);
|
||||||
|
} else {
|
||||||
|
console.log(`QQ群消息:${ids}推送成功!`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function webhookNotify(text, desp) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const { formatBody, formatUrl } = formatNotifyContentFun(
|
||||||
|
WEBHOOK_URL,
|
||||||
|
WEBHOOK_BODY,
|
||||||
|
text,
|
||||||
|
desp,
|
||||||
|
);
|
||||||
|
if (!formatUrl && !formatBody) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const headers = parseHeaders(WEBHOOK_HEADERS);
|
||||||
|
const body = parseBody(formatBody, WEBHOOK_CONTENT_TYPE);
|
||||||
|
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
|
||||||
|
const options = {
|
||||||
|
method: WEBHOOK_METHOD,
|
||||||
|
headers,
|
||||||
|
allowGetBody: true,
|
||||||
|
...bodyParam,
|
||||||
|
timeout,
|
||||||
|
retry: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (WEBHOOK_METHOD) {
|
||||||
|
got(formatUrl, options).then((resp) => {
|
||||||
|
try {
|
||||||
|
if (resp.statusCode !== 200) {
|
||||||
|
console.log('自定义发送通知消息失败!!\n');
|
||||||
|
console.log(resp.body);
|
||||||
|
} else {
|
||||||
|
console.log('自定义发送通知消息成功🎉。\n');
|
||||||
|
console.log(resp.body);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
$.logErr(e, resp);
|
||||||
|
} finally {
|
||||||
|
resolve(resp.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHeaders(headers) {
|
||||||
|
if (!headers) return {};
|
||||||
|
|
||||||
|
const parsed = {};
|
||||||
|
let key;
|
||||||
|
let val;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
headers &&
|
||||||
|
headers.split('\n').forEach(function parser(line) {
|
||||||
|
i = line.indexOf(':');
|
||||||
|
key = line.substring(0, i).trim().toLowerCase();
|
||||||
|
val = line.substring(i + 1).trim();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBody(body, contentType) {
|
||||||
|
if (!body) return '';
|
||||||
|
|
||||||
|
const parsed = {};
|
||||||
|
let key;
|
||||||
|
let val;
|
||||||
|
let i;
|
||||||
|
|
||||||
|
body &&
|
||||||
|
body.split('\n').forEach(function parser(line) {
|
||||||
|
i = line.indexOf(':');
|
||||||
|
key = line.substring(0, i).trim().toLowerCase();
|
||||||
|
val = line.substring(i + 1).trim();
|
||||||
|
|
||||||
|
if (!key || parsed[key]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonValue = JSON.parse(val);
|
||||||
|
parsed[key] = jsonValue;
|
||||||
|
} catch (error) {
|
||||||
|
parsed[key] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (contentType) {
|
||||||
|
case 'multipart/form-data':
|
||||||
|
return Object.keys(parsed).reduce((p, c) => {
|
||||||
|
p.append(c, parsed[c]);
|
||||||
|
return p;
|
||||||
|
}, new FormData());
|
||||||
|
case 'application/x-www-form-urlencoded':
|
||||||
|
return Object.keys(parsed).reduce((p, c) => {
|
||||||
|
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBodyFun(contentType, body) {
|
||||||
|
if (!body) return {};
|
||||||
|
switch (contentType) {
|
||||||
|
case 'application/json':
|
||||||
|
return { json: body };
|
||||||
|
case 'multipart/form-data':
|
||||||
|
return { form: body };
|
||||||
|
case 'application/x-www-form-urlencoded':
|
||||||
|
return { body };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatNotifyContentFun(url, body, title, content) {
|
||||||
|
if (!url.includes('$title') && !body.includes('$title')) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
formatUrl: url
|
||||||
|
.replaceAll('$title', encodeURIComponent(title))
|
||||||
|
.replaceAll('$content', encodeURIComponent(content)),
|
||||||
|
formatBody: body
|
||||||
|
.replaceAll('$title', title)
|
||||||
|
.replaceAll('$content', content),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sendNotify,
|
sendNotify,
|
||||||
BARK_PUSH,
|
BARK_PUSH,
|
||||||
|
|||||||
+176
-2
@@ -102,6 +102,16 @@ push_config = {
|
|||||||
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
|
||||||
|
|
||||||
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
|
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
|
||||||
|
|
||||||
|
'CHRONOCAT_QQ': '', # qq号
|
||||||
|
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
|
||||||
|
'CHRONOCAT_URL': '', # CHRONOCAT的url地址
|
||||||
|
|
||||||
|
'WEBHOOK_URL': '', # 自定义通知 请求地址
|
||||||
|
'WEBHOOK_BODY': '', # 自定义通知 请求体
|
||||||
|
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
|
||||||
|
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
|
||||||
|
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
|
||||||
}
|
}
|
||||||
notify_function = []
|
notify_function = []
|
||||||
# fmt: on
|
# fmt: on
|
||||||
@@ -446,7 +456,9 @@ class WeCom:
|
|||||||
return data["access_token"]
|
return data["access_token"]
|
||||||
|
|
||||||
def send_text(self, message, touser="@all"):
|
def send_text(self, message, touser="@all"):
|
||||||
send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
send_url = (
|
||||||
|
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||||
|
)
|
||||||
send_values = {
|
send_values = {
|
||||||
"touser": touser,
|
"touser": touser,
|
||||||
"msgtype": "text",
|
"msgtype": "text",
|
||||||
@@ -460,7 +472,9 @@ class WeCom:
|
|||||||
return respone["errmsg"]
|
return respone["errmsg"]
|
||||||
|
|
||||||
def send_mpnews(self, title, message, media_id, touser="@all"):
|
def send_mpnews(self, title, message, media_id, touser="@all"):
|
||||||
send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
send_url = (
|
||||||
|
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
|
||||||
|
)
|
||||||
send_values = {
|
send_values = {
|
||||||
"touser": touser,
|
"touser": touser,
|
||||||
"msgtype": "mpnews",
|
"msgtype": "mpnews",
|
||||||
@@ -643,6 +657,7 @@ def smtp(title: str, content: str) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"SMTP 邮件 推送失败!{e}")
|
print(f"SMTP 邮件 推送失败!{e}")
|
||||||
|
|
||||||
|
|
||||||
def pushme(title: str, content: str) -> None:
|
def pushme(title: str, content: str) -> None:
|
||||||
"""
|
"""
|
||||||
使用 PushMe 推送消息。
|
使用 PushMe 推送消息。
|
||||||
@@ -665,6 +680,157 @@ def pushme(title: str, content: str) -> None:
|
|||||||
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
print(f"PushMe 推送失败!{response.status_code} {response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
def chronocat(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
使用 CHRONOCAT 推送消息。
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
not push_config.get("CHRONOCAT_URL")
|
||||||
|
or not push_config.get("CHRONOCAT_QQ")
|
||||||
|
or not push_config.get("CHRONOCAT_TOKEN")
|
||||||
|
):
|
||||||
|
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("CHRONOCAT 服务启动")
|
||||||
|
|
||||||
|
user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
|
||||||
|
group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
|
||||||
|
|
||||||
|
url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send'
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": f'Bearer {push_config.get("CHRONOCAT_TOKEN")}',
|
||||||
|
}
|
||||||
|
|
||||||
|
for chat_type, ids in [(1, user_ids), (2, group_ids)]:
|
||||||
|
if not ids:
|
||||||
|
continue
|
||||||
|
for chat_id in ids:
|
||||||
|
data = {
|
||||||
|
"peer": {"chatType": chat_type, "peerUin": chat_id},
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"elementType": 1,
|
||||||
|
"textElement": {"content": f"{title}\n\n{content}"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, data=json.dumps(data))
|
||||||
|
if response.status_code == 200:
|
||||||
|
if chat_type == 1:
|
||||||
|
print(f"QQ个人消息:{ids}推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"QQ群消息:{ids}推送成功!")
|
||||||
|
else:
|
||||||
|
if chat_type == 1:
|
||||||
|
print(f"QQ个人消息:{ids}推送失败!")
|
||||||
|
else:
|
||||||
|
print(f"QQ群消息:{ids}推送失败!")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_headers(headers):
|
||||||
|
if not headers:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
lines = headers.split("\n")
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
i = line.find(":")
|
||||||
|
if i == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = line[:i].strip().lower()
|
||||||
|
val = line[i + 1 :].strip()
|
||||||
|
parsed[key] = parsed.get(key, "") + ", " + val if key in parsed else val
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def parse_body(body, content_type):
|
||||||
|
if not body:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
parsed = {}
|
||||||
|
lines = body.split("\n")
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
i = line.find(":")
|
||||||
|
if i == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = line[:i].strip().lower()
|
||||||
|
val = line[i + 1 :].strip()
|
||||||
|
|
||||||
|
if not key or key in parsed:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
json_value = json.loads(val)
|
||||||
|
parsed[key] = json_value
|
||||||
|
except:
|
||||||
|
parsed[key] = val
|
||||||
|
|
||||||
|
if content_type == "application/x-www-form-urlencoded":
|
||||||
|
data = urlencode(parsed, doseq=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
if content_type == "application/json":
|
||||||
|
data = json.dumps(parsed)
|
||||||
|
return data
|
||||||
|
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def format_notify_content(url, body, title, content):
|
||||||
|
if "$title" not in url and "$title" not in body:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
formatted_url = url.replace("$title", urllib.parse.quote_plus(title)).replace(
|
||||||
|
"$content", urllib.parse.quote_plus(content)
|
||||||
|
)
|
||||||
|
formatted_body = body.replace("$title", title).replace("$content", content)
|
||||||
|
|
||||||
|
return formatted_url, formatted_body
|
||||||
|
|
||||||
|
|
||||||
|
def custom_notify(title: str, content: str) -> None:
|
||||||
|
"""
|
||||||
|
通过 自定义通知 推送消息。
|
||||||
|
"""
|
||||||
|
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
|
||||||
|
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("自定义通知服务启动")
|
||||||
|
|
||||||
|
WEBHOOK_URL = push_config.get("WEBHOOK_URL")
|
||||||
|
WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD")
|
||||||
|
WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE")
|
||||||
|
WEBHOOK_BODY = push_config.get("WEBHOOK_BODY")
|
||||||
|
WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS")
|
||||||
|
|
||||||
|
formatUrl, formatBody = format_notify_content(
|
||||||
|
WEBHOOK_URL, WEBHOOK_BODY, title, content
|
||||||
|
)
|
||||||
|
|
||||||
|
if not formatUrl and not formatBody:
|
||||||
|
print("请求头或者请求体中必须包含 $title 和 $content")
|
||||||
|
return
|
||||||
|
|
||||||
|
headers = parse_headers(WEBHOOK_HEADERS)
|
||||||
|
body = parse_body(formatBody, WEBHOOK_CONTENT_TYPE)
|
||||||
|
response = requests.request(
|
||||||
|
method=WEBHOOK_METHOD, url=formatUrl, headers=headers, timeout=15, data=body
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("自定义通知推送成功!")
|
||||||
|
else:
|
||||||
|
print(f"自定义通知推送失败!{response.status_code} {response.text}")
|
||||||
|
|
||||||
|
|
||||||
def one() -> str:
|
def one() -> str:
|
||||||
"""
|
"""
|
||||||
获取一条一言。
|
获取一条一言。
|
||||||
@@ -721,6 +887,14 @@ if (
|
|||||||
notify_function.append(smtp)
|
notify_function.append(smtp)
|
||||||
if push_config.get("PUSHME_KEY"):
|
if push_config.get("PUSHME_KEY"):
|
||||||
notify_function.append(pushme)
|
notify_function.append(pushme)
|
||||||
|
if (
|
||||||
|
push_config.get("CHRONOCAT_URL")
|
||||||
|
and push_config.get("CHRONOCAT_QQ")
|
||||||
|
and push_config.get("CHRONOCAT_TOKEN")
|
||||||
|
):
|
||||||
|
notify_function.append(chronocat)
|
||||||
|
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||||
|
notify_function.append(custom_notify)
|
||||||
|
|
||||||
|
|
||||||
def send(title: str, content: str) -> None:
|
def send(title: str, content: str) -> None:
|
||||||
|
|||||||
+8
-6
@@ -105,7 +105,7 @@ run_normal() {
|
|||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
$timeoutCmd $which_program $file_param
|
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||||
@@ -147,7 +147,7 @@ run_concurrent() {
|
|||||||
for i in "${!array[@]}"; do
|
for i in "${!array[@]}"; do
|
||||||
export "${env_param}=${array[i]}"
|
export "${env_param}=${array[i]}"
|
||||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||||
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
|
||||||
done
|
done
|
||||||
|
|
||||||
wait
|
wait
|
||||||
@@ -190,7 +190,7 @@ run_designated() {
|
|||||||
cd ${relative_path}
|
cd ${relative_path}
|
||||||
file_param=${file_param/$relative_path\//}
|
file_param=${file_param/$relative_path\//}
|
||||||
fi
|
fi
|
||||||
$timeoutCmd $which_program $file_param
|
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
## 运行其他命令
|
## 运行其他命令
|
||||||
@@ -241,6 +241,8 @@ main() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_task_before "$@"
|
handle_task_start "${task_shell_params[@]}"
|
||||||
main "$@"
|
run_task_before "${task_shell_params[@]}"
|
||||||
handle_task_after "$@"
|
main "${task_shell_params[@]}"
|
||||||
|
run_task_after "${task_shell_params[@]}"
|
||||||
|
handle_task_end "${task_shell_params[@]}"
|
||||||
|
|||||||
+18
-11
@@ -100,9 +100,6 @@ set_proxy() {
|
|||||||
unset_proxy() {
|
unset_proxy() {
|
||||||
unset http_proxy
|
unset http_proxy
|
||||||
unset https_proxy
|
unset https_proxy
|
||||||
unset ftp_proxy
|
|
||||||
unset all_proxy
|
|
||||||
unset no_proxy
|
|
||||||
}
|
}
|
||||||
|
|
||||||
make_dir() {
|
make_dir() {
|
||||||
@@ -311,10 +308,8 @@ random_range() {
|
|||||||
|
|
||||||
reload_pm2() {
|
reload_pm2() {
|
||||||
cd $dir_root
|
cd $dir_root
|
||||||
# 代理会影响 grpc 服务
|
|
||||||
unset_proxy
|
|
||||||
pm2 flush &>/dev/null
|
pm2 flush &>/dev/null
|
||||||
pm2 startOrGracefulReload $file_ecosystem_js --update-env
|
env ALL_PROXY= HTTP_PROXY= HTTPS_PROXY= all_proxy= http_proxy= https_proxy= pm2 startOrGracefulReload $file_ecosystem_js --update-env
|
||||||
}
|
}
|
||||||
|
|
||||||
diff_time() {
|
diff_time() {
|
||||||
@@ -407,8 +402,18 @@ init_nginx() {
|
|||||||
local aliasStr=""
|
local aliasStr=""
|
||||||
local rootStr=""
|
local rootStr=""
|
||||||
if [[ $ql_base_url != "/" ]]; then
|
if [[ $ql_base_url != "/" ]]; then
|
||||||
|
if [[ $ql_base_url != /* ]]; then
|
||||||
|
ql_base_url="/$ql_base_url"
|
||||||
|
fi
|
||||||
|
if [[ $ql_base_url != */ ]]; then
|
||||||
|
ql_base_url="$ql_base_url/"
|
||||||
|
fi
|
||||||
location_url="^~${ql_base_url%*/}"
|
location_url="^~${ql_base_url%*/}"
|
||||||
aliasStr="alias ${dir_static}/dist;"
|
aliasStr="alias ${dir_static}/dist;"
|
||||||
|
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
|
||||||
|
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
|
||||||
|
mv temp.html "${dir_static}/dist/index.html"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
rootStr="root ${dir_static}/dist;"
|
rootStr="root ${dir_static}/dist;"
|
||||||
fi
|
fi
|
||||||
@@ -428,11 +433,12 @@ init_nginx() {
|
|||||||
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
|
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_task_before() {
|
handle_task_start() {
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||||
|
|
||||||
echo -e "## 开始执行... $begin_time\n"
|
echo -e "## 开始执行... $begin_time\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_task_before() {
|
||||||
[[ $is_macos -eq 0 ]] && check_server
|
[[ $is_macos -eq 0 ]] && check_server
|
||||||
|
|
||||||
. $file_task_before "$@"
|
. $file_task_before "$@"
|
||||||
@@ -444,7 +450,7 @@ handle_task_before() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_task_after() {
|
run_task_after() {
|
||||||
. $file_task_after "$@"
|
. $file_task_after "$@"
|
||||||
|
|
||||||
if [[ $task_after ]]; then
|
if [[ $task_after ]]; then
|
||||||
@@ -452,7 +458,9 @@ handle_task_after() {
|
|||||||
eval "$task_after"
|
eval "$task_after"
|
||||||
echo -e "\n执行后置命令结束"
|
echo -e "\n执行后置命令结束"
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_task_end() {
|
||||||
local etime=$(date "+$time_format")
|
local etime=$(date "+$time_format")
|
||||||
local end_time=$(format_time "$time_format" "$etime")
|
local end_time=$(format_time "$time_format" "$etime")
|
||||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||||
@@ -460,8 +468,7 @@ handle_task_after() {
|
|||||||
|
|
||||||
[[ "$diff_time" == 0 ]] && diff_time=1
|
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||||
|
|
||||||
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||||
|
|
||||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-9
@@ -5,9 +5,9 @@ dir_shell=$QL_DIR/shell
|
|||||||
. $dir_shell/share.sh
|
. $dir_shell/share.sh
|
||||||
. $dir_shell/api.sh
|
. $dir_shell/api.sh
|
||||||
|
|
||||||
trap "single_hanle" 2 3 20 15 14
|
trap "single_hanle" 2 3 20 15 14 19 1
|
||||||
single_hanle() {
|
single_hanle() {
|
||||||
eval handle_task_after "$@" "$cmd"
|
eval handle_task_end "$@" "$cmd"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,10 +70,10 @@ handle_log_path() {
|
|||||||
log_path="$real_log_path"
|
log_path="$real_log_path"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd=">> $dir_log/$log_path 2>&1"
|
|
||||||
make_dir "$dir_log/$log_dir"
|
|
||||||
if [[ "$show_log" == "true" ]]; then
|
|
||||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||||
|
make_dir "$dir_log/$log_dir"
|
||||||
|
if [[ "$no_tee" == "true" ]]; then
|
||||||
|
cmd=">> $dir_log/$log_path 2>&1"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$real_time" == "true" ]]; then
|
if [[ "$real_time" == "true" ]]; then
|
||||||
@@ -95,6 +95,21 @@ format_params() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||||
|
|
||||||
|
# 分割 task 内置参数和脚本参数
|
||||||
|
task_shell_params=()
|
||||||
|
script_params=()
|
||||||
|
found_double_dash=false
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
if $found_double_dash; then
|
||||||
|
script_params+=("$arg")
|
||||||
|
elif [ "$arg" == "--" ]; then
|
||||||
|
found_double_dash=true
|
||||||
|
else
|
||||||
|
task_shell_params+=("$arg")
|
||||||
|
fi
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
init_begin_time() {
|
init_begin_time() {
|
||||||
@@ -119,11 +134,9 @@ if [[ $max_time ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
format_params "$@"
|
format_params "$@"
|
||||||
define_program "$@"
|
define_program "${task_shell_params[@]}"
|
||||||
handle_log_path "$@"
|
handle_log_path "${task_shell_params[@]}"
|
||||||
init_begin_time
|
init_begin_time
|
||||||
|
|
||||||
eval . $dir_shell/otask.sh "$cmd"
|
eval . $dir_shell/otask.sh "$cmd"
|
||||||
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -24,9 +24,13 @@ body {
|
|||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
padding-right: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-modal-body {
|
.ant-modal-body {
|
||||||
max-height: calc(90vh - 110px);
|
max-height: calc(80vh - 110px);
|
||||||
max-height: calc(90vh - var(--vh-offset, 110px));
|
max-height: calc(80vh - var(--vh-offset, 110px));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +185,7 @@ body {
|
|||||||
.react-codemirror2,
|
.react-codemirror2,
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export default function () {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!user || !user.username) return;
|
if (!user || !user.username) return;
|
||||||
const ws = WebSocketManager.getInstance(
|
const ws = WebSocketManager.getInstance(
|
||||||
`${window.location.origin}/api/ws?token=${localStorage.getItem(
|
`${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem(
|
||||||
config.authKey,
|
config.authKey,
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
@@ -320,7 +320,7 @@ export default function () {
|
|||||||
shape="square"
|
shape="square"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
src={user.avatar ? `/api/static/${user.avatar}` : ''}
|
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
|
||||||
/>
|
/>
|
||||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -342,7 +342,7 @@ export default function () {
|
|||||||
shape="square"
|
shape="square"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<UserOutlined />}
|
icon={<UserOutlined />}
|
||||||
src={user.avatar ? `/api/static/${user.avatar}` : ''}
|
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
|
||||||
/>
|
/>
|
||||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -459,6 +459,12 @@
|
|||||||
"新增定时规则": "Add Timing Rules",
|
"新增定时规则": "Add Timing Rules",
|
||||||
"运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "Run commands before executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js",
|
"运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "Run commands before executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js",
|
||||||
"运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "Run commands after executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js",
|
"运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "Run commands after executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js",
|
||||||
"请输入运行任务前要执行的命令": "Please enter the command to run before executing the task",
|
"请输入运行任务前要执行的命令,不能包含 task 命令": "Please enter the command to run before executing the task, cannot contain task commands",
|
||||||
"请输入运行任务后要执行的命令": "Please enter the command to run after executing the task"
|
"请输入运行任务后要执行的命令,不能包含 task 命令": "Please enter the command to run after executing the task, cannot contain task commands",
|
||||||
|
"不能包含 task 命令": "Cannot contain task commands",
|
||||||
|
"Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/": "Connection address of the Chronocat Red service https://chronocat.vercel.app/install/docker/official/",
|
||||||
|
"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx": "Individuals: user_id=individual QQ Groups fill in group_id=QQ Groups more than one with English; separated by the same time to support individuals and groups such as: user_id=xxx;group_id=xxxx;group_id=xxxxx",
|
||||||
|
"docker安装在持久化config目录下的chronocat.yml文件可找到": "The docker installation can be found in the persistence config directory in the chronocat.yml file",
|
||||||
|
"请选择": "Please select",
|
||||||
|
"请输入": "Please input"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -459,6 +459,12 @@
|
|||||||
"新增定时规则": "新增定时规则",
|
"新增定时规则": "新增定时规则",
|
||||||
"运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js",
|
"运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js",
|
||||||
"运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js",
|
"运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js": "运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js",
|
||||||
"请输入运行任务前要执行的命令": "请输入运行任务前要执行的命令",
|
"请输入运行任务前要执行的命令,不能包含 task 命令": "请输入运行任务前要执行的命令,不能包含 task 命令",
|
||||||
"请输入运行任务后要执行的命令": "请输入运行任务后要执行的命令"
|
"请输入运行任务后要执行的命令,不能包含 task 命令": "请输入运行任务后要执行的命令,不能包含 task 命令",
|
||||||
|
"不能包含 task 命令": "不能包含 task 命令",
|
||||||
|
"Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/": "Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/",
|
||||||
|
"个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx": "个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx",
|
||||||
|
"docker安装在持久化config目录下的chronocat.yml文件可找到": "docker安装在持久化config目录下的chronocat.yml文件可找到",
|
||||||
|
"请选择": "请选择",
|
||||||
|
"请输入": "请输入"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import IconFont from '@/components/iconfont';
|
|||||||
import { getCommandScript, getEditorMode } from '@/utils';
|
import { getCommandScript, getEditorMode } from '@/utils';
|
||||||
import VirtualList from 'rc-virtual-list';
|
import VirtualList from 'rc-virtual-list';
|
||||||
import useScrollHeight from '@/hooks/useScrollHeight';
|
import useScrollHeight from '@/hooks/useScrollHeight';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -52,8 +53,6 @@ interface LogItem {
|
|||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const language = navigator.language || navigator.languages[0];
|
|
||||||
|
|
||||||
const CronDetailModal = ({
|
const CronDetailModal = ({
|
||||||
cron = {},
|
cron = {},
|
||||||
handleCancel,
|
handleCancel,
|
||||||
@@ -121,10 +120,11 @@ const CronDetailModal = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClickItem = (item: LogItem) => {
|
const onClickItem = (item: LogItem) => {
|
||||||
localStorage.setItem('logCron', currentCron.id);
|
const url = `${config.apiPrefix}logs/${item.filename}?path=${
|
||||||
setLogUrl(
|
item.directory || ''
|
||||||
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
}`;
|
||||||
);
|
localStorage.setItem('logCron', url);
|
||||||
|
setLogUrl(url);
|
||||||
request
|
request
|
||||||
.get(
|
.get(
|
||||||
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
||||||
@@ -498,7 +498,12 @@ const CronDetailModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="cron-detail-info-item">
|
<div className="cron-detail-info-item">
|
||||||
<div className="cron-detail-info-title">{intl.get('定时')}</div>
|
<div className="cron-detail-info-title">{intl.get('定时')}</div>
|
||||||
<div className="cron-detail-info-value">{currentCron.schedule}</div>
|
<div className="cron-detail-info-value">
|
||||||
|
<div>{currentCron.schedule}</div>
|
||||||
|
{currentCron.extra_schedules?.map((x) => (
|
||||||
|
<div key={x.schedule}>{x.schedule}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="cron-detail-info-item">
|
<div className="cron-detail-info-item">
|
||||||
<div className="cron-detail-info-title">
|
<div className="cron-detail-info-title">
|
||||||
@@ -506,11 +511,9 @@ const CronDetailModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="cron-detail-info-value">
|
<div className="cron-detail-info-value">
|
||||||
{currentCron.last_execution_time
|
{currentCron.last_execution_time
|
||||||
? new Date(currentCron.last_execution_time * 1000)
|
? dayjs(currentCron.last_execution_time * 1000).format(
|
||||||
.toLocaleString(language, {
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
hour12: false,
|
)
|
||||||
})
|
|
||||||
.replace(' 24:', ' 00:')
|
|
||||||
: '-'}
|
: '-'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -530,11 +533,7 @@ const CronDetailModal = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="cron-detail-info-value">
|
<div className="cron-detail-info-value">
|
||||||
{currentCron.nextRunTime &&
|
{currentCron.nextRunTime &&
|
||||||
currentCron.nextRunTime
|
dayjs(currentCron.nextRunTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||||
.toLocaleString(language, {
|
|
||||||
hour12: false,
|
|
||||||
})
|
|
||||||
.replace(' 24:', ' 00:')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
min-width: 600px;
|
min-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cron-detail-info-item {
|
.cron-detail-info-item {
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
min-width: 600px;
|
min-width: 1000px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +78,6 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-right: 32px;
|
|
||||||
|
|
||||||
.operations {
|
.operations {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
+35
-26
@@ -51,13 +51,14 @@ import ViewManageModal from './viewManageModal';
|
|||||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
import { getCommandScript, parseCrontab } from '@/utils';
|
import { getCommandScript, getCrontabsNextDate, parseCrontab } from '@/utils';
|
||||||
import { ColumnProps } from 'antd/lib/table';
|
import { ColumnProps } from 'antd/lib/table';
|
||||||
import { useVT } from 'virtualizedtableforantd4';
|
import { useVT } from 'virtualizedtableforantd4';
|
||||||
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
|
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
|
||||||
import Name from '@/components/name';
|
import Name from '@/components/name';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography;
|
const { Text, Paragraph, Link } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
|
|
||||||
const Crontab = () => {
|
const Crontab = () => {
|
||||||
@@ -74,17 +75,18 @@ const Crontab = () => {
|
|||||||
style={{
|
style={{
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
marginBottom: 0,
|
marginBottom: 0,
|
||||||
|
color: '#1890ff'
|
||||||
}}
|
}}
|
||||||
ellipsis={{ tooltip: text, rows: 2 }}
|
ellipsis={{ tooltip: text, rows: 2 }}
|
||||||
>
|
>
|
||||||
<a
|
<Link
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDetailCron(record);
|
setDetailCron(record);
|
||||||
setIsDetailModalVisible(true);
|
setIsDetailModalVisible(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{record.name || '-'}
|
{record.name || '-'}
|
||||||
</a>
|
</Link>
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
),
|
),
|
||||||
sorter: {
|
sorter: {
|
||||||
@@ -183,17 +185,29 @@ const Crontab = () => {
|
|||||||
compare: (a, b) => a.schedule.localeCompare(b.schedule),
|
compare: (a, b) => a.schedule.localeCompare(b.schedule),
|
||||||
},
|
},
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
return record.extra_schedules?.length ? (
|
return (
|
||||||
<Popover
|
<Paragraph
|
||||||
placement="right"
|
style={{
|
||||||
content={record.extra_schedules?.map((x) => (
|
wordBreak: 'break-all',
|
||||||
<div>{x.schedule}</div>
|
marginBottom: 0,
|
||||||
|
}}
|
||||||
|
ellipsis={{
|
||||||
|
tooltip: {
|
||||||
|
placement: 'right',
|
||||||
|
title: (
|
||||||
|
<>
|
||||||
|
<div>{text}</div>
|
||||||
|
{record.extra_schedules?.map((x) => (
|
||||||
|
<div key={x.schedule}>{x.schedule}</div>
|
||||||
))}
|
))}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
rows: 2,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</Popover>
|
</Paragraph>
|
||||||
) : (
|
|
||||||
text
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -224,7 +238,6 @@ const Crontab = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -232,11 +245,9 @@ const Crontab = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{record.last_execution_time
|
{record.last_execution_time
|
||||||
? new Date(record.last_execution_time * 1000)
|
? dayjs(record.last_execution_time * 1000).format(
|
||||||
.toLocaleString(language, {
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
hour12: false,
|
)
|
||||||
})
|
|
||||||
.replace(' 24:', ' 00:')
|
|
||||||
: '-'}
|
: '-'}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
@@ -251,12 +262,7 @@ const Crontab = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text, record) => {
|
render: (text, record) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
|
||||||
return record.nextRunTime
|
|
||||||
.toLocaleString(language, {
|
|
||||||
hour12: false,
|
|
||||||
})
|
|
||||||
.replace(' 24:', ' 00:');
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -389,7 +395,7 @@ const Crontab = () => {
|
|||||||
data.map((x) => {
|
data.map((x) => {
|
||||||
return {
|
return {
|
||||||
...x,
|
...x,
|
||||||
nextRunTime: parseCrontab(x.schedule),
|
nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -677,7 +683,10 @@ const Crontab = () => {
|
|||||||
if (code === 200) {
|
if (code === 200) {
|
||||||
const index = value.findIndex((x) => x.id === cron.id);
|
const index = value.findIndex((x) => x.id === cron.id);
|
||||||
const result = [...value];
|
const result = [...value];
|
||||||
data.nextRunTime = parseCrontab(data.schedule);
|
data.nextRunTime = getCrontabsNextDate(
|
||||||
|
data.schedule,
|
||||||
|
data.extra_schedules,
|
||||||
|
);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
result.splice(index, 1, {
|
result.splice(index, 1, {
|
||||||
...cron,
|
...cron,
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import intl from 'react-intl-universal';
|
import intl from 'react-intl-universal';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Modal, message, Input, Form, Statistic, Button } from 'antd';
|
import {
|
||||||
|
Modal,
|
||||||
|
message,
|
||||||
|
Input,
|
||||||
|
Form,
|
||||||
|
Statistic,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
import {
|
import {
|
||||||
@@ -10,6 +18,7 @@ import {
|
|||||||
import { PageLoading } from '@ant-design/pro-layout';
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
import { logEnded } from '@/utils';
|
import { logEnded } from '@/utils';
|
||||||
import { CrontabStatus } from './type';
|
import { CrontabStatus } from './type';
|
||||||
|
import Ansi from 'ansi-to-react';
|
||||||
|
|
||||||
const { Countdown } = Statistic;
|
const { Countdown } = Statistic;
|
||||||
|
|
||||||
@@ -31,6 +40,7 @@ const CronLogModal = ({
|
|||||||
const [executing, setExecuting] = useState<any>(true);
|
const [executing, setExecuting] = useState<any>(true);
|
||||||
const [isPhone, setIsPhone] = useState(false);
|
const [isPhone, setIsPhone] = useState(false);
|
||||||
const scrollInfoRef = useRef({ value: 0, down: true });
|
const scrollInfoRef = useRef({ value: 0, down: true });
|
||||||
|
const uniqPath = logUrl ? logUrl : String(cron?.id);
|
||||||
|
|
||||||
const getCronLog = (isFirst?: boolean) => {
|
const getCronLog = (isFirst?: boolean) => {
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
@@ -41,7 +51,7 @@ const CronLogModal = ({
|
|||||||
.then(({ code, data }) => {
|
.then(({ code, data }) => {
|
||||||
if (
|
if (
|
||||||
code === 200 &&
|
code === 200 &&
|
||||||
localStorage.getItem('logCron') === String(cron.id) &&
|
localStorage.getItem('logCron') === uniqPath &&
|
||||||
data !== value
|
data !== value
|
||||||
) {
|
) {
|
||||||
const log = data as string;
|
const log = data as string;
|
||||||
@@ -49,10 +59,15 @@ const CronLogModal = ({
|
|||||||
const hasNext = Boolean(
|
const hasNext = Boolean(
|
||||||
log && !logEnded(log) && !log.includes('任务未运行'),
|
log && !logEnded(log) && !log.includes('任务未运行'),
|
||||||
);
|
);
|
||||||
|
if (!hasNext && !logEnded(value) && value !== intl.get('启动中...')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
autoScroll();
|
||||||
|
});
|
||||||
|
}
|
||||||
setExecuting(hasNext);
|
setExecuting(hasNext);
|
||||||
if (hasNext) {
|
if (hasNext) {
|
||||||
autoScroll();
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
autoScroll();
|
||||||
getCronLog();
|
getCronLog();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
@@ -87,18 +102,20 @@ const CronLogModal = ({
|
|||||||
if (scrollInfoRef.current.down) {
|
if (scrollInfoRef.current.down) {
|
||||||
scrollInfoRef.current = {
|
scrollInfoRef.current = {
|
||||||
value: sTop,
|
value: sTop,
|
||||||
down: sTop > scrollInfoRef.current.value || !sTop,
|
down: sTop - scrollInfoRef.current.value > -5 || !sTop,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const titleElement = () => {
|
const titleElement = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
||||||
{!executing && !loading && <CheckCircleOutlined />}
|
{!executing && !loading && <CheckCircleOutlined />}
|
||||||
<span style={{ marginLeft: 5 }}>{cron && cron.name}</span>
|
<Typography.Text ellipsis={true} style={{ marginLeft: 5 }}>
|
||||||
</>
|
{cron && cron.name}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -148,7 +165,7 @@ const CronLogModal = ({
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{value}
|
<Ansi>{value}</Ansi>
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
<div id="log-flag"></div>
|
<div id="log-flag"></div>
|
||||||
|
|||||||
@@ -74,7 +74,11 @@ const CronModal = ({
|
|||||||
name="form_in_modal"
|
name="form_in_modal"
|
||||||
initialValues={cron}
|
initialValues={cron}
|
||||||
>
|
>
|
||||||
<Form.Item name="name" label={intl.get('名称')}>
|
<Form.Item
|
||||||
|
name="name"
|
||||||
|
label={intl.get('名称')}
|
||||||
|
rules={[{ required: true, whitespace: true }]}
|
||||||
|
>
|
||||||
<Input placeholder={intl.get('请输入任务名称')} />
|
<Input placeholder={intl.get('请输入任务名称')} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -149,11 +153,26 @@ const CronModal = ({
|
|||||||
tooltip={intl.get(
|
tooltip={intl.get(
|
||||||
'运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js',
|
'运行任务前执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js',
|
||||||
)}
|
)}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
(value.includes(' task ') || value.startsWith('task '))
|
||||||
|
) {
|
||||||
|
return Promise.reject(intl.get('不能包含 task 命令'));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||||
placeholder={intl.get('请输入运行任务前要执行的命令')}
|
placeholder={intl.get(
|
||||||
|
'请输入运行任务前要执行的命令,不能包含 task 命令',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -162,11 +181,26 @@ const CronModal = ({
|
|||||||
tooltip={intl.get(
|
tooltip={intl.get(
|
||||||
'运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js',
|
'运行任务后执行的命令,比如 cp/mv/python3 xxx.py/node xxx.js',
|
||||||
)}
|
)}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
validator(rule, value) {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
(value.includes(' task ') || value.startsWith('task '))
|
||||||
|
) {
|
||||||
|
return Promise.reject(intl.get('不能包含 task 命令'));
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||||
placeholder={intl.get('请输入运行任务后要执行的命令')}
|
placeholder={intl.get(
|
||||||
|
'请输入运行任务后要执行的命令,不能包含 task 命令',
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const Dependence = () => {
|
|||||||
const columns: any = [
|
const columns: any = [
|
||||||
{
|
{
|
||||||
title: intl.get('序号'),
|
title: intl.get('序号'),
|
||||||
width: 80,
|
width: 90,
|
||||||
render: (text: string, record: any, index: number) => {
|
render: (text: string, record: any, index: number) => {
|
||||||
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
|
||||||
},
|
},
|
||||||
|
|||||||
Vendored
+4
-7
@@ -41,6 +41,7 @@ import { SharedContext } from '@/layouts';
|
|||||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||||
import Copy from '../../components/copy';
|
import Copy from '../../components/copy';
|
||||||
import { useVT } from 'virtualizedtableforantd4';
|
import { useVT } from 'virtualizedtableforantd4';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { Paragraph } = Typography;
|
const { Paragraph } = Typography;
|
||||||
const { Search } = Input;
|
const { Search } = Input;
|
||||||
@@ -137,13 +138,9 @@ const Env = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
const language = navigator.language || navigator.languages[0];
|
const date = dayjs(record.updatedAt || record.timestamp).format(
|
||||||
const time = record.updatedAt || record.timestamp;
|
'YYYY-MM-DD HH:mm:ss',
|
||||||
const date = new Date(time)
|
);
|
||||||
.toLocaleString(language, {
|
|
||||||
hour12: false,
|
|
||||||
})
|
|
||||||
.replace(' 24:', ' 00:');
|
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
placement="topLeft"
|
placement="topLeft"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { request } from '@/utils/http';
|
|||||||
import { useTheme } from '@/utils/hooks';
|
import { useTheme } from '@/utils/hooks';
|
||||||
import { MobileOutlined } from '@ant-design/icons';
|
import { MobileOutlined } from '@ant-design/icons';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const FormItem = Form.Item;
|
const FormItem = Form.Item;
|
||||||
const { Countdown } = Statistic;
|
const { Countdown } = Statistic;
|
||||||
@@ -86,7 +87,7 @@ const Login = () => {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{intl.get('上次登录时间:')}
|
{intl.get('上次登录时间:')}
|
||||||
{lastlogon ? new Date(lastlogon).toLocaleString() : '-'}
|
{lastlogon ? dayjs(lastlogon).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{intl.get('上次登录地点:')}
|
{intl.get('上次登录地点:')}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import SettingModal from './setting';
|
|||||||
import { useTheme } from '@/utils/hooks';
|
import { useTheme } from '@/utils/hooks';
|
||||||
import { getEditorMode, logEnded } from '@/utils';
|
import { getEditorMode, logEnded } from '@/utils';
|
||||||
import WebSocketManager from '@/utils/websocket';
|
import WebSocketManager from '@/utils/websocket';
|
||||||
|
import Ansi from 'ansi-to-react';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ const EditModal = ({
|
|||||||
}, 300);
|
}, 300);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLog(p=>`${p}${_message}`);
|
setLog((p) => `${p}${_message}`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -246,7 +247,7 @@ const EditModal = ({
|
|||||||
padding: '0 15px',
|
padding: '0 15px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{log}
|
<Ansi>{log}</Ansi>
|
||||||
</pre>
|
</pre>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
<SaveModal
|
<SaveModal
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
content: <pre>{lastLog}</pre>,
|
content: <pre><Ansi>{lastLog}</Ansi></pre>,
|
||||||
okText: intl.get('下载更新'),
|
okText: intl.get('下载更新'),
|
||||||
cancelText: intl.get('以后再说'),
|
cancelText: intl.get('以后再说'),
|
||||||
onOk() {
|
onOk() {
|
||||||
@@ -102,7 +102,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
|
|||||||
okButtonProps: { disabled: true },
|
okButtonProps: { disabled: true },
|
||||||
title: intl.get('下载更新中...'),
|
title: intl.get('下载更新中...'),
|
||||||
centered: true,
|
centered: true,
|
||||||
content: <pre>{value}</pre>,
|
content: <pre><Ansi>{value}</Ansi></pre>,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ import About from './about';
|
|||||||
import { useOutletContext } from '@umijs/max';
|
import { useOutletContext } from '@umijs/max';
|
||||||
import { SharedContext } from '@/layouts';
|
import { SharedContext } from '@/layouts';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
|
||||||
import useResizeObserver from '@react-hook/resize-observer';
|
import useResizeObserver from '@react-hook/resize-observer';
|
||||||
|
import SystemLog from './systemLog';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
|
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
|
||||||
@@ -334,7 +334,7 @@ const Setting = () => {
|
|||||||
dataSource={dataSource}
|
dataSource={dataSource}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
size="middle"
|
size="middle"
|
||||||
scroll={{ x: 768 }}
|
scroll={{ x: 1000 }}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -347,22 +347,7 @@ const Setting = () => {
|
|||||||
{
|
{
|
||||||
key: 'syslog',
|
key: 'syslog',
|
||||||
label: intl.get('系统日志'),
|
label: intl.get('系统日志'),
|
||||||
children: (
|
children: <SystemLog data={systemLogData} height={height} theme={theme}/>,
|
||||||
<CodeMirror
|
|
||||||
maxHeight={`${height}px`}
|
|
||||||
value={systemLogData}
|
|
||||||
onCreateEditor={(view) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
view.scrollDOM.scrollTo({
|
|
||||||
top: view.scrollDOM.scrollHeight,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
}}
|
|
||||||
readOnly={true}
|
|
||||||
theme={theme.includes('dark') ? 'dark' : 'light'}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'login',
|
key: 'login',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { Typography, Table, Tag, Button, Spin, message } from 'antd';
|
import { Typography, Table, Tag, Button, Spin, message } from 'antd';
|
||||||
import { request } from '@/utils/http';
|
import { request } from '@/utils/http';
|
||||||
import config from '@/utils/config';
|
import config from '@/utils/config';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { Text, Link } = Typography;
|
const { Text, Link } = Typography;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ const columns = [
|
|||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
width: 120,
|
width: 120,
|
||||||
render: (text: string, record: any) => {
|
render: (text: string, record: any) => {
|
||||||
return new Date(record.timestamp).toLocaleString();
|
return dayjs(record.timestamp).format('YYYY-MM-DD HH:mm:ss');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const NotificationSetting = ({ data }: any) => {
|
|||||||
>
|
>
|
||||||
{x.items ? (
|
{x.items ? (
|
||||||
<Select
|
<Select
|
||||||
placeholder={x.placeholder || `请选择${x.label}`}
|
placeholder={x.placeholder || `${intl.get('请选择')} ${x.label}`}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
{x.items.map((y) => (
|
{x.items.map((y) => (
|
||||||
@@ -89,7 +89,7 @@ const NotificationSetting = ({ data }: any) => {
|
|||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||||
placeholder={x.placeholder || `请输入${x.label}`}
|
placeholder={x.placeholder || `${intl.get('请输入')} ${x.label}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ const Other = ({
|
|||||||
method="put"
|
method="put"
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
maxCount={1}
|
maxCount={1}
|
||||||
action="/api/system/data/import"
|
action={`${config.apiPrefix}system/data/import`}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.event?.percent) {
|
if (e.event?.percent) {
|
||||||
showUploadProgress(parseFloat(e.event?.percent.toFixed(1)));
|
showUploadProgress(parseFloat(e.event?.percent.toFixed(1)));
|
||||||
|
|||||||
@@ -97,14 +97,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
|
|||||||
|
|
||||||
const onChange = (e) => {
|
const onChange = (e) => {
|
||||||
if (e.file && e.file.response) {
|
if (e.file && e.file.response) {
|
||||||
setAvatar(`/api/static/${e.file.response.data}`);
|
setAvatar(
|
||||||
|
`${config.apiPrefix}static/${e.file.response.data}`,
|
||||||
|
);
|
||||||
userChange();
|
userChange();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTwoFactorActivated(user && user.twoFactorActivated);
|
setTwoFactorActivated(user && user.twoFactorActivated);
|
||||||
setAvatar(user.avatar && `/api/static/${user.avatar}`);
|
setAvatar(user.avatar && `${config.apiPrefix}static/${user.avatar}`);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
return twoFactoring ? (
|
return twoFactoring ? (
|
||||||
@@ -250,7 +252,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
|
|||||||
method="put"
|
method="put"
|
||||||
showUploadList={false}
|
showUploadList={false}
|
||||||
maxCount={1}
|
maxCount={1}
|
||||||
action="/api/user/avatar"
|
action={`${config.apiPrefix}user/avatar`}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
name="avatar"
|
name="avatar"
|
||||||
headers={{
|
headers={{
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React, { useRef } from 'react';
|
||||||
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import {
|
||||||
|
VerticalAlignBottomOutlined,
|
||||||
|
VerticalAlignTopOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
|
||||||
|
const SystemLog = ({ data, height, theme }: any) => {
|
||||||
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const scrollTo = (position: 'start' | 'end') => {
|
||||||
|
editorRef.current.scrollDOM.scrollTo({
|
||||||
|
top: position === 'start' ? 0 : editorRef.current.scrollDOM.scrollHeight,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: 'relative' }}>
|
||||||
|
<CodeMirror
|
||||||
|
maxHeight={`${height}px`}
|
||||||
|
value={data}
|
||||||
|
onCreateEditor={(view) => {
|
||||||
|
editorRef.current = view;
|
||||||
|
}}
|
||||||
|
readOnly={true}
|
||||||
|
theme={theme.includes('dark') ? 'dark' : 'light'}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 20,
|
||||||
|
right: 20,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
icon={<VerticalAlignTopOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
scrollTo('start');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
size='small'
|
||||||
|
icon={<VerticalAlignBottomOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
scrollTo('end');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemLog;
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { PageLoading } from '@ant-design/pro-layout';
|
import { PageLoading } from '@ant-design/pro-layout';
|
||||||
import { logEnded } from '@/utils';
|
import { logEnded } from '@/utils';
|
||||||
|
import Ansi from 'ansi-to-react';
|
||||||
|
|
||||||
const SubscriptionLogModal = ({
|
const SubscriptionLogModal = ({
|
||||||
subscription,
|
subscription,
|
||||||
@@ -122,7 +123,7 @@ const SubscriptionLogModal = ({
|
|||||||
: {}
|
: {}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{value}
|
<Ansi>{value}</Ansi>
|
||||||
</pre>
|
</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+62
-19
@@ -98,6 +98,7 @@ export default {
|
|||||||
{ value: 'email', label: intl.get('邮箱') },
|
{ value: 'email', label: intl.get('邮箱') },
|
||||||
{ value: 'lark', label: intl.get('飞书机器人') },
|
{ value: 'lark', label: intl.get('飞书机器人') },
|
||||||
{ value: 'pushMe', label: 'PushMe' },
|
{ value: 'pushMe', label: 'PushMe' },
|
||||||
|
{ value: 'chronocat', label: 'Chronocat' },
|
||||||
{ value: 'webhook', label: intl.get('自定义通知') },
|
{ value: 'webhook', label: intl.get('自定义通知') },
|
||||||
{ value: 'closed', label: intl.get('已关闭') },
|
{ value: 'closed', label: intl.get('已关闭') },
|
||||||
],
|
],
|
||||||
@@ -126,14 +127,16 @@ export default {
|
|||||||
goCqHttpBot: [
|
goCqHttpBot: [
|
||||||
{
|
{
|
||||||
label: 'goCqHttpBotUrl',
|
label: 'goCqHttpBotUrl',
|
||||||
tip: intl.get('推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg',
|
tip: intl.get(
|
||||||
|
'推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{ label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true },
|
{ label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true },
|
||||||
{
|
{
|
||||||
label: 'goCqHttpBotQq',
|
label: 'goCqHttpBotQq',
|
||||||
tip: intl.get('如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群',
|
tip: intl.get(
|
||||||
|
'如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -153,14 +156,16 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'pushDeerUrl',
|
label: 'pushDeerUrl',
|
||||||
tip: intl.get('PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push',
|
tip: intl.get(
|
||||||
|
'PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
bark: [
|
bark: [
|
||||||
{
|
{
|
||||||
label: 'barkPush',
|
label: 'barkPush',
|
||||||
tip: intl.get('Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
|
tip: intl.get(
|
||||||
|
'Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -188,7 +193,8 @@ export default {
|
|||||||
telegramBot: [
|
telegramBot: [
|
||||||
{
|
{
|
||||||
label: 'telegramBotToken',
|
label: 'telegramBotToken',
|
||||||
tip: intl.get('telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',
|
tip: intl.get(
|
||||||
|
'telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -201,7 +207,8 @@ export default {
|
|||||||
{ label: 'telegramBotProxyPort', tip: intl.get('代理端口') },
|
{ label: 'telegramBotProxyPort', tip: intl.get('代理端口') },
|
||||||
{
|
{
|
||||||
label: 'telegramBotProxyAuth',
|
label: 'telegramBotProxyAuth',
|
||||||
tip: intl.get('telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password',
|
tip: intl.get(
|
||||||
|
'telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -212,20 +219,23 @@ export default {
|
|||||||
dingtalkBot: [
|
dingtalkBot: [
|
||||||
{
|
{
|
||||||
label: 'dingtalkBotToken',
|
label: 'dingtalkBotToken',
|
||||||
tip: intl.get('钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd',
|
tip: intl.get(
|
||||||
|
'钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'dingtalkBotSecret',
|
label: 'dingtalkBotSecret',
|
||||||
tip: intl.get('密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串',
|
tip: intl.get(
|
||||||
|
'密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
weWorkBot: [
|
weWorkBot: [
|
||||||
{
|
{
|
||||||
label: 'weWorkBotKey',
|
label: 'weWorkBotKey',
|
||||||
tip: intl.get('企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa',
|
tip: intl.get(
|
||||||
|
'企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -237,7 +247,8 @@ export default {
|
|||||||
weWorkApp: [
|
weWorkApp: [
|
||||||
{
|
{
|
||||||
label: 'weWorkAppKey',
|
label: 'weWorkAppKey',
|
||||||
tip: intl.get('corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat',
|
tip: intl.get(
|
||||||
|
'corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -249,7 +260,8 @@ export default {
|
|||||||
aibotk: [
|
aibotk: [
|
||||||
{
|
{
|
||||||
label: 'aibotkKey',
|
label: 'aibotkKey',
|
||||||
tip: intl.get('密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql',
|
tip: intl.get(
|
||||||
|
'密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -265,7 +277,8 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'aibotkName',
|
label: 'aibotkName',
|
||||||
tip: intl.get('要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
|
tip: intl.get(
|
||||||
|
'要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -273,7 +286,8 @@ export default {
|
|||||||
iGot: [
|
iGot: [
|
||||||
{
|
{
|
||||||
label: 'iGotPushKey',
|
label: 'iGotPushKey',
|
||||||
tip: intl.get('iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX',
|
tip: intl.get(
|
||||||
|
'iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -281,20 +295,23 @@ export default {
|
|||||||
pushPlus: [
|
pushPlus: [
|
||||||
{
|
{
|
||||||
label: 'pushPlusToken',
|
label: 'pushPlusToken',
|
||||||
tip: intl.get('微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
|
tip: intl.get(
|
||||||
|
'微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'pushPlusUser',
|
label: 'pushPlusUser',
|
||||||
tip: intl.get('一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
|
tip: intl.get(
|
||||||
|
'一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
lark: [
|
lark: [
|
||||||
{
|
{
|
||||||
label: 'larkKey',
|
label: 'larkKey',
|
||||||
tip: intl.get('飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973',
|
tip: intl.get(
|
||||||
|
'飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -302,7 +319,8 @@ export default {
|
|||||||
email: [
|
email: [
|
||||||
{
|
{
|
||||||
label: 'emailService',
|
label: 'emailService',
|
||||||
tip: intl.get('邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/',
|
tip: intl.get(
|
||||||
|
'邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -316,6 +334,29 @@ export default {
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
chronocat: [
|
||||||
|
{
|
||||||
|
label: 'chronocatURL',
|
||||||
|
tip: intl.get(
|
||||||
|
'Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/',
|
||||||
|
),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'chronocatQQ',
|
||||||
|
tip: intl.get(
|
||||||
|
'个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx',
|
||||||
|
),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'chronocatToken',
|
||||||
|
tip: intl.get(
|
||||||
|
'docker安装在持久化config目录下的chronocat.yml文件可找到',
|
||||||
|
),
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
webhook: [
|
webhook: [
|
||||||
{
|
{
|
||||||
label: 'webhookMethod',
|
label: 'webhookMethod',
|
||||||
@@ -335,7 +376,8 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'webhookUrl',
|
label: 'webhookUrl',
|
||||||
tip: intl.get('请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置',
|
tip: intl.get(
|
||||||
|
'请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置',
|
||||||
),
|
),
|
||||||
required: true,
|
required: true,
|
||||||
placeholder: 'https://xxx.cn/api?content=$title\n',
|
placeholder: 'https://xxx.cn/api?content=$title\n',
|
||||||
@@ -347,7 +389,8 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'webhookBody',
|
label: 'webhookBody',
|
||||||
tip: intl.get('请求体格式key1: value1,多个换行分割。url或者body中必须包含$title,$content可选,对应api内容的位置',
|
tip: intl.get(
|
||||||
|
'请求体格式key1: value1,多个换行分割。url或者body中必须包含$title,$content可选,对应api内容的位置',
|
||||||
),
|
),
|
||||||
placeholder: 'key1: $title\nkey2: $content',
|
placeholder: 'key1: $title\nkey2: $content',
|
||||||
},
|
},
|
||||||
|
|||||||
+15
-2
@@ -329,7 +329,7 @@ export function getCommandScript(
|
|||||||
return [s, p];
|
return [s, p];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCrontab(schedule: string): Date {
|
export function parseCrontab(schedule: string): Date | null {
|
||||||
try {
|
try {
|
||||||
const time = cron_parser.parseExpression(schedule);
|
const time = cron_parser.parseExpression(schedule);
|
||||||
if (time) {
|
if (time) {
|
||||||
@@ -337,7 +337,20 @@ export function parseCrontab(schedule: string): Date {
|
|||||||
}
|
}
|
||||||
} catch (error) { }
|
} catch (error) { }
|
||||||
|
|
||||||
return new Date('1970');
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCrontabsNextDate(schedule: string, extra_schedules: string[]): Date | null {
|
||||||
|
let date = parseCrontab(schedule)
|
||||||
|
if (extra_schedules?.length) {
|
||||||
|
extra_schedules.forEach(x => {
|
||||||
|
const _date = parseCrontab(x)
|
||||||
|
if (_date && (!date || _date < date)) {
|
||||||
|
date = _date;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExtension(filename: string) {
|
export function getExtension(filename: string) {
|
||||||
|
|||||||
+2
-9
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ES2015",
|
"module": "commonjs",
|
||||||
"moduleResolution": "Bundler",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
@@ -25,14 +25,7 @@
|
|||||||
"include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"],
|
"include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules",
|
"node_modules",
|
||||||
"lib",
|
|
||||||
"es",
|
|
||||||
"static",
|
"static",
|
||||||
"data",
|
"data",
|
||||||
"typings",
|
|
||||||
"**/__test__",
|
|
||||||
"test",
|
|
||||||
"docs",
|
|
||||||
"tests"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-10
@@ -1,11 +1,18 @@
|
|||||||
version: 2.16.3
|
version: 2.16.4
|
||||||
changeLogLink: https://t.me/jiao_long/394
|
changeLogLink: https://t.me/jiao_long/395
|
||||||
publishTime: 2023-09-23 12:00
|
publishTime: 2023-10-18 23:00
|
||||||
changeLog: |
|
changeLog: |
|
||||||
1. 定时任务支持设置多定时规则、任务执行前和执行后命令
|
1. 脚本推送增加自定义 webhook 方式
|
||||||
2. 增加启动环境变量 QlPort, 支持修改 hosts 模式青龙启动端口
|
3. 增加 chronocat 无头模式的QQNT推送
|
||||||
3. 消息通知 Bark 增加额外参数: 时效性通知、跳转Url
|
2. task 命令支持给脚本传参,使用 -- 分割,后面的参数都会传给脚本
|
||||||
4. 修复调试脚本日志丢失
|
4. 系统日志增加置顶置底按钮
|
||||||
5. 环境变量名称增加复制功能
|
5. 定时任务详情增加额外定时展示
|
||||||
6. 修复系统设置展示版本失败
|
6. 定时任务增加前后规则校验,不能包含 task 命令
|
||||||
7. 其他 bug 修复
|
7. 修复定时任务间隔较小,任务状态不准确
|
||||||
|
8. 修复定时任务日志滚动
|
||||||
|
9. 修复 QlBaseUrl 末尾不加斜杠无法访问
|
||||||
|
10. 修复定时任务详情页查看历史日志异常
|
||||||
|
11. 修复定时任务添加 task_after 命令无法停止
|
||||||
|
12. 修复清空文件夹报错
|
||||||
|
13. 修复设置代理时, schedule 服务通信异常
|
||||||
|
14. 其他优化
|
||||||
|
|||||||
Reference in New Issue
Block a user