mirror of
https://github.com/whyour/qinglong.git
synced 2026-08-06 16:54:33 +08:00
Compare commits
24 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 |
+65
-58
@@ -36,6 +36,7 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
- Support cell phone operation
|
||||
|
||||
## Version
|
||||
|
||||
### docker
|
||||
|
||||
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
|
||||
@@ -53,6 +54,66 @@ The npm version supports `debian/ubuntu/centos/alpine` systems and requires `nod
|
||||
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
|
||||
|
||||
### Docker (Recommended)
|
||||
@@ -63,7 +124,7 @@ docker run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||
-p 5700:5700 \
|
||||
# Deployment paths are not required and begin and end with a slash, e.g. /test/.
|
||||
# Deployment paths are not required, e.g. /test.
|
||||
-e QlBaseUrl="/" \
|
||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||
-e QlPort="5700" \
|
||||
@@ -95,7 +156,7 @@ podman run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
|
||||
-p 5700:5700 \
|
||||
# Deployment paths are not required and begin and end with a slash, e.g. /test/.
|
||||
# Deployment paths are not required, e.g. /test.
|
||||
-e QlBaseUrl="/" \
|
||||
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
|
||||
-e QlPort="5700" \
|
||||
@@ -118,60 +179,6 @@ export QL_DATA_DIR=""
|
||||
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
|
||||
|
||||
```bash
|
||||
@@ -184,7 +191,7 @@ $ pnpm install
|
||||
$ 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
|
||||
|
||||
@@ -202,4 +209,4 @@ The Green Dragon, also known as the Canglong, is one of the four elephants and o
|
||||
|
||||
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
|
||||
|
||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
|
||||
|
||||
@@ -38,6 +38,7 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
|
||||
- 支持手机端操作
|
||||
|
||||
## 版本
|
||||
|
||||
### docker
|
||||
|
||||
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
|
||||
@@ -55,6 +56,65 @@ npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node
|
||||
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 (推荐)
|
||||
@@ -65,7 +125,7 @@ docker run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
@@ -97,7 +157,7 @@ podman run -dit \
|
||||
-v $PWD/ql/data:/ql/data \
|
||||
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
|
||||
-p 5700:5700 \
|
||||
# 部署路径非必须,以斜杠开头和结尾,比如 /test/
|
||||
# 部署路径非必须,比如 /test
|
||||
-e QlBaseUrl="/" \
|
||||
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
|
||||
-e QlPort="5700" \
|
||||
@@ -120,59 +180,6 @@ export QL_DATA_DIR=""
|
||||
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
|
||||
|
||||
+2
-3
@@ -458,13 +458,12 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
const cronService = Container.get(CronService);
|
||||
const data = await cronService.status({
|
||||
...req.body,
|
||||
status: parseInt(req.body.status),
|
||||
pid: parseInt(req.body.pid) || '',
|
||||
status: req.body.status ? parseInt(req.body.status) : undefined,
|
||||
pid: req.body.pid ? parseInt(req.body.pid) : undefined,
|
||||
});
|
||||
return res.send({ code: 200, data });
|
||||
} catch (e) {
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ export default (app: Router) => {
|
||||
};
|
||||
const filePath = join(config.logPath, path, filename);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
await emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
+4
-3
@@ -183,7 +183,7 @@ export default (app: Router) => {
|
||||
};
|
||||
const filePath = join(config.scriptPath, path, filename);
|
||||
if (type === 'directory') {
|
||||
emptyDir(filePath);
|
||||
await emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
@@ -260,7 +260,6 @@ export default (app: Router) => {
|
||||
}),
|
||||
}),
|
||||
async (req: Request, res: Response, next: NextFunction) => {
|
||||
const logger: Logger = Container.get('logger');
|
||||
try {
|
||||
let { filename, path, pid } = req.body;
|
||||
const { name, ext } = parse(filename);
|
||||
@@ -269,7 +268,9 @@ export default (app: Router) => {
|
||||
|
||||
const scriptService = Container.get(ScriptService);
|
||||
const result = await scriptService.stopScript(filePath, pid);
|
||||
emptyDir(logPath);
|
||||
setTimeout(() => {
|
||||
emptyDir(logPath);
|
||||
}, 3000);
|
||||
res.send(result);
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
|
||||
import config from './config';
|
||||
import express from 'express';
|
||||
import Logger from './loaders/logger';
|
||||
import path from 'path';
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
|
||||
@@ -6,6 +6,14 @@ let pickedEnv: Record<string, string>;
|
||||
function getPickedEnv() {
|
||||
if (pickedEnv) return pickedEnv;
|
||||
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
|
||||
if (picked.QlBaseUrl) {
|
||||
if (!picked.QlBaseUrl.startsWith('/')) {
|
||||
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
|
||||
}
|
||||
if (!picked.QlBaseUrl.endsWith('/')) {
|
||||
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
|
||||
}
|
||||
}
|
||||
pickedEnv = picked as Record<string, string>;
|
||||
return picked;
|
||||
}
|
||||
|
||||
+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')) {
|
||||
ip = '';
|
||||
}
|
||||
|
||||
if (!ip) {
|
||||
return { address: `获取失败`, ip };
|
||||
}
|
||||
|
||||
try {
|
||||
const baiduApi = got
|
||||
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
|
||||
@@ -298,17 +303,21 @@ export function readDir(
|
||||
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);
|
||||
files.forEach((file) => {
|
||||
for (const file of files) {
|
||||
const filePath = `${path}/${file}`;
|
||||
const stats = fs.statSync(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
emptyDir(filePath);
|
||||
await emptyDir(filePath);
|
||||
} else {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
fs.rmdirSync(path);
|
||||
}
|
||||
|
||||
@@ -414,11 +423,11 @@ export function psTree(pid: number): Promise<number[]> {
|
||||
|
||||
export async function killTask(pid: number) {
|
||||
const pids = await psTree(pid);
|
||||
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
|
||||
|
||||
if (pids.length) {
|
||||
try {
|
||||
[pid, ...pids].forEach((x) => {
|
||||
process.kill(x, 2);
|
||||
[pid, ...pids].reverse().forEach((x) => {
|
||||
process.kill(x, 15);
|
||||
});
|
||||
} catch (error) { }
|
||||
} else {
|
||||
|
||||
@@ -18,6 +18,7 @@ export enum NotificationMode {
|
||||
'pushMe' = 'pushMe',
|
||||
'feishu' = 'feishu',
|
||||
'webhook' = 'webhook',
|
||||
'chronocat' = 'Chronocat',
|
||||
}
|
||||
|
||||
abstract class NotificationBaseInfo {
|
||||
@@ -108,6 +109,12 @@ export class PushMeNotification extends NotificationBaseInfo {
|
||||
public pushMeKey: string = '';
|
||||
}
|
||||
|
||||
export class ChronocatNotification extends NotificationBaseInfo {
|
||||
public chronocatURL: string = '';
|
||||
public chronocatQQ: string = '';
|
||||
public chronocatToekn: string = '';
|
||||
}
|
||||
|
||||
export class WebhookNotification extends NotificationBaseInfo {
|
||||
public webhookHeaders: string = '';
|
||||
public webhookBody: string = '';
|
||||
@@ -140,4 +147,6 @@ export interface NotificationInfo
|
||||
EmailNotification,
|
||||
PushMeNotification,
|
||||
WebhookNotification,
|
||||
ChronocatNotification,
|
||||
LarkNotification {}
|
||||
|
||||
|
||||
@@ -30,13 +30,9 @@ export default async ({ server }: { server: Server }) => {
|
||||
|
||||
process.on('uncaughtException', (error) => {
|
||||
Logger.error('Uncaught exception:', error);
|
||||
console.error('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (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 command = 3;
|
||||
repeated ISchedule extra_schedules = 4;
|
||||
string name = 5;
|
||||
}
|
||||
|
||||
message AddCronRequest { repeated ICron crons = 1; }
|
||||
|
||||
+15
-1
@@ -24,6 +24,7 @@ export interface ICron {
|
||||
schedule: string;
|
||||
command: string;
|
||||
extraSchedules: ISchedule[];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AddCronRequest {
|
||||
@@ -97,7 +98,7 @@ export const ISchedule = {
|
||||
};
|
||||
|
||||
function createBaseICron(): ICron {
|
||||
return { id: "", schedule: "", command: "", extraSchedules: [] };
|
||||
return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
|
||||
}
|
||||
|
||||
export const ICron = {
|
||||
@@ -114,6 +115,9 @@ export const ICron = {
|
||||
for (const v of message.extraSchedules) {
|
||||
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
|
||||
}
|
||||
if (message.name !== "") {
|
||||
writer.uint32(42).string(message.name);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
@@ -152,6 +156,13 @@ export const ICron = {
|
||||
|
||||
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
|
||||
continue;
|
||||
case 5:
|
||||
if (tag !== 42) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.name = reader.string();
|
||||
continue;
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
@@ -169,6 +180,7 @@ export const ICron = {
|
||||
extraSchedules: Array.isArray(object?.extraSchedules)
|
||||
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
|
||||
: [],
|
||||
name: isSet(object.name) ? String(object.name) : "",
|
||||
};
|
||||
},
|
||||
|
||||
@@ -182,6 +194,7 @@ export const ICron = {
|
||||
} else {
|
||||
obj.extraSchedules = [];
|
||||
}
|
||||
message.name !== undefined && (obj.name = message.name);
|
||||
return obj;
|
||||
},
|
||||
|
||||
@@ -195,6 +208,7 @@ export const ICron = {
|
||||
message.schedule = object.schedule ?? "";
|
||||
message.command = object.command ?? "";
|
||||
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
|
||||
message.name = object.name ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { AddCronRequest, AddCronResponse } from '../protos/cron';
|
||||
import nodeSchedule from 'node-schedule';
|
||||
import { scheduleStacks } from './data';
|
||||
import { runCron } from '../shared/runCron';
|
||||
import { QL_PREFIX, TASK_PREFIX } from '../config/const';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
const addCron = (
|
||||
@@ -11,14 +10,15 @@ const addCron = (
|
||||
callback: sendUnaryData<AddCronResponse>,
|
||||
) => {
|
||||
for (const item of call.request.crons) {
|
||||
const { id, schedule, command, extraSchedules } = item;
|
||||
const { id, schedule, command, extraSchedules, name } = item;
|
||||
if (scheduleStacks.has(id)) {
|
||||
scheduleStacks.get(id)?.forEach((x) => x.cancel());
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
'[schedule][创建定时任务], 任务ID: %s, cron: %s, 执行命令: %s',
|
||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||
id,
|
||||
name,
|
||||
schedule,
|
||||
command,
|
||||
);
|
||||
@@ -26,8 +26,9 @@ const addCron = (
|
||||
if (extraSchedules?.length) {
|
||||
extraSchedules.forEach(x => {
|
||||
Logger.info(
|
||||
'[schedule][创建定时任务], 任务ID: %s, cron: %s, 执行命令: %s',
|
||||
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
|
||||
id,
|
||||
name,
|
||||
x.schedule,
|
||||
command,
|
||||
);
|
||||
@@ -37,13 +38,13 @@ const addCron = (
|
||||
scheduleStacks.set(id, [
|
||||
nodeSchedule.scheduleJob(id, schedule, async () => {
|
||||
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
|
||||
runCron(command);
|
||||
runCron(command, { name, schedule, extraSchedules });
|
||||
}),
|
||||
...(extraSchedules?.length
|
||||
? extraSchedules.map((x) =>
|
||||
nodeSchedule.scheduleJob(id, x.schedule, async () => {
|
||||
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 { spawn } from 'cross-spawn';
|
||||
import dayjs from 'dayjs';
|
||||
import pickBy from 'lodash/pickBy';
|
||||
import omit from 'lodash/omit';
|
||||
|
||||
@Service()
|
||||
export default class CronService {
|
||||
@@ -33,7 +35,7 @@ export default class CronService {
|
||||
const doc = await this.insert(tab);
|
||||
if (this.isSixCron(doc) || doc.extra_schedules?.length) {
|
||||
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();
|
||||
@@ -58,6 +60,7 @@ export default class CronService {
|
||||
if (this.isSixCron(newDoc) || newDoc.extra_schedules?.length) {
|
||||
await cronClient.addCron([
|
||||
{
|
||||
name: doc.name || '',
|
||||
id: String(newDoc.id),
|
||||
schedule: newDoc.schedule!,
|
||||
command: this.makeCommand(newDoc),
|
||||
@@ -89,7 +92,7 @@ export default class CronService {
|
||||
last_running_time: number;
|
||||
last_execution_time: number;
|
||||
}) {
|
||||
const options: any = {
|
||||
let options: any = {
|
||||
status,
|
||||
pid,
|
||||
log_path,
|
||||
@@ -99,7 +102,13 @@ export default class CronService {
|
||||
options.last_running_time = last_running_time;
|
||||
}
|
||||
|
||||
return await CrontabModel.update({ ...options }, { where: { id: ids } });
|
||||
for (const id of ids) {
|
||||
const cron = await this.getDb({ id });
|
||||
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
|
||||
options = omit(options, ['status', 'log_path', 'pid']);
|
||||
}
|
||||
await CrontabModel.update({ ...pickBy(options, (v) => v === 0 || !!v) }, { where: { id } });
|
||||
}
|
||||
}
|
||||
|
||||
public async remove(ids: number[]) {
|
||||
@@ -383,15 +392,18 @@ export default class CronService {
|
||||
);
|
||||
}
|
||||
|
||||
private async runSingle(cronId: number): Promise<number> {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
private async runSingle(cronId: number): Promise<number | void> {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
const cron = await this.getDb({ id: cronId });
|
||||
const params = { name: cron.name, command: cron.command, schedule: cron.schedule, extraSchedules: cron.extra_schedules };
|
||||
if (cron.status !== CrontabStatus.queued) {
|
||||
resolve();
|
||||
resolve(params);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`);
|
||||
|
||||
let { id, command, log_path } = cron;
|
||||
const uniqPath = await getUniqPath(command, `${id}`);
|
||||
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 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' });
|
||||
|
||||
await CrontabModel.update(
|
||||
@@ -419,17 +427,12 @@ export default class CronService {
|
||||
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
|
||||
});
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`[panel][任务退出] 任务 ${command} 进程id: ${cp.pid}, 退出码 ${code}`,
|
||||
);
|
||||
});
|
||||
cp.on('close', async (code) => {
|
||||
cp.on('exit', async (code) => {
|
||||
await CrontabModel.update(
|
||||
{ status: CrontabStatus.idle, pid: undefined },
|
||||
{ where: { id } },
|
||||
);
|
||||
resolve();
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -447,6 +450,7 @@ export default class CronService {
|
||||
const sixCron = docs
|
||||
.filter((x) => this.isSixCron(x))
|
||||
.map((doc) => ({
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
@@ -498,7 +502,7 @@ export default class CronService {
|
||||
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
|
||||
command = `${TASK_PREFIX}${tab.command}`;
|
||||
}
|
||||
let commandVariable = `ID=${tab.id} `
|
||||
let commandVariable = `no_tee=true ID=${tab.id} `
|
||||
if (tab.task_before) {
|
||||
commandVariable += `task_before='${tab.task_before.replace(/'/g, "'\\''")
|
||||
.trim()}' `;
|
||||
@@ -578,6 +582,7 @@ export default class CronService {
|
||||
const sixCron = tabs.data
|
||||
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
|
||||
.map((doc) => ({
|
||||
name: doc.name || '',
|
||||
id: String(doc.id),
|
||||
schedule: doc.schedule!,
|
||||
command: this.makeCommand(doc),
|
||||
|
||||
@@ -273,7 +273,7 @@ export default class DependenceService {
|
||||
this.updateLog(depIds, JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
cp.on('exit', async (code) => {
|
||||
const endTime = dayjs();
|
||||
const isSucceed = code === 0;
|
||||
const resultText = isSucceed ? '成功' : '失败';
|
||||
|
||||
+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 got from 'got';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import winston from 'winston';
|
||||
import { parseBody, parseHeaders } from '../config/util';
|
||||
import { NotificationInfo } from '../data/notify';
|
||||
import UserService from './user';
|
||||
|
||||
@Service()
|
||||
export default class NotificationService {
|
||||
@@ -31,6 +31,7 @@ export default class NotificationService {
|
||||
['pushMe', this.pushMe],
|
||||
['webhook', this.webhook],
|
||||
['lark', this.lark],
|
||||
['chronocat', this.chronocat],
|
||||
]);
|
||||
|
||||
private title = '';
|
||||
@@ -195,7 +196,8 @@ export default class NotificationService {
|
||||
}
|
||||
|
||||
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')) {
|
||||
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() {
|
||||
const {
|
||||
webhookUrl,
|
||||
|
||||
+29
-14
@@ -42,15 +42,22 @@ export default class ScheduleService {
|
||||
|
||||
private maxBuffer = 200 * 1024 * 1024;
|
||||
|
||||
constructor(@Inject('logger') private logger: winston.Logger) {}
|
||||
constructor(@Inject('logger') private logger: winston.Logger) { }
|
||||
|
||||
async runTask(
|
||||
command: string,
|
||||
callbacks: TaskCallbacks = {},
|
||||
params: {
|
||||
schedule?: string;
|
||||
name?: string;
|
||||
command?: string;
|
||||
},
|
||||
completionTime: 'start' | 'end' = 'end',
|
||||
) {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
|
||||
|
||||
try {
|
||||
const startTime = dayjs();
|
||||
await callbacks.onBefore?.(startTime);
|
||||
@@ -82,20 +89,14 @@ export default class ScheduleService {
|
||||
await callbacks.onError?.(JSON.stringify(err));
|
||||
});
|
||||
|
||||
cp.on('exit', async (code, signal) => {
|
||||
this.logger.info(
|
||||
`[panel][任务退出] ${command} 进程id: ${cp.pid}, 退出码 ${code}`,
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
cp.on('exit', async (code) => {
|
||||
const endTime = dayjs();
|
||||
await callbacks.onEnd?.(
|
||||
cp,
|
||||
endTime,
|
||||
endTime.diff(startTime, 'seconds'),
|
||||
);
|
||||
resolve(null);
|
||||
resolve({ ...params, pid: cp.pid, code });
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
@@ -126,12 +127,20 @@ export default class ScheduleService {
|
||||
this.scheduleStacks.set(
|
||||
_id,
|
||||
nodeSchedule.scheduleJob(_id, schedule, async () => {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
schedule,
|
||||
command,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
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(
|
||||
name,
|
||||
() => {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
command,
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
this.logger.error(
|
||||
@@ -180,7 +192,10 @@ export default class ScheduleService {
|
||||
this.intervalSchedule.addIntervalJob(job);
|
||||
|
||||
if (runImmediately) {
|
||||
this.runTask(command, callbacks);
|
||||
this.runTask(command, callbacks, {
|
||||
name,
|
||||
command,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export default class ScriptService {
|
||||
private sockService: SockService,
|
||||
private cronService: CronService,
|
||||
private scheduleService: ScheduleService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
private taskCallbacks(filePath: string): TaskCallbacks {
|
||||
return {
|
||||
onEnd: async (cp, endTime, diff) => {
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
},
|
||||
onError: async (message: string) => {
|
||||
this.sockService.sendMessage({
|
||||
@@ -46,6 +46,7 @@ export default class ScriptService {
|
||||
const pid = await this.scheduleService.runTask(
|
||||
command,
|
||||
this.taskCallbacks(filePath),
|
||||
{ command },
|
||||
'start',
|
||||
);
|
||||
|
||||
@@ -59,7 +60,7 @@ export default class ScriptService {
|
||||
}
|
||||
try {
|
||||
await killTask(pid);
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
|
||||
return { code: 200 };
|
||||
}
|
||||
|
||||
@@ -320,7 +320,11 @@ export default class SubscriptionService {
|
||||
|
||||
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[]) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class SystemService {
|
||||
@Inject('logger') private logger: winston.Logger,
|
||||
private scheduleService: ScheduleService,
|
||||
private sockService: SockService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
public async getSystemConfig() {
|
||||
const doc = await this.getDb({ type: AuthDataType.systemConfig });
|
||||
@@ -114,7 +114,7 @@ export default class SystemService {
|
||||
},
|
||||
);
|
||||
lastVersionContent = await parseContentVersion(result.body);
|
||||
} catch (error) {}
|
||||
} catch (error) { }
|
||||
|
||||
if (!lastVersionContent) {
|
||||
lastVersionContent = currentVersionContent;
|
||||
@@ -232,6 +232,9 @@ export default class SystemService {
|
||||
this.scheduleService.runTask(
|
||||
`real_log_path=${logPath} real_time=true ${command}`,
|
||||
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 { AuthDataType, AuthModel } from '../data/auth';
|
||||
import Logger from '../loaders/logger';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
class TaskLimit {
|
||||
private oneLimit = pLimit(1);
|
||||
private updateLogLimit = pLimit(1);
|
||||
private cpuLimit = pLimit(Math.max(os.cpus().length, 4));
|
||||
private oneLimit = new PQueue({ concurrency: 1 });
|
||||
private updateLogLimit = new PQueue({ concurrency: 1 });
|
||||
private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
|
||||
|
||||
get cpuLimitActiveCount() {
|
||||
return this.cpuLimit.activeCount;
|
||||
get cronLimitActiveCount() {
|
||||
return this.cronLimit.pending;
|
||||
}
|
||||
|
||||
get cpuLimitPendingCount() {
|
||||
return this.cpuLimit.pendingCount;
|
||||
get cronLimitPendingCount() {
|
||||
return this.cronLimit.size;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.setCustomLimit();
|
||||
this.handleEvents();
|
||||
}
|
||||
|
||||
private handleEvents() {
|
||||
this.cronLimit.on('add', () => {
|
||||
Logger.info(
|
||||
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||
);
|
||||
})
|
||||
this.cronLimit.on('active', () => {
|
||||
Logger.info(
|
||||
`[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||
);
|
||||
})
|
||||
this.cronLimit.on('completed', (param) => {
|
||||
Logger.info(
|
||||
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
|
||||
);
|
||||
});
|
||||
this.cronLimit.on('error', error => {
|
||||
Logger.error(
|
||||
`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
|
||||
);
|
||||
});
|
||||
this.cronLimit.on('next', () => {
|
||||
Logger.info(
|
||||
`[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
|
||||
);
|
||||
});
|
||||
this.cronLimit.on('idle', () => {
|
||||
Logger.info(
|
||||
`[schedule][任务队列] 空闲中...`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async setCustomLimit(limit?: number) {
|
||||
if (limit) {
|
||||
this.cpuLimit = pLimit(limit);
|
||||
this.cronLimit.concurrency = limit;
|
||||
return;
|
||||
}
|
||||
await AuthModel.sync();
|
||||
@@ -31,23 +64,20 @@ class TaskLimit {
|
||||
where: { type: AuthDataType.systemConfig },
|
||||
});
|
||||
if (doc?.info?.cronConcurrency) {
|
||||
this.cpuLimit = pLimit(doc?.info?.cronConcurrency);
|
||||
this.cronLimit.concurrency = doc.info.cronConcurrency;
|
||||
}
|
||||
}
|
||||
|
||||
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> {
|
||||
Logger.info(
|
||||
`[schedule][任务加入队列] 运行中任务数: ${this.cpuLimitActiveCount}, 等待中任务数: ${this.cpuLimitPendingCount}`,
|
||||
);
|
||||
return this.cpuLimit(fn);
|
||||
public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||
return this.cronLimit.add(fn, options);
|
||||
}
|
||||
|
||||
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return this.oneLimit(fn);
|
||||
public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||
return this.oneLimit.add(fn, options);
|
||||
}
|
||||
|
||||
public updateDepLog<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return this.updateLogLimit(fn);
|
||||
public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
|
||||
return this.updateLogLimit.add(fn, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,10 @@ import { spawn } from 'cross-spawn';
|
||||
import taskLimit from './pLimit';
|
||||
import Logger from '../loaders/logger';
|
||||
|
||||
export function runCron(cmd: string): Promise<number> {
|
||||
return taskLimit.runWithCpuLimit(() => {
|
||||
export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
|
||||
return taskLimit.runWithCronLimit(() => {
|
||||
return new Promise(async (resolve: any) => {
|
||||
Logger.info(`[schedule][开始执行任务] 运行命令: ${cmd}`);
|
||||
|
||||
Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
|
||||
const cp = spawn(cmd, { shell: '/bin/bash' });
|
||||
|
||||
cp.stderr.on('data', (data) => {
|
||||
@@ -24,9 +23,8 @@ export function runCron(cmd: string): Promise<number> {
|
||||
);
|
||||
});
|
||||
|
||||
cp.on('close', async (code) => {
|
||||
Logger.info(`[schedule][任务退出] ${cmd} 进程id: ${cp.pid} 退出, 退出码 ${code}`);
|
||||
resolve();
|
||||
cp.on('exit', async (code) => {
|
||||
resolve({ ...options, command: cmd, pid: cp.pid, code });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@
|
||||
"nedb": "^1.8.0",
|
||||
"node-schedule": "^2.1.0",
|
||||
"nodemailer": "^6.7.2",
|
||||
"p-limit": "3.1.0",
|
||||
"p-queue-cjs": "7.3.4",
|
||||
"protobufjs": "^7.2.3",
|
||||
"pstree.remy": "^1.1.8",
|
||||
"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
|
||||
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 变量名= 声明即可
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
const querystring = require('querystring');
|
||||
const got = require('got');
|
||||
const $ = new Env();
|
||||
const timeout = 15000; //超时时间(单位毫秒)
|
||||
// =======================================gotify通知设置区域==============================================
|
||||
@@ -150,6 +151,23 @@ let SMTP_NAME = '';
|
||||
//此处填你的PushMe KEY.
|
||||
let PUSHME_KEY = '';
|
||||
|
||||
// =======================================CHRONOCAT通知设置区域===========================================
|
||||
// CHRONOCAT_URL Red协议连接地址 例: http://127.0.0.1:16530
|
||||
// CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
|
||||
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
|
||||
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
|
||||
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
|
||||
let CHRONOCAT_TOKEN = ''; //CHRONOCAT 生成的访问密钥
|
||||
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
|
||||
|
||||
// =======================================自定义通知设置区域=======================================
|
||||
// 自定义通知 接收回调的URL
|
||||
let WEBHOOK_URL = '';
|
||||
let WEBHOOK_BODY = '';
|
||||
let WEBHOOK_HEADERS = '';
|
||||
let WEBHOOK_METHOD = '';
|
||||
let WEBHOOK_CONTENT_TYPE = '';
|
||||
|
||||
//==========================云端环境变量的判断与接收=========================
|
||||
if (process.env.GOTIFY_URL) {
|
||||
GOTIFY_URL = process.env.GOTIFY_URL;
|
||||
@@ -306,6 +324,32 @@ if (process.env.SMTP_NAME) {
|
||||
if (process.env.PUSHME_KEY) {
|
||||
PUSHME_KEY = process.env.PUSHME_KEY;
|
||||
}
|
||||
|
||||
if (process.env.CHRONOCAT_URL) {
|
||||
CHRONOCAT_URL = process.env.CHRONOCAT_URL;
|
||||
}
|
||||
if (process.env.CHRONOCAT_QQ) {
|
||||
CHRONOCAT_QQ = process.env.CHRONOCAT_QQ;
|
||||
}
|
||||
if (process.env.CHRONOCAT_TOKEN) {
|
||||
CHRONOCAT_TOKEN = process.env.CHRONOCAT_TOKEN;
|
||||
}
|
||||
|
||||
if (process.env.WEBHOOK_URL) {
|
||||
WEBHOOK_URL = process.env.WEBHOOK_URL;
|
||||
}
|
||||
if (process.env.WEBHOOK_BODY) {
|
||||
WEBHOOK_BODY = process.env.WEBHOOK_BODY;
|
||||
}
|
||||
if (process.env.WEBHOOK_HEADERS) {
|
||||
WEBHOOK_HEADERS = process.env.WEBHOOK_HEADERS;
|
||||
}
|
||||
if (process.env.WEBHOOK_METHOD) {
|
||||
WEBHOOK_METHOD = process.env.WEBHOOK_METHOD;
|
||||
}
|
||||
if (process.env.WEBHOOK_CONTENT_TYPE) {
|
||||
WEBHOOK_CONTENT_TYPE = process.env.WEBHOOK_CONTENT_TYPE;
|
||||
}
|
||||
//==========================云端环境变量的判断与接收=========================
|
||||
|
||||
/**
|
||||
@@ -355,6 +399,8 @@ async function sendNotify(
|
||||
fsBotNotify(text, desp), //飞书机器人
|
||||
smtpNotify(text, desp), //SMTP 邮件
|
||||
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 = {
|
||||
sendNotify,
|
||||
BARK_PUSH,
|
||||
|
||||
+177
-3
@@ -101,7 +101,17 @@ push_config = {
|
||||
'SMTP_PASSWORD': '', # 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 = []
|
||||
# fmt: on
|
||||
@@ -446,7 +456,9 @@ class WeCom:
|
||||
return data["access_token"]
|
||||
|
||||
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 = {
|
||||
"touser": touser,
|
||||
"msgtype": "text",
|
||||
@@ -460,7 +472,9 @@ class WeCom:
|
||||
return respone["errmsg"]
|
||||
|
||||
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 = {
|
||||
"touser": touser,
|
||||
"msgtype": "mpnews",
|
||||
@@ -643,6 +657,7 @@ def smtp(title: str, content: str) -> None:
|
||||
except Exception as e:
|
||||
print(f"SMTP 邮件 推送失败!{e}")
|
||||
|
||||
|
||||
def pushme(title: str, content: str) -> None:
|
||||
"""
|
||||
使用 PushMe 推送消息。
|
||||
@@ -665,6 +680,157 @@ def pushme(title: str, content: str) -> None:
|
||||
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:
|
||||
"""
|
||||
获取一条一言。
|
||||
@@ -721,6 +887,14 @@ if (
|
||||
notify_function.append(smtp)
|
||||
if push_config.get("PUSHME_KEY"):
|
||||
notify_function.append(pushme)
|
||||
if (
|
||||
push_config.get("CHRONOCAT_URL")
|
||||
and push_config.get("CHRONOCAT_QQ")
|
||||
and push_config.get("CHRONOCAT_TOKEN")
|
||||
):
|
||||
notify_function.append(chronocat)
|
||||
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
|
||||
notify_function.append(custom_notify)
|
||||
|
||||
|
||||
def send(title: str, content: str) -> None:
|
||||
|
||||
+8
-6
@@ -105,7 +105,7 @@ run_normal() {
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
|
||||
$timeoutCmd $which_program $file_param
|
||||
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||
}
|
||||
|
||||
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
|
||||
@@ -147,7 +147,7 @@ run_concurrent() {
|
||||
for i in "${!array[@]}"; do
|
||||
export "${env_param}=${array[i]}"
|
||||
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
|
||||
eval $timeoutCmd $which_program $file_param &>$single_log_path &
|
||||
eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
|
||||
done
|
||||
|
||||
wait
|
||||
@@ -190,7 +190,7 @@ run_designated() {
|
||||
cd ${relative_path}
|
||||
file_param=${file_param/$relative_path\//}
|
||||
fi
|
||||
$timeoutCmd $which_program $file_param
|
||||
$timeoutCmd $which_program $file_param "${script_params[@]}"
|
||||
}
|
||||
|
||||
## 运行其他命令
|
||||
@@ -241,6 +241,8 @@ main() {
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_before "$@"
|
||||
main "$@"
|
||||
handle_task_after "$@"
|
||||
handle_task_start "${task_shell_params[@]}"
|
||||
run_task_before "${task_shell_params[@]}"
|
||||
main "${task_shell_params[@]}"
|
||||
run_task_after "${task_shell_params[@]}"
|
||||
handle_task_end "${task_shell_params[@]}"
|
||||
|
||||
+18
-11
@@ -100,9 +100,6 @@ set_proxy() {
|
||||
unset_proxy() {
|
||||
unset http_proxy
|
||||
unset https_proxy
|
||||
unset ftp_proxy
|
||||
unset all_proxy
|
||||
unset no_proxy
|
||||
}
|
||||
|
||||
make_dir() {
|
||||
@@ -311,10 +308,8 @@ random_range() {
|
||||
|
||||
reload_pm2() {
|
||||
cd $dir_root
|
||||
# 代理会影响 grpc 服务
|
||||
unset_proxy
|
||||
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() {
|
||||
@@ -407,8 +402,18 @@ init_nginx() {
|
||||
local aliasStr=""
|
||||
local rootStr=""
|
||||
if [[ $ql_base_url != "/" ]]; then
|
||||
if [[ $ql_base_url != /* ]]; then
|
||||
ql_base_url="/$ql_base_url"
|
||||
fi
|
||||
if [[ $ql_base_url != */ ]]; then
|
||||
ql_base_url="$ql_base_url/"
|
||||
fi
|
||||
location_url="^~${ql_base_url%*/}"
|
||||
aliasStr="alias ${dir_static}/dist;"
|
||||
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
|
||||
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
|
||||
mv temp.html "${dir_static}/dist/index.html"
|
||||
fi
|
||||
else
|
||||
rootStr="root ${dir_static}/dist;"
|
||||
fi
|
||||
@@ -428,11 +433,12 @@ init_nginx() {
|
||||
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
|
||||
}
|
||||
|
||||
handle_task_before() {
|
||||
handle_task_start() {
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
|
||||
|
||||
echo -e "## 开始执行... $begin_time\n"
|
||||
}
|
||||
|
||||
run_task_before() {
|
||||
[[ $is_macos -eq 0 ]] && check_server
|
||||
|
||||
. $file_task_before "$@"
|
||||
@@ -444,7 +450,7 @@ handle_task_before() {
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_after() {
|
||||
run_task_after() {
|
||||
. $file_task_after "$@"
|
||||
|
||||
if [[ $task_after ]]; then
|
||||
@@ -452,7 +458,9 @@ handle_task_after() {
|
||||
eval "$task_after"
|
||||
echo -e "\n执行后置命令结束"
|
||||
fi
|
||||
}
|
||||
|
||||
handle_task_end() {
|
||||
local etime=$(date "+$time_format")
|
||||
local end_time=$(format_time "$time_format" "$etime")
|
||||
local end_timestamp=$(format_timestamp "$time_format" "$etime")
|
||||
@@ -460,8 +468,7 @@ handle_task_after() {
|
||||
|
||||
[[ "$diff_time" == 0 ]] && diff_time=1
|
||||
|
||||
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||
|
||||
echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒 "
|
||||
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
|
||||
}
|
||||
|
||||
|
||||
+22
-9
@@ -5,9 +5,9 @@ dir_shell=$QL_DIR/shell
|
||||
. $dir_shell/share.sh
|
||||
. $dir_shell/api.sh
|
||||
|
||||
trap "single_hanle" 2 3 20 15 14
|
||||
trap "single_hanle" 2 3 20 15 14 19 1
|
||||
single_hanle() {
|
||||
eval handle_task_after "$@" "$cmd"
|
||||
eval handle_task_end "$@" "$cmd"
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@ handle_log_path() {
|
||||
log_path="$real_log_path"
|
||||
fi
|
||||
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||
make_dir "$dir_log/$log_dir"
|
||||
if [[ "$show_log" == "true" ]]; then
|
||||
cmd="2>&1 | tee -a $dir_log/$log_path"
|
||||
if [[ "$no_tee" == "true" ]]; then
|
||||
cmd=">> $dir_log/$log_path 2>&1"
|
||||
fi
|
||||
|
||||
if [[ "$real_time" == "true" ]]; then
|
||||
@@ -95,6 +95,21 @@ format_params() {
|
||||
fi
|
||||
fi
|
||||
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
|
||||
|
||||
# 分割 task 内置参数和脚本参数
|
||||
task_shell_params=()
|
||||
script_params=()
|
||||
found_double_dash=false
|
||||
|
||||
for arg in "$@"; do
|
||||
if $found_double_dash; then
|
||||
script_params+=("$arg")
|
||||
elif [ "$arg" == "--" ]; then
|
||||
found_double_dash=true
|
||||
else
|
||||
task_shell_params+=("$arg")
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
init_begin_time() {
|
||||
@@ -119,11 +134,9 @@ if [[ $max_time ]]; then
|
||||
fi
|
||||
|
||||
format_params "$@"
|
||||
define_program "$@"
|
||||
handle_log_path "$@"
|
||||
define_program "${task_shell_params[@]}"
|
||||
handle_log_path "${task_shell_params[@]}"
|
||||
init_begin_time
|
||||
|
||||
eval . $dir_shell/otask.sh "$cmd"
|
||||
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
|
||||
|
||||
exit 0
|
||||
|
||||
@@ -24,9 +24,13 @@ body {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ant-modal-header {
|
||||
padding-right: 54px;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
max-height: calc(90vh - 110px);
|
||||
max-height: calc(90vh - var(--vh-offset, 110px));
|
||||
max-height: calc(80vh - 110px);
|
||||
max-height: calc(80vh - var(--vh-offset, 110px));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -181,6 +185,7 @@ body {
|
||||
.react-codemirror2,
|
||||
.CodeMirror {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ export default function () {
|
||||
useEffect(() => {
|
||||
if (!user || !user.username) return;
|
||||
const ws = WebSocketManager.getInstance(
|
||||
`${window.location.origin}/api/ws?token=${localStorage.getItem(
|
||||
`${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem(
|
||||
config.authKey,
|
||||
)}`,
|
||||
);
|
||||
@@ -320,7 +320,7 @@ export default function () {
|
||||
shape="square"
|
||||
size="small"
|
||||
icon={<UserOutlined />}
|
||||
src={user.avatar ? `/api/static/${user.avatar}` : ''}
|
||||
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||
</span>
|
||||
@@ -342,7 +342,7 @@ export default function () {
|
||||
shape="square"
|
||||
size="small"
|
||||
icon={<UserOutlined />}
|
||||
src={user.avatar ? `/api/static/${user.avatar}` : ''}
|
||||
src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
|
||||
/>
|
||||
<span style={{ marginLeft: 5 }}>{user.username}</span>
|
||||
</span>
|
||||
|
||||
@@ -459,6 +459,12 @@
|
||||
"新增定时规则": "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 after executing the task, e.g., cp/mv/python3 xxx.py/node xxx.js",
|
||||
"请输入运行任务前要执行的命令": "Please enter the command to run before executing the task",
|
||||
"请输入运行任务后要执行的命令": "Please enter the command to run after executing the task"
|
||||
"请输入运行任务前要执行的命令,不能包含 task 命令": "Please enter the command to run before executing the task, cannot contain task commands",
|
||||
"请输入运行任务后要执行的命令,不能包含 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",
|
||||
"请输入运行任务前要执行的命令": "请输入运行任务前要执行的命令",
|
||||
"请输入运行任务后要执行的命令": "请输入运行任务后要执行的命令"
|
||||
"请输入运行任务前要执行的命令,不能包含 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 VirtualList from 'rc-virtual-list';
|
||||
import useScrollHeight from '@/hooks/useScrollHeight';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -52,8 +53,6 @@ interface LogItem {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
|
||||
const CronDetailModal = ({
|
||||
cron = {},
|
||||
handleCancel,
|
||||
@@ -121,10 +120,11 @@ const CronDetailModal = ({
|
||||
};
|
||||
|
||||
const onClickItem = (item: LogItem) => {
|
||||
localStorage.setItem('logCron', currentCron.id);
|
||||
setLogUrl(
|
||||
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
||||
);
|
||||
const url = `${config.apiPrefix}logs/${item.filename}?path=${
|
||||
item.directory || ''
|
||||
}`;
|
||||
localStorage.setItem('logCron', url);
|
||||
setLogUrl(url);
|
||||
request
|
||||
.get(
|
||||
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
|
||||
@@ -498,7 +498,12 @@ const CronDetailModal = ({
|
||||
</div>
|
||||
<div className="cron-detail-info-item">
|
||||
<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 className="cron-detail-info-item">
|
||||
<div className="cron-detail-info-title">
|
||||
@@ -506,11 +511,9 @@ const CronDetailModal = ({
|
||||
</div>
|
||||
<div className="cron-detail-info-value">
|
||||
{currentCron.last_execution_time
|
||||
? new Date(currentCron.last_execution_time * 1000)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
? dayjs(currentCron.last_execution_time * 1000).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
@@ -530,11 +533,7 @@ const CronDetailModal = ({
|
||||
</div>
|
||||
<div className="cron-detail-info-value">
|
||||
{currentCron.nextRunTime &&
|
||||
currentCron.nextRunTime
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')}
|
||||
dayjs(currentCron.nextRunTime).format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
overflow: auto;
|
||||
|
||||
.ant-card-body {
|
||||
min-width: 600px;
|
||||
min-width: 1000px;
|
||||
}
|
||||
|
||||
.cron-detail-info-item {
|
||||
@@ -58,7 +58,7 @@
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-width: 600px;
|
||||
min-width: 1000px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-right: 32px;
|
||||
|
||||
.operations {
|
||||
display: flex;
|
||||
|
||||
+36
-27
@@ -51,13 +51,14 @@ import ViewManageModal from './viewManageModal';
|
||||
import { FilterValue, SorterResult } from 'antd/lib/table/interface';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import useTableScrollHeight from '@/hooks/useTableScrollHeight';
|
||||
import { getCommandScript, parseCrontab } from '@/utils';
|
||||
import { getCommandScript, getCrontabsNextDate, parseCrontab } from '@/utils';
|
||||
import { ColumnProps } from 'antd/lib/table';
|
||||
import { useVT } from 'virtualizedtableforantd4';
|
||||
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
|
||||
import Name from '@/components/name';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { Text, Paragraph, Link } = Typography;
|
||||
const { Search } = Input;
|
||||
|
||||
const Crontab = () => {
|
||||
@@ -74,17 +75,18 @@ const Crontab = () => {
|
||||
style={{
|
||||
wordBreak: 'break-all',
|
||||
marginBottom: 0,
|
||||
color: '#1890ff'
|
||||
}}
|
||||
ellipsis={{ tooltip: text, rows: 2 }}
|
||||
>
|
||||
<a
|
||||
<Link
|
||||
onClick={() => {
|
||||
setDetailCron(record);
|
||||
setIsDetailModalVisible(true);
|
||||
}}
|
||||
>
|
||||
{record.name || '-'}
|
||||
</a>
|
||||
</Link>
|
||||
</Paragraph>
|
||||
),
|
||||
sorter: {
|
||||
@@ -183,17 +185,29 @@ const Crontab = () => {
|
||||
compare: (a, b) => a.schedule.localeCompare(b.schedule),
|
||||
},
|
||||
render: (text, record) => {
|
||||
return record.extra_schedules?.length ? (
|
||||
<Popover
|
||||
placement="right"
|
||||
content={record.extra_schedules?.map((x) => (
|
||||
<div>{x.schedule}</div>
|
||||
))}
|
||||
return (
|
||||
<Paragraph
|
||||
style={{
|
||||
wordBreak: 'break-all',
|
||||
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}
|
||||
</Popover>
|
||||
) : (
|
||||
text
|
||||
</Paragraph>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -224,7 +238,6 @@ const Crontab = () => {
|
||||
},
|
||||
},
|
||||
render: (text, record) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
@@ -232,11 +245,9 @@ const Crontab = () => {
|
||||
}}
|
||||
>
|
||||
{record.last_execution_time
|
||||
? new Date(record.last_execution_time * 1000)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:')
|
||||
? dayjs(record.last_execution_time * 1000).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
)
|
||||
: '-'}
|
||||
</span>
|
||||
);
|
||||
@@ -251,12 +262,7 @@ const Crontab = () => {
|
||||
},
|
||||
},
|
||||
render: (text, record) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
return record.nextRunTime
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:');
|
||||
return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -389,7 +395,7 @@ const Crontab = () => {
|
||||
data.map((x) => {
|
||||
return {
|
||||
...x,
|
||||
nextRunTime: parseCrontab(x.schedule),
|
||||
nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -677,7 +683,10 @@ const Crontab = () => {
|
||||
if (code === 200) {
|
||||
const index = value.findIndex((x) => x.id === cron.id);
|
||||
const result = [...value];
|
||||
data.nextRunTime = parseCrontab(data.schedule);
|
||||
data.nextRunTime = getCrontabsNextDate(
|
||||
data.schedule,
|
||||
data.extra_schedules,
|
||||
);
|
||||
if (index !== -1) {
|
||||
result.splice(index, 1, {
|
||||
...cron,
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import intl from 'react-intl-universal';
|
||||
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 config from '@/utils/config';
|
||||
import {
|
||||
@@ -10,6 +18,7 @@ import {
|
||||
import { PageLoading } from '@ant-design/pro-layout';
|
||||
import { logEnded } from '@/utils';
|
||||
import { CrontabStatus } from './type';
|
||||
import Ansi from 'ansi-to-react';
|
||||
|
||||
const { Countdown } = Statistic;
|
||||
|
||||
@@ -31,6 +40,7 @@ const CronLogModal = ({
|
||||
const [executing, setExecuting] = useState<any>(true);
|
||||
const [isPhone, setIsPhone] = useState(false);
|
||||
const scrollInfoRef = useRef({ value: 0, down: true });
|
||||
const uniqPath = logUrl ? logUrl : String(cron?.id);
|
||||
|
||||
const getCronLog = (isFirst?: boolean) => {
|
||||
if (isFirst) {
|
||||
@@ -41,7 +51,7 @@ const CronLogModal = ({
|
||||
.then(({ code, data }) => {
|
||||
if (
|
||||
code === 200 &&
|
||||
localStorage.getItem('logCron') === String(cron.id) &&
|
||||
localStorage.getItem('logCron') === uniqPath &&
|
||||
data !== value
|
||||
) {
|
||||
const log = data as string;
|
||||
@@ -49,10 +59,15 @@ const CronLogModal = ({
|
||||
const hasNext = Boolean(
|
||||
log && !logEnded(log) && !log.includes('任务未运行'),
|
||||
);
|
||||
if (!hasNext && !logEnded(value) && value !== intl.get('启动中...')) {
|
||||
setTimeout(() => {
|
||||
autoScroll();
|
||||
});
|
||||
}
|
||||
setExecuting(hasNext);
|
||||
if (hasNext) {
|
||||
autoScroll();
|
||||
setTimeout(() => {
|
||||
autoScroll();
|
||||
getCronLog();
|
||||
}, 2000);
|
||||
}
|
||||
@@ -87,18 +102,20 @@ const CronLogModal = ({
|
||||
if (scrollInfoRef.current.down) {
|
||||
scrollInfoRef.current = {
|
||||
value: sTop,
|
||||
down: sTop > scrollInfoRef.current.value || !sTop,
|
||||
down: sTop - scrollInfoRef.current.value > -5 || !sTop,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const titleElement = () => {
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
{(executing || loading) && <Loading3QuartersOutlined spin />}
|
||||
{!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>
|
||||
)}
|
||||
<div id="log-flag"></div>
|
||||
|
||||
@@ -74,7 +74,11 @@ const CronModal = ({
|
||||
name="form_in_modal"
|
||||
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('请输入任务名称')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -149,11 +153,26 @@ const CronModal = ({
|
||||
tooltip={intl.get(
|
||||
'运行任务前执行的命令,比如 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
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={intl.get('请输入运行任务前要执行的命令')}
|
||||
placeholder={intl.get(
|
||||
'请输入运行任务前要执行的命令,不能包含 task 命令',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
@@ -162,11 +181,26 @@ const CronModal = ({
|
||||
tooltip={intl.get(
|
||||
'运行任务后执行的命令,比如 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
|
||||
rows={4}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={intl.get('请输入运行任务后要执行的命令')}
|
||||
placeholder={intl.get(
|
||||
'请输入运行任务后要执行的命令,不能包含 task 命令',
|
||||
)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
@@ -92,7 +92,7 @@ const Dependence = () => {
|
||||
const columns: any = [
|
||||
{
|
||||
title: intl.get('序号'),
|
||||
width: 80,
|
||||
width: 90,
|
||||
render: (text: string, record: any, index: number) => {
|
||||
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 Copy from '../../components/copy';
|
||||
import { useVT } from 'virtualizedtableforantd4';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Paragraph } = Typography;
|
||||
const { Search } = Input;
|
||||
@@ -137,13 +138,9 @@ const Env = () => {
|
||||
},
|
||||
},
|
||||
render: (text: string, record: any) => {
|
||||
const language = navigator.language || navigator.languages[0];
|
||||
const time = record.updatedAt || record.timestamp;
|
||||
const date = new Date(time)
|
||||
.toLocaleString(language, {
|
||||
hour12: false,
|
||||
})
|
||||
.replace(' 24:', ' 00:');
|
||||
const date = dayjs(record.updatedAt || record.timestamp).format(
|
||||
'YYYY-MM-DD HH:mm:ss',
|
||||
);
|
||||
return (
|
||||
<Tooltip
|
||||
placement="topLeft"
|
||||
|
||||
@@ -16,6 +16,7 @@ import { request } from '@/utils/http';
|
||||
import { useTheme } from '@/utils/hooks';
|
||||
import { MobileOutlined } from '@ant-design/icons';
|
||||
import { SharedContext } from '@/layouts';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
const { Countdown } = Statistic;
|
||||
@@ -86,7 +87,7 @@ const Login = () => {
|
||||
<>
|
||||
<div>
|
||||
{intl.get('上次登录时间:')}
|
||||
{lastlogon ? new Date(lastlogon).toLocaleString() : '-'}
|
||||
{lastlogon ? dayjs(lastlogon).format('YYYY-MM-DD HH:mm:ss') : '-'}
|
||||
</div>
|
||||
<div>
|
||||
{intl.get('上次登录地点:')}
|
||||
|
||||
@@ -16,6 +16,7 @@ import SettingModal from './setting';
|
||||
import { useTheme } from '@/utils/hooks';
|
||||
import { getEditorMode, logEnded } from '@/utils';
|
||||
import WebSocketManager from '@/utils/websocket';
|
||||
import Ansi from 'ansi-to-react';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -116,7 +117,7 @@ const EditModal = ({
|
||||
}, 300);
|
||||
}
|
||||
|
||||
setLog(p=>`${p}${_message}`);
|
||||
setLog((p) => `${p}${_message}`);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -246,7 +247,7 @@ const EditModal = ({
|
||||
padding: '0 15px',
|
||||
}}
|
||||
>
|
||||
{log}
|
||||
<Ansi>{log}</Ansi>
|
||||
</pre>
|
||||
</SplitPane>
|
||||
<SaveModal
|
||||
|
||||
@@ -77,7 +77,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
content: <pre>{lastLog}</pre>,
|
||||
content: <pre><Ansi>{lastLog}</Ansi></pre>,
|
||||
okText: intl.get('下载更新'),
|
||||
cancelText: intl.get('以后再说'),
|
||||
onOk() {
|
||||
@@ -102,7 +102,7 @@ const CheckUpdate = ({ systemInfo }: any) => {
|
||||
okButtonProps: { disabled: true },
|
||||
title: intl.get('下载更新中...'),
|
||||
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 { SharedContext } from '@/layouts';
|
||||
import './index.less';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import useResizeObserver from '@react-hook/resize-observer';
|
||||
import SystemLog from './systemLog';
|
||||
|
||||
const { Text } = Typography;
|
||||
const isDemoEnv = window.__ENV__DeployEnv === 'demo';
|
||||
@@ -334,7 +334,7 @@ const Setting = () => {
|
||||
dataSource={dataSource}
|
||||
rowKey="id"
|
||||
size="middle"
|
||||
scroll={{ x: 768 }}
|
||||
scroll={{ x: 1000 }}
|
||||
loading={loading}
|
||||
/>
|
||||
),
|
||||
@@ -347,22 +347,7 @@ const Setting = () => {
|
||||
{
|
||||
key: 'syslog',
|
||||
label: intl.get('系统日志'),
|
||||
children: (
|
||||
<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'}
|
||||
/>
|
||||
),
|
||||
children: <SystemLog data={systemLogData} height={height} theme={theme}/>,
|
||||
},
|
||||
{
|
||||
key: 'login',
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Typography, Table, Tag, Button, Spin, message } from 'antd';
|
||||
import { request } from '@/utils/http';
|
||||
import config from '@/utils/config';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Text, Link } = Typography;
|
||||
|
||||
@@ -30,7 +31,7 @@ const columns = [
|
||||
key: 'timestamp',
|
||||
width: 120,
|
||||
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 ? (
|
||||
<Select
|
||||
placeholder={x.placeholder || `请选择${x.label}`}
|
||||
placeholder={x.placeholder || `${intl.get('请选择')} ${x.label}`}
|
||||
disabled={loading}
|
||||
>
|
||||
{x.items.map((y) => (
|
||||
@@ -89,7 +89,7 @@ const NotificationSetting = ({ data }: any) => {
|
||||
<Input.TextArea
|
||||
disabled={loading}
|
||||
autoSize={{ minRows: 1, maxRows: 5 }}
|
||||
placeholder={x.placeholder || `请输入${x.label}`}
|
||||
placeholder={x.placeholder || `${intl.get('请输入')} ${x.label}`}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
@@ -253,7 +253,7 @@ const Other = ({
|
||||
method="put"
|
||||
showUploadList={false}
|
||||
maxCount={1}
|
||||
action="/api/system/data/import"
|
||||
action={`${config.apiPrefix}system/data/import`}
|
||||
onChange={(e) => {
|
||||
if (e.event?.percent) {
|
||||
showUploadProgress(parseFloat(e.event?.percent.toFixed(1)));
|
||||
|
||||
@@ -97,14 +97,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
|
||||
|
||||
const onChange = (e) => {
|
||||
if (e.file && e.file.response) {
|
||||
setAvatar(`/api/static/${e.file.response.data}`);
|
||||
setAvatar(
|
||||
`${config.apiPrefix}static/${e.file.response.data}`,
|
||||
);
|
||||
userChange();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTwoFactorActivated(user && user.twoFactorActivated);
|
||||
setAvatar(user.avatar && `/api/static/${user.avatar}`);
|
||||
setAvatar(user.avatar && `${config.apiPrefix}static/${user.avatar}`);
|
||||
}, [user]);
|
||||
|
||||
return twoFactoring ? (
|
||||
@@ -250,7 +252,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
|
||||
method="put"
|
||||
showUploadList={false}
|
||||
maxCount={1}
|
||||
action="/api/user/avatar"
|
||||
action={`${config.apiPrefix}user/avatar`}
|
||||
onChange={onChange}
|
||||
name="avatar"
|
||||
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';
|
||||
import { PageLoading } from '@ant-design/pro-layout';
|
||||
import { logEnded } from '@/utils';
|
||||
import Ansi from 'ansi-to-react';
|
||||
|
||||
const SubscriptionLogModal = ({
|
||||
subscription,
|
||||
@@ -122,7 +123,7 @@ const SubscriptionLogModal = ({
|
||||
: {}
|
||||
}
|
||||
>
|
||||
{value}
|
||||
<Ansi>{value}</Ansi>
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+62
-19
@@ -98,6 +98,7 @@ export default {
|
||||
{ value: 'email', label: intl.get('邮箱') },
|
||||
{ value: 'lark', label: intl.get('飞书机器人') },
|
||||
{ value: 'pushMe', label: 'PushMe' },
|
||||
{ value: 'chronocat', label: 'Chronocat' },
|
||||
{ value: 'webhook', label: intl.get('自定义通知') },
|
||||
{ value: 'closed', label: intl.get('已关闭') },
|
||||
],
|
||||
@@ -126,14 +127,16 @@ export default {
|
||||
goCqHttpBot: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{ label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true },
|
||||
{
|
||||
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,
|
||||
},
|
||||
@@ -153,14 +156,16 @@ export default {
|
||||
},
|
||||
{
|
||||
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: [
|
||||
{
|
||||
label: 'barkPush',
|
||||
tip: intl.get('Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
|
||||
tip: intl.get(
|
||||
'Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
@@ -188,7 +193,8 @@ export default {
|
||||
telegramBot: [
|
||||
{
|
||||
label: 'telegramBotToken',
|
||||
tip: intl.get('telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',
|
||||
tip: intl.get(
|
||||
'telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
@@ -201,7 +207,8 @@ export default {
|
||||
{ label: 'telegramBotProxyPort', tip: intl.get('代理端口') },
|
||||
{
|
||||
label: 'telegramBotProxyAuth',
|
||||
tip: intl.get('telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password',
|
||||
tip: intl.get(
|
||||
'telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password',
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -212,20 +219,23 @@ export default {
|
||||
dingtalkBot: [
|
||||
{
|
||||
label: 'dingtalkBotToken',
|
||||
tip: intl.get('钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd',
|
||||
tip: intl.get(
|
||||
'钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
label: 'dingtalkBotSecret',
|
||||
tip: intl.get('密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串',
|
||||
tip: intl.get(
|
||||
'密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串',
|
||||
),
|
||||
},
|
||||
],
|
||||
weWorkBot: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
@@ -237,7 +247,8 @@ export default {
|
||||
weWorkApp: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
@@ -249,7 +260,8 @@ export default {
|
||||
aibotk: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
@@ -265,7 +277,8 @@ export default {
|
||||
},
|
||||
{
|
||||
label: 'aibotkName',
|
||||
tip: intl.get('要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
|
||||
tip: intl.get(
|
||||
'要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
@@ -273,7 +286,8 @@ export default {
|
||||
iGot: [
|
||||
{
|
||||
label: 'iGotPushKey',
|
||||
tip: intl.get('iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX',
|
||||
tip: intl.get(
|
||||
'iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX',
|
||||
),
|
||||
required: true,
|
||||
},
|
||||
@@ -281,20 +295,23 @@ export default {
|
||||
pushPlus: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
label: 'pushPlusUser',
|
||||
tip: intl.get('一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
|
||||
tip: intl.get(
|
||||
'一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
|
||||
),
|
||||
},
|
||||
],
|
||||
lark: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
@@ -302,7 +319,8 @@ export default {
|
||||
email: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
@@ -316,6 +334,29 @@ export default {
|
||||
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: [
|
||||
{
|
||||
label: 'webhookMethod',
|
||||
@@ -335,7 +376,8 @@ export default {
|
||||
},
|
||||
{
|
||||
label: 'webhookUrl',
|
||||
tip: intl.get('请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置',
|
||||
tip: intl.get(
|
||||
'请求链接以http或者https开头。url或者body中必须包含$title,$content可选,对应api内容的位置',
|
||||
),
|
||||
required: true,
|
||||
placeholder: 'https://xxx.cn/api?content=$title\n',
|
||||
@@ -347,7 +389,8 @@ export default {
|
||||
},
|
||||
{
|
||||
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',
|
||||
},
|
||||
|
||||
+15
-2
@@ -329,7 +329,7 @@ export function getCommandScript(
|
||||
return [s, p];
|
||||
}
|
||||
|
||||
export function parseCrontab(schedule: string): Date {
|
||||
export function parseCrontab(schedule: string): Date | null {
|
||||
try {
|
||||
const time = cron_parser.parseExpression(schedule);
|
||||
if (time) {
|
||||
@@ -337,7 +337,20 @@ export function parseCrontab(schedule: string): Date {
|
||||
}
|
||||
} 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) {
|
||||
|
||||
+2
-9
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ES2015",
|
||||
"moduleResolution": "Bundler",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"jsx": "react-jsx",
|
||||
"esModuleInterop": true,
|
||||
@@ -25,14 +25,7 @@
|
||||
"include": ["src/**/*", ".umirc.ts", "typings.d.ts", "back/**/*"],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib",
|
||||
"es",
|
||||
"static",
|
||||
"data",
|
||||
"typings",
|
||||
"**/__test__",
|
||||
"test",
|
||||
"docs",
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
|
||||
+17
-10
@@ -1,11 +1,18 @@
|
||||
version: 2.16.3
|
||||
changeLogLink: https://t.me/jiao_long/394
|
||||
publishTime: 2023-09-23 12:00
|
||||
version: 2.16.4
|
||||
changeLogLink: https://t.me/jiao_long/395
|
||||
publishTime: 2023-10-18 23:00
|
||||
changeLog: |
|
||||
1. 定时任务支持设置多定时规则、任务执行前和执行后命令
|
||||
2. 增加启动环境变量 QlPort, 支持修改 hosts 模式青龙启动端口
|
||||
3. 消息通知 Bark 增加额外参数: 时效性通知、跳转Url
|
||||
4. 修复调试脚本日志丢失
|
||||
5. 环境变量名称增加复制功能
|
||||
6. 修复系统设置展示版本失败
|
||||
7. 其他 bug 修复
|
||||
1. 脚本推送增加自定义 webhook 方式
|
||||
3. 增加 chronocat 无头模式的QQNT推送
|
||||
2. task 命令支持给脚本传参,使用 -- 分割,后面的参数都会传给脚本
|
||||
4. 系统日志增加置顶置底按钮
|
||||
5. 定时任务详情增加额外定时展示
|
||||
6. 定时任务增加前后规则校验,不能包含 task 命令
|
||||
7. 修复定时任务间隔较小,任务状态不准确
|
||||
8. 修复定时任务日志滚动
|
||||
9. 修复 QlBaseUrl 末尾不加斜杠无法访问
|
||||
10. 修复定时任务详情页查看历史日志异常
|
||||
11. 修复定时任务添加 task_after 命令无法停止
|
||||
12. 修复清空文件夹报错
|
||||
13. 修复设置代理时, schedule 服务通信异常
|
||||
14. 其他优化
|
||||
|
||||
Reference in New Issue
Block a user