Compare commits

..
69 Commits
Author SHA1 Message Date
whyour 046239404f 更新版本 v2.16.4 2023-10-19 21:37:00 +08:00
whyour b90243f55c 修改 task 日志展示逻辑 2023-10-19 21:36:39 +08:00
whyour 43c0cd8132 修复 mac 任务无法正常退出 2023-10-16 23:44:39 +08:00
whyour 533e12a796 脚本推送增加自定义 webhook 方式 2023-10-15 20:42:05 +08:00
whyour bd004a0489 修复设置代理时,schedule 服务通信异常 2023-10-14 23:05:25 +08:00
whyour 41befcc0a8 更新 readme 2023-10-14 21:50:12 +08:00
whyour 9fb9b3d121 task 命令支持给脚本传参,使用 -- 分割,后面的参数都会传给脚本
例如 task x.js -n whyour,脚本就能接收到参数 -n whyour
2023-10-14 21:47:46 +08:00
5055045d22 增加chronocat无头模式的QQNT推送 用于无法使go-cqhttp的接替 (#2141)
* 增加chronocat无头模式的QQNT推送

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

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

* 增加系统通知

---------

Co-authored-by: child <wulincheng@javalc.com>
2023-10-14 20:56:32 +08:00
whyour 9f7beb934d 系统日志增加置顶置底按钮 2023-10-13 23:44:28 +08:00
whyour 26b06c17c5 修复清空文件夹报错 2023-10-13 15:35:50 +08:00
whyour b8a9b26ca3 修复定时任务任务前后命令校验 2023-10-12 00:55:22 +08:00
whyour b6376ed2e8 修复定时任务 task_after 命令无法停止 2023-10-12 00:45:49 +08:00
whyour 99f6073c8e 定时任务增加前后规则校验,不能包含 task 命令 2023-10-10 23:06:01 +08:00
whyour 5e73f0390f 修复定时任务详情页查看历史日志异常 2023-10-09 23:56:35 +08:00
whyour c35cfba8b0 修改任务队列重新设置并发 2023-10-08 22:18:54 +08:00
whyour aac109621a 修改任务队列日志打印 2023-10-08 22:11:46 +08:00
whyour a340964c82 修改获取登录 ip 判断 2023-10-08 00:10:46 +08:00
whyour 00818b694a 使用 spawn exit 方法监听进程退出 2023-10-07 22:51:44 +08:00
whyour ec5b885476 修改任务队列执行日志 2023-10-06 02:34:40 +08:00
whyour 9d55cb108c 定时任务详情增加额外定时展示 2023-09-29 23:16:07 +08:00
whyour 4c19054b30 修复 api 前缀 2023-09-29 16:26:03 +08:00
whyour 2a41f64d1b 修复 QlBaseUrl 末尾不加斜杠无法访问 2023-09-29 16:07:41 +08:00
whyour d3023d31e3 修复定时任务日志滚动 2023-09-27 23:04:16 +08:00
whyour eddc03e295 修复定时任务间隔较小,任务状态不准确 2023-09-26 22:51:33 +08:00
whyour 77a8e00b17 更新版本 v2.16.3 2023-09-24 13:39:53 +08:00
whyour 99281a061e 修复更新历史定时任务规则未生效 2023-09-24 13:39:52 +08:00
whyour 22eedebf14 修复任务管理创建视图 2023-09-24 00:47:56 +08:00
whyour d7e0531935 修复系统设置展示版本失败 2023-09-23 11:14:24 +08:00
whyour 16734326cf 修复 ws 变量错误 2023-09-22 20:44:47 +08:00
whyour 956cfe18be 环境变量名称增加复制功能 2023-09-22 00:47:36 +08:00
whyour a864a56917 修复调试脚本日志丢失 2023-09-22 00:46:16 +08:00
whyour ab3fc9b5f1 定时任务增加运行任务前和运行任务后参数 2023-09-19 22:44:32 +08:00
whyour 8d899f1a53 更新 workflow 2023-09-19 00:30:45 +08:00
whyour a1eef2c644 修复使用 ubuntu-latest linux/s390x 无法使用 npm 2023-09-18 13:09:14 +08:00
zui-langandGitHub aab096843c 消息通知 Bark 增加额外参数:时效性通知、跳转Url (#2112) 2023-09-17 01:01:34 +08:00
whyour acc7443004 定时任务支持多个定时规则 2023-09-16 00:15:50 +08:00
whyour 042d7d3b8e 修复任务管理创建视图 placeholder,更新 npm 包 2023-09-15 21:27:53 +08:00
whyour 0511a4af0d 增加 QlPort 环境变量,支持设置服务启动的端口,修复定时任务视图操作符筛选 2023-09-15 17:57:28 +08:00
whyour 748a099087 修复日志滚动逻辑 2023-09-12 22:28:26 +08:00
whyour 742d0b2135 更新版本 v2.16.2 2023-09-02 00:41:16 +08:00
whyour 2081364325 修改定时任务置顶样式 2023-09-02 00:41:13 +08:00
whyour 39dc31916e 修复日志弹框样式 2023-09-01 23:16:12 +08:00
whyour 5395cebdb4 修复环境变量有空格时并发数量错误 2023-09-01 22:35:15 +08:00
whyour fe934fa4af 修复环境变量中单引号转义 2023-09-01 15:15:25 +08:00
whyour 461fa8e131 修改系统任务日志 2023-08-29 23:29:02 +08:00
whyour 4618a19c04 修改本地监听 ip 2023-08-29 21:50:23 +08:00
whyour e2bd15683e 修复 JSON.parse 错误,修复删除环境变量名称过长 2023-08-27 12:41:06 +08:00
whyour 648b9c4520 修改系统日志时间打印 2023-08-26 20:51:17 +08:00
whyour b4e4e84bbd 修复多语言翻译 2023-08-26 15:29:44 +08:00
whyour aa5d6f3cb6 系统设置增加系统语言切换 2023-08-26 00:16:15 +08:00
whyour e9416c23df 增加任务运行数日志 2023-08-25 19:25:12 +08:00
NANSIYVandGitHub ef4999be55 修复 python 企业微信推送错误 (#2087)
close #2086
2023-08-25 10:37:03 +08:00
whyour e7be4999b0 修复定时任务日志滚动 2023-08-25 09:05:32 +08:00
whyour 5907553670 修改定时任务置顶样式 2023-08-25 00:46:28 +08:00
whyour 0b9066525a 修复任务运行错误时没有日志,修改 tooltip 样式 2023-08-24 23:14:22 +08:00
whyour ac904cae61 修复停止订阅执行日志 2023-08-23 22:57:18 +08:00
whyour 4d5fa320ea 修改私有仓库 ssh 配置 2023-08-23 18:06:28 +08:00
whyour 003defedcf 修复环境变量中$变量被计算 2023-08-23 14:55:57 +08:00
whyour 40e8041401 修复退出调试自动删除调试日志目录 2023-08-22 22:18:18 +08:00
whyour a8174e89be 更新版本 v2.16.1 2023-08-21 22:51:00 +08:00
whyour cce83f7a4c 修复日志中多语言文本判断 2023-08-21 22:50:57 +08:00
whyour 4f7649f157 系统设置增加系统运行日志 2023-08-21 00:10:43 +08:00
whyour b002cbef3a 修复手机端日志弹框样式 2023-08-19 09:29:35 +08:00
whyour 1d93fe0de0 修复添加订阅粘贴 ql 链接 2023-08-18 09:17:18 +08:00
whyour 2d936f1341 修复定时任务页面样式 2023-08-17 08:26:48 +08:00
whyour b40673e9ac 修改日志弹框样式 2023-08-16 22:14:45 +08:00
whyour 637874e426 初始化数据增加错误捕获 2023-08-11 22:50:53 +08:00
whyour de71d1a258 修复任务筛选条件切换 2023-08-11 22:25:14 +08:00
whyour e174e190ee 修复 python 企业微信推送,debian 版本关于页面报错 2023-08-11 21:40:01 +08:00
105 changed files with 3711 additions and 1704 deletions
+11 -11
View File
@@ -6,10 +6,9 @@ on:
- 'master' - 'master'
- 'develop' - 'develop'
tags: tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 - 'v*'
schedule: schedule:
# 参考 https://jasonet.co/posts/scheduled-actions/ - cron: '00 20 * * *'
- cron: '00 14 * * *' # GMT 15:00 => 北京时间 23:00
workflow_dispatch: workflow_dispatch:
jobs: jobs:
@@ -87,14 +86,15 @@ jobs:
build: build:
needs: build-static needs: build-static
runs-on: ubuntu-latest # 由于 ubuntu-latest 中使用 linux6.x 内核 linux/s390x npm 无法使用
runs-on: ubuntu-20.04
permissions: permissions:
packages: write packages: write
contents: read contents: read
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: '8.3.1' version: '8.3.1'
@@ -108,13 +108,13 @@ jobs:
timezone: Asia/Shanghai timezone: Asia/Shanghai
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v2 uses: docker/login-action@v3
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -122,7 +122,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v4 uses: docker/metadata-action@v5
with: with:
images: | images: |
${{ github.repository }} ${{ github.repository }}
@@ -142,14 +142,14 @@ jobs:
type=semver,pattern={{major}} type=semver,pattern={{major}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Build and push - name: Build and push
id: docker_build id: docker_build
uses: docker/build-push-action@v3 uses: docker/build-push-action@v5
with: with:
build-args: | build-args: |
MAINTAINER=${{ github.repository_owner }} MAINTAINER=${{ github.repository_owner }}
+1 -1
View File
@@ -20,7 +20,7 @@ src/.umi
src/.umi-production src/.umi-production
src/.umi-test src/.umi-test
.env.local .env.local
env .env
history history
version.ts version.ts
config config
+1
View File
@@ -4,6 +4,7 @@ const CompressionPlugin = require('compression-webpack-plugin');
const baseUrl = process.env.QlBaseUrl || '/'; const baseUrl = process.env.QlBaseUrl || '/';
export default defineConfig({ export default defineConfig({
hash: true, hash: true,
jsMinifier: 'terser',
antd: {}, antd: {},
locale: { locale: {
antd: true, antd: true,
+72 -55
View File
@@ -36,6 +36,7 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
- Support cell phone operation - Support cell phone operation
## Version ## Version
### docker ### docker
The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image. The `latest` image is built on `alpine` and the `debian` image is built on `debian-slim`. If you need to use a dependency that is not supported by `alpine`, it is recommended that you use the `debian` image.
@@ -53,6 +54,66 @@ The npm version supports `debian/ubuntu/centos/alpine` systems and requires `nod
npm i @whyour/qinglong npm i @whyour/qinglong
``` ```
## Built-in commands
1. task
```bash
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
# Use -- to split, -- followed by a parameter that is passed to the script, as in the following example, the script receives the parameter -u whyour -p password
task <file_path> -- -u whyour -p password
```
1. ql
```bash
# Update and restart Green Dragon
ql update
# Run custom scripts extra.sh
ql extra
# Adding a single script file
ql raw <file_url>
# Add a specific script for a single repository
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# Delete old logs
ql rmlog <days>
# Start bot
ql bot
# Detecting the Green Dragon environment and repairing it
ql check
# Reset the number of login errors
ql resetlet
# Disable two-step login
ql resettfa
```
1. parameter description
- file_url: Script address
- repo_url: Repository address
- whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
- blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
- dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
- branch: Pull the branch of the repository
- days: Number of days of logs to be kept
- file_path: File path for task execution
- env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
- account_number: Specify the account number of an environment variable to be executed when the task is executed
- max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Deployment ## Deployment
### Docker (Recommended) ### Docker (Recommended)
@@ -61,7 +122,12 @@ npm i @whyour/qinglong
# curl -sSL get.docker.com | sh # curl -sSL get.docker.com | sh
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
-p 5700:5700 \ -p 5700:5700 \
# Deployment paths are not required, e.g. /test.
-e QlBaseUrl="/" \
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
-e QlPort="5700" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \ --restart unless-stopped \
@@ -88,7 +154,12 @@ docker-compose down
podman run -dit \ podman run -dit \
--network bridge \ --network bridge \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
# The 5700 after the colon is the default port, if QlPort is set, it needs to be the same as QlPort.
-p 5700:5700 \ -p 5700:5700 \
# Deployment paths are not required, e.g. /test.
-e QlBaseUrl="/" \
# Deployment port is not required, when using host mode, you can set the port after service startup, default 5700
-e QlPort="5700" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
@@ -108,60 +179,6 @@ export QL_DATA_DIR=""
qinglong qinglong
``` ```
## Use
1. built-in commands
```bash
# Update and restart Green Dragon
ql update
# Run custom scripts extra.sh
ql extra
# Adding a single script file
ql raw <file_url>
# Add a specific script for a single repository
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch>
# Delete old logs
ql rmlog <days>
# Start bot
ql bot
# Detecting the Green Dragon environment and repairing it
ql check
# Reset the number of login errors
ql resetlet
# Disable two-step login
ql resettfa
# Execute in sequence, if a random delay is set, it will be randomly delayed by a certain number of seconds
task <file_path>
# Execute in sequence, regardless of whether a random delay is set, all run immediately,
# and the foreground will output the day, while recorded in the log file
task <file_path> now
# Concurrent execution, regardless of whether a random delay is set, are run immediately,
# the foreground does not generate the day, directly recorded in the log file, and can be specified account execution
task <file_path> conc <env_name> <account_number>(Optional)
# Specify the account to execute and run immediately regardless of whether a random delay is set
task <file_path> desi <env_name> <account_number>
# Set task timeout
task -m <max_time> <file_path>
# Print task log in real time, no need to carry this parameter when creating timed tasks
task -l <file_path>
```
2. parameter description
* file_url: Script address
* repo_url: Repository address
* whitelist: The whitelist when pulling the repository, i.e., the string contained in the path of the script to be pulled
* blacklist: Blacklisting when pulling repositories, i.e. strings that are not included in the path of the script to be pulled
* dependence: Pulling the dependencies needed for the repository will be copied directly from the repository to the repository directory under scripts, regardless of the blacklist
* branch: Pull the branch of the repository
* days: Number of days of logs to be kept
* file_path: File path for task execution
* env_name: The name of the environment variable that needs to be concurrent or specified at the time of task execution
* account_number: Specify the account number of an environment variable to be executed when the task is executed
* max_time: Timeout, suffix "s" for seconds (default), "m" for minutes, "h" for hours, "d" for days
## Development ## Development
```bash ```bash
@@ -174,7 +191,7 @@ $ pnpm install
$ pnpm start $ pnpm start
``` ```
Open your browser and visit http://127.0.0.1:5700 Open your browser and visit <http://127.0.0.1:5700>
## Links ## Links
+68 -55
View File
@@ -38,6 +38,7 @@ Timed task management platform supporting Python3, JavaScript, Shell, Typescript
- 支持手机端操作 - 支持手机端操作
## 版本 ## 版本
### docker ### docker
`latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像 `latest` 镜像是基于 `alpine` 构建,`debian` 镜像是基于 `debian-slim` 构建。如果需要使用 `alpine` 不支持的依赖,建议使用 `debian` 镜像
@@ -55,6 +56,65 @@ npm 版本支持 `debian/ubuntu/centos/alpine` 系统,需要自行安装 `node
npm i @whyour/qinglong npm i @whyour/qinglong
``` ```
## 内置命令
1. task
```bash
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
# 使用 -- 分割,-- 后面的参数会传给脚本,下面的例子,脚本就可接收到参数 -u whyour -p password
task <file_path> -- -u whyour -p password
```
1. ql
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
```
1. 参数说明
- file_url: 脚本地址
- repo_url: 仓库地址
- whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
- blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
- dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
- extensions: 拉取仓库的文件后缀,多个竖线分割
- branch: 拉取仓库的分支
- days: 需要保留的日志的天数
- file_path: 任务执行时的文件路径
- env_name: 任务执行时需要并发或者指定时的环境变量名称
- account_number: 任务执行时指定某个环境变量需要执行的账号序号
- max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 部署 ## 部署
### docker (推荐) ### docker (推荐)
@@ -63,9 +123,12 @@ npm i @whyour/qinglong
# curl -sSL get.docker.com | sh # curl -sSL get.docker.com | sh
docker run -dit \ docker run -dit \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \ -p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/ # 部署路径非必须,比如 /test
-e QlBaseUrl="/" \ -e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
--restart unless-stopped \ --restart unless-stopped \
@@ -92,9 +155,12 @@ docker-compose down
podman run -dit \ podman run -dit \
--network bridge \ --network bridge \
-v $PWD/ql/data:/ql/data \ -v $PWD/ql/data:/ql/data \
# 冒号后面的 5700 为默认端口,如果设置了 QlPort, 需要跟 QlPort 保持一致
-p 5700:5700 \ -p 5700:5700 \
# 部署路径非必须,以斜杠开头和结尾,比如 /test/ # 部署路径非必须,比如 /test
-e QlBaseUrl="/" \ -e QlBaseUrl="/" \
# 部署端口非必须,当使用 host 模式时,可以设置服务启动后的端口,默认 5700
-e QlPort="5700" \
--name qinglong \ --name qinglong \
--hostname qinglong \ --hostname qinglong \
docker.io/whyour/qinglong:latest docker.io/whyour/qinglong:latest
@@ -114,59 +180,6 @@ export QL_DATA_DIR=""
qinglong qinglong
``` ```
## 使用
1. 内置命令
```bash
# 更新并重启青龙
ql update
# 运行自定义脚本extra.sh
ql extra
# 添加单个脚本文件
ql raw <file_url>
# 添加单个仓库的指定脚本
ql repo <repo_url> <whitelist> <blacklist> <dependence> <branch> <extensions>
# 删除旧日志
ql rmlog <days>
# 启动tg-bot
ql bot
# 检测青龙环境并修复
ql check
# 重置登录错误次数
ql resetlet
# 禁用两步登录
ql resettfa
# 依次执行,如果设置了随机延迟,将随机延迟一定秒数
task <file_path>
# 依次执行,无论是否设置了随机延迟,均立即运行,前台会输出日,同时记录在日志文件中
task <file_path> now
# 并发执行,无论是否设置了随机延迟,均立即运行,前台不产生日,直接记录在日志文件中,且可指定账号执行
task <file_path> conc <env_name> <account_number>(可选的)
# 指定账号执行,无论是否设置了随机延迟,均立即运行
task <file_path> desi <env_name> <account_number>
# 设置任务超时时间
task -m <max_time> <file_path>
# 实时打印任务日志,创建定时任务时,不用携带此参数
task -l <file_path>
```
2. 参数说明
* file_url: 脚本地址
* repo_url: 仓库地址
* whitelist: 拉取仓库时的白名单,即就是需要拉取的脚本的路径包含的字符串,多个竖线分割
* blacklist: 拉取仓库时的黑名单,即就是需要拉取的脚本的路径不包含的字符串,多个竖线分割
* dependence: 拉取仓库需要的依赖文件,会直接从仓库拷贝到scripts下的仓库目录,不受黑名单影响,多个竖线分割
* extensions: 拉取仓库的文件后缀,多个竖线分割
* branch: 拉取仓库的分支
* days: 需要保留的日志的天数
* file_path: 任务执行时的文件路径
* env_name: 任务执行时需要并发或者指定时的环境变量名称
* account_number: 任务执行时指定某个环境变量需要执行的账号序号
* max_time: 超时时间,后缀"s"代表秒(默认值), "m"代表分, "h"代表小时, "d"代表天
## 开发 ## 开发
```bash ```bash
+8 -3
View File
@@ -176,6 +176,9 @@ export default (app: Router) => {
name: Joi.string().optional(), name: Joi.string().optional(),
labels: Joi.array().optional(), labels: Joi.array().optional(),
sub_id: Joi.number().optional().allow(null), sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
@@ -333,6 +336,9 @@ export default (app: Router) => {
schedule: Joi.string().required(), schedule: Joi.string().required(),
name: Joi.string().optional().allow(null), name: Joi.string().optional().allow(null),
sub_id: Joi.number().optional().allow(null), sub_id: Joi.number().optional().allow(null),
extra_schedules: Joi.array().optional().allow(null),
task_before: Joi.string().optional().allow('').allow(null),
task_after: Joi.string().optional().allow('').allow(null),
id: Joi.number().required(), id: Joi.number().required(),
}), }),
}), }),
@@ -452,13 +458,12 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
const data = await cronService.status({ const data = await cronService.status({
...req.body, ...req.body,
status: parseInt(req.body.status), status: req.body.status ? parseInt(req.body.status) : undefined,
pid: parseInt(req.body.pid) || '', pid: req.body.pid ? parseInt(req.body.pid) : undefined,
}); });
return res.send({ code: 200, data }); return res.send({ code: 200, data });
} catch (e) { } catch (e) {
+2 -1
View File
@@ -6,6 +6,7 @@ import { celebrate, Joi } from 'celebrate';
import multer from 'multer'; import multer from 'multer';
import config from '../config'; import config from '../config';
import fs from 'fs'; import fs from 'fs';
import { safeJSONParse } from '../config/util';
const route = Router(); const route = Router();
const storage = multer.diskStorage({ const storage = multer.diskStorage({
@@ -200,7 +201,7 @@ export default (app: Router) => {
try { try {
const envService = Container.get(EnvService); const envService = Container.get(EnvService);
const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8'); const fileContent = await fs.promises.readFile(req!.file!.path, 'utf8');
const parseContent = JSON.parse(fileContent); const parseContent = safeJSONParse(fileContent);
const data = Array.isArray(parseContent) const data = Array.isArray(parseContent)
? parseContent ? parseContent
: [parseContent]; : [parseContent];
+1 -1
View File
@@ -65,7 +65,7 @@ export default (app: Router) => {
}; };
const filePath = join(config.logPath, path, filename); const filePath = join(config.logPath, path, filename);
if (type === 'directory') { if (type === 'directory') {
emptyDir(filePath); await emptyDir(filePath);
} else { } else {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
+6 -3
View File
@@ -34,7 +34,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
let result = []; let result = [];
const blacklist = ['node_modules', '.git']; const blacklist = ['node_modules', '.git', '.pnpm'];
if (req.query.path) { if (req.query.path) {
const targetPath = path.join( const targetPath = path.join(
config.scriptPath, config.scriptPath,
@@ -183,7 +183,7 @@ export default (app: Router) => {
}; };
const filePath = join(config.scriptPath, path, filename); const filePath = join(config.scriptPath, path, filename);
if (type === 'directory') { if (type === 'directory') {
emptyDir(filePath); await emptyDir(filePath);
} else { } else {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
@@ -260,14 +260,17 @@ export default (app: Router) => {
}), }),
}), }),
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const logger: Logger = Container.get('logger');
try { try {
let { filename, path, pid } = req.body; let { filename, path, pid } = req.body;
const { name, ext } = parse(filename); const { name, ext } = parse(filename);
const filePath = join(config.scriptPath, path, `${name}.swap${ext}`); const filePath = join(config.scriptPath, path, `${name}.swap${ext}`);
const logPath = join(config.logPath, path, `${name}.swap`);
const scriptService = Container.get(ScriptService); const scriptService = Container.get(ScriptService);
const result = await scriptService.stopScript(filePath, pid); const result = await scriptService.stopScript(filePath, pid);
setTimeout(() => {
emptyDir(logPath);
}, 3000);
res.send(result); res.send(result);
} catch (e) { } catch (e) {
return next(e); return next(e);
+17 -1
View File
@@ -174,7 +174,10 @@ export default (app: Router) => {
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
try { try {
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const uniqPath = await getUniqPath(req.body.command); const command = req.body.command
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`;
let id = await promiseExec(idStr);
const uniqPath = await getUniqPath(command, id);
const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS'); const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logPath = `${uniqPath}/${logTime}.log`; const logPath = `${uniqPath}/${logTime}.log`;
res.setHeader('Content-type', 'application/octet-stream'); res.setHeader('Content-type', 'application/octet-stream');
@@ -249,4 +252,17 @@ export default (app: Router) => {
} }
}, },
); );
route.get(
'/log',
async (req: Request, res: Response, next: NextFunction) => {
try {
const systemService = Container.get(SystemService);
await systemService.getSystemLog(res);
} catch (e) {
return next(e);
}
},
);
}; };
+2 -1
View File
@@ -2,7 +2,6 @@ import 'reflect-metadata'; // We need this in order to use @Decorators
import config from './config'; import config from './config';
import express from 'express'; import express from 'express';
import Logger from './loaders/logger'; import Logger from './loaders/logger';
import path from 'path';
async function startServer() { async function startServer() {
const app = express(); const app = express();
@@ -18,10 +17,12 @@ async function startServer() {
const server = app const server = app
.listen(config.port, () => { .listen(config.port, () => {
Logger.debug(`✌️ 后端服务启动成功!`); Logger.debug(`✌️ 后端服务启动成功!`);
console.debug(`✌️ 后端服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
console.error(err);
process.exit(1); process.exit(1);
}); });
+3 -1
View File
@@ -1,6 +1,6 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import { createRandomString } from './util'; import { createRandomString } from './share';
process.env.NODE_ENV = process.env.NODE_ENV || 'development'; process.env.NODE_ENV = process.env.NODE_ENV || 'development';
@@ -30,6 +30,7 @@ const logPath = path.join(dataPath, 'log/');
const dbPath = path.join(dataPath, 'db/'); const dbPath = path.join(dataPath, 'db/');
const uploadPath = path.join(dataPath, 'upload/'); const uploadPath = path.join(dataPath, 'upload/');
const sshdPath = path.join(dataPath, 'ssh.d/'); const sshdPath = path.join(dataPath, 'ssh.d/');
const systemLogPath = path.join(dataPath, 'syslog/');
const envFile = path.join(configPath, 'env.sh'); const envFile = path.join(configPath, 'env.sh');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
@@ -110,4 +111,5 @@ export default {
lastVersionFile, lastVersionFile,
sqliteFile, sqliteFile,
sshdPath, sshdPath,
systemLogPath,
}; };
+9 -1
View File
@@ -1,11 +1,19 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { pick } from 'lodash'; import pick from 'lodash/pick';
let pickedEnv: Record<string, string>; let pickedEnv: Record<string, string>;
function getPickedEnv() { function getPickedEnv() {
if (pickedEnv) return pickedEnv; if (pickedEnv) return pickedEnv;
const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']); const picked = pick(process.env, ['QlBaseUrl', 'DeployEnv']);
if (picked.QlBaseUrl) {
if (!picked.QlBaseUrl.startsWith('/')) {
picked.QlBaseUrl = `/${picked.QlBaseUrl}`
}
if (!picked.QlBaseUrl.endsWith('/')) {
picked.QlBaseUrl = `${picked.QlBaseUrl}/`
}
}
pickedEnv = picked as Record<string, string>; pickedEnv = picked as Record<string, string>;
return picked; return picked;
} }
+84
View File
@@ -0,0 +1,84 @@
export function createRandomString(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
const ENGLISH = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const special = ['-', '_'];
const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = [];
arr.push(getOne(num));
arr.push(getOne(english));
arr.push(getOne(ENGLISH));
arr.push(getOne(special));
const len = min + Math.floor(Math.random() * (max - min + 1));
for (let i = 4; i < len; i++) {
arr.push(config[Math.floor(Math.random() * config.length)]);
}
const newArr = [];
for (let j = 0; j < len; j++) {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
}
function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)];
}
return newArr.join('');
}
+46 -102
View File
@@ -9,6 +9,9 @@ import { promisify } from 'util';
import { load } from 'js-yaml'; import { load } from 'js-yaml';
import config from './index'; import config from './index';
import { TASK_COMMAND } from './const'; import { TASK_COMMAND } from './const';
import Logger from '../loaders/logger';
export * from './share';
export function getFileContentByName(fileName: string) { export function getFileContentByName(fileName: string) {
if (fs.existsSync(fileName)) { if (fs.existsSync(fileName)) {
@@ -36,91 +39,6 @@ export function getLastModifyFilePath(dir: string) {
return filePath; return filePath;
} }
export function createRandomString(min: number, max: number): string {
const num = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const english = [
'a',
'b',
'c',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k',
'l',
'm',
'n',
'o',
'p',
'q',
'r',
's',
't',
'u',
'v',
'w',
'x',
'y',
'z',
];
const ENGLISH = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
const special = ['-', '_'];
const config = num.concat(english).concat(ENGLISH).concat(special);
const arr = [];
arr.push(getOne(num));
arr.push(getOne(english));
arr.push(getOne(ENGLISH));
arr.push(getOne(special));
const len = min + Math.floor(Math.random() * (max - min + 1));
for (let i = 4; i < len; i++) {
arr.push(config[Math.floor(Math.random() * config.length)]);
}
const newArr = [];
for (let j = 0; j < len; j++) {
newArr.push(arr.splice(Math.random() * arr.length, 1)[0]);
}
function getOne(arr: any[]) {
return arr[Math.floor(Math.random() * arr.length)];
}
return newArr.join('');
}
export function getToken(req: any) { export function getToken(req: any) {
const { authorization = '' } = req.headers; const { authorization = '' } = req.headers;
if (authorization && authorization.split(' ')[0] === 'Bearer') { if (authorization && authorization.split(' ')[0] === 'Bearer') {
@@ -166,6 +84,11 @@ export async function getNetIp(req: any) {
if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) { if (ip.includes('127.0') || ip.includes('192.168') || ip.includes('10.7')) {
ip = ''; ip = '';
} }
if (!ip) {
return { address: `获取失败`, ip };
}
try { try {
const baiduApi = got const baiduApi = got
.get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 }) .get(`https://www.cip.cc/${ip}`, { timeout: 10000, retry: 0 })
@@ -307,22 +230,28 @@ interface IFile {
type: 'directory' | 'file'; type: 'directory' | 'file';
parent: string; parent: string;
mtime: number; mtime: number;
size?: number;
children?: IFile[]; children?: IFile[];
} }
export function dirSort(a: IFile, b: IFile) { export function dirSort(a: IFile, b: IFile): number {
if (a.type !== b.type) return FileType[a.type] < FileType[b.type] ? -1 : 1; if (a.type !== b.type) {
else if (a.mtime !== b.mtime) return a.mtime > b.mtime ? -1 : 1; return FileType[a.type] < FileType[b.type] ? -1 : 1
} else if (a.mtime !== b.mtime) {
return a.mtime > b.mtime ? -1 : 1
} else {
return 0;
}
} }
export function readDirs( export function readDirs(
dir: string, dir: string,
baseDir: string = '', baseDir: string = '',
blacklist: string[] = [], blacklist: string[] = [],
) { ): IFile[] {
const relativePath = path.relative(baseDir, dir); const relativePath = path.relative(baseDir, dir);
const files = fs.readdirSync(dir); const files = fs.readdirSync(dir);
const result: any = files const result: IFile[] = files
.filter((x) => !blacklist.includes(x)) .filter((x) => !blacklist.includes(x))
.map((file: string) => { .map((file: string) => {
const subPath = path.join(dir, file); const subPath = path.join(dir, file);
@@ -344,6 +273,7 @@ export function readDirs(
isLeaf: true, isLeaf: true,
key, key,
parent: relativePath, parent: relativePath,
size: stats.size,
mtime: stats.mtime.getTime(), mtime: stats.mtime.getTime(),
}; };
}); });
@@ -373,17 +303,21 @@ export function readDir(
return result; return result;
} }
export function emptyDir(path: string) { export async function emptyDir(path: string) {
const pathExist = await fileExist(path);
if (!pathExist) {
return;
}
const files = fs.readdirSync(path); const files = fs.readdirSync(path);
files.forEach((file) => { for (const file of files) {
const filePath = `${path}/${file}`; const filePath = `${path}/${file}`;
const stats = fs.statSync(filePath); const stats = fs.statSync(filePath);
if (stats.isDirectory()) { if (stats.isDirectory()) {
emptyDir(filePath); await emptyDir(filePath);
} else { } else {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} }
}); }
fs.rmdirSync(path); fs.rmdirSync(path);
} }
@@ -489,11 +423,11 @@ export function psTree(pid: number): Promise<number[]> {
export async function killTask(pid: number) { export async function killTask(pid: number) {
const pids = await psTree(pid); const pids = await psTree(pid);
// SIGINT 2 程序终止(interrupt)信号,不会打印额外信息
if (pids.length) { if (pids.length) {
try { try {
[pid, ...pids].forEach((x) => { [pid, ...pids].reverse().forEach((x) => {
process.kill(x, 2); process.kill(x, 15);
}); });
} catch (error) { } } catch (error) { }
} else { } else {
@@ -522,11 +456,8 @@ export async function parseContentVersion(content: string): Promise<IVersion> {
return load(content) as IVersion; return load(content) as IVersion;
} }
export async function getUniqPath(command: string): Promise<string> { export async function getUniqPath(command: string, id: string): Promise<string> {
const idStr = `cat ${config.crontabFile} | grep -E "${command}" | perl -pe "s|.*ID=(.*) ${command}.*|\\1|" | head -1 | awk -F " " '{print $1}' | xargs echo -n`; if (/^\d+$/.test(id)) {
let id = await promiseExec(idStr);
if (/^\d\d*\d$/.test(id)) {
id = `_${id}`; id = `_${id}`;
} else { } else {
id = ''; id = '';
@@ -558,3 +489,16 @@ export async function getUniqPath(command: string): Promise<string> {
return `${str}${id}`; return `${str}${id}`;
} }
export function safeJSONParse(value?: string) {
if (!value) {
return {};
}
try {
return JSON.parse(value);
} catch (error) {
Logger.error('[JSON.parse失败]', error)
return {};
}
}
+10 -1
View File
@@ -18,6 +18,9 @@ export class Crontab {
last_running_time?: number; last_running_time?: number;
last_execution_time?: number; last_execution_time?: number;
sub_id?: number; sub_id?: number;
extra_schedules?: Array<{ schedule: string }>;
task_before?: string;
task_after?: string;
constructor(options: Crontab) { constructor(options: Crontab) {
this.name = options.name; this.name = options.name;
@@ -39,6 +42,9 @@ export class Crontab {
this.last_running_time = options.last_running_time || 0; this.last_running_time = options.last_running_time || 0;
this.last_execution_time = options.last_execution_time || 0; this.last_execution_time = options.last_execution_time || 0;
this.sub_id = options.sub_id; this.sub_id = options.sub_id;
this.extra_schedules = options.extra_schedules;
this.task_before = options.task_before;
this.task_after = options.task_after;
} }
} }
@@ -49,7 +55,7 @@ export enum CrontabStatus {
'disabled', 'disabled',
} }
export interface CronInstance extends Model<Crontab, Crontab>, Crontab {} export interface CronInstance extends Model<Crontab, Crontab>, Crontab { }
export const CrontabModel = sequelize.define<CronInstance>('Crontab', { export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
name: { name: {
unique: 'compositeIndex', unique: 'compositeIndex',
@@ -75,4 +81,7 @@ export const CrontabModel = sequelize.define<CronInstance>('Crontab', {
last_running_time: DataTypes.NUMBER, last_running_time: DataTypes.NUMBER,
last_execution_time: DataTypes.NUMBER, last_execution_time: DataTypes.NUMBER,
sub_id: { type: DataTypes.NUMBER, allowNull: true }, sub_id: { type: DataTypes.NUMBER, allowNull: true },
extra_schedules: DataTypes.JSON,
task_before: DataTypes.STRING,
task_after: DataTypes.STRING,
}); });
+11
View File
@@ -18,6 +18,7 @@ export enum NotificationMode {
'pushMe' = 'pushMe', 'pushMe' = 'pushMe',
'feishu' = 'feishu', 'feishu' = 'feishu',
'webhook' = 'webhook', 'webhook' = 'webhook',
'chronocat' = 'Chronocat',
} }
abstract class NotificationBaseInfo { abstract class NotificationBaseInfo {
@@ -55,6 +56,8 @@ export class BarkNotification extends NotificationBaseInfo {
public barkIcon = 'https://qn.whyour.cn/logo.png'; public barkIcon = 'https://qn.whyour.cn/logo.png';
public barkSound = ''; public barkSound = '';
public barkGroup = 'qinglong'; public barkGroup = 'qinglong';
public barkLevel = 'active';
public barkUrl = '';
} }
export class TelegramBotNotification extends NotificationBaseInfo { export class TelegramBotNotification extends NotificationBaseInfo {
@@ -106,6 +109,12 @@ export class PushMeNotification extends NotificationBaseInfo {
public pushMeKey: string = ''; public pushMeKey: string = '';
} }
export class ChronocatNotification extends NotificationBaseInfo {
public chronocatURL: string = '';
public chronocatQQ: string = '';
public chronocatToekn: string = '';
}
export class WebhookNotification extends NotificationBaseInfo { export class WebhookNotification extends NotificationBaseInfo {
public webhookHeaders: string = ''; public webhookHeaders: string = '';
public webhookBody: string = ''; public webhookBody: string = '';
@@ -138,4 +147,6 @@ export interface NotificationInfo
EmailNotification, EmailNotification,
PushMeNotification, PushMeNotification,
WebhookNotification, WebhookNotification,
ChronocatNotification,
LarkNotification {} LarkNotification {}
+2
View File
@@ -3,3 +3,5 @@ declare namespace Express {
platform: 'desktop' | 'mobile'; platform: 'desktop' | 'mobile';
} }
} }
declare module 'pstree.remy';
+12 -2
View File
@@ -7,18 +7,28 @@ import linkDeps from './deps';
import initTask from './initTask'; import initTask from './initTask';
export default async ({ expressApp }: { expressApp: Application }) => { export default async ({ expressApp }: { expressApp: Application }) => {
await depInjectorLoader(); try {
depInjectorLoader();
Logger.info('✌️ Dependency Injector loaded'); Logger.info('✌️ Dependency Injector loaded');
console.log('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp }); expressLoader({ app: expressApp });
Logger.info('✌️ Express loaded'); Logger.info('✌️ Express loaded');
console.log('✌️ Express loaded');
await initData(); await initData();
Logger.info('✌️ init data loaded'); Logger.info('✌️ init data loaded');
console.log('✌️ init data loaded');
await linkDeps(); await linkDeps();
Logger.info('✌️ link deps loaded'); Logger.info('✌️ link deps loaded');
console.log('✌️ link deps loaded');
initTask(); initTask();
Logger.info('✌️ init task loaded'); Logger.info('✌️ init task loaded');
console.log('✌️ init task loaded');
} catch (error) {
Logger.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed, ${error}`);
console.error(`✌️ depInjectorLoader expressLoader initData linkDeps failed ${error}`);
}
}; };
+13 -3
View File
@@ -48,7 +48,16 @@ export default async () => {
} catch (error) {} } catch (error) {}
try { try {
await sequelize.query('alter table Crontabs add column sub_id NUMBER'); await sequelize.query('alter table Crontabs add column sub_id NUMBER');
} catch (error) {} } catch (error) { }
try {
await sequelize.query('alter table Crontabs add column extra_schedules JSON');
} catch (error) { }
try {
await sequelize.query('alter table Crontabs add column task_before TEXT');
} catch (error) { }
try {
await sequelize.query('alter table Crontabs add column task_after TEXT');
} catch (error) { }
// 2.10-2.11 升级 // 2.10-2.11 升级
const cronDbFile = path.join(config.rootPath, 'db/crontab.db'); const cronDbFile = path.join(config.rootPath, 'db/crontab.db');
@@ -122,9 +131,10 @@ export default async () => {
}); });
} }
console.log('✌️ DB loaded');
Logger.info('✌️ DB loaded'); Logger.info('✌️ DB loaded');
} catch (error) { } catch (error) {
Logger.info('✌️ DB load failed'); console.error('✌️ DB load failed');
Logger.info(error); Logger.error(error);
} }
}; };
+6 -4
View File
@@ -5,7 +5,7 @@ import routes from '../api';
import config from '../config'; import config from '../config';
import jwt, { UnauthorizedError } from 'express-jwt'; import jwt, { UnauthorizedError } from 'express-jwt';
import fs from 'fs'; import fs from 'fs';
import { getPlatform, getToken } from '../config/util'; import { getPlatform, getToken, safeJSONParse } from '../config/util';
import Container from 'typedi'; import Container from 'typedi';
import OpenService from '../services/open'; import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
@@ -15,6 +15,7 @@ import { EnvModel } from '../data/env';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv'; import { serveEnv } from '../config/serverEnv';
import Logger from './logger';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.enable('trust proxy'); app.enable('trust proxy');
@@ -25,9 +26,10 @@ export default ({ app }: { app: Application }) => {
app.use( app.use(
'/api/public', '/api/public',
createProxyMiddleware({ createProxyMiddleware({
target: `http://localhost:${config.publicPort}/api`, target: `http://0.0.0.0:${config.publicPort}/api`,
changeOrigin: true, changeOrigin: true,
pathRewrite: { '/api/public': '' }, pathRewrite: { '/api/public': '' },
logProvider: () => Logger
}), }),
); );
@@ -36,7 +38,7 @@ export default ({ app }: { app: Application }) => {
app.use( app.use(
jwt({ jwt({
secret: config.secret as string, secret: config.secret,
algorithms: ['HS384'], algorithms: ['HS384'],
}).unless({ }).unless({
path: [...config.apiWhiteList, /^\/open\//], path: [...config.apiWhiteList, /^\/open\//],
@@ -83,7 +85,7 @@ export default ({ app }: { app: Application }) => {
const data = fs.readFileSync(config.authConfigFile, 'utf8'); const data = fs.readFileSync(config.authConfigFile, 'utf8');
if (data && headerToken) { if (data && headerToken) {
const { token = '', tokens = {} } = JSON.parse(data); const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
+7
View File
@@ -28,6 +28,7 @@ const TaskAfterFile = path.join(configPath, 'task_after.sh');
const homedir = os.homedir(); const homedir = os.homedir();
const sshPath = path.resolve(homedir, '.ssh'); const sshPath = path.resolve(homedir, '.ssh');
const sshdPath = path.join(dataPath, 'ssh.d'); const sshdPath = path.join(dataPath, 'ssh.d');
const systemLogPath = path.join(dataPath, 'syslog');
export default async () => { export default async () => {
const authFileExist = await fileExist(authConfigFile); const authFileExist = await fileExist(authConfigFile);
@@ -39,6 +40,7 @@ export default async () => {
const sshDirExist = await fileExist(sshPath); const sshDirExist = await fileExist(sshPath);
const bakDirExist = await fileExist(bakPath); const bakDirExist = await fileExist(bakPath);
const sshdDirExist = await fileExist(sshdPath); const sshdDirExist = await fileExist(sshdPath);
const systemLogDirExist = await fileExist(systemLogPath);
const tmpDirExist = await fileExist(tmpPath); const tmpDirExist = await fileExist(tmpPath);
const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile); const scriptNotifyJsFileExist = await fileExist(scriptNotifyJsFile);
const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile); const scriptNotifyPyFileExist = await fileExist(scriptNotifyPyFile);
@@ -77,6 +79,10 @@ export default async () => {
fs.mkdirSync(sshdPath); fs.mkdirSync(sshdPath);
} }
if (!systemLogDirExist) {
fs.mkdirSync(systemLogPath);
}
// 初始化文件 // 初始化文件
if (!authFileExist) { if (!authFileExist) {
fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile)); fs.writeFileSync(authConfigFile, fs.readFileSync(sampleAuthFile));
@@ -105,4 +111,5 @@ export default async () => {
dotenv.config({ path: confFile }); dotenv.config({ path: confFile });
Logger.info('✌️ Init file down'); Logger.info('✌️ Init file down');
console.log('✌️ Init file down');
}; };
+2 -2
View File
@@ -1,6 +1,6 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import SystemService from '../services/system'; import SystemService from '../services/system';
import ScheduleService from '../services/schedule'; import ScheduleService, { ScheduleTaskType } from '../services/schedule';
import SubscriptionService from '../services/subscription'; import SubscriptionService from '../services/subscription';
import config from '../config'; import config from '../config';
import { fileExist } from '../config/util'; import { fileExist } from '../config/util';
@@ -22,7 +22,7 @@ export default async () => {
id: NaN, id: NaN,
name: '生成token', name: '生成token',
command: tokenCommand, command: tokenCommand,
}; } as ScheduleTaskType;
await scheduleService.cancelIntervalTask(cron); await scheduleService.cancelIntervalTask(cron);
scheduleService.createIntervalTask(cron, { scheduleService.createIntervalTask(cron, {
days: 28, days: 28,
+27 -21
View File
@@ -1,32 +1,38 @@
import winston from 'winston'; import winston from 'winston';
import 'winston-daily-rotate-file';
import config from '../config'; import config from '../config';
import path from 'path';
const transports = []; const levelMap: Record<string, string> = {
if (process.env.NODE_ENV !== 'development') { info: '🔵',
transports.push(new winston.transports.Console()); warn: '🟡',
} else { error: '🔴',
transports.push( debug: '🔶'
new winston.transports.Console({
format: winston.format.combine(
winston.format.cli(),
winston.format.splat(),
),
}),
);
} }
const customFormat = winston.format.combine(
winston.format.splat(),
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
winston.format.align(),
winston.format.printf((i) => `[${levelMap[i.level]}${i.level}] [${[i.timestamp]}]: ${i.message}`),
);
const defaultOptions = {
format: customFormat,
datePattern: "YYYY-MM-DD",
maxSize: "20m",
maxFiles: "7d",
};
const LoggerInstance = winston.createLogger({ const LoggerInstance = winston.createLogger({
level: config.logs.level, level: config.logs.level,
levels: winston.config.npm.levels, levels: winston.config.npm.levels,
format: winston.format.combine( transports: [
winston.format.timestamp({ new winston.transports.DailyRotateFile({
format: 'YYYY-MM-DD HH:mm:ss', filename: path.join(config.systemLogPath, '%DATE%.log'),
}), ...defaultOptions,
winston.format.errors({ stack: true }), })
winston.format.splat(), ],
winston.format.json(),
),
transports,
}); });
export default LoggerInstance; export default LoggerInstance;
+1
View File
@@ -29,4 +29,5 @@ export default async ({ expressApp }: { expressApp: Application }) => {
expressApp.use(Sentry.Handlers.tracingHandler()); expressApp.use(Sentry.Handlers.tracingHandler());
Logger.info('✌️ Sentry loaded'); Logger.info('✌️ Sentry loaded');
console.log('✌️ Sentry loaded');
}; };
+3 -3
View File
@@ -10,9 +10,11 @@ export default async ({ server }: { server: Server }) => {
process.on('SIGINT', (singal) => { process.on('SIGINT', (singal) => {
Logger.warn(`Server need close, singal ${singal}`); Logger.warn(`Server need close, singal ${singal}`);
console.warn(`Server need close, singal ${singal}`);
exitTime++; exitTime++;
if (exitTime >= 3) { if (exitTime >= 3) {
Logger.warn('Forcing server close'); Logger.warn('Forcing server close');
console.warn('Forcing server close');
clearTimeout(timer); clearTimeout(timer);
process.exit(1); process.exit(1);
} }
@@ -28,11 +30,9 @@ export default async ({ server }: { server: Server }) => {
process.on('uncaughtException', (error) => { process.on('uncaughtException', (error) => {
Logger.error('Uncaught exception:', error); Logger.error('Uncaught exception:', error);
process.exit(1);
}); });
process.on('unhandledRejection', (reason, promise) => { process.on('unhandledRejection', (reason, promise) => {
Logger.error('Unhandled rejection:', reason); Logger.error('Unhandled rejection:', reason, promise);
process.exit(1);
}); });
}; };
+2 -2
View File
@@ -5,7 +5,7 @@ import { Container } from 'typedi';
import SockService from '../services/sock'; import SockService from '../services/sock';
import config from '../config/index'; import config from '../config/index';
import fs from 'fs'; import fs from 'fs';
import { getPlatform } from '../config/util'; import { getPlatform, safeJSONParse } from '../config/util';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} }); const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
@@ -20,7 +20,7 @@ export default async ({ server }: { server: Server }) => {
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) { if (data) {
const { token = '', tokens = {} } = JSON.parse(data); const { token = '', tokens = {} } = safeJSONParse(data);
if (headerToken === token || tokens[platform] === headerToken) { if (headerToken === token || tokens[platform] === headerToken) {
conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' })); conn.write(JSON.stringify({ type: 'ping', message: 'hanhh' }));
sockService.addClient(conn); sockService.addClient(conn);
+4
View File
@@ -7,10 +7,14 @@ service Cron {
rpc delCron(DeleteCronRequest) returns (DeleteCronResponse); rpc delCron(DeleteCronRequest) returns (DeleteCronResponse);
} }
message ISchedule { string schedule = 1; }
message ICron { message ICron {
string id = 1; string id = 1;
string schedule = 2; string schedule = 2;
string command = 3; string command = 3;
repeated ISchedule extra_schedules = 4;
string name = 5;
} }
message AddCronRequest { repeated ICron crons = 1; } message AddCronRequest { repeated ICron crons = 1; }
+158 -138
View File
@@ -10,78 +10,161 @@ import {
Metadata, Metadata,
ServiceError, ServiceError,
UntypedServiceImplementation, UntypedServiceImplementation,
} from '@grpc/grpc-js'; } from "@grpc/grpc-js";
import _m0 from 'protobufjs/minimal'; import _m0 from "protobufjs/minimal";
export const protobufPackage = 'com.ql.cron'; export const protobufPackage = "com.ql.cron";
export interface ISchedule {
schedule: string;
}
export interface ICron { export interface ICron {
id: string; id: string;
schedule: string; schedule: string;
command: string; command: string;
extraSchedules: ISchedule[];
name: string;
} }
export interface AddCronRequest { export interface AddCronRequest {
crons: ICron[]; crons: ICron[];
} }
export interface AddCronResponse {} export interface AddCronResponse {
}
export interface DeleteCronRequest { export interface DeleteCronRequest {
ids: string[]; ids: string[];
} }
export interface DeleteCronResponse {} export interface DeleteCronResponse {
}
function createBaseISchedule(): ISchedule {
return { schedule: "" };
}
export const ISchedule = {
encode(message: ISchedule, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.schedule !== "") {
writer.uint32(10).string(message.schedule);
}
return writer;
},
decode(input: _m0.Reader | Uint8Array, length?: number): ISchedule {
const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseISchedule();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
if (tag !== 10) {
break;
}
message.schedule = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break;
}
reader.skipType(tag & 7);
}
return message;
},
fromJSON(object: any): ISchedule {
return { schedule: isSet(object.schedule) ? String(object.schedule) : "" };
},
toJSON(message: ISchedule): unknown {
const obj: any = {};
message.schedule !== undefined && (obj.schedule = message.schedule);
return obj;
},
create<I extends Exact<DeepPartial<ISchedule>, I>>(base?: I): ISchedule {
return ISchedule.fromPartial(base ?? {});
},
fromPartial<I extends Exact<DeepPartial<ISchedule>, I>>(object: I): ISchedule {
const message = createBaseISchedule();
message.schedule = object.schedule ?? "";
return message;
},
};
function createBaseICron(): ICron { function createBaseICron(): ICron {
return { id: '', schedule: '', command: '' }; return { id: "", schedule: "", command: "", extraSchedules: [], name: "" };
} }
export const ICron = { export const ICron = {
encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { encode(message: ICron, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
if (message.id !== '') { if (message.id !== "") {
writer.uint32(10).string(message.id); writer.uint32(10).string(message.id);
} }
if (message.schedule !== '') { if (message.schedule !== "") {
writer.uint32(18).string(message.schedule); writer.uint32(18).string(message.schedule);
} }
if (message.command !== '') { if (message.command !== "") {
writer.uint32(26).string(message.command); writer.uint32(26).string(message.command);
} }
for (const v of message.extraSchedules) {
ISchedule.encode(v!, writer.uint32(34).fork()).ldelim();
}
if (message.name !== "") {
writer.uint32(42).string(message.name);
}
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): ICron { decode(input: _m0.Reader | Uint8Array, length?: number): ICron {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseICron(); const message = createBaseICron();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1:
if (tag != 10) { if (tag !== 10) {
break; break;
} }
message.id = reader.string(); message.id = reader.string();
continue; continue;
case 2: case 2:
if (tag != 18) { if (tag !== 18) {
break; break;
} }
message.schedule = reader.string(); message.schedule = reader.string();
continue; continue;
case 3: case 3:
if (tag != 26) { if (tag !== 26) {
break; break;
} }
message.command = reader.string(); message.command = reader.string();
continue; continue;
case 4:
if (tag !== 34) {
break;
} }
if ((tag & 7) == 4 || tag == 0) {
message.extraSchedules.push(ISchedule.decode(reader, reader.uint32()));
continue;
case 5:
if (tag !== 42) {
break;
}
message.name = reader.string();
continue;
}
if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -91,9 +174,13 @@ export const ICron = {
fromJSON(object: any): ICron { fromJSON(object: any): ICron {
return { return {
id: isSet(object.id) ? String(object.id) : '', id: isSet(object.id) ? String(object.id) : "",
schedule: isSet(object.schedule) ? String(object.schedule) : '', schedule: isSet(object.schedule) ? String(object.schedule) : "",
command: isSet(object.command) ? String(object.command) : '', command: isSet(object.command) ? String(object.command) : "",
extraSchedules: Array.isArray(object?.extraSchedules)
? object.extraSchedules.map((e: any) => ISchedule.fromJSON(e))
: [],
name: isSet(object.name) ? String(object.name) : "",
}; };
}, },
@@ -102,6 +189,12 @@ export const ICron = {
message.id !== undefined && (obj.id = message.id); message.id !== undefined && (obj.id = message.id);
message.schedule !== undefined && (obj.schedule = message.schedule); message.schedule !== undefined && (obj.schedule = message.schedule);
message.command !== undefined && (obj.command = message.command); message.command !== undefined && (obj.command = message.command);
if (message.extraSchedules) {
obj.extraSchedules = message.extraSchedules.map((e) => e ? ISchedule.toJSON(e) : undefined);
} else {
obj.extraSchedules = [];
}
message.name !== undefined && (obj.name = message.name);
return obj; return obj;
}, },
@@ -111,9 +204,11 @@ export const ICron = {
fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron { fromPartial<I extends Exact<DeepPartial<ICron>, I>>(object: I): ICron {
const message = createBaseICron(); const message = createBaseICron();
message.id = object.id ?? ''; message.id = object.id ?? "";
message.schedule = object.schedule ?? ''; message.schedule = object.schedule ?? "";
message.command = object.command ?? ''; message.command = object.command ?? "";
message.extraSchedules = object.extraSchedules?.map((e) => ISchedule.fromPartial(e)) || [];
message.name = object.name ?? "";
return message; return message;
}, },
}; };
@@ -123,10 +218,7 @@ function createBaseAddCronRequest(): AddCronRequest {
} }
export const AddCronRequest = { export const AddCronRequest = {
encode( encode(message: AddCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
message: AddCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.crons) { for (const v of message.crons) {
ICron.encode(v!, writer.uint32(10).fork()).ldelim(); ICron.encode(v!, writer.uint32(10).fork()).ldelim();
} }
@@ -134,22 +226,21 @@ export const AddCronRequest = {
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest { decode(input: _m0.Reader | Uint8Array, length?: number): AddCronRequest {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1:
if (tag != 10) { if (tag !== 10) {
break; break;
} }
message.crons.push(ICron.decode(reader, reader.uint32())); message.crons.push(ICron.decode(reader, reader.uint32()));
continue; continue;
} }
if ((tag & 7) == 4 || tag == 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -158,32 +249,24 @@ export const AddCronRequest = {
}, },
fromJSON(object: any): AddCronRequest { fromJSON(object: any): AddCronRequest {
return { return { crons: Array.isArray(object?.crons) ? object.crons.map((e: any) => ICron.fromJSON(e)) : [] };
crons: Array.isArray(object?.crons)
? object.crons.map((e: any) => ICron.fromJSON(e))
: [],
};
}, },
toJSON(message: AddCronRequest): unknown { toJSON(message: AddCronRequest): unknown {
const obj: any = {}; const obj: any = {};
if (message.crons) { if (message.crons) {
obj.crons = message.crons.map((e) => (e ? ICron.toJSON(e) : undefined)); obj.crons = message.crons.map((e) => e ? ICron.toJSON(e) : undefined);
} else { } else {
obj.crons = []; obj.crons = [];
} }
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<AddCronRequest>, I>>( create<I extends Exact<DeepPartial<AddCronRequest>, I>>(base?: I): AddCronRequest {
base?: I,
): AddCronRequest {
return AddCronRequest.fromPartial(base ?? {}); return AddCronRequest.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>( fromPartial<I extends Exact<DeepPartial<AddCronRequest>, I>>(object: I): AddCronRequest {
object: I,
): AddCronRequest {
const message = createBaseAddCronRequest(); const message = createBaseAddCronRequest();
message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || []; message.crons = object.crons?.map((e) => ICron.fromPartial(e)) || [];
return message; return message;
@@ -195,23 +278,19 @@ function createBaseAddCronResponse(): AddCronResponse {
} }
export const AddCronResponse = { export const AddCronResponse = {
encode( encode(_: AddCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
_: AddCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse { decode(input: _m0.Reader | Uint8Array, length?: number): AddCronResponse {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
} }
if ((tag & 7) == 4 || tag == 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -228,15 +307,11 @@ export const AddCronResponse = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<AddCronResponse>, I>>( create<I extends Exact<DeepPartial<AddCronResponse>, I>>(base?: I): AddCronResponse {
base?: I,
): AddCronResponse {
return AddCronResponse.fromPartial(base ?? {}); return AddCronResponse.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>( fromPartial<I extends Exact<DeepPartial<AddCronResponse>, I>>(_: I): AddCronResponse {
_: I,
): AddCronResponse {
const message = createBaseAddCronResponse(); const message = createBaseAddCronResponse();
return message; return message;
}, },
@@ -247,10 +322,7 @@ function createBaseDeleteCronRequest(): DeleteCronRequest {
} }
export const DeleteCronRequest = { export const DeleteCronRequest = {
encode( encode(message: DeleteCronRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
message: DeleteCronRequest,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
for (const v of message.ids) { for (const v of message.ids) {
writer.uint32(10).string(v!); writer.uint32(10).string(v!);
} }
@@ -258,22 +330,21 @@ export const DeleteCronRequest = {
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest { decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronRequest {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1:
if (tag != 10) { if (tag !== 10) {
break; break;
} }
message.ids.push(reader.string()); message.ids.push(reader.string());
continue; continue;
} }
if ((tag & 7) == 4 || tag == 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -282,11 +353,7 @@ export const DeleteCronRequest = {
}, },
fromJSON(object: any): DeleteCronRequest { fromJSON(object: any): DeleteCronRequest {
return { return { ids: Array.isArray(object?.ids) ? object.ids.map((e: any) => String(e)) : [] };
ids: Array.isArray(object?.ids)
? object.ids.map((e: any) => String(e))
: [],
};
}, },
toJSON(message: DeleteCronRequest): unknown { toJSON(message: DeleteCronRequest): unknown {
@@ -299,15 +366,11 @@ export const DeleteCronRequest = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>( create<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(base?: I): DeleteCronRequest {
base?: I,
): DeleteCronRequest {
return DeleteCronRequest.fromPartial(base ?? {}); return DeleteCronRequest.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>( fromPartial<I extends Exact<DeepPartial<DeleteCronRequest>, I>>(object: I): DeleteCronRequest {
object: I,
): DeleteCronRequest {
const message = createBaseDeleteCronRequest(); const message = createBaseDeleteCronRequest();
message.ids = object.ids?.map((e) => e) || []; message.ids = object.ids?.map((e) => e) || [];
return message; return message;
@@ -319,23 +382,19 @@ function createBaseDeleteCronResponse(): DeleteCronResponse {
} }
export const DeleteCronResponse = { export const DeleteCronResponse = {
encode( encode(_: DeleteCronResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
_: DeleteCronResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse { decode(input: _m0.Reader | Uint8Array, length?: number): DeleteCronResponse {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
} }
if ((tag & 7) == 4 || tag == 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -352,15 +411,11 @@ export const DeleteCronResponse = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>( create<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(base?: I): DeleteCronResponse {
base?: I,
): DeleteCronResponse {
return DeleteCronResponse.fromPartial(base ?? {}); return DeleteCronResponse.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>( fromPartial<I extends Exact<DeepPartial<DeleteCronResponse>, I>>(_: I): DeleteCronResponse {
_: I,
): DeleteCronResponse {
const message = createBaseDeleteCronResponse(); const message = createBaseDeleteCronResponse();
return message; return message;
}, },
@@ -369,25 +424,21 @@ export const DeleteCronResponse = {
export type CronService = typeof CronService; export type CronService = typeof CronService;
export const CronService = { export const CronService = {
addCron: { addCron: {
path: '/com.ql.cron.Cron/addCron', path: "/com.ql.cron.Cron/addCron",
requestStream: false, requestStream: false,
responseStream: false, responseStream: false,
requestSerialize: (value: AddCronRequest) => requestSerialize: (value: AddCronRequest) => Buffer.from(AddCronRequest.encode(value).finish()),
Buffer.from(AddCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => AddCronRequest.decode(value), requestDeserialize: (value: Buffer) => AddCronRequest.decode(value),
responseSerialize: (value: AddCronResponse) => responseSerialize: (value: AddCronResponse) => Buffer.from(AddCronResponse.encode(value).finish()),
Buffer.from(AddCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => AddCronResponse.decode(value), responseDeserialize: (value: Buffer) => AddCronResponse.decode(value),
}, },
delCron: { delCron: {
path: '/com.ql.cron.Cron/delCron', path: "/com.ql.cron.Cron/delCron",
requestStream: false, requestStream: false,
responseStream: false, responseStream: false,
requestSerialize: (value: DeleteCronRequest) => requestSerialize: (value: DeleteCronRequest) => Buffer.from(DeleteCronRequest.encode(value).finish()),
Buffer.from(DeleteCronRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value), requestDeserialize: (value: Buffer) => DeleteCronRequest.decode(value),
responseSerialize: (value: DeleteCronResponse) => responseSerialize: (value: DeleteCronResponse) => Buffer.from(DeleteCronResponse.encode(value).finish()),
Buffer.from(DeleteCronResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value), responseDeserialize: (value: Buffer) => DeleteCronResponse.decode(value),
}, },
} as const; } as const;
@@ -415,67 +466,36 @@ export interface CronClient extends Client {
): ClientUnaryCall; ): ClientUnaryCall;
delCron( delCron(
request: DeleteCronRequest, request: DeleteCronRequest,
callback: ( callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
delCron( delCron(
request: DeleteCronRequest, request: DeleteCronRequest,
metadata: Metadata, metadata: Metadata,
callback: ( callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
delCron( delCron(
request: DeleteCronRequest, request: DeleteCronRequest,
metadata: Metadata, metadata: Metadata,
options: Partial<CallOptions>, options: Partial<CallOptions>,
callback: ( callback: (error: ServiceError | null, response: DeleteCronResponse) => void,
error: ServiceError | null,
response: DeleteCronResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
} }
export const CronClient = makeGenericClientConstructor( export const CronClient = makeGenericClientConstructor(CronService, "com.ql.cron.Cron") as unknown as {
CronService, new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): CronClient;
'com.ql.cron.Cron',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): CronClient;
service: typeof CronService; service: typeof CronService;
}; };
type Builtin = type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin export type DeepPartial<T> = T extends Builtin ? T
? T : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends Array<infer U> : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never; type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin export type Exact<P, I extends P> = P extends Builtin ? P
? P : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;
+51 -114
View File
@@ -12,10 +12,10 @@ import {
Metadata, Metadata,
ServiceError, ServiceError,
UntypedServiceImplementation, UntypedServiceImplementation,
} from '@grpc/grpc-js'; } from "@grpc/grpc-js";
import _m0 from 'protobufjs/minimal'; import _m0 from "protobufjs/minimal";
export const protobufPackage = 'com.ql.health'; export const protobufPackage = "com.ql.health";
export interface HealthCheckRequest { export interface HealthCheckRequest {
service: string; service: string;
@@ -33,79 +33,71 @@ export enum HealthCheckResponse_ServingStatus {
UNRECOGNIZED = -1, UNRECOGNIZED = -1,
} }
export function healthCheckResponse_ServingStatusFromJSON( export function healthCheckResponse_ServingStatusFromJSON(object: any): HealthCheckResponse_ServingStatus {
object: any,
): HealthCheckResponse_ServingStatus {
switch (object) { switch (object) {
case 0: case 0:
case 'UNKNOWN': case "UNKNOWN":
return HealthCheckResponse_ServingStatus.UNKNOWN; return HealthCheckResponse_ServingStatus.UNKNOWN;
case 1: case 1:
case 'SERVING': case "SERVING":
return HealthCheckResponse_ServingStatus.SERVING; return HealthCheckResponse_ServingStatus.SERVING;
case 2: case 2:
case 'NOT_SERVING': case "NOT_SERVING":
return HealthCheckResponse_ServingStatus.NOT_SERVING; return HealthCheckResponse_ServingStatus.NOT_SERVING;
case 3: case 3:
case 'SERVICE_UNKNOWN': case "SERVICE_UNKNOWN":
return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN; return HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN;
case -1: case -1:
case 'UNRECOGNIZED': case "UNRECOGNIZED":
default: default:
return HealthCheckResponse_ServingStatus.UNRECOGNIZED; return HealthCheckResponse_ServingStatus.UNRECOGNIZED;
} }
} }
export function healthCheckResponse_ServingStatusToJSON( export function healthCheckResponse_ServingStatusToJSON(object: HealthCheckResponse_ServingStatus): string {
object: HealthCheckResponse_ServingStatus,
): string {
switch (object) { switch (object) {
case HealthCheckResponse_ServingStatus.UNKNOWN: case HealthCheckResponse_ServingStatus.UNKNOWN:
return 'UNKNOWN'; return "UNKNOWN";
case HealthCheckResponse_ServingStatus.SERVING: case HealthCheckResponse_ServingStatus.SERVING:
return 'SERVING'; return "SERVING";
case HealthCheckResponse_ServingStatus.NOT_SERVING: case HealthCheckResponse_ServingStatus.NOT_SERVING:
return 'NOT_SERVING'; return "NOT_SERVING";
case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN: case HealthCheckResponse_ServingStatus.SERVICE_UNKNOWN:
return 'SERVICE_UNKNOWN'; return "SERVICE_UNKNOWN";
case HealthCheckResponse_ServingStatus.UNRECOGNIZED: case HealthCheckResponse_ServingStatus.UNRECOGNIZED:
default: default:
return 'UNRECOGNIZED'; return "UNRECOGNIZED";
} }
} }
function createBaseHealthCheckRequest(): HealthCheckRequest { function createBaseHealthCheckRequest(): HealthCheckRequest {
return { service: '' }; return { service: "" };
} }
export const HealthCheckRequest = { export const HealthCheckRequest = {
encode( encode(message: HealthCheckRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
message: HealthCheckRequest, if (message.service !== "") {
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.service !== '') {
writer.uint32(10).string(message.service); writer.uint32(10).string(message.service);
} }
return writer; return writer;
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest { decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckRequest {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1:
if (tag != 10) { if (tag !== 10) {
break; break;
} }
message.service = reader.string(); message.service = reader.string();
continue; continue;
} }
if ((tag & 7) == 4 || tag == 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -114,7 +106,7 @@ export const HealthCheckRequest = {
}, },
fromJSON(object: any): HealthCheckRequest { fromJSON(object: any): HealthCheckRequest {
return { service: isSet(object.service) ? String(object.service) : '' }; return { service: isSet(object.service) ? String(object.service) : "" };
}, },
toJSON(message: HealthCheckRequest): unknown { toJSON(message: HealthCheckRequest): unknown {
@@ -123,17 +115,13 @@ export const HealthCheckRequest = {
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>( create<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(base?: I): HealthCheckRequest {
base?: I,
): HealthCheckRequest {
return HealthCheckRequest.fromPartial(base ?? {}); return HealthCheckRequest.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>( fromPartial<I extends Exact<DeepPartial<HealthCheckRequest>, I>>(object: I): HealthCheckRequest {
object: I,
): HealthCheckRequest {
const message = createBaseHealthCheckRequest(); const message = createBaseHealthCheckRequest();
message.service = object.service ?? ''; message.service = object.service ?? "";
return message; return message;
}, },
}; };
@@ -143,10 +131,7 @@ function createBaseHealthCheckResponse(): HealthCheckResponse {
} }
export const HealthCheckResponse = { export const HealthCheckResponse = {
encode( encode(message: HealthCheckResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer {
message: HealthCheckResponse,
writer: _m0.Writer = _m0.Writer.create(),
): _m0.Writer {
if (message.status !== 0) { if (message.status !== 0) {
writer.uint32(8).int32(message.status); writer.uint32(8).int32(message.status);
} }
@@ -154,22 +139,21 @@ export const HealthCheckResponse = {
}, },
decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse { decode(input: _m0.Reader | Uint8Array, length?: number): HealthCheckResponse {
const reader = const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input);
input instanceof _m0.Reader ? input : _m0.Reader.create(input);
let end = length === undefined ? reader.len : reader.pos + length; let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
while (reader.pos < end) { while (reader.pos < end) {
const tag = reader.uint32(); const tag = reader.uint32();
switch (tag >>> 3) { switch (tag >>> 3) {
case 1: case 1:
if (tag != 8) { if (tag !== 8) {
break; break;
} }
message.status = reader.int32() as any; message.status = reader.int32() as any;
continue; continue;
} }
if ((tag & 7) == 4 || tag == 0) { if ((tag & 7) === 4 || tag === 0) {
break; break;
} }
reader.skipType(tag & 7); reader.skipType(tag & 7);
@@ -178,29 +162,20 @@ export const HealthCheckResponse = {
}, },
fromJSON(object: any): HealthCheckResponse { fromJSON(object: any): HealthCheckResponse {
return { return { status: isSet(object.status) ? healthCheckResponse_ServingStatusFromJSON(object.status) : 0 };
status: isSet(object.status)
? healthCheckResponse_ServingStatusFromJSON(object.status)
: 0,
};
}, },
toJSON(message: HealthCheckResponse): unknown { toJSON(message: HealthCheckResponse): unknown {
const obj: any = {}; const obj: any = {};
message.status !== undefined && message.status !== undefined && (obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
(obj.status = healthCheckResponse_ServingStatusToJSON(message.status));
return obj; return obj;
}, },
create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>( create<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(base?: I): HealthCheckResponse {
base?: I,
): HealthCheckResponse {
return HealthCheckResponse.fromPartial(base ?? {}); return HealthCheckResponse.fromPartial(base ?? {});
}, },
fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>( fromPartial<I extends Exact<DeepPartial<HealthCheckResponse>, I>>(object: I): HealthCheckResponse {
object: I,
): HealthCheckResponse {
const message = createBaseHealthCheckResponse(); const message = createBaseHealthCheckResponse();
message.status = object.status ?? 0; message.status = object.status ?? 0;
return message; return message;
@@ -210,25 +185,21 @@ export const HealthCheckResponse = {
export type HealthService = typeof HealthService; export type HealthService = typeof HealthService;
export const HealthService = { export const HealthService = {
check: { check: {
path: '/com.ql.health.Health/Check', path: "/com.ql.health.Health/Check",
requestStream: false, requestStream: false,
responseStream: false, responseStream: false,
requestSerialize: (value: HealthCheckRequest) => requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value), requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) => responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value), responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
}, },
watch: { watch: {
path: '/com.ql.health.Health/Watch', path: "/com.ql.health.Health/Watch",
requestStream: false, requestStream: false,
responseStream: true, responseStream: true,
requestSerialize: (value: HealthCheckRequest) => requestSerialize: (value: HealthCheckRequest) => Buffer.from(HealthCheckRequest.encode(value).finish()),
Buffer.from(HealthCheckRequest.encode(value).finish()),
requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value), requestDeserialize: (value: Buffer) => HealthCheckRequest.decode(value),
responseSerialize: (value: HealthCheckResponse) => responseSerialize: (value: HealthCheckResponse) => Buffer.from(HealthCheckResponse.encode(value).finish()),
Buffer.from(HealthCheckResponse.encode(value).finish()),
responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value), responseDeserialize: (value: Buffer) => HealthCheckResponse.decode(value),
}, },
} as const; } as const;
@@ -241,32 +212,20 @@ export interface HealthServer extends UntypedServiceImplementation {
export interface HealthClient extends Client { export interface HealthClient extends Client {
check( check(
request: HealthCheckRequest, request: HealthCheckRequest,
callback: ( callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
check( check(
request: HealthCheckRequest, request: HealthCheckRequest,
metadata: Metadata, metadata: Metadata,
callback: ( callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
check( check(
request: HealthCheckRequest, request: HealthCheckRequest,
metadata: Metadata, metadata: Metadata,
options: Partial<CallOptions>, options: Partial<CallOptions>,
callback: ( callback: (error: ServiceError | null, response: HealthCheckResponse) => void,
error: ServiceError | null,
response: HealthCheckResponse,
) => void,
): ClientUnaryCall; ): ClientUnaryCall;
watch( watch(request: HealthCheckRequest, options?: Partial<CallOptions>): ClientReadableStream<HealthCheckResponse>;
request: HealthCheckRequest,
options?: Partial<CallOptions>,
): ClientReadableStream<HealthCheckResponse>;
watch( watch(
request: HealthCheckRequest, request: HealthCheckRequest,
metadata?: Metadata, metadata?: Metadata,
@@ -274,43 +233,21 @@ export interface HealthClient extends Client {
): ClientReadableStream<HealthCheckResponse>; ): ClientReadableStream<HealthCheckResponse>;
} }
export const HealthClient = makeGenericClientConstructor( export const HealthClient = makeGenericClientConstructor(HealthService, "com.ql.health.Health") as unknown as {
HealthService, new (address: string, credentials: ChannelCredentials, options?: Partial<ClientOptions>): HealthClient;
'com.ql.health.Health',
) as unknown as {
new (
address: string,
credentials: ChannelCredentials,
options?: Partial<ClientOptions>,
): HealthClient;
service: typeof HealthService; service: typeof HealthService;
}; };
type Builtin = type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
| Date
| Function
| Uint8Array
| string
| number
| boolean
| undefined;
export type DeepPartial<T> = T extends Builtin export type DeepPartial<T> = T extends Builtin ? T
? T : T extends Array<infer U> ? Array<DeepPartial<U>> : T extends ReadonlyArray<infer U> ? ReadonlyArray<DeepPartial<U>>
: T extends Array<infer U> : T extends {} ? { [K in keyof T]?: DeepPartial<T[K]> }
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>; : Partial<T>;
type KeysOfUnion<T> = T extends T ? keyof T : never; type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin export type Exact<P, I extends P> = P extends Builtin ? P
? P : P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };
: P & { [K in keyof P]: Exact<P[K], I[K]> } & {
[K in Exclude<keyof I, KeysOfUnion<P>>]: never;
};
function isSet(value: any): boolean { function isSet(value: any): boolean {
return value !== null && value !== undefined; return value !== null && value !== undefined;
+3 -1
View File
@@ -6,7 +6,7 @@ import { credentials } from '@grpc/grpc-js';
const app = express(); const app = express();
const client = new HealthClient( const client = new HealthClient(
`localhost:${config.cronPort}`, `0.0.0.0:${config.cronPort}`,
credentials.createInsecure(), credentials.createInsecure(),
); );
@@ -25,9 +25,11 @@ app
await require('./loaders/db').default(); await require('./loaders/db').default();
Logger.debug(`✌️ 公共服务启动成功!`); Logger.debug(`✌️ 公共服务启动成功!`);
console.debug(`✌️ 公共服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
}) })
.on('error', (err) => { .on('error', (err) => {
Logger.error(err); Logger.error(err);
console.error(err);
process.exit(1); process.exit(1);
}); });
+33 -17
View File
@@ -3,36 +3,52 @@ import { AddCronRequest, AddCronResponse } from '../protos/cron';
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { scheduleStacks } from './data'; import { scheduleStacks } from './data';
import { runCron } from '../shared/runCron'; import { runCron } from '../shared/runCron';
import { QL_PREFIX, TASK_PREFIX } from '../config/const';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
import dayjs from 'dayjs';
const addCron = ( const addCron = (
call: ServerUnaryCall<AddCronRequest, AddCronResponse>, call: ServerUnaryCall<AddCronRequest, AddCronResponse>,
callback: sendUnaryData<AddCronResponse>, callback: sendUnaryData<AddCronResponse>,
) => { ) => {
for (const item of call.request.crons) { for (const item of call.request.crons) {
const { id, schedule, command } = item; const { id, schedule, command, extraSchedules, name } = item;
if (scheduleStacks.has(id)) { if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel(); scheduleStacks.get(id)?.forEach((x) => x.cancel());
} }
let cmdStr = command.trim();
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
scheduleStacks.set(
id,
nodeSchedule.scheduleJob(id, schedule, async () => {
Logger.info( Logger.info(
`当前时间: ${dayjs().format( '[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
'YYYY-MM-DD HH:mm:ss', id,
)},运行命令: ${cmdStr}`, name,
schedule,
command,
); );
runCron(`ID=${id} ${cmdStr}`);
if (extraSchedules?.length) {
extraSchedules.forEach(x => {
Logger.info(
'[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
id,
name,
x.schedule,
command,
);
})
}
scheduleStacks.set(id, [
nodeSchedule.scheduleJob(id, schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, { name, schedule, extraSchedules });
}), }),
); ...(extraSchedules?.length
? extraSchedules.map((x) =>
nodeSchedule.scheduleJob(id, x.schedule, async () => {
Logger.info(`[schedule][准备运行任务] 命令: ${command}`);
runCron(command, { name, schedule, extraSchedules });
}),
)
: []),
]);
} }
callback(null, null); callback(null, null);
+1 -1
View File
@@ -10,7 +10,7 @@ import config from '../config';
class Client { class Client {
private client = new CronClient( private client = new CronClient(
`localhost:${config.cronPort}`, `0.0.0.0:${config.cronPort}`,
credentials.createInsecure(), credentials.createInsecure(),
); );
+1 -1
View File
@@ -1,6 +1,6 @@
import nodeSchedule from 'node-schedule'; import nodeSchedule from 'node-schedule';
import { ToadScheduler } from 'toad-scheduler'; import { ToadScheduler } from 'toad-scheduler';
export const scheduleStacks = new Map<string, nodeSchedule.Job>(); export const scheduleStacks = new Map<string, nodeSchedule.Job[]>();
export const intervalSchedule = new ToadScheduler(); export const intervalSchedule = new ToadScheduler();
+6 -1
View File
@@ -1,6 +1,7 @@
import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js'; import { ServerUnaryCall, sendUnaryData } from '@grpc/grpc-js';
import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron'; import { DeleteCronRequest, DeleteCronResponse } from '../protos/cron';
import { scheduleStacks } from './data'; import { scheduleStacks } from './data';
import Logger from '../loaders/logger';
const delCron = ( const delCron = (
call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>, call: ServerUnaryCall<DeleteCronRequest, DeleteCronResponse>,
@@ -8,7 +9,11 @@ const delCron = (
) => { ) => {
for (const id of call.request.ids) { for (const id of call.request.ids) {
if (scheduleStacks.has(id)) { if (scheduleStacks.has(id)) {
scheduleStacks.get(id)?.cancel(); Logger.info(
'[schedule][取消定时任务], 任务ID: %s',
id,
);
scheduleStacks.get(id)?.forEach(x => x.cancel());
scheduleStacks.delete(id); scheduleStacks.delete(id);
} }
} }
+1 -1
View File
@@ -10,7 +10,7 @@ const check = async (
switch (call.request.service) { switch (call.request.service) {
case 'cron': case 'cron':
const res = await promiseExec( const res = await promiseExec(
`curl -s http://localhost:${config.port}/api/system`, `curl -s http://0.0.0.0:${config.port}/api/system`,
); );
if (res.includes('200')) { if (res.includes('200')) {
+2 -1
View File
@@ -11,7 +11,7 @@ const server = new Server();
server.addService(HealthService, { check }); server.addService(HealthService, { check });
server.addService(CronService, { addCron, delCron }); server.addService(CronService, { addCron, delCron });
server.bindAsync( server.bindAsync(
`localhost:${config.cronPort}`, `0.0.0.0:${config.cronPort}`,
ServerCredentials.createInsecure(), ServerCredentials.createInsecure(),
(err, port) => { (err, port) => {
if (err) { if (err) {
@@ -19,6 +19,7 @@ server.bindAsync(
} }
server.start(); server.start();
Logger.debug(`✌️ 定时服务启动成功!`); Logger.debug(`✌️ 定时服务启动成功!`);
console.debug(`✌️ 定时服务启动成功!`);
process.send?.('ready'); process.send?.('ready');
}, },
); );
+65 -59
View File
@@ -5,7 +5,7 @@ import { Crontab, CrontabModel, CrontabStatus } from '../data/cron';
import { exec, execSync } from 'child_process'; import { exec, execSync } from 'child_process';
import fs from 'fs'; import fs from 'fs';
import cron_parser from 'cron-parser'; import cron_parser from 'cron-parser';
import { getFileContentByName, fileExist, killTask } from '../config/util'; import { getFileContentByName, fileExist, killTask, getUniqPath, safeJSONParse } from '../config/util';
import { promises, existsSync } from 'fs'; import { promises, existsSync } from 'fs';
import { Op, where, col as colFn, FindOptions, fn } from 'sequelize'; import { Op, where, col as colFn, FindOptions, fn } from 'sequelize';
import path from 'path'; import path from 'path';
@@ -13,10 +13,13 @@ import { TASK_PREFIX, QL_PREFIX } from '../config/const';
import cronClient from '../schedule/client'; import cronClient from '../schedule/client';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
import dayjs from 'dayjs';
import pickBy from 'lodash/pickBy';
import omit from 'lodash/omit';
@Service() @Service()
export default class CronService { export default class CronService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
private isSixCron(cron: Crontab) { private isSixCron(cron: Crontab) {
const { schedule } = cron; const { schedule } = cron;
@@ -30,9 +33,9 @@ export default class CronService {
const tab = new Crontab(payload); const tab = new Crontab(payload);
tab.saved = false; tab.saved = false;
const doc = await this.insert(tab); const doc = await this.insert(tab);
if (this.isSixCron(doc)) { if (this.isSixCron(doc) || doc.extra_schedules?.length) {
await cronClient.addCron([ await cronClient.addCron([
{ id: String(doc.id), schedule: doc.schedule!, command: doc.command }, { name: doc.name || '', id: String(doc.id), schedule: doc.schedule!, command: this.makeCommand(doc), extraSchedules: doc.extra_schedules || [] },
]); ]);
} }
await this.set_crontab(); await this.set_crontab();
@@ -51,15 +54,17 @@ export default class CronService {
if (doc.isDisabled === 1) { if (doc.isDisabled === 1) {
return newDoc; return newDoc;
} }
if (this.isSixCron(doc)) { if (this.isSixCron(doc) || doc.extra_schedules?.length) {
await cronClient.delCron([String(newDoc.id)]); await cronClient.delCron([String(doc.id)]);
} }
if (this.isSixCron(newDoc)) { if (this.isSixCron(newDoc) || newDoc.extra_schedules?.length) {
await cronClient.addCron([ await cronClient.addCron([
{ {
name: doc.name || '',
id: String(newDoc.id), id: String(newDoc.id),
schedule: newDoc.schedule!, schedule: newDoc.schedule!,
command: newDoc.command, command: this.makeCommand(newDoc),
extraSchedules: newDoc.extra_schedules || []
}, },
]); ]);
} }
@@ -87,7 +92,7 @@ export default class CronService {
last_running_time: number; last_running_time: number;
last_execution_time: number; last_execution_time: number;
}) { }) {
const options: any = { let options: any = {
status, status,
pid, pid,
log_path, log_path,
@@ -97,7 +102,13 @@ export default class CronService {
options.last_running_time = last_running_time; options.last_running_time = last_running_time;
} }
return await CrontabModel.update({ ...options }, { where: { id: ids } }); for (const id of ids) {
const cron = await this.getDb({ id });
if (status === CrontabStatus.idle && log_path !== cron.log_path) {
options = omit(options, ['status', 'log_path', 'pid']);
}
await CrontabModel.update({ ...pickBy(options, (v) => v === 0 || !!v) }, { where: { id } });
}
} }
public async remove(ids: number[]) { public async remove(ids: number[]) {
@@ -161,7 +172,7 @@ export default class CronService {
case 'In': case 'In':
q[Op.or] = [ q[Op.or] = [
{ {
[property]: value, [property]: Array.isArray(value) ? value : [value],
}, },
property === 'status' && value.includes(2) property === 'status' && value.includes(2)
? { isDisabled: 1 } ? { isDisabled: 1 }
@@ -172,7 +183,7 @@ export default class CronService {
q[Op.and] = [ q[Op.and] = [
{ {
[property]: { [property]: {
[Op.notIn]: value, [Op.notIn]: Array.isArray(value) ? value : [value],
}, },
}, },
property === 'status' && value.includes(2) property === 'status' && value.includes(2)
@@ -308,9 +319,9 @@ export default class CronService {
const searchText = params?.searchValue; const searchText = params?.searchValue;
const page = Number(params?.page || '0'); const page = Number(params?.page || '0');
const size = Number(params?.size || '0'); const size = Number(params?.size || '0');
const viewQuery = JSON.parse(params?.queryString || '{}'); const viewQuery = safeJSONParse(params?.queryString);
const filterQuery = JSON.parse(params?.filters || '{}'); const filterQuery = safeJSONParse(params?.filters);
const sorterQuery = JSON.parse(params?.sorter || '{}'); const sorterQuery = safeJSONParse(params?.sorter);
let query: any = {}; let query: any = {};
let order = [ let order = [
@@ -370,7 +381,7 @@ export default class CronService {
try { try {
await killTask(doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.error(error);
} }
} }
} }
@@ -381,65 +392,47 @@ export default class CronService {
); );
} }
private async runSingle(cronId: number): Promise<number> { private async runSingle(cronId: number): Promise<number | void> {
return taskLimit.runWithCpuLimit(() => { return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
const cron = await this.getDb({ id: cronId }); const cron = await this.getDb({ id: cronId });
const params = { name: cron.name, command: cron.command, schedule: cron.schedule, extraSchedules: cron.extra_schedules };
if (cron.status !== CrontabStatus.queued) { if (cron.status !== CrontabStatus.queued) {
resolve(); resolve(params);
return; return;
} }
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify(params)}`);
let { id, command, log_path } = cron; let { id, command, log_path } = cron;
const absolutePath = path.resolve(config.logPath, `${log_path}`); const uniqPath = await getUniqPath(command, `${id}`);
const logFileExist = log_path && (await fileExist(absolutePath)); const logTime = dayjs().format('YYYY-MM-DD-HH-mm-ss-SSS');
const logDirPath = path.resolve(config.logPath, `${uniqPath}`);
this.logger.silly('Running job'); if (log_path?.split('/')?.every(x => x !== uniqPath)) {
this.logger.silly('ID: ' + id); fs.mkdirSync(logDirPath, { recursive: true });
this.logger.silly('Original command: ' + command);
let cmdStr = command;
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) {
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
if (
cmdStr.endsWith('.js') ||
cmdStr.endsWith('.py') ||
cmdStr.endsWith('.pyc') ||
cmdStr.endsWith('.sh') ||
cmdStr.endsWith('.ts')
) {
cmdStr = `${cmdStr} now`;
} }
const logPath = `${uniqPath}/${logTime}.log`;
const absolutePath = path.resolve(config.logPath, `${logPath}`);
const cp = spawn(`ID=${id} ${cmdStr}`, { shell: '/bin/bash' }); const cp = spawn(`real_log_path=${logPath} no_delay=true ${this.makeCommand(cron)}`, { shell: '/bin/bash' });
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.running, pid: cp.pid }, { status: CrontabStatus.running, pid: cp.pid, log_path: logPath },
{ where: { id } }, { where: { id } },
); );
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${data.toString()}`); fs.appendFileSync(`${absolutePath}`, `${data.toString()}`);
}
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
if (logFileExist) {
fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`); fs.appendFileSync(`${absolutePath}`, `${JSON.stringify(err)}`);
}
}); });
cp.on('exit', async (code, signal) => { cp.on('exit', async (code) => {
this.logger.info(
`任务 ${command} 进程id: ${cp.pid} 退出,退出码 ${code}`,
);
});
cp.on('close', async (code) => {
await CrontabModel.update( await CrontabModel.update(
{ status: CrontabStatus.idle, pid: undefined }, { status: CrontabStatus.idle, pid: undefined },
{ where: { id } }, { where: { id } },
); );
resolve(); resolve({ ...params, pid: cp.pid, code });
}); });
}); });
}); });
@@ -457,9 +450,11 @@ export default class CronService {
const sixCron = docs const sixCron = docs
.filter((x) => this.isSixCron(x)) .filter((x) => this.isSixCron(x))
.map((doc) => ({ .map((doc) => ({
name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: doc.command, command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || []
})); }));
await cronClient.addCron(sixCron); await cronClient.addCron(sixCron);
await this.set_crontab(); await this.set_crontab();
@@ -502,12 +497,22 @@ export default class CronService {
} }
} }
private make_command(tab: Crontab) { private makeCommand(tab: Crontab) {
let command = tab.command.trim(); let command = tab.command.trim();
if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) { if (!command.startsWith(TASK_PREFIX) && !command.startsWith(QL_PREFIX)) {
command = `${TASK_PREFIX}${tab.command}`; command = `${TASK_PREFIX}${tab.command}`;
} }
const crontab_job_string = `ID=${tab.id} ${command}`; let commandVariable = `no_tee=true ID=${tab.id} `
if (tab.task_before) {
commandVariable += `task_before='${tab.task_before.replace(/'/g, "'\\''")
.trim()}' `;
}
if (tab.task_after) {
commandVariable += `task_after='${tab.task_after.replace(/'/g, "'\\''")
.trim()}' `;
}
const crontab_job_string = `${commandVariable}${command}`;
return crontab_job_string; return crontab_job_string;
} }
@@ -516,21 +521,20 @@ export default class CronService {
var crontab_string = ''; var crontab_string = '';
tabs.data.forEach((tab) => { tabs.data.forEach((tab) => {
const _schedule = tab.schedule && tab.schedule.split(/ +/); const _schedule = tab.schedule && tab.schedule.split(/ +/);
if (tab.isDisabled === 1 || _schedule!.length !== 5) { if (tab.isDisabled === 1 || _schedule!.length !== 5 || tab.extra_schedules?.length) {
crontab_string += '# '; crontab_string += '# ';
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';
crontab_string += this.make_command(tab); crontab_string += this.makeCommand(tab);
crontab_string += '\n'; crontab_string += '\n';
} else { } else {
crontab_string += tab.schedule; crontab_string += tab.schedule;
crontab_string += ' '; crontab_string += ' ';
crontab_string += this.make_command(tab); crontab_string += this.makeCommand(tab);
crontab_string += '\n'; crontab_string += '\n';
} }
}); });
this.logger.silly(crontab_string);
fs.writeFileSync(config.crontabFile, crontab_string); fs.writeFileSync(config.crontabFile, crontab_string);
execSync(`crontab ${config.crontabFile}`); execSync(`crontab ${config.crontabFile}`);
@@ -578,9 +582,11 @@ export default class CronService {
const sixCron = tabs.data const sixCron = tabs.data
.filter((x) => this.isSixCron(x) && x.isDisabled !== 1) .filter((x) => this.isSixCron(x) && x.isDisabled !== 1)
.map((doc) => ({ .map((doc) => ({
name: doc.name || '',
id: String(doc.id), id: String(doc.id),
schedule: doc.schedule!, schedule: doc.schedule!,
command: doc.command, command: this.makeCommand(doc),
extraSchedules: doc.extra_schedules || []
})); }));
await cronClient.addCron(sixCron); await cronClient.addCron(sixCron);
} }
+1 -1
View File
@@ -273,7 +273,7 @@ export default class DependenceService {
this.updateLog(depIds, JSON.stringify(err)); this.updateLog(depIds, JSON.stringify(err));
}); });
cp.on('close', async (code) => { cp.on('exit', async (code) => {
const endTime = dayjs(); const endTime = dayjs();
const isSucceed = code === 0; const isSucceed = code === 0;
const resultText = isSucceed ? '成功' : '失败'; const resultText = isSucceed ? '成功' : '失败';
+2 -4
View File
@@ -202,11 +202,9 @@ export default class EnvService {
let value = group let value = group
.map((x) => x.value) .map((x) => x.value)
.join('&') .join('&')
.replace(/(\\)[^n]/g, '\\\\') .replace(/'/g, "'\\''")
.replace(/(\\$)/, '\\\\')
.replace(/"/g, '\\"')
.trim(); .trim();
env_string += `export ${key}="${value}"\n`; env_string += `export ${key}='${value}'\n`;
} }
} }
} }
+67 -8
View File
@@ -1,12 +1,12 @@
import { NotificationInfo } from '../data/notify';
import { Service, Inject } from 'typedi';
import winston from 'winston';
import UserService from './user';
import got from 'got';
import nodemailer from 'nodemailer';
import crypto from 'crypto'; import crypto from 'crypto';
import got from 'got';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import nodemailer from 'nodemailer';
import { Inject, Service } from 'typedi';
import winston from 'winston';
import { parseBody, parseHeaders } from '../config/util'; import { parseBody, parseHeaders } from '../config/util';
import { NotificationInfo } from '../data/notify';
import UserService from './user';
@Service() @Service()
export default class NotificationService { export default class NotificationService {
@@ -31,6 +31,7 @@ export default class NotificationService {
['pushMe', this.pushMe], ['pushMe', this.pushMe],
['webhook', this.webhook], ['webhook', this.webhook],
['lark', this.lark], ['lark', this.lark],
['chronocat', this.chronocat],
]); ]);
private title = ''; private title = '';
@@ -195,7 +196,8 @@ export default class NotificationService {
} }
private async bark() { private async bark() {
let { barkPush, barkIcon, barkSound, barkGroup } = this.params; let { barkPush, barkIcon, barkSound, barkGroup, barkLevel, barkUrl } =
this.params;
if (!barkPush.startsWith('http')) { if (!barkPush.startsWith('http')) {
barkPush = `https://api.day.app/${barkPush}`; barkPush = `https://api.day.app/${barkPush}`;
} }
@@ -203,7 +205,7 @@ export default class NotificationService {
this.title, this.title,
)}/${encodeURIComponent( )}/${encodeURIComponent(
this.content, this.content,
)}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}`; )}?icon=${barkIcon}&sound=${barkSound}&group=${barkGroup}&level=${barkLevel}&url=${barkUrl}`;
try { try {
const res: any = await got const res: any = await got
@@ -588,6 +590,63 @@ export default class NotificationService {
} }
} }
private async chronocat() {
const { chronocatURL, chronocatQQ, chronocatToekn } = this.params;
try {
const user_ids = chronocatQQ
.match(/user_id=(\d+)/g)
?.map((match: any) => match.split('=')[1]);
const group_ids = chronocatQQ
.match(/group_id=(\d+)/g)
?.map((match: any) => match.split('=')[1]);
const url = `${chronocatURL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${chronocatToekn}`,
};
for (const [chat_type, ids] of [
[1, user_ids],
[2, group_ids],
]) {
if (!ids) {
continue;
}
let _ids: any = ids;
for (const chat_id of _ids) {
const data = {
peer: {
chatType: chat_type,
peerUin: chat_id,
},
elements: [
{
elementType: 1,
textElement: {
content: `${this.title}\n\n${this.content}`,
},
},
],
};
const res: any = await got.post(url, {
...this.gotOption,
json: data,
headers,
});
if (res.body === 'success') {
return true;
} else {
throw new Error(res.body);
}
}
}
return false;
} catch (error: any) {
throw new Error(error.response ? error.response.body : error);
}
}
private async webhook() { private async webhook() {
const { const {
webhookUrl, webhookUrl,
+39 -28
View File
@@ -12,7 +12,7 @@ import dayjs from 'dayjs';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import { spawn } from 'cross-spawn'; import { spawn } from 'cross-spawn';
interface ScheduleTaskType { export interface ScheduleTaskType {
id: number; id: number;
command: string; command: string;
name?: string; name?: string;
@@ -42,15 +42,22 @@ export default class ScheduleService {
private maxBuffer = 200 * 1024 * 1024; private maxBuffer = 200 * 1024 * 1024;
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
async runTask( async runTask(
command: string, command: string,
callbacks: TaskCallbacks = {}, callbacks: TaskCallbacks = {},
params: {
schedule?: string;
name?: string;
command?: string;
},
completionTime: 'start' | 'end' = 'end', completionTime: 'start' | 'end' = 'end',
) { ) {
return taskLimit.runWithCpuLimit(() => { return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
this.logger.info(`[panel][开始执行任务] 参数 ${JSON.stringify({ ...params, command })}`);
try { try {
const startTime = dayjs(); const startTime = dayjs();
await callbacks.onBefore?.(startTime); await callbacks.onBefore?.(startTime);
@@ -66,9 +73,8 @@ export default class ScheduleService {
cp.stderr.on('data', async (data) => { cp.stderr.on('data', async (data) => {
this.logger.info( this.logger.info(
'[执行任务失败] %s,时间:%s, 错误信息%j', '[panel][执行任务失败] 命令: %s, 错误信息: %j',
command, command,
new Date().toLocaleString(),
data.toString(), data.toString(),
); );
await callbacks.onError?.(data.toString()); await callbacks.onError?.(data.toString());
@@ -76,34 +82,26 @@ export default class ScheduleService {
cp.on('error', async (err) => { cp.on('error', async (err) => {
this.logger.error( this.logger.error(
'[创建任务失败] %s,时间:%s, 错误信息%j', '[panel][创建任务失败] 命令: %s, 错误信息: %j',
command, command,
new Date().toLocaleString(),
err, err,
); );
await callbacks.onError?.(JSON.stringify(err)); await callbacks.onError?.(JSON.stringify(err));
}); });
cp.on('exit', async (code, signal) => { cp.on('exit', async (code) => {
this.logger.info(
`[任务退出] ${command} 进程id: ${cp.pid},退出码 ${code}`,
);
});
cp.on('close', async (code) => {
const endTime = dayjs(); const endTime = dayjs();
await callbacks.onEnd?.( await callbacks.onEnd?.(
cp, cp,
endTime, endTime,
endTime.diff(startTime, 'seconds'), endTime.diff(startTime, 'seconds'),
); );
resolve(null); resolve({ ...params, pid: cp.pid, code });
}); });
} catch (error) { } catch (error) {
await this.logger.error( this.logger.error(
'执行任务%s失败,时间:%s, 错误信息%j', '[panel][执行任务失败] 命令: %s, 错误信息: %j',
command, command,
new Date().toLocaleString(),
error, error,
); );
await callbacks.onError?.(JSON.stringify(error)); await callbacks.onError?.(JSON.stringify(error));
@@ -119,7 +117,7 @@ export default class ScheduleService {
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建cron任务]任务ID: %scron: %s任务名: %s执行命令: %s', '[panel][创建cron任务], 任务ID: %s, cron: %s, 任务名: %s, 执行命令: %s',
_id, _id,
schedule, schedule,
name, name,
@@ -129,18 +127,26 @@ export default class ScheduleService {
this.scheduleStacks.set( this.scheduleStacks.set(
_id, _id,
nodeSchedule.scheduleJob(_id, schedule, async () => { nodeSchedule.scheduleJob(_id, schedule, async () => {
this.runTask(command, callbacks); this.runTask(command, callbacks, {
name,
schedule,
command,
});
}), }),
); );
if (runImmediately) { if (runImmediately) {
this.runTask(command, callbacks); this.runTask(command, callbacks, {
name,
schedule,
command,
});
} }
} }
async cancelCronTask({ id = 0, name }: ScheduleTaskType) { async cancelCronTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消定时任务]任务名%s', name); this.logger.info('[panel][取消定时任务], 任务名: %s', name);
if (this.scheduleStacks.has(_id)) { if (this.scheduleStacks.has(_id)) {
this.scheduleStacks.get(_id)?.cancel(); this.scheduleStacks.get(_id)?.cancel();
this.scheduleStacks.delete(_id); this.scheduleStacks.delete(_id);
@@ -155,7 +161,7 @@ export default class ScheduleService {
) { ) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info( this.logger.info(
'[创建interval任务]任务ID: %s任务名: %s执行命令: %s', '[panel][创建interval任务], 任务ID: %s, 任务名: %s, 执行命令: %s',
_id, _id,
name, name,
command, command,
@@ -163,13 +169,15 @@ export default class ScheduleService {
const task = new Task( const task = new Task(
name, name,
() => { () => {
this.runTask(command, callbacks); this.runTask(command, callbacks, {
name,
command,
});
}, },
(err) => { (err) => {
this.logger.error( this.logger.error(
'执行任务%s失败,时间:%s, 错误信息%j', '[执行任务失败] 命令: %s, 错误信息: %j',
command, command,
new Date().toLocaleString(),
err, err,
); );
}, },
@@ -184,13 +192,16 @@ export default class ScheduleService {
this.intervalSchedule.addIntervalJob(job); this.intervalSchedule.addIntervalJob(job);
if (runImmediately) { if (runImmediately) {
this.runTask(command, callbacks); this.runTask(command, callbacks, {
name,
command,
});
} }
} }
async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) { async cancelIntervalTask({ id = 0, name }: ScheduleTaskType) {
const _id = this.formatId(id); const _id = this.formatId(id);
this.logger.info('[取消interval任务]任务ID: %s任务名%s', _id, name); this.logger.info('[取消interval任务], 任务ID: %s, 任务名: %s', _id, name);
this.intervalSchedule.removeById(_id); this.intervalSchedule.removeById(_id);
} }
+5 -5
View File
@@ -16,14 +16,14 @@ export default class ScriptService {
private sockService: SockService, private sockService: SockService,
private cronService: CronService, private cronService: CronService,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
) {} ) { }
private taskCallbacks(filePath: string): TaskCallbacks { private taskCallbacks(filePath: string): TaskCallbacks {
return { return {
onEnd: async (cp, endTime, diff) => { onEnd: async (cp, endTime, diff) => {
try { try {
fs.unlinkSync(filePath); fs.unlinkSync(filePath);
} catch (error) {} } catch (error) { }
}, },
onError: async (message: string) => { onError: async (message: string) => {
this.sockService.sendMessage({ this.sockService.sendMessage({
@@ -46,6 +46,7 @@ export default class ScriptService {
const pid = await this.scheduleService.runTask( const pid = await this.scheduleService.runTask(
command, command,
this.taskCallbacks(filePath), this.taskCallbacks(filePath),
{ command },
'start', 'start',
); );
@@ -53,14 +54,13 @@ export default class ScriptService {
} }
public async stopScript(filePath: string, pid: number) { public async stopScript(filePath: string, pid: number) {
let str = '';
if (!pid) { if (!pid) {
const relativePath = path.relative(config.scriptPath, filePath); const relativePath = path.relative(config.scriptPath, filePath);
pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`); pid = await getPid(`${TASK_COMMAND} -l ${relativePath} now`) as number;
} }
try { try {
await killTask(pid); await killTask(pid);
} catch (error) {} } catch (error) { }
return { code: 200 }; return { code: 200 };
} }
+1 -1
View File
@@ -7,7 +7,7 @@ import { SockMessage } from '../data/sock';
export default class SockService { export default class SockService {
private clients: Connection[] = []; private clients: Connection[] = [];
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) { }
public getClients() { public getClients() {
return this.clients; return this.clients;
+1 -1
View File
@@ -58,7 +58,7 @@ export default class SshKeyService {
private generateSingleSshConfig(alias: string, host: string, proxy?: string) { private generateSingleSshConfig(alias: string, host: string, proxy?: string) {
if (host === 'github.com') { if (host === 'github.com') {
host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa\n PubkeyAcceptedAlgorithms +ssh-rsa`; host = `ssh.github.com\n Port 443\n HostkeyAlgorithms +ssh-rsa`;
} }
const proxyStr = proxy const proxyStr = proxy
? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n` ? ` ProxyCommand nc -v -x ${proxy} %h %p 2>/dev/null\n`
+6 -10
View File
@@ -301,17 +301,9 @@ export default class SubscriptionService {
try { try {
await killTask(doc.pid); await killTask(doc.pid);
} catch (error) { } catch (error) {
this.logger.silly(error); this.logger.error(error);
} }
} }
const absolutePath = await handleLogPath(doc.log_path as string);
fs.appendFileSync(
`${absolutePath}`,
`\n## 执行结束... ${dayjs().format(
'YYYY-MM-DD HH:mm:ss',
)}${LOG_END_SYMBOL}`,
);
} }
await SubscriptionModel.update( await SubscriptionModel.update(
@@ -328,7 +320,11 @@ export default class SubscriptionService {
const command = formatCommand(subscription); const command = formatCommand(subscription);
this.scheduleService.runTask(command, this.taskCallbacks(subscription)); this.scheduleService.runTask(command, this.taskCallbacks(subscription), {
name: subscription.name,
schedule: subscription.schedule,
command
});
} }
public async disabled(ids: number[]) { public async disabled(ids: number[]) {
+34 -3
View File
@@ -21,11 +21,14 @@ import {
parseContentVersion, parseContentVersion,
parseVersion, parseVersion,
promiseExec, promiseExec,
readDirs,
} from '../config/util'; } from '../config/util';
import { TASK_COMMAND } from '../config/const'; import { TASK_COMMAND } from '../config/const';
import taskLimit from '../shared/pLimit'; import taskLimit from '../shared/pLimit';
import tar from 'tar'; import tar from 'tar';
import path from 'path'; import path from 'path';
import fs from 'fs';
import sum from 'lodash/sum';
@Service() @Service()
export default class SystemService { export default class SystemService {
@@ -36,7 +39,7 @@ export default class SystemService {
@Inject('logger') private logger: winston.Logger, @Inject('logger') private logger: winston.Logger,
private scheduleService: ScheduleService, private scheduleService: ScheduleService,
private sockService: SockService, private sockService: SockService,
) {} ) { }
public async getSystemConfig() { public async getSystemConfig() {
const doc = await this.getDb({ type: AuthDataType.systemConfig }); const doc = await this.getDb({ type: AuthDataType.systemConfig });
@@ -81,7 +84,7 @@ export default class SystemService {
}); });
if (info.logRemoveFrequency) { if (info.logRemoveFrequency) {
const cron = { const cron = {
id: result.id, id: result.id || NaN,
name: '删除日志', name: '删除日志',
command: `ql rmlog ${info.logRemoveFrequency}`, command: `ql rmlog ${info.logRemoveFrequency}`,
}; };
@@ -111,7 +114,7 @@ export default class SystemService {
}, },
); );
lastVersionContent = await parseContentVersion(result.body); lastVersionContent = await parseContentVersion(result.body);
} catch (error) {} } catch (error) { }
if (!lastVersionContent) { if (!lastVersionContent) {
lastVersionContent = currentVersionContent; lastVersionContent = currentVersionContent;
@@ -229,6 +232,9 @@ export default class SystemService {
this.scheduleService.runTask( this.scheduleService.runTask(
`real_log_path=${logPath} real_time=true ${command}`, `real_log_path=${logPath} real_time=true ${command}`,
callback, callback,
{
command,
}
); );
} }
@@ -275,4 +281,29 @@ export default class SystemService {
return { code: 400, message: error.message }; return { code: 400, message: error.message };
} }
} }
public async getSystemLog(res: Response) {
const result = readDirs(config.systemLogPath, config.systemLogPath);
const logs = result.reverse().filter((x) => x.title.endsWith('.log'));
res.set({
'Content-Length': sum(logs.map((x) => x.size)),
});
(function sendFiles(res, fileNames) {
if (fileNames.length === 0) {
res.end();
return;
}
const currentLog = fileNames.shift();
if (currentLog) {
const currentFileStream = fs.createReadStream(
path.join(config.systemLogPath, currentLog.title),
);
currentFileStream.on('end', () => {
sendFiles(res, fileNames);
});
currentFileStream.pipe(res, { end: false });
}
})(res, logs);
}
} }
+2 -1
View File
@@ -5,6 +5,7 @@ import {
fileExist, fileExist,
getNetIp, getNetIp,
getPlatform, getPlatform,
safeJSONParse,
} from '../config/util'; } from '../config/util';
import config from '../config'; import config from '../config';
import * as fs from 'fs'; import * as fs from 'fs';
@@ -327,7 +328,7 @@ export default class UserService {
private getAuthInfo() { private getAuthInfo() {
const content = fs.readFileSync(config.authConfigFile, 'utf8'); const content = fs.readFileSync(config.authConfigFile, 'utf8');
return JSON.parse(content || '{}'); return safeJSONParse(content);
} }
private updateAuthInfo(authInfo: any, info: any) { private updateAuthInfo(authInfo: any, info: any) {
+55 -12
View File
@@ -1,19 +1,62 @@
import pLimit from 'p-limit'; import PQueue, { QueueAddOptions } from 'p-queue-cjs';
import os from 'os'; import os from 'os';
import { AuthDataType, AuthModel } from '../data/auth'; import { AuthDataType, AuthModel } from '../data/auth';
import Logger from '../loaders/logger';
class TaskLimit { class TaskLimit {
private oneLimit = pLimit(1); private oneLimit = new PQueue({ concurrency: 1 });
private updateLogLimit = pLimit(1); private updateLogLimit = new PQueue({ concurrency: 1 });
private cpuLimit = pLimit(Math.max(os.cpus().length, 4)); private cronLimit = new PQueue({ concurrency: Math.max(os.cpus().length, 4) });
get cronLimitActiveCount() {
return this.cronLimit.pending;
}
get cronLimitPendingCount() {
return this.cronLimit.size;
}
constructor() { constructor() {
this.setCustomLimit(); this.setCustomLimit();
this.handleEvents();
}
private handleEvents() {
this.cronLimit.on('add', () => {
Logger.info(
`[schedule][任务加入队列] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
})
this.cronLimit.on('active', () => {
Logger.info(
`[schedule][开始处理任务] 运行中任务数: ${this.cronLimitActiveCount + 1}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
})
this.cronLimit.on('completed', (param) => {
Logger.info(
`[schedule][任务处理成功] 参数 ${JSON.stringify(param)}`,
);
});
this.cronLimit.on('error', error => {
Logger.error(
`[schedule][任务处理错误] 参数 ${JSON.stringify(error)}`,
);
});
this.cronLimit.on('next', () => {
Logger.info(
`[schedule][任务处理结束] 运行中任务数: ${this.cronLimitActiveCount}, 等待中任务数: ${this.cronLimitPendingCount}`,
);
});
this.cronLimit.on('idle', () => {
Logger.info(
`[schedule][任务队列] 空闲中...`,
);
});
} }
public async setCustomLimit(limit?: number) { public async setCustomLimit(limit?: number) {
if (limit) { if (limit) {
this.cpuLimit = pLimit(limit); this.cronLimit.concurrency = limit;
return; return;
} }
await AuthModel.sync(); await AuthModel.sync();
@@ -21,20 +64,20 @@ class TaskLimit {
where: { type: AuthDataType.systemConfig }, where: { type: AuthDataType.systemConfig },
}); });
if (doc?.info?.cronConcurrency) { if (doc?.info?.cronConcurrency) {
this.cpuLimit = pLimit(doc?.info?.cronConcurrency); this.cronLimit.concurrency = doc.info.cronConcurrency;
} }
} }
public runWithCpuLimit<T>(fn: () => Promise<T>): Promise<T> { public async runWithCronLimit<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.cpuLimit(fn); return this.cronLimit.add(fn, options);
} }
public runOneByOne<T>(fn: () => Promise<T>): Promise<T> { public runOneByOne<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.oneLimit(fn); return this.oneLimit.add(fn, options);
} }
public updateDepLog<T>(fn: () => Promise<T>): Promise<T> { public updateDepLog<T>(fn: () => Promise<T>, options?: Partial<QueueAddOptions>): Promise<T | void> {
return this.updateLogLimit(fn); return this.updateLogLimit.add(fn, options);
} }
} }
+7 -11
View File
@@ -2,33 +2,29 @@ import { spawn } from 'cross-spawn';
import taskLimit from './pLimit'; import taskLimit from './pLimit';
import Logger from '../loaders/logger'; import Logger from '../loaders/logger';
export function runCron(cmd: string): Promise<number> { export function runCron(cmd: string, options?: { schedule: string; extraSchedules: Array<{ schedule: string }>; name: string }): Promise<number | void> {
return taskLimit.runWithCpuLimit(() => { return taskLimit.runWithCronLimit(() => {
return new Promise(async (resolve: any) => { return new Promise(async (resolve: any) => {
Logger.silly('运行命令: ' + cmd); Logger.info(`[schedule][开始执行任务] 参数 ${JSON.stringify({ ...options, command: cmd })}`);
const cp = spawn(cmd, { shell: '/bin/bash' }); const cp = spawn(cmd, { shell: '/bin/bash' });
cp.stderr.on('data', (data) => { cp.stderr.on('data', (data) => {
Logger.info( Logger.info(
'[执行任务失败] %s,时间:%s, 错误信息%j', '[schedule][执行任务失败] 命令: %s, 错误信息: %j',
cmd, cmd,
new Date().toLocaleString(),
data.toString(), data.toString(),
); );
}); });
cp.on('error', (err) => { cp.on('error', (err) => {
Logger.error( Logger.error(
'[创建任务失败] %s,时间:%s, 错误信息%j', '[schedule][创建任务失败] 命令: %s, 错误信息: %j',
cmd, cmd,
new Date().toLocaleString(),
err, err,
); );
}); });
cp.on('close', async (code) => { cp.on('exit', async (code) => {
Logger.info(`[任务退出] ${cmd} 进程id: ${cp.pid} 退出,退出码 ${code}`); resolve({ ...options, command: cmd, pid: cp.pid, code });
resolve();
}); });
}); });
}); });
+1 -1
View File
@@ -12,7 +12,7 @@ map $http_upgrade $connection_upgrade {
} }
server { server {
listen 5700; IPV4_CONFIG
IPV6_CONFIG IPV6_CONFIG
ssl_session_timeout 5m; ssl_session_timeout 5m;
+1 -2
View File
@@ -1,6 +1,5 @@
{ {
"watch": ["back", ".env"], "watch": ["back", ".env"],
"ext": "js,ts,json", "ext": "js,ts,json",
"ignore": ["src/**/*.spec.ts"], "exec": "ts-node -P tsconfig.back.json ./back/app.ts"
"exec": "ts-node --transpile-only ./back/app.ts"
} }
+9 -8
View File
@@ -4,8 +4,8 @@
"start": "concurrently -n w: npm:start:*", "start": "concurrently -n w: npm:start:*",
"start:front": "max dev", "start:front": "max dev",
"start:back": "nodemon", "start:back": "nodemon",
"start:public": "ts-node --transpile-only ./back/public.ts", "start:public": "ts-node -P tsconfig.back.json ./back/public.ts",
"start:rpc": "ts-node --transpile-only ./back/schedule/index.ts", "start:rpc": "ts-node -P tsconfig.back.json ./back/schedule/index.ts",
"build:front": "max build", "build:front": "max build",
"build:back": "tsc -p tsconfig.back.json", "build:back": "tsc -p tsconfig.back.json",
"panel": "npm run build:back && node static/build/app.js", "panel": "npm run build:back && node static/build/app.js",
@@ -69,11 +69,11 @@
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.17.3", "express": "^4.17.3",
"express-jwt": "^6.1.1", "express-jwt": "^6.1.1",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^7.0.0",
"express-urlrewrite": "^1.4.0", "express-urlrewrite": "^1.4.0",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"got": "^11.8.2", "got": "^11.8.2",
"hpagent": "^0.1.2", "hpagent": "^1.2.0",
"http-proxy-middleware": "^2.0.6", "http-proxy-middleware": "^2.0.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
@@ -83,7 +83,7 @@
"nedb": "^1.8.0", "nedb": "^1.8.0",
"node-schedule": "^2.1.0", "node-schedule": "^2.1.0",
"nodemailer": "^6.7.2", "nodemailer": "^6.7.2",
"p-limit": "3.1.0", "p-queue-cjs": "7.3.4",
"protobufjs": "^7.2.3", "protobufjs": "^7.2.3",
"pstree.remy": "^1.1.8", "pstree.remy": "^1.1.8",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
@@ -96,6 +96,7 @@
"typedi": "^0.10.0", "typedi": "^0.10.0",
"uuid": "^8.3.2", "uuid": "^8.3.2",
"winston": "^3.6.0", "winston": "^3.6.0",
"winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.3.1" "yargs": "^17.3.1"
}, },
"devDependencies": { "devDependencies": {
@@ -127,6 +128,8 @@
"@types/sockjs-client": "^1.5.1", "@types/sockjs-client": "^1.5.1",
"@types/tar": "^6.1.5", "@types/tar": "^6.1.5",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@uiw/codemirror-extensions-langs": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9",
"@umijs/max": "^4.0.72", "@umijs/max": "^4.0.72",
"@umijs/ssr-darkreader": "^4.9.45", "@umijs/ssr-darkreader": "^4.9.45",
"ahooks": "^3.7.8", "ahooks": "^3.7.8",
@@ -134,7 +137,6 @@
"antd": "^4.24.8", "antd": "^4.24.8",
"antd-img-crop": "^4.2.3", "antd-img-crop": "^4.2.3",
"axios": "^1.4.0", "axios": "^1.4.0",
"codemirror": "^5.65.2",
"compression-webpack-plugin": "9.2.0", "compression-webpack-plugin": "9.2.0",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
@@ -148,7 +150,6 @@
"rc-tween-one": "^3.0.6", "rc-tween-one": "^3.0.6",
"rc-virtual-list": "3.5.3", "rc-virtual-list": "3.5.3",
"react": "18.2.0", "react": "18.2.0",
"react-codemirror2": "^7.2.1",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-diff-viewer": "^3.1.1", "react-diff-viewer": "^3.1.1",
"react-dnd": "^14.0.2", "react-dnd": "^14.0.2",
@@ -161,7 +162,7 @@
"ts-proto": "^1.146.0", "ts-proto": "^1.146.0",
"tslib": "^2.4.0", "tslib": "^2.4.0",
"tsx": "^3.12.3", "tsx": "^3.12.3",
"typescript": "4.8.4", "typescript": "5.2.2",
"vh-check": "^2.0.5", "vh-check": "^2.0.5",
"virtualizedtableforantd4": "1.3.0", "virtualizedtableforantd4": "1.3.0",
"webpack": "^5.70.0", "webpack": "^5.70.0",
+942 -244
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -173,4 +173,13 @@ export SMTP_NAME=""
## PUSHME_KEY (必填)填写PushMe APP上获取的push_key ## PUSHME_KEY (必填)填写PushMe APP上获取的push_key
export PUSHME_KEY="" export PUSHME_KEY=""
## 13. CHRONOCAT
## CHRONOCAT_URL 推送 http://127.0.0.1:16530
## CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
## CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
## CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
export CHRONOCAT_URL=""
export CHRONOCAT_QQ="" #
export CHRONOCAT_TOKEN=""
## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可 ## 其他需要的变量,脚本中需要的变量使用 export 变量名= 声明即可
+271 -1
View File
@@ -11,6 +11,7 @@
*/ */
const querystring = require('querystring'); const querystring = require('querystring');
const got = require('got');
const $ = new Env(); const $ = new Env();
const timeout = 15000; //超时时间(单位毫秒) const timeout = 15000; //超时时间(单位毫秒)
// =======================================gotify通知设置区域============================================== // =======================================gotify通知设置区域==============================================
@@ -55,6 +56,10 @@ let BARK_ICON = 'https://qn.whyour.cn/logo.png';
let BARK_SOUND = ''; let BARK_SOUND = '';
//BARK app推送消息的分组, 默认为"QingLong" //BARK app推送消息的分组, 默认为"QingLong"
let BARK_GROUP = 'QingLong'; let BARK_GROUP = 'QingLong';
//BARK app推送消息的时效性, 默认为"active"
let BARK_LEVEL = 'active';
//BARK app推送消息的跳转URL
let BARK_URL = '';
// =======================================telegram机器人通知设置区域=========================================== // =======================================telegram机器人通知设置区域===========================================
//此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw //此处填你telegram bot 的Tokentelegram机器人通知推送必填项.例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw
@@ -146,6 +151,23 @@ let SMTP_NAME = '';
//此处填你的PushMe KEY. //此处填你的PushMe KEY.
let PUSHME_KEY = ''; let PUSHME_KEY = '';
// =======================================CHRONOCAT通知设置区域===========================================
// CHRONOCAT_URL Red协议连接地址 例: http://127.0.0.1:16530
// CHRONOCAT_TOKEN 填写在CHRONOCAT文件生成的访问密钥
// CHRONOCAT_QQ 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群
// CHRONOCAT相关API https://chronocat.vercel.app/install/docker/official/
let CHRONOCAT_URL = ''; // CHRONOCAT Red协议连接地址
let CHRONOCAT_TOKEN = ''; //CHRONOCAT 生成的访问密钥
let CHRONOCAT_QQ = ''; // 个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx
// =======================================自定义通知设置区域=======================================
// 自定义通知 接收回调的URL
let WEBHOOK_URL = '';
let WEBHOOK_BODY = '';
let WEBHOOK_HEADERS = '';
let WEBHOOK_METHOD = '';
let WEBHOOK_CONTENT_TYPE = '';
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
if (process.env.GOTIFY_URL) { if (process.env.GOTIFY_URL) {
GOTIFY_URL = process.env.GOTIFY_URL; GOTIFY_URL = process.env.GOTIFY_URL;
@@ -211,6 +233,12 @@ if (process.env.BARK_PUSH) {
if (process.env.BARK_GROUP) { if (process.env.BARK_GROUP) {
BARK_GROUP = process.env.BARK_GROUP; BARK_GROUP = process.env.BARK_GROUP;
} }
if (process.env.BARK_LEVEL) {
BARK_LEVEL = process.env.BARK_LEVEL;
}
if (process.env.BARK_URL) {
BARK_URL = process.env.BARK_URL;
}
} else { } else {
if ( if (
BARK_PUSH && BARK_PUSH &&
@@ -296,6 +324,32 @@ if (process.env.SMTP_NAME) {
if (process.env.PUSHME_KEY) { if (process.env.PUSHME_KEY) {
PUSHME_KEY = process.env.PUSHME_KEY; PUSHME_KEY = process.env.PUSHME_KEY;
} }
if (process.env.CHRONOCAT_URL) {
CHRONOCAT_URL = process.env.CHRONOCAT_URL;
}
if (process.env.CHRONOCAT_QQ) {
CHRONOCAT_QQ = process.env.CHRONOCAT_QQ;
}
if (process.env.CHRONOCAT_TOKEN) {
CHRONOCAT_TOKEN = process.env.CHRONOCAT_TOKEN;
}
if (process.env.WEBHOOK_URL) {
WEBHOOK_URL = process.env.WEBHOOK_URL;
}
if (process.env.WEBHOOK_BODY) {
WEBHOOK_BODY = process.env.WEBHOOK_BODY;
}
if (process.env.WEBHOOK_HEADERS) {
WEBHOOK_HEADERS = process.env.WEBHOOK_HEADERS;
}
if (process.env.WEBHOOK_METHOD) {
WEBHOOK_METHOD = process.env.WEBHOOK_METHOD;
}
if (process.env.WEBHOOK_CONTENT_TYPE) {
WEBHOOK_CONTENT_TYPE = process.env.WEBHOOK_CONTENT_TYPE;
}
//==========================云端环境变量的判断与接收========================= //==========================云端环境变量的判断与接收=========================
/** /**
@@ -345,6 +399,8 @@ async function sendNotify(
fsBotNotify(text, desp), //飞书机器人 fsBotNotify(text, desp), //飞书机器人
smtpNotify(text, desp), //SMTP 邮件 smtpNotify(text, desp), //SMTP 邮件
PushMeNotify(text, desp, params), //PushMe PushMeNotify(text, desp, params), //PushMe
ChronocatNotify(text, desp), // Chronocat
webhookNotify(text, desp), //自定义通知
]); ]);
} }
@@ -552,7 +608,7 @@ function BarkNotify(text, desp, params = {}) {
const options = { const options = {
url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent( url: `${BARK_PUSH}/${encodeURIComponent(text)}/${encodeURIComponent(
desp, desp,
)}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&${querystring.stringify( )}?icon=${BARK_ICON}&sound=${BARK_SOUND}&group=${BARK_GROUP}&level=${BARK_LEVEL}&url=${BARK_URL}&${querystring.stringify(
params, params,
)}`, )}`,
headers: { headers: {
@@ -1161,6 +1217,220 @@ function PushMeNotify(text, desp, params = {}) {
}); });
} }
function ChronocatNotify(title, desp) {
return new Promise((resolve) => {
if (!CHRONOCAT_TOKEN || !CHRONOCAT_QQ || !CHRONOCAT_URL) {
console.log(
'CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送',
);
return;
}
console.log('CHRONOCAT 服务启动');
const user_ids = CHRONOCAT_QQ.match(/user_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
const group_ids = CHRONOCAT_QQ.match(/group_id=(\d+)/g)?.map(
(match) => match.split('=')[1],
);
const url = `${CHRONOCAT_URL}/api/message/send`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${CHRONOCAT_TOKEN}`,
};
for (const [chat_type, ids] of [
[1, user_ids],
[2, group_ids],
]) {
if (!ids) {
continue;
}
for (const chat_id of ids) {
const data = {
peer: {
chatType: chat_type,
peerUin: chat_id,
},
elements: [
{
elementType: 1,
textElement: {
content: `${title}\n\n${desp}`,
},
},
],
};
const options = {
url: url,
json: data,
headers,
timeout,
};
$.post(options, (err, resp, data) => {
try {
if (err) {
console.log('Chronocat发送QQ通知消息失败!!\n');
console.log(err);
} else {
data = JSON.parse(data);
if (chat_type === 1) {
console.log(`QQ个人消息:${ids}推送成功!`);
} else {
console.log(`QQ群消息:${ids}推送成功!`);
}
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(data);
}
});
}
}
});
}
function webhookNotify(text, desp) {
return new Promise((resolve) => {
const { formatBody, formatUrl } = formatNotifyContentFun(
WEBHOOK_URL,
WEBHOOK_BODY,
text,
desp,
);
if (!formatUrl && !formatBody) {
resolve();
return;
}
const headers = parseHeaders(WEBHOOK_HEADERS);
const body = parseBody(formatBody, WEBHOOK_CONTENT_TYPE);
const bodyParam = formatBodyFun(WEBHOOK_CONTENT_TYPE, body);
const options = {
method: WEBHOOK_METHOD,
headers,
allowGetBody: true,
...bodyParam,
timeout,
retry: 1,
};
if (WEBHOOK_METHOD) {
got(formatUrl, options).then((resp) => {
try {
if (resp.statusCode !== 200) {
console.log('自定义发送通知消息失败!!\n');
console.log(resp.body);
} else {
console.log('自定义发送通知消息成功🎉。\n');
console.log(resp.body);
}
} catch (e) {
$.logErr(e, resp);
} finally {
resolve(resp.body);
}
});
} else {
resolve();
}
});
}
function parseHeaders(headers) {
if (!headers) return {};
const parsed = {};
let key;
let val;
let i;
headers &&
headers.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key) {
return;
}
parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
});
return parsed;
}
function parseBody(body, contentType) {
if (!body) return '';
const parsed = {};
let key;
let val;
let i;
body &&
body.split('\n').forEach(function parser(line) {
i = line.indexOf(':');
key = line.substring(0, i).trim().toLowerCase();
val = line.substring(i + 1).trim();
if (!key || parsed[key]) {
return;
}
try {
const jsonValue = JSON.parse(val);
parsed[key] = jsonValue;
} catch (error) {
parsed[key] = val;
}
});
switch (contentType) {
case 'multipart/form-data':
return Object.keys(parsed).reduce((p, c) => {
p.append(c, parsed[c]);
return p;
}, new FormData());
case 'application/x-www-form-urlencoded':
return Object.keys(parsed).reduce((p, c) => {
return p ? `${p}&${c}=${parsed[c]}` : `${c}=${parsed[c]}`;
});
}
return parsed;
}
function formatBodyFun(contentType, body) {
if (!body) return {};
switch (contentType) {
case 'application/json':
return { json: body };
case 'multipart/form-data':
return { form: body };
case 'application/x-www-form-urlencoded':
return { body };
}
return {};
}
function formatNotifyContentFun(url, body, title, content) {
if (!url.includes('$title') && !body.includes('$title')) {
return {};
}
return {
formatUrl: url
.replaceAll('$title', encodeURIComponent(title))
.replaceAll('$content', encodeURIComponent(content)),
formatBody: body
.replaceAll('$title', title)
.replaceAll('$content', content),
};
}
module.exports = { module.exports = {
sendNotify, sendNotify,
BARK_PUSH, BARK_PUSH,
+180 -2
View File
@@ -40,6 +40,8 @@ push_config = {
'BARK_GROUP': '', # bark 推送分组 'BARK_GROUP': '', # bark 推送分组
'BARK_SOUND': '', # bark 推送声音 'BARK_SOUND': '', # bark 推送声音
'BARK_ICON': '', # bark 推送图标 'BARK_ICON': '', # bark 推送图标
'BARK_LEVEL': '', # bark 推送时效性
'BARK_URL': '', # bark 推送跳转URL
'CONSOLE': True, # 控制台输出 'CONSOLE': True, # 控制台输出
@@ -100,6 +102,16 @@ push_config = {
'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写 'SMTP_NAME': '', # SMTP 收发件人姓名,可随意填写
'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY 'PUSHME_KEY': '', # PushMe 酱的 PUSHME_KEY
'CHRONOCAT_QQ': '', # qq号
'CHRONOCAT_TOKEN': '', # CHRONOCAT 的token
'CHRONOCAT_URL': '', # CHRONOCAT的url地址
'WEBHOOK_URL': '', # 自定义通知 请求地址
'WEBHOOK_BODY': '', # 自定义通知 请求体
'WEBHOOK_HEADERS': '', # 自定义通知 请求头
'WEBHOOK_METHOD': '', # 自定义通知 请求方法
'WEBHOOK_CONTENT_TYPE': '' # 自定义通知 content-type
} }
notify_function = [] notify_function = []
# fmt: on # fmt: on
@@ -130,6 +142,8 @@ def bark(title: str, content: str) -> None:
"BARK_GROUP": "group", "BARK_GROUP": "group",
"BARK_SOUND": "sound", "BARK_SOUND": "sound",
"BARK_ICON": "icon", "BARK_ICON": "icon",
"BARK_LEVEL": "level",
"BARK_URL": "url",
} }
params = "" params = ""
for pair in filter( for pair in filter(
@@ -442,7 +456,9 @@ class WeCom:
return data["access_token"] return data["access_token"]
def send_text(self, message, touser="@all"): def send_text(self, message, touser="@all"):
send_url = f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}" send_url = (
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
)
send_values = { send_values = {
"touser": touser, "touser": touser,
"msgtype": "text", "msgtype": "text",
@@ -456,7 +472,9 @@ class WeCom:
return respone["errmsg"] return respone["errmsg"]
def send_mpnews(self, title, message, media_id, touser="@all"): def send_mpnews(self, title, message, media_id, touser="@all"):
send_url = f"https://{self.HOST}/cgi-bin/message/send?access_token={self.get_access_token()}" send_url = (
f"{self.ORIGIN}/cgi-bin/message/send?access_token={self.get_access_token()}"
)
send_values = { send_values = {
"touser": touser, "touser": touser,
"msgtype": "mpnews", "msgtype": "mpnews",
@@ -639,6 +657,7 @@ def smtp(title: str, content: str) -> None:
except Exception as e: except Exception as e:
print(f"SMTP 邮件 推送失败!{e}") print(f"SMTP 邮件 推送失败!{e}")
def pushme(title: str, content: str) -> None: def pushme(title: str, content: str) -> None:
""" """
使用 PushMe 推送消息。 使用 PushMe 推送消息。
@@ -661,6 +680,157 @@ def pushme(title: str, content: str) -> None:
print(f"PushMe 推送失败!{response.status_code} {response.text}") print(f"PushMe 推送失败!{response.status_code} {response.text}")
def chronocat(title: str, content: str) -> None:
"""
使用 CHRONOCAT 推送消息。
"""
if (
not push_config.get("CHRONOCAT_URL")
or not push_config.get("CHRONOCAT_QQ")
or not push_config.get("CHRONOCAT_TOKEN")
):
print("CHRONOCAT 服务的 CHRONOCAT_URL 或 CHRONOCAT_QQ 未设置!!\n取消推送")
return
print("CHRONOCAT 服务启动")
user_ids = re.findall(r"user_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
group_ids = re.findall(r"group_id=(\d+)", push_config.get("CHRONOCAT_QQ"))
url = f'{push_config.get("CHRONOCAT_URL")}/api/message/send'
headers = {
"Content-Type": "application/json",
"Authorization": f'Bearer {push_config.get("CHRONOCAT_TOKEN")}',
}
for chat_type, ids in [(1, user_ids), (2, group_ids)]:
if not ids:
continue
for chat_id in ids:
data = {
"peer": {"chatType": chat_type, "peerUin": chat_id},
"elements": [
{
"elementType": 1,
"textElement": {"content": f"{title}\n\n{content}"},
}
],
}
response = requests.post(url, headers=headers, data=json.dumps(data))
if response.status_code == 200:
if chat_type == 1:
print(f"QQ个人消息:{ids}推送成功!")
else:
print(f"QQ群消息:{ids}推送成功!")
else:
if chat_type == 1:
print(f"QQ个人消息:{ids}推送失败!")
else:
print(f"QQ群消息:{ids}推送失败!")
def parse_headers(headers):
if not headers:
return {}
parsed = {}
lines = headers.split("\n")
for line in lines:
i = line.find(":")
if i == -1:
continue
key = line[:i].strip().lower()
val = line[i + 1 :].strip()
parsed[key] = parsed.get(key, "") + ", " + val if key in parsed else val
return parsed
def parse_body(body, content_type):
if not body:
return ""
parsed = {}
lines = body.split("\n")
for line in lines:
i = line.find(":")
if i == -1:
continue
key = line[:i].strip().lower()
val = line[i + 1 :].strip()
if not key or key in parsed:
continue
try:
json_value = json.loads(val)
parsed[key] = json_value
except:
parsed[key] = val
if content_type == "application/x-www-form-urlencoded":
data = urlencode(parsed, doseq=True)
return data
if content_type == "application/json":
data = json.dumps(parsed)
return data
return parsed
def format_notify_content(url, body, title, content):
if "$title" not in url and "$title" not in body:
return {}
formatted_url = url.replace("$title", urllib.parse.quote_plus(title)).replace(
"$content", urllib.parse.quote_plus(content)
)
formatted_body = body.replace("$title", title).replace("$content", content)
return formatted_url, formatted_body
def custom_notify(title: str, content: str) -> None:
"""
通过 自定义通知 推送消息。
"""
if not push_config.get("WEBHOOK_URL") or not push_config.get("WEBHOOK_METHOD"):
print("自定义通知的 WEBHOOK_URL 或 WEBHOOK_METHOD 未设置!!\n取消推送")
return
print("自定义通知服务启动")
WEBHOOK_URL = push_config.get("WEBHOOK_URL")
WEBHOOK_METHOD = push_config.get("WEBHOOK_METHOD")
WEBHOOK_CONTENT_TYPE = push_config.get("WEBHOOK_CONTENT_TYPE")
WEBHOOK_BODY = push_config.get("WEBHOOK_BODY")
WEBHOOK_HEADERS = push_config.get("WEBHOOK_HEADERS")
formatUrl, formatBody = format_notify_content(
WEBHOOK_URL, WEBHOOK_BODY, title, content
)
if not formatUrl and not formatBody:
print("请求头或者请求体中必须包含 $title 和 $content")
return
headers = parse_headers(WEBHOOK_HEADERS)
body = parse_body(formatBody, WEBHOOK_CONTENT_TYPE)
response = requests.request(
method=WEBHOOK_METHOD, url=formatUrl, headers=headers, timeout=15, data=body
)
if response.status_code == 200:
print("自定义通知推送成功!")
else:
print(f"自定义通知推送失败!{response.status_code} {response.text}")
def one() -> str: def one() -> str:
""" """
获取一条一言。 获取一条一言。
@@ -717,6 +887,14 @@ if (
notify_function.append(smtp) notify_function.append(smtp)
if push_config.get("PUSHME_KEY"): if push_config.get("PUSHME_KEY"):
notify_function.append(pushme) notify_function.append(pushme)
if (
push_config.get("CHRONOCAT_URL")
and push_config.get("CHRONOCAT_QQ")
and push_config.get("CHRONOCAT_TOKEN")
):
notify_function.append(chronocat)
if push_config.get("WEBHOOK_URL") and push_config.get("WEBHOOK_METHOD"):
notify_function.append(custom_notify)
def send(title: str, content: str) -> None: def send(title: str, content: str) -> None:
+1 -1
View File
@@ -36,7 +36,7 @@ add_cron_api() {
local sub_id="$4" local sub_id="$4"
fi fi
if [[ ! $sub_id ]];then if [[ ! $sub_id ]]; then
sub_id="null" sub_id="null"
fi fi
+28 -18
View File
@@ -86,10 +86,15 @@ check_server() {
fi fi
} }
env_str_to_array() {
local IFS="&"
read -ra array <<<"${!env_param}"
}
## 正常运行单个脚本,$1:传入参数 ## 正常运行单个脚本,$1:传入参数
run_normal() { run_normal() {
local file_param=$1 local file_param=$1
if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]]; then if [[ $# -eq 1 ]] && [[ "$real_time" != "true" ]] && [[ "$no_delay" != "true" ]]; then
random_delay "$file_param" random_delay "$file_param"
fi fi
@@ -100,7 +105,7 @@ run_normal() {
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
$timeoutCmd $which_program $file_param $timeoutCmd $which_program $file_param "${script_params[@]}"
} }
## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行 ## 并发执行时,设定的 RandomDelay 不会生效,即所有任务立即执行
@@ -113,8 +118,7 @@ run_concurrent() {
exit 1 exit 1
fi fi
local envs=$(eval echo "\$${env_param}") env_str_to_array
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g") local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr)) local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]})) runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
@@ -125,11 +129,13 @@ run_concurrent() {
let n++ let n++
done done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr} IFS="&"
echo "${array_run[*]}"
)
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
local envs=$(eval echo "\$${env_param}") env_str_to_array
local array=($(echo $envs | sed 's/&/ /g'))
single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N") single_log_time=$(date "+%Y-%m-%d-%H-%M-%S.%3N")
cd $dir_scripts cd $dir_scripts
@@ -139,9 +145,9 @@ run_concurrent() {
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
for i in "${!array[@]}"; do for i in "${!array[@]}"; do
export ${env_param}=${array[i]} export "${env_param}=${array[i]}"
single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log" single_log_path="$dir_log/$log_dir/${single_log_time}_$((i + 1)).log"
eval $timeoutCmd $which_program $file_param &>$single_log_path & eval $timeoutCmd $which_program $file_param "${script_params[@]}" &>$single_log_path &
done done
wait wait
@@ -161,8 +167,7 @@ run_designated() {
exit 1 exit 1
fi fi
local envs=$(eval echo "\$${env_param}") env_str_to_array
local array=($(echo $envs | sed 's/&/ /g'))
local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g") local tempArr=$(echo $num_param | sed "s/-max/-${#array[@]}/g" | sed "s/max-/${#array[@]}-/g" | perl -pe "s|(\d+)(-\|~\|_)(\d+)|{\1..\3}|g")
local runArr=($(eval echo $tempArr)) local runArr=($(eval echo $tempArr))
runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]})) runArr=($(awk -v RS=' ' '!a[$1]++' <<<${runArr[@]}))
@@ -173,8 +178,11 @@ run_designated() {
let n++ let n++
done done
local cookieStr=$(echo ${array_run[*]} | sed 's/\ /\&/g') local cookieStr=$(
[[ ! -z $cookieStr ]] && export ${env_param}=${cookieStr} IFS="&"
echo "${array_run[*]}"
)
[[ ! -z $cookieStr ]] && export "${env_param}=${cookieStr}"
cd $dir_scripts cd $dir_scripts
local relative_path="${file_param%/*}" local relative_path="${file_param%/*}"
@@ -182,7 +190,7 @@ run_designated() {
cd ${relative_path} cd ${relative_path}
file_param=${file_param/$relative_path\//} file_param=${file_param/$relative_path\//}
fi fi
$timeoutCmd $which_program $file_param $timeoutCmd $which_program $file_param "${script_params[@]}"
} }
## 运行其他命令 ## 运行其他命令
@@ -233,6 +241,8 @@ main() {
fi fi
} }
handle_task_before "$@" handle_task_start "${task_shell_params[@]}"
main "$@" run_task_before "${task_shell_params[@]}"
handle_task_after "$@" main "${task_shell_params[@]}"
run_task_after "${task_shell_params[@]}"
handle_task_end "${task_shell_params[@]}"
+38 -15
View File
@@ -69,6 +69,7 @@ import_config() {
[[ -f $file_env ]] && . $file_env [[ -f $file_env ]] && . $file_env
ql_base_url=${QlBaseUrl:-"/"} ql_base_url=${QlBaseUrl:-"/"}
ql_port=${QlPort:-"5700"}
command_timeout_time=${CommandTimeoutTime:-""} command_timeout_time=${CommandTimeoutTime:-""}
proxy_url=${ProxyUrl:-""} proxy_url=${ProxyUrl:-""}
file_extensions=${RepoFileExtensions:-"js py"} file_extensions=${RepoFileExtensions:-"js py"}
@@ -99,9 +100,6 @@ set_proxy() {
unset_proxy() { unset_proxy() {
unset http_proxy unset http_proxy
unset https_proxy unset https_proxy
unset ftp_proxy
unset all_proxy
unset no_proxy
} }
make_dir() { make_dir() {
@@ -296,7 +294,7 @@ git_clone_scripts() {
set_proxy "$proxy" set_proxy "$proxy"
git clone --depth=1 $part_cmd $url $dir git clone --depth=1 $part_cmd $url $dir 2>&1
exit_status=$? exit_status=$?
unset_proxy unset_proxy
@@ -310,10 +308,8 @@ random_range() {
reload_pm2() { reload_pm2() {
cd $dir_root cd $dir_root
# 代理会影响 grpc 服务
unset_proxy
pm2 flush &>/dev/null pm2 flush &>/dev/null
pm2 startOrGracefulReload $file_ecosystem_js --update-env env ALL_PROXY= HTTP_PROXY= HTTPS_PROXY= all_proxy= http_proxy= https_proxy= pm2 startOrGracefulReload $file_ecosystem_js --update-env
} }
diff_time() { diff_time() {
@@ -406,8 +402,18 @@ init_nginx() {
local aliasStr="" local aliasStr=""
local rootStr="" local rootStr=""
if [[ $ql_base_url != "/" ]]; then if [[ $ql_base_url != "/" ]]; then
if [[ $ql_base_url != /* ]]; then
ql_base_url="/$ql_base_url"
fi
if [[ $ql_base_url != */ ]]; then
ql_base_url="$ql_base_url/"
fi
location_url="^~${ql_base_url%*/}" location_url="^~${ql_base_url%*/}"
aliasStr="alias ${dir_static}/dist;" aliasStr="alias ${dir_static}/dist;"
if ! grep -q "<base href=\"$ql_base_url\">" "${dir_static}/dist/index.html"; then
awk -v text="<base href=\"$ql_base_url\">" '/<link/ && !inserted {print text; inserted=1} 1' "${dir_static}/dist/index.html" >temp.html
mv temp.html "${dir_static}/dist/index.html"
fi
else else
rootStr="root ${dir_static}/dist;" rootStr="root ${dir_static}/dist;"
fi fi
@@ -416,27 +422,45 @@ init_nginx() {
sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf sed -i "s,QL_BASE_URL_LOCATION,${location_url},g" /etc/nginx/conf.d/front.conf
sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf sed -i "s,QL_BASE_URL,${ql_base_url},g" /etc/nginx/conf.d/front.conf
ipv6=$(ip a | grep inet6) local ipv6=$(ip a | grep inet6)
ipv6Str="" local ipv6Str=""
if [[ $ipv6 ]]; then if [[ $ipv6 ]]; then
ipv6Str="listen [::]:5700 ipv6only=on;" ipv6Str="listen [::]:${ql_port} ipv6only=on;"
fi fi
local ipv4Str="listen ${ql_port};"
sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf sed -i "s,IPV6_CONFIG,${ipv6Str},g" /etc/nginx/conf.d/front.conf
sed -i "s,IPV4_CONFIG,${ipv4Str},g" /etc/nginx/conf.d/front.conf
} }
handle_task_before() { handle_task_start() {
[[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp" [[ $ID ]] && update_cron "\"$ID\"" "0" "$$" "$log_path" "$begin_timestamp"
echo -e "## 开始执行... $begin_time\n" echo -e "## 开始执行... $begin_time\n"
}
run_task_before() {
[[ $is_macos -eq 0 ]] && check_server [[ $is_macos -eq 0 ]] && check_server
. $file_task_before "$@" . $file_task_before "$@"
if [[ $task_before ]]; then
echo -e "执行前置命令\n"
eval "$task_before"
echo -e "\n执行前置命令结束\n"
fi
} }
handle_task_after() { run_task_after() {
. $file_task_after "$@" . $file_task_after "$@"
if [[ $task_after ]]; then
echo -e "\n执行后置命令\n"
eval "$task_after"
echo -e "\n执行后置命令结束"
fi
}
handle_task_end() {
local etime=$(date "+$time_format") local etime=$(date "+$time_format")
local end_time=$(format_time "$time_format" "$etime") local end_time=$(format_time "$time_format" "$etime")
local end_timestamp=$(format_timestamp "$time_format" "$etime") local end_timestamp=$(format_timestamp "$time_format" "$etime")
@@ -444,8 +468,7 @@ handle_task_after() {
[[ "$diff_time" == 0 ]] && diff_time=1 [[ "$diff_time" == 0 ]] && diff_time=1
echo -e "\n\n## 执行结束... $end_time 耗时 $diff_time 秒     " echo -e "\n## 执行结束... $end_time 耗时 $diff_time 秒     "
[[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time" [[ $ID ]] && update_cron "\"$ID\"" "1" "" "$log_path" "$begin_timestamp" "$diff_time"
} }
+22 -9
View File
@@ -5,9 +5,9 @@ dir_shell=$QL_DIR/shell
. $dir_shell/share.sh . $dir_shell/share.sh
. $dir_shell/api.sh . $dir_shell/api.sh
trap "single_hanle" 2 3 20 15 14 trap "single_hanle" 2 3 20 15 14 19 1
single_hanle() { single_hanle() {
eval handle_task_after "$@" "$cmd" eval handle_task_end "$@" "$cmd"
exit 1 exit 1
} }
@@ -70,10 +70,10 @@ handle_log_path() {
log_path="$real_log_path" log_path="$real_log_path"
fi fi
cmd=">> $dir_log/$log_path 2>&1"
make_dir "$dir_log/$log_dir"
if [[ "$show_log" == "true" ]]; then
cmd="2>&1 | tee -a $dir_log/$log_path" cmd="2>&1 | tee -a $dir_log/$log_path"
make_dir "$dir_log/$log_dir"
if [[ "$no_tee" == "true" ]]; then
cmd=">> $dir_log/$log_path 2>&1"
fi fi
if [[ "$real_time" == "true" ]]; then if [[ "$real_time" == "true" ]]; then
@@ -95,6 +95,21 @@ format_params() {
fi fi
fi fi
# params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g') # params=$(echo "$@" | sed -E 's/([^ ])&([^ ])/\1\\\&\2/g')
# 分割 task 内置参数和脚本参数
task_shell_params=()
script_params=()
found_double_dash=false
for arg in "$@"; do
if $found_double_dash; then
script_params+=("$arg")
elif [ "$arg" == "--" ]; then
found_double_dash=true
else
task_shell_params+=("$arg")
fi
done
} }
init_begin_time() { init_begin_time() {
@@ -119,11 +134,9 @@ if [[ $max_time ]]; then
fi fi
format_params "$@" format_params "$@"
define_program "$@" define_program "${task_shell_params[@]}"
handle_log_path "$@" handle_log_path "${task_shell_params[@]}"
init_begin_time init_begin_time
eval . $dir_shell/otask.sh "$cmd" eval . $dir_shell/otask.sh "$cmd"
[[ -f "$dir_log/$log_path" ]] && [[ ! $show_log ]] && [[ "$real_time" != "true" ]] && cat "$dir_log/$log_path"
exit 0 exit 0
+10 -9
View File
@@ -31,7 +31,7 @@ output_list_add_drop() {
local list=$1 local list=$1
local type=$2 local type=$2
if [[ -s $list ]]; then if [[ -s $list ]]; then
echo -e "检测到有${type}的定时任务\n" echo -e "检测到有${type}的定时任务:"
cat $list cat $list
echo echo
fi fi
@@ -134,10 +134,10 @@ update_repo() {
git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}" git_clone_scripts "${formatUrl}" ${repo_path} "${branch}" "${proxy}"
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新${repo_path}成功...\n" echo -e "\n拉取 ${uniq_path} 成功...\n"
diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron" diff_scripts "$repo_path" "$author" "$path" "$blackword" "$dependence" "$extensions" "$autoAddCron" "$autoDelCron"
else else
echo -e "\n更新${repo_path}失败,请检查网络...\n" echo -e "\n拉取 ${uniq_path} 失败,请检查网络...\n"
fi fi
} }
@@ -273,13 +273,13 @@ update_qinglong() {
exit_status=$? exit_status=$?
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙源文件成功...\n" echo -e "更新青龙源文件成功...\n"
unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp} unzip -oq ${dir_tmp}/ql.zip -d ${dir_tmp}
update_qinglong_static update_qinglong_static
else else
echo -e "\n更新青龙源文件失败,请检查网络...\n" echo -e "更新青龙源文件失败,请检查网络...\n"
fi fi
} }
@@ -288,28 +288,29 @@ update_qinglong_static() {
exit_status=$? exit_status=$?
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n更新青龙静态资源成功...\n" echo -e "更新青龙静态资源成功...\n"
unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp} unzip -oq ${dir_tmp}/static.zip -d ${dir_tmp}
check_update_dep check_update_dep
else else
echo -e "\n更新青龙静态资源失败,请检查网络...\n" echo -e "更新青龙静态资源失败,请检查网络...\n"
fi fi
} }
check_update_dep() { check_update_dep() {
echo -e "\n开始检测依赖...\n" echo -e "\n开始检测依赖...\n"
if [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then if [[ ! -s $dir_scripts/package.json ]] || [[ $(diff $dir_sample/package.json $dir_scripts/package.json) ]]; then
cp -f $dir_sample/package.json $dir_scripts/package.json cp -f $dir_sample/package.json $dir_scripts/package.json
npm_install_2 $dir_scripts npm_install_2 $dir_scripts
fi fi
if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then if [[ $(diff $dir_root/package.json ${dir_tmp}/qinglong-${primary_branch}/package.json) ]]; then
npm_install_2 "${dir_tmp}/qinglong-${primary_branch}" npm_install_2 "${dir_tmp}/qinglong-${primary_branch}"
fi fi
if [[ $exit_status -eq 0 ]]; then if [[ $exit_status -eq 0 ]]; then
echo -e "\n依赖检测安装成功...\n" echo -e "\n依赖检测安装成功...\n"
echo -e "\n更新包下载成功...\n" echo -e "更新包下载成功..."
if [[ "$needRestart" == 'true' ]]; then if [[ "$needRestart" == 'true' ]]; then
cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/ cp -rf ${dir_tmp}/qinglong-${primary_branch}/* ${dir_root}/
+3
View File
@@ -1,4 +1,5 @@
const baseUrl = window.__ENV__QlBaseUrl || '/'; const baseUrl = window.__ENV__QlBaseUrl || '/';
import { setLocale } from '@umijs/max';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
export function rootContainer(container: any) { export function rootContainer(container: any) {
@@ -9,6 +10,7 @@ export function rootContainer(container: any) {
let currentLocale = intl.determineLocale({ let currentLocale = intl.determineLocale({
urlLocaleKey: 'lang', urlLocaleKey: 'lang',
cookieLocaleKey: 'lang', cookieLocaleKey: 'lang',
localStorageLocaleKey: 'lang',
}).slice(0, 2); }).slice(0, 2);
if (!currentLocale || !Object.keys(locales).includes(currentLocale)) { if (!currentLocale || !Object.keys(locales).includes(currentLocale)) {
@@ -16,6 +18,7 @@ export function rootContainer(container: any) {
} }
intl.init({ currentLocale, locales }); intl.init({ currentLocale, locales });
setLocale(currentLocale === 'zh' ? 'zh-CN' : 'en-US');
return container; return container;
} }
+44 -11
View File
@@ -1,6 +1,5 @@
@import '~antd/es/style/themes/default.less'; @import '~antd/es/style/themes/default.less';
@import '~@/styles/variable.less'; @import '~@/styles/variable.less';
@import '~codemirror/lib/codemirror.css';
@font-face { @font-face {
font-family: 'Source Code Pro'; font-family: 'Source Code Pro';
@@ -25,16 +24,35 @@ body {
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
} }
.ant-modal-header {
padding-right: 54px;
}
.ant-modal-body { .ant-modal-body {
max-height: calc(90vh - 110px); max-height: calc(80vh - 110px);
max-height: calc(90vh - var(--vh-offset, 110px)); max-height: calc(80vh - var(--vh-offset, 110px));
overflow-y: auto; overflow-y: auto;
} }
.log-modal { .log-modal {
.ant-modal { &.ant-modal {
padding-bottom: 0 !important; max-width: 1000px !important;
width: 580px !important; width: 80vw !important;
}
.ant-modal-body {
overflow-y: auto;
min-height: 300px;
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
padding: 0;
display: flex;
.log-container {
width: 100%;
padding: 24px;
overflow-y: auto;
}
} }
pre { pre {
@@ -87,10 +105,12 @@ body {
} }
.ant-tooltip { .ant-tooltip {
max-width: 500px !important; max-width: 300px !important;
.ant-tooltip-inner { .ant-tooltip-inner {
word-break: break-all !important; word-break: break-all !important;
max-height: 300px !important;
overflow-y: auto !important;
} }
} }
@@ -165,6 +185,7 @@ body {
.react-codemirror2, .react-codemirror2,
.CodeMirror { .CodeMirror {
height: 100%; height: 100%;
overflow: auto;
} }
} }
} }
@@ -182,8 +203,14 @@ body {
} }
} }
.log-modal {
&.ant-modal {
width: calc(100vw - 16px) !important;
}
}
.ant-tooltip { .ant-tooltip {
max-width: 250px !important; max-width: 300px !important;
} }
} }
@@ -368,9 +395,9 @@ select:-webkit-autofill:focus {
} }
pre { pre {
word-break: break-all !important; word-break: break-all;
white-space: break-spaces !important; white-space: break-spaces;
padding: 0 !important; padding: 0;
} }
.virtuallist { .virtuallist {
@@ -429,3 +456,9 @@ body[data-mode='phone'] header {
.ant-table.ant-table-middle tfoot > tr > td { .ant-table.ant-table-middle tfoot > tr > td {
padding: 12px 16px; padding: 12px 16px;
} }
body[data-dark='true'] {
.ant-popover-arrow-content {
--antd-arrow-background-color: rgb(24, 26, 27);
}
}
+7 -33
View File
@@ -1,5 +1,5 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState } from 'react';
import ProLayout, { PageLoading } from '@ant-design/pro-layout'; import ProLayout, { PageLoading } from '@ant-design/pro-layout';
import * as DarkReader from '@umijs/ssr-darkreader'; import * as DarkReader from '@umijs/ssr-darkreader';
import defaultProps from './defaultProps'; import defaultProps from './defaultProps';
@@ -29,12 +29,9 @@ import {
MenuProps, MenuProps,
} from 'antd'; } from 'antd';
// @ts-ignore // @ts-ignore
import SockJS from 'sockjs-client';
import * as Sentry from '@sentry/react'; import * as Sentry from '@sentry/react';
import { init } from '../utils/init'; import { init } from '../utils/init';
import 'codemirror/mode/javascript/javascript'; import WebSocketManager from '../utils/websocket';
import 'codemirror/mode/python/python';
import 'codemirror/mode/shell/shell';
export interface SharedContext { export interface SharedContext {
headerStyle: React.CSSProperties; headerStyle: React.CSSProperties;
@@ -43,7 +40,6 @@ export interface SharedContext {
user: any; user: any;
reloadUser: (needLoading?: boolean) => void; reloadUser: (needLoading?: boolean) => void;
reloadTheme: () => void; reloadTheme: () => void;
socketMessage: any;
systemInfo: TSystemInfo; systemInfo: TSystemInfo;
} }
@@ -63,8 +59,6 @@ export default function () {
const [user, setUser] = useState<any>({}); const [user, setUser] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true); const [loading, setLoading] = useState<boolean>(true);
const [systemInfo, setSystemInfo] = useState<TSystemInfo>(); const [systemInfo, setSystemInfo] = useState<TSystemInfo>();
const ws = useRef<any>(null);
const [socketMessage, setSocketMessage] = useState<any>();
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [initLoading, setInitLoading] = useState<boolean>(true); const [initLoading, setInitLoading] = useState<boolean>(true);
const { const {
@@ -183,32 +177,14 @@ export default function () {
useEffect(() => { useEffect(() => {
if (!user || !user.username) return; if (!user || !user.username) return;
ws.current = new SockJS( const ws = WebSocketManager.getInstance(
`${window.location.origin}/api/ws?token=${localStorage.getItem( `${window.location.origin}${config.apiPrefix}ws?token=${localStorage.getItem(
config.authKey, config.authKey,
)}`, )}`,
); );
ws.current.onmessage = (e: any) => {
try {
const data = JSON.parse(e.data);
if (data.type === 'ping') {
if (data && data.message === 'hanhh') {
console.log('WS connection succeeded !!!');
} else {
console.log('WS connection Failed !!!', e);
}
}
setSocketMessage(data);
} catch (error) {
console.log('websocket连接失败', e);
}
};
const wsCurrent = ws.current;
return () => { return () => {
wsCurrent.close(); ws.close();
}; };
}, [user]); }, [user]);
@@ -249,7 +225,6 @@ export default function () {
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
ws: ws.current,
}} }}
/> />
); );
@@ -345,7 +320,7 @@ export default function () {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={user.avatar ? `/api/static/${user.avatar}` : ''} src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
@@ -367,7 +342,7 @@ export default function () {
shape="square" shape="square"
size="small" size="small"
icon={<UserOutlined />} icon={<UserOutlined />}
src={user.avatar ? `/api/static/${user.avatar}` : ''} src={user.avatar ? `${config.apiPrefix}static/${user.avatar}` : ''}
/> />
<span style={{ marginLeft: 5 }}>{user.username}</span> <span style={{ marginLeft: 5 }}>{user.username}</span>
</span> </span>
@@ -390,7 +365,6 @@ export default function () {
user, user,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage,
systemInfo, systemInfo,
}} }}
/> />
+19 -1
View File
@@ -352,6 +352,8 @@
"BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)": "BARK push icon, custom push icon (requires iOS 15 or above to display)", "BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)": "BARK push icon, custom push icon (requires iOS 15 or above to display)",
"BARK推送铃声,铃声列表去APP查看复制填写": "BARK push ringtone, check and copy from the APP's ringtone list", "BARK推送铃声,铃声列表去APP查看复制填写": "BARK push ringtone, check and copy from the APP's ringtone list",
"BARK推送消息的分组,默认为qinglong": "BARK push message grouping, default is qinglong", "BARK推送消息的分组,默认为qinglong": "BARK push message grouping, default is qinglong",
"BARK推送消息的时效性,默认为active": "BARK push message redirecting URL",
"BARK推送消息的跳转URL": "BARK push message grouping, default is qinglong",
"telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "Telegram Bot token, e.g., 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw", "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "Telegram Bot token, e.g., 1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw",
"telegram用户的id,例如:129xxx206": "Telegram user ID, e.g., 129xxx206", "telegram用户的id,例如:129xxx206": "Telegram user ID, e.g., 129xxx206",
"代理IP": "Proxy IP", "代理IP": "Proxy IP",
@@ -448,5 +450,21 @@
"编辑订阅": "Edit Subscription", "编辑订阅": "Edit Subscription",
"Subscription表达式格式有误": "Incorrect Subscription Expression Format", "Subscription表达式格式有误": "Incorrect Subscription Expression Format",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "The 'Group Code' for One-to-Many Push (Below One-to-Many Push->Your Group (if not, create one)->Group Code, if you are the creator of the group, you also need to click 'View QR Code' to scan and bind, otherwise you cannot receive group messages)", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "The 'Group Code' for One-to-Many Push (Below One-to-Many Push->Your Group (if not, create one)->Group Code, if you are the creator of the group, you also need to click 'View QR Code' to scan and bind, otherwise you cannot receive group messages)",
"登录已过期,请重新登录": "Login session has expired, please log in again" "登录已过期,请重新登录": "Login session has expired, please log in again",
"系统日志": "System Logs",
"主题": "Theme",
"语言": "Language",
"中...": "ing...",
"请选择操作符": "Please select operator",
"新增定时规则": "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",
"请输入运行任务前要执行的命令,不能包含 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"
} }
+27 -9
View File
@@ -337,7 +337,7 @@
"飞书机器人": "飞书机器人", "飞书机器人": "飞书机器人",
"自定义通知": "自定义通知", "自定义通知": "自定义通知",
"已关闭": "已关闭", "已关闭": "已关闭",
"gotify的url地址,例如 https://push.example.de:8080": "gotify的url地址,例如 https://push.example.de:8080", "gotify的url地址例如 https://push.example.de:8080": "gotify的url地址例如 https://push.example.de:8080",
"gotify的消息应用token码": "gotify的消息应用token码", "gotify的消息应用token码": "gotify的消息应用token码",
"推送消息的优先级": "推送消息的优先级", "推送消息的优先级": "推送消息的优先级",
"chat的url地址": "chat的url地址", "chat的url地址": "chat的url地址",
@@ -349,21 +349,23 @@
"PushDeer的Keyhttps://github.com/easychen/pushdeer": "PushDeer的Keyhttps://github.com/easychen/pushdeer", "PushDeer的Keyhttps://github.com/easychen/pushdeer": "PushDeer的Keyhttps://github.com/easychen/pushdeer",
"PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push": "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push", "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push": "PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push",
"Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX": "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX", "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX": "Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX",
"BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)": "BARK推送图标,自定义推送图标 (需iOS15或以上才能显示)", "BARK推送图标自定义推送图标 (需iOS15或以上才能显示)": "BARK推送图标自定义推送图标 (需iOS15或以上才能显示)",
"BARK推送铃声,铃声列表去APP查看复制填写": "BARK推送铃声,铃声列表去APP查看复制填写", "BARK推送铃声铃声列表去APP查看复制填写": "BARK推送铃声铃声列表去APP查看复制填写",
"BARK推送消息的分组, 默认为qinglong": "BARK推送消息的分组, 默认为qinglong", "BARK推送消息的分组默认为qinglong": "BARK推送消息的分组默认为qinglong",
"BARK推送消息的时效性,默认为active": "BARK推送消息的时效性,默认为active",
"BARK推送消息的跳转URL": "BARK推送消息的跳转URL",
"telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw", "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw": "telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw",
"telegram用户的id,例如:129xxx206": "telegram用户的id,例如:129xxx206", "telegram用户的id,例如:129xxx206": "telegram用户的id,例如:129xxx206",
"代理IP": "代理IP", "代理IP": "代理IP",
"代理端口": "代理端口", "代理端口": "代理端口",
"telegram代理配置认证参数, 用户名与密码用英文冒号连接 user:password": "telegram代理配置认证参数, 用户名与密码用英文冒号连接 user:password", "telegram代理配置认证参数用户名与密码用英文冒号连接 user:password": "telegram代理配置认证参数, 用户名与密码用英文冒号连接 user:password",
"telegram api自建的反向代理地址,默认tg官方api": "telegram api自建的反向代理地址,默认tg官方api", "telegram api自建的反向代理地址,默认tg官方api": "telegram api自建的反向代理地址,默认tg官方api",
"钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd": "钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd", "钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd": "钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd",
"密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串": "密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串", "密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串": "密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串",
"企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa": "企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa", "企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa": "企业微信机器人的 webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa",
"企业微信代理地址": "企业微信代理地址", "企业微信代理地址": "企业微信代理地址",
"corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat": "corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat", "corpidcorpsecrettouser(注:多个成员ID使用|隔开)agentid消息类型(选填不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat": "corpid,corpsecret,touser(注:多个成员ID使用|隔开),agentid,消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat",
"密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql": "密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql", "密钥key智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql": "密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql",
"发送的目标,群组或者好友": "发送的目标,群组或者好友", "发送的目标,群组或者好友": "发送的目标,群组或者好友",
"请输入要发送的目标": "请输入要发送的目标", "请输入要发送的目标": "请输入要发送的目标",
"群聊": "群聊", "群聊": "群聊",
@@ -448,5 +450,21 @@
"编辑订阅": "编辑订阅", "编辑订阅": "编辑订阅",
"Subscription表达式格式有误": "Subscription表达式格式有误", "Subscription表达式格式有误": "Subscription表达式格式有误",
"一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)", "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)": "一对多推送的“群组编码”(一对多推送下面->您的群组(如无则新建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)",
"登录已过期,请重新登录": "登录已过期,请重新登录" "登录已过期,请重新登录": "登录已过期,请重新登录",
"系统日志": "系统日志",
"主题": "主题",
"语言": "语言",
"中...": "中...",
"请选择操作符": "请选择操作符",
"新增定时规则": "新增定时规则",
"运行任务前执行的命令,比如 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文件可找到",
"请选择": "请选择",
"请输入": "请输入"
} }
+5 -9
View File
@@ -11,9 +11,10 @@ import config from '@/utils/config';
import { PageContainer } from '@ant-design/pro-layout'; import { PageContainer } from '@ant-design/pro-layout';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { Controlled as CodeMirror } from 'react-codemirror2'; import CodeMirror from '@uiw/react-codemirror';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import { langs } from '@uiw/codemirror-extensions-langs';
const Config = () => { const Config = () => {
const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>(); const { headerStyle, isPhone, theme } = useOutletContext<SharedContext>();
@@ -104,16 +105,11 @@ const Config = () => {
{isPhone ? ( {isPhone ? (
<CodeMirror <CodeMirror
value={value} value={value}
options={{ theme={theme.includes('dark') ? 'dark' : 'light'}
lineNumbers: true, extensions={[langs.shell()]}
styleActiveLine: true, onChange={(value) => {
matchBrackets: true,
mode: 'shell',
}}
onBeforeChange={(editor, data, value) => {
setValue(value); setValue(value);
}} }}
onChange={(editor, data, value) => {}}
/> />
) : ( ) : (
<Editor <Editor
+16 -17
View File
@@ -33,6 +33,7 @@ import IconFont from '@/components/iconfont';
import { getCommandScript, getEditorMode } from '@/utils'; import { getCommandScript, getEditorMode } from '@/utils';
import VirtualList from 'rc-virtual-list'; import VirtualList from 'rc-virtual-list';
import useScrollHeight from '@/hooks/useScrollHeight'; import useScrollHeight from '@/hooks/useScrollHeight';
import dayjs from 'dayjs';
const { Text } = Typography; const { Text } = Typography;
@@ -52,8 +53,6 @@ interface LogItem {
filename: string; filename: string;
} }
const language = navigator.language || navigator.languages[0];
const CronDetailModal = ({ const CronDetailModal = ({
cron = {}, cron = {},
handleCancel, handleCancel,
@@ -121,10 +120,11 @@ const CronDetailModal = ({
}; };
const onClickItem = (item: LogItem) => { const onClickItem = (item: LogItem) => {
localStorage.setItem('logCron', currentCron.id); const url = `${config.apiPrefix}logs/${item.filename}?path=${
setLogUrl( item.directory || ''
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`, }`;
); localStorage.setItem('logCron', url);
setLogUrl(url);
request request
.get( .get(
`${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`, `${config.apiPrefix}logs/${item.filename}?path=${item.directory || ''}`,
@@ -498,7 +498,12 @@ const CronDetailModal = ({
</div> </div>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title">{intl.get('定时')}</div> <div className="cron-detail-info-title">{intl.get('定时')}</div>
<div className="cron-detail-info-value">{currentCron.schedule}</div> <div className="cron-detail-info-value">
<div>{currentCron.schedule}</div>
{currentCron.extra_schedules?.map((x) => (
<div key={x.schedule}>{x.schedule}</div>
))}
</div>
</div> </div>
<div className="cron-detail-info-item"> <div className="cron-detail-info-item">
<div className="cron-detail-info-title"> <div className="cron-detail-info-title">
@@ -506,11 +511,9 @@ const CronDetailModal = ({
</div> </div>
<div className="cron-detail-info-value"> <div className="cron-detail-info-value">
{currentCron.last_execution_time {currentCron.last_execution_time
? new Date(currentCron.last_execution_time * 1000) ? dayjs(currentCron.last_execution_time * 1000).format(
.toLocaleString(language, { 'YYYY-MM-DD HH:mm:ss',
hour12: false, )
})
.replace(' 24:', ' 00:')
: '-'} : '-'}
</div> </div>
</div> </div>
@@ -530,11 +533,7 @@ const CronDetailModal = ({
</div> </div>
<div className="cron-detail-info-value"> <div className="cron-detail-info-value">
{currentCron.nextRunTime && {currentCron.nextRunTime &&
currentCron.nextRunTime dayjs(currentCron.nextRunTime).format('YYYY-MM-DD HH:mm:ss')}
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:')}
</div> </div>
</div> </div>
</Card> </Card>
+7 -16
View File
@@ -35,7 +35,7 @@
overflow: auto; overflow: auto;
.ant-card-body { .ant-card-body {
min-width: 600px; min-width: 1000px;
} }
.cron-detail-info-item { .cron-detail-info-item {
@@ -58,7 +58,7 @@
.ant-card-body { .ant-card-body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
min-width: 600px; min-width: 1000px;
} }
} }
@@ -78,7 +78,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-right: 32px;
.operations { .operations {
display: flex; display: flex;
@@ -94,7 +93,7 @@
.log-item { .log-item {
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background: #fafafa; background: #f2f2f2;
} }
} }
@@ -186,18 +185,6 @@ tr.drop-over-upward td {
} }
} }
.log-modal {
.log-container {
overflow-y: auto;
min-height: 300px;
max-height: calc(80vh - 110px);
max-height: calc(80vh - var(--vh-offset, 110px));
padding: 24px;
margin: -24px;
}
}
body[data-mode='desktop'] { body[data-mode='desktop'] {
.crontab-wrapper { .crontab-wrapper {
tbody .ant-table-cell { tbody .ant-table-cell {
@@ -205,3 +192,7 @@ body[data-mode='desktop'] {
} }
} }
} }
.cron.pinned-cron > td {
background: #f2f2f2;
}
+60 -72
View File
@@ -43,7 +43,6 @@ import { request } from '@/utils/http';
import CronModal, { CronLabelModal } from './modal'; import CronModal, { CronLabelModal } from './modal';
import CronLogModal from './logModal'; import CronLogModal from './logModal';
import CronDetailModal from './detail'; import CronDetailModal from './detail';
import cron_parser from 'cron-parser';
import { diffTime } from '@/utils/date'; import { diffTime } from '@/utils/date';
import { history, useOutletContext } from '@umijs/max'; import { history, useOutletContext } from '@umijs/max';
import './index.less'; import './index.less';
@@ -52,13 +51,14 @@ import ViewManageModal from './viewManageModal';
import { FilterValue, SorterResult } from 'antd/lib/table/interface'; import { FilterValue, SorterResult } from 'antd/lib/table/interface';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import { getCommandScript, parseCrontab } from '@/utils'; import { getCommandScript, getCrontabsNextDate, parseCrontab } from '@/utils';
import { ColumnProps } from 'antd/lib/table'; import { ColumnProps } from 'antd/lib/table';
import { useVT } from 'virtualizedtableforantd4'; import { useVT } from 'virtualizedtableforantd4';
import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type'; import { ICrontab, OperationName, OperationPath, CrontabStatus } from './type';
import Name from '@/components/name'; import Name from '@/components/name';
import dayjs from 'dayjs';
const { Text, Paragraph } = Typography; const { Text, Paragraph, Link } = Typography;
const { Search } = Input; const { Search } = Input;
const Crontab = () => { const Crontab = () => {
@@ -68,53 +68,26 @@ const Crontab = () => {
title: intl.get('名称'), title: intl.get('名称'),
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
fixed: isPhone ? undefined : 'left', fixed: 'left',
width: 120, width: 120,
render: (text: string, record: any) => ( render: (text: string, record: any) => (
<> <Paragraph
<a style={{
wordBreak: 'break-all',
marginBottom: 0,
color: '#1890ff'
}}
ellipsis={{ tooltip: text, rows: 2 }}
>
<Link
onClick={() => { onClick={() => {
setDetailCron(record); setDetailCron(record);
setIsDetailModalVisible(true); setIsDetailModalVisible(true);
}} }}
>
{record.labels?.length > 0 && record.labels[0] !== '' && false ? (
<Popover
placement="right"
trigger={isPhone ? 'click' : 'hover'}
content={
<div>
{record.labels?.map((label: string) => (
<Tag
color="blue"
key={label}
style={{ cursor: 'point' }}
onClick={(e) => {
e.stopPropagation();
setSearchValue(`label:${label}`);
setSearchText(`label:${label}`);
}}
>
<a>{label}</a>
</Tag>
))}
</div>
}
> >
{record.name || '-'} {record.name || '-'}
</Popover> </Link>
) : ( </Paragraph>
record.name || '-'
)}
{record.isPinned ? (
<span>
<PushpinOutlined />
</span>
) : (
''
)}
</a>
</>
), ),
sorter: { sorter: {
compare: (a, b) => a?.name?.localeCompare(b?.name), compare: (a, b) => a?.name?.localeCompare(b?.name),
@@ -124,7 +97,7 @@ const Crontab = () => {
title: intl.get('命令/脚本'), title: intl.get('命令/脚本'),
dataIndex: 'command', dataIndex: 'command',
key: 'command', key: 'command',
width: 200, width: 240,
render: (text, record) => { render: (text, record) => {
return ( return (
<Paragraph <Paragraph
@@ -211,10 +184,36 @@ const Crontab = () => {
sorter: { sorter: {
compare: (a, b) => a.schedule.localeCompare(b.schedule), compare: (a, b) => a.schedule.localeCompare(b.schedule),
}, },
render: (text, record) => {
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}
</Paragraph>
);
},
}, },
{ {
title: intl.get('最后运行时长'), title: intl.get('最后运行时长'),
width: 180, width: 167,
dataIndex: 'last_running_time', dataIndex: 'last_running_time',
key: 'last_running_time', key: 'last_running_time',
sorter: { sorter: {
@@ -232,14 +231,13 @@ const Crontab = () => {
title: intl.get('最后运行时间'), title: intl.get('最后运行时间'),
dataIndex: 'last_execution_time', dataIndex: 'last_execution_time',
key: 'last_execution_time', key: 'last_execution_time',
width: 150, width: 141,
sorter: { sorter: {
compare: (a, b) => { compare: (a, b) => {
return (a.last_execution_time || 0) - (b.last_execution_time || 0); return (a.last_execution_time || 0) - (b.last_execution_time || 0);
}, },
}, },
render: (text, record) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0];
return ( return (
<span <span
style={{ style={{
@@ -247,11 +245,9 @@ const Crontab = () => {
}} }}
> >
{record.last_execution_time {record.last_execution_time
? new Date(record.last_execution_time * 1000) ? dayjs(record.last_execution_time * 1000).format(
.toLocaleString(language, { 'YYYY-MM-DD HH:mm:ss',
hour12: false, )
})
.replace(' 24:', ' 00:')
: '-'} : '-'}
</span> </span>
); );
@@ -259,24 +255,19 @@ const Crontab = () => {
}, },
{ {
title: intl.get('下次运行时间'), title: intl.get('下次运行时间'),
width: 150, width: 144,
sorter: { sorter: {
compare: (a: any, b: any) => { compare: (a: any, b: any) => {
return a.nextRunTime - b.nextRunTime; return a.nextRunTime - b.nextRunTime;
}, },
}, },
render: (text, record) => { render: (text, record) => {
const language = navigator.language || navigator.languages[0]; return dayjs(record.nextRunTime).format('YYYY-MM-DD HH:mm:ss');
return record.nextRunTime
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
}, },
}, },
{ {
title: intl.get('关联订阅'), title: intl.get('关联订阅'),
width: 190, width: 185,
render: (text, record: any) => render: (text, record: any) =>
record.sub_id ? ( record.sub_id ? (
<Name <Name
@@ -292,46 +283,40 @@ const Crontab = () => {
{ {
title: intl.get('操作'), title: intl.get('操作'),
key: 'action', key: 'action',
width: 130, width: 140,
fixed: isPhone ? undefined : 'right', fixed: isPhone ? undefined : 'right',
render: (text, record, index) => { render: (text, record, index) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
{record.status === CrontabStatus.idle && ( {record.status === CrontabStatus.idle && (
<Tooltip title={isPc ? intl.get('运行') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
runCron(record, index); runCron(record, index);
}} }}
> >
<PlayCircleOutlined /> {intl.get('运行')}
</a> </a>
</Tooltip>
)} )}
{record.status !== CrontabStatus.idle && ( {record.status !== CrontabStatus.idle && (
<Tooltip title={isPc ? intl.get('停止') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
stopCron(record, index); stopCron(record, index);
}} }}
> >
<PauseCircleOutlined /> {intl.get('停止')}
</a> </a>
</Tooltip>
)} )}
<Tooltip title={isPc ? intl.get('日志') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() }); setLogCron({ ...record, timestamp: Date.now() });
}} }}
> >
<FileTextOutlined /> {intl.get('日志')}
</a> </a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} /> <MoreBtn key="more" record={record} index={index} />
</Space> </Space>
); );
@@ -410,7 +395,7 @@ const Crontab = () => {
data.map((x) => { data.map((x) => {
return { return {
...x, ...x,
nextRunTime: parseCrontab(x.schedule), nextRunTime: getCrontabsNextDate(x.schedule, x.extra_schedules),
}; };
}), }),
); );
@@ -698,7 +683,10 @@ const Crontab = () => {
if (code === 200) { if (code === 200) {
const index = value.findIndex((x) => x.id === cron.id); const index = value.findIndex((x) => x.id === cron.id);
const result = [...value]; const result = [...value];
data.nextRunTime = parseCrontab(data.schedule); data.nextRunTime = getCrontabsNextDate(
data.schedule,
data.extra_schedules,
);
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...cron, ...cron,
@@ -1041,7 +1029,7 @@ const Crontab = () => {
dataSource={value} dataSource={value}
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1200, y: tableScrollHeight }}
loading={loading} loading={loading}
rowSelection={rowSelection} rowSelection={rowSelection}
rowClassName={getRowClassName} rowClassName={getRowClassName}
+29 -15
View File
@@ -1,6 +1,14 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Modal, message, Input, Form, Statistic, Button } from 'antd'; import {
Modal,
message,
Input,
Form,
Statistic,
Button,
Typography,
} from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import { import {
@@ -10,6 +18,7 @@ import {
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils'; import { logEnded } from '@/utils';
import { CrontabStatus } from './type'; import { CrontabStatus } from './type';
import Ansi from 'ansi-to-react';
const { Countdown } = Statistic; const { Countdown } = Statistic;
@@ -31,6 +40,7 @@ const CronLogModal = ({
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
const [isPhone, setIsPhone] = useState(false); const [isPhone, setIsPhone] = useState(false);
const scrollInfoRef = useRef({ value: 0, down: true }); const scrollInfoRef = useRef({ value: 0, down: true });
const uniqPath = logUrl ? logUrl : String(cron?.id);
const getCronLog = (isFirst?: boolean) => { const getCronLog = (isFirst?: boolean) => {
if (isFirst) { if (isFirst) {
@@ -41,7 +51,7 @@ const CronLogModal = ({
.then(({ code, data }) => { .then(({ code, data }) => {
if ( if (
code === 200 && code === 200 &&
localStorage.getItem('logCron') === String(cron.id) && localStorage.getItem('logCron') === uniqPath &&
data !== value data !== value
) { ) {
const log = data as string; const log = data as string;
@@ -49,10 +59,15 @@ const CronLogModal = ({
const hasNext = Boolean( const hasNext = Boolean(
log && !logEnded(log) && !log.includes('任务未运行'), log && !logEnded(log) && !log.includes('任务未运行'),
); );
setExecuting(hasNext); if (!hasNext && !logEnded(value) && value !== intl.get('启动中...')) {
setTimeout(() => {
autoScroll(); autoScroll();
});
}
setExecuting(hasNext);
if (hasNext) { if (hasNext) {
setTimeout(() => { setTimeout(() => {
autoScroll();
getCronLog(); getCronLog();
}, 2000); }, 2000);
} }
@@ -74,7 +89,7 @@ const CronLogModal = ({
document document
.querySelector('#log-flag')! .querySelector('#log-flag')!
.scrollIntoView({ behavior: 'smooth' }); .scrollIntoView({ behavior: 'smooth' });
}, 1000); }, 600);
}; };
const cancel = () => { const cancel = () => {
@@ -82,29 +97,32 @@ const CronLogModal = ({
handleCancel(); handleCancel();
}; };
const handleScroll = (e) => { const handleScroll: React.UIEventHandler<HTMLDivElement> = (e) => {
const sTop = e.target.scrollTop; const sTop = (e.target as HTMLDivElement).scrollTop;
if (scrollInfoRef.current.down) { if (scrollInfoRef.current.down) {
scrollInfoRef.current = { scrollInfoRef.current = {
value: sTop, value: sTop,
down: sTop > scrollInfoRef.current.value, down: sTop - scrollInfoRef.current.value > -5 || !sTop,
}; };
} }
}; };
const titleElement = () => { const titleElement = () => {
return ( return (
<> <div style={{ display: 'flex', alignItems: 'center' }}>
{(executing || loading) && <Loading3QuartersOutlined spin />} {(executing || loading) && <Loading3QuartersOutlined spin />}
{!executing && !loading && <CheckCircleOutlined />} {!executing && !loading && <CheckCircleOutlined />}
<span style={{ marginLeft: 5 }}>{cron && cron.name}</span> <Typography.Text ellipsis={true} style={{ marginLeft: 5 }}>
</> {cron && cron.name}
</Typography.Text>
</div>
); );
}; };
useEffect(() => { useEffect(() => {
if (cron && cron.id && visible) { if (cron && cron.id && visible) {
getCronLog(true); getCronLog(true);
scrollInfoRef.current.down = true;
} }
}, [cron, visible]); }, [cron, visible]);
@@ -124,9 +142,6 @@ const CronLogModal = ({
open={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{
minHeight: '300px',
}}
forceRender forceRender
onOk={() => cancel()} onOk={() => cancel()}
onCancel={() => cancel()} onCancel={() => cancel()}
@@ -145,13 +160,12 @@ const CronLogModal = ({
isPhone isPhone
? { ? {
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
width: 375,
zoom: 0.83, zoom: 0.83,
} }
: {} : {}
} }
> >
{value} <Ansi>{value}</Ansi>
</pre> </pre>
)} )}
<div id="log-flag"></div> <div id="log-flag"></div>
+96 -3
View File
@@ -1,10 +1,11 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Button } from 'antd'; import { Modal, message, Input, Form, Button, Space } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import cronParse from 'cron-parser'; import cronParse from 'cron-parser';
import EditableTagGroup from '@/components/tag'; import EditableTagGroup from '@/components/tag';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
const CronModal = ({ const CronModal = ({
cron, cron,
@@ -73,7 +74,11 @@ const CronModal = ({
name="form_in_modal" name="form_in_modal"
initialValues={cron} initialValues={cron}
> >
<Form.Item name="name" label={intl.get('名称')}> <Form.Item
name="name"
label={intl.get('名称')}
rules={[{ required: true, whitespace: true }]}
>
<Input placeholder={intl.get('请输入任务名称')} /> <Input placeholder={intl.get('请输入任务名称')} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -83,7 +88,7 @@ const CronModal = ({
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get( placeholder={intl.get(
'支持输入脚本路径/任意系统可执行命令/task 脚本路径', '支持输入脚本路径/任意系统可执行命令/task 脚本路径',
)} )}
@@ -107,9 +112,97 @@ const CronModal = ({
> >
<Input placeholder={intl.get('秒(可选) 分 时 天 月 周')} /> <Input placeholder={intl.get('秒(可选) 分 时 天 月 周')} />
</Form.Item> </Form.Item>
<Form.List name="extra_schedules">
{(fields, { add, remove }, { errors }) => (
<>
{fields.map(({ key, name, ...restField }) => (
<Form.Item key={key} noStyle>
<Space className="view-create-modal-sorts" align="baseline">
<Form.Item
{...restField}
name={[name, 'schedule']}
rules={[{ required: true }]}
>
<Input
placeholder={intl.get('秒(可选) 分 时 天 月 周')}
/>
</Form.Item>
<MinusCircleOutlined
className="dynamic-delete-button"
onClick={() => remove(name)}
/>
</Space>
</Form.Item>
))}
<Form.Item>
<a onClick={() => add({ schedule: '' })}>
<PlusOutlined />
{intl.get('新增定时规则')}
</a>
</Form.Item>
<Form.ErrorList errors={errors} />
</>
)}
</Form.List>
<Form.Item name="labels" label={intl.get('标签')}> <Form.Item name="labels" label={intl.get('标签')}>
<EditableTagGroup /> <EditableTagGroup />
</Form.Item> </Form.Item>
<Form.Item
name="task_before"
label={intl.get('执行前')}
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(
'请输入运行任务前要执行的命令,不能包含 task 命令',
)}
/>
</Form.Item>
<Form.Item
name="task_after"
label={intl.get('执行后')}
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(
'请输入运行任务后要执行的命令,不能包含 task 命令',
)}
/>
</Form.Item>
</Form> </Form>
</Modal> </Modal>
); );
+2
View File
@@ -35,4 +35,6 @@ export interface ICrontab {
last_running_time?: number; last_running_time?: number;
last_execution_time?: number; last_execution_time?: number;
nextRunTime: Date; nextRunTime: Date;
sub_id: number;
extra_schedules?: Array<{ schedule: string; }>;
} }
+21 -8
View File
@@ -107,20 +107,29 @@ const ViewCreateModal = ({
} }
form.setFieldsValue( form.setFieldsValue(
view || { view || {
filters: [{ property: 'command', operation: 'Reg' }], filters: [{ property: 'command' }],
}, },
); );
}, [view, visible]); }, [view, visible]);
const operationElement = ( const OperationElement = ({ name, ...others }: { name: number }) => {
<Select style={{ width: 120 }}> const property = form.getFieldValue(['filters', name, 'property']);
{OPERATIONS.map((x) => ( return (
<Select
style={{ width: 120 }}
placeholder={intl.get('请选择操作符')}
{...others}
>
{OPERATIONS.filter((x) =>
STATUS_MAP[property as 'status' | 'sub_id'] ? x.type === 'select' : x,
).map((x) => (
<Select.Option key={x.name} value={x.value}> <Select.Option key={x.name} value={x.value}>
{x.name} {x.name}
</Select.Option> </Select.Option>
))} ))}
</Select> </Select>
); );
};
const propertyElement = (props: any, style: React.CSSProperties = {}) => { const propertyElement = (props: any, style: React.CSSProperties = {}) => {
return ( return (
@@ -190,7 +199,7 @@ const ViewCreateModal = ({
<Input placeholder={intl.get('请输入视图名称')} /> <Input placeholder={intl.get('请输入视图名称')} />
</Form.Item> </Form.Item>
<Form.List name="filters"> <Form.List name="filters">
{(fields, { add, remove }) => ( {(fields, { add, remove }, { errors }) => (
<div <div
style={{ position: 'relative' }} style={{ position: 'relative' }}
className={`view-filters-container ${ className={`view-filters-container ${
@@ -256,9 +265,11 @@ const ViewCreateModal = ({
<Form.Item <Form.Item
{...restField} {...restField}
name={[name, 'operation']} name={[name, 'operation']}
rules={[{ required: true }]} rules={[
{ required: true, message: intl.get('请选择操作符') },
]}
> >
{operationElement} <OperationElement name={name} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
{...restField} {...restField}
@@ -290,12 +301,13 @@ const ViewCreateModal = ({
{intl.get('新增筛选条件')} {intl.get('新增筛选条件')}
</a> </a>
</Form.Item> </Form.Item>
<Form.ErrorList errors={errors} />
</div> </div>
</div> </div>
)} )}
</Form.List> </Form.List>
<Form.List name="sorts"> <Form.List name="sorts">
{(fields, { add, remove }) => ( {(fields, { add, remove }, { errors }) => (
<div <div
style={{ position: 'relative' }} style={{ position: 'relative' }}
className={`view-filters-container ${ className={`view-filters-container ${
@@ -365,6 +377,7 @@ const ViewCreateModal = ({
{intl.get('新增排序方式')} {intl.get('新增排序方式')}
</a> </a>
</Form.Item> </Form.Item>
<Form.ErrorList errors={errors} />
</div> </div>
</div> </div>
)} )}
+53 -52
View File
@@ -35,6 +35,7 @@ import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import WebSocketManager from '@/utils/websocket';
const { Text } = Typography; const { Text } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -87,12 +88,11 @@ const StatusMap: Record<number, { icon: React.ReactNode; color: string }> = {
}; };
const Dependence = () => { const Dependence = () => {
const { headerStyle, isPhone, socketMessage } = const { headerStyle, isPhone } = useOutletContext<SharedContext>();
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
title: intl.get('序号'), title: intl.get('序号'),
width: 80, width: 90,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>; return <span style={{ cursor: 'text' }}>{index + 1} </span>;
}, },
@@ -100,7 +100,7 @@ const Dependence = () => {
{ {
title: intl.get('名称'), title: intl.get('名称'),
dataIndex: 'name', dataIndex: 'name',
width: 120, width: 180,
key: 'name', key: 'name',
}, },
{ {
@@ -109,11 +109,12 @@ const Dependence = () => {
width: 120, width: 120,
dataIndex: 'status', dataIndex: 'status',
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
console.log(record.status);
return ( return (
<Space size="middle" style={{ cursor: 'text' }}> <Space size="middle" style={{ cursor: 'text' }}>
<Tag <Tag
color={StatusMap[record.status].color} color={StatusMap[record.status]?.color}
icon={StatusMap[record.status].icon} icon={StatusMap[record.status]?.icon}
style={{ marginRight: 0 }} style={{ marginRight: 0 }}
> >
{intl.get(Status[record.status])} {intl.get(Status[record.status])}
@@ -125,7 +126,7 @@ const Dependence = () => {
{ {
title: intl.get('备注'), title: intl.get('备注'),
dataIndex: 'remark', dataIndex: 'remark',
width: 120, width: 100,
key: 'remark', key: 'remark',
}, },
{ {
@@ -149,7 +150,7 @@ const Dependence = () => {
{ {
title: intl.get('操作'), title: intl.get('操作'),
key: 'action', key: 'action',
width: 150, width: 140,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
@@ -395,63 +396,62 @@ const Dependence = () => {
} }
}, [logDependence]); }, [logDependence]);
useEffect(() => { const handleMessage = useCallback((payload: any) => {
if (!socketMessage) return; const { message, references } = payload;
const { type, message, references } = socketMessage; let status: number | undefined = undefined;
if ( if (message.includes('开始时间') && references.length > 0) {
type === 'installDependence' && status = message.includes('安装') ? Status.安装中 : Status.删除中;
message.includes(intl.get('开始时间')) &&
references.length > 0
) {
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...value[index],
status: message.includes(intl.get('安装')) ? Status.安装中 : Status.删除中,
});
} }
} if (message.includes('结束时间') && references.length > 0) {
setValue(result); if (message.includes('安装')) {
} status = message.includes('成功') ? Status.已安装 : Status.安装失败;
if (
type === 'installDependence' &&
message.includes(intl.get('结束时间')) &&
references.length > 0
) {
let status;
if (message.includes(intl.get('安装'))) {
status = message.includes(intl.get('成功')) ? Status.已安装 : Status.安装失败;
} else { } else {
status = message.includes(intl.get('成功')) ? Status.已删除 : Status.删除失败; status = message.includes('成功') ? Status.已删除 : Status.删除失败;
} }
const result = [...value];
for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...value[index],
status,
});
}
}
setValue(result);
if (status === Status.) { if (status === Status.) {
setTimeout(() => { setTimeout(() => {
const _result = [...value]; setValue((p) => {
const _result = [...p];
for (let i = 0; i < references.length; i++) { for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]); const index = p.findIndex((x) => x.id === references[i]);
if (index !== -1) { if (index !== -1) {
_result.splice(index, 1); _result.splice(index, 1);
} }
} }
setValue(_result); return _result;
});
}, 5000); }, 5000);
return;
} }
} }
}, [socketMessage]); if (typeof status === 'number') {
setValue((p) => {
const result = [...p];
for (let i = 0; i < references.length; i++) {
const index = p.findIndex((x) => x.id === references[i]);
if (index !== -1) {
result.splice(index, 1, {
...p[index],
status,
});
}
}
return result;
});
}
}, []);
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('installDependence', handleMessage);
ws.subscribe('uninstallDependence', handleMessage);
return () => {
ws.unsubscribe('installDependence', handleMessage);
ws.unsubscribe('uninstallDependence', handleMessage);
};
}, []);
const onTabChange = (activeKey: string) => { const onTabChange = (activeKey: string) => {
setSelectedRowIds([]); setSelectedRowIds([]);
@@ -548,6 +548,7 @@ const Dependence = () => {
dependence={editedDependence} dependence={editedDependence}
defaultType={type} defaultType={type}
/> />
{logDependence && (
<DependenceLogModal <DependenceLogModal
visible={isLogModalVisible} visible={isLogModalVisible}
handleCancel={(needRemove?: boolean) => { handleCancel={(needRemove?: boolean) => {
@@ -563,9 +564,9 @@ const Dependence = () => {
getDependenceDetail(logDependence); getDependenceDetail(logDependence);
} }
}} }}
socketMessage={socketMessage}
dependence={logDependence} dependence={logDependence}
/> />
)}
</PageContainer> </PageContainer>
); );
}; };
+22 -23
View File
@@ -9,17 +9,16 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import Ansi from 'ansi-to-react'; import Ansi from 'ansi-to-react';
import WebSocketManager from '@/utils/websocket';
const DependenceLogModal = ({ const DependenceLogModal = ({
dependence, dependence,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
dependence?: any; dependence?: any;
visible: boolean; visible: boolean;
handleCancel: (needRemove?: boolean) => void; handleCancel: (needRemove?: boolean) => void;
socketMessage: any;
}) => { }) => {
const [value, setValue] = useState<string>(''); const [value, setValue] = useState<string>('');
const [executing, setExecuting] = useState<any>(true); const [executing, setExecuting] = useState<any>(true);
@@ -54,10 +53,10 @@ const DependenceLogModal = ({
code === 200 && code === 200 &&
localStorage.getItem('logDependence') === String(dependence.id) localStorage.getItem('logDependence') === String(dependence.id)
) { ) {
const log = (data.log || []).join('') as string; const log = (data?.log || []).join('') as string;
setValue(log); setValue(log);
setExecuting(!log.includes(intl.get('结束时间'))); setExecuting(!log.includes('结束时间'));
setIsRemoveFailed(log.includes(intl.get('删除失败'))); setIsRemoveFailed(log.includes('删除失败'));
} }
}) })
.finally(() => { .finally(() => {
@@ -95,21 +94,25 @@ const DependenceLogModal = ({
} }
}, [dependence]); }, [dependence]);
useEffect(() => { const handleMessage = (payload: any) => {
if (!socketMessage || !dependence) return; const { message, references } = payload;
const { type, message, references } = socketMessage; if (references.length > 0 && references.includes(dependence.id)) {
if ( if (message.includes('结束时间')) {
type === 'installDependence' &&
references.length > 0 &&
references.includes(dependence.id)
) {
if (message.includes(intl.get('结束时间'))) {
setExecuting(false); setExecuting(false);
setIsRemoveFailed(message.includes(intl.get('删除失败'))); setIsRemoveFailed(message.includes('删除失败'));
} }
setValue(`${value}${message}`); setValue((p) => `${p}${message}`);
} }
}, [socketMessage]); };
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('installDependence', handleMessage);
return () => {
ws.unsubscribe('installDependence', handleMessage);
};
}, []);
useEffect(() => { useEffect(() => {
setIsPhone(document.body.clientWidth < 768); setIsPhone(document.body.clientWidth < 768);
@@ -121,11 +124,6 @@ const DependenceLogModal = ({
open={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{
overflowY: 'auto',
maxHeight: 'calc(70vh - var(--vh-offset, 0px))',
minHeight: '300px',
}}
forceRender forceRender
onOk={() => cancel()} onOk={() => cancel()}
onCancel={() => cancel()} onCancel={() => cancel()}
@@ -135,6 +133,7 @@ const DependenceLogModal = ({
</Button>, </Button>,
]} ]}
> >
<div className="log-container">
{loading ? ( {loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
@@ -143,7 +142,6 @@ const DependenceLogModal = ({
isPhone isPhone
? { ? {
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
width: 375,
zoom: 0.83, zoom: 0.83,
} }
: {} : {}
@@ -152,6 +150,7 @@ const DependenceLogModal = ({
<Ansi>{value}</Ansi> <Ansi>{value}</Ansi>
</pre> </pre>
)} )}
</div>
</Modal> </Modal>
); );
}; };
+1 -1
View File
@@ -130,7 +130,7 @@ const DependenceModal = ({
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get('请输入依赖名称')} placeholder={intl.get('请输入依赖名称')}
/> />
</Form.Item> </Form.Item>
+58 -21
View File
@@ -1,4 +1,4 @@
import intl from 'react-intl-universal' import intl from 'react-intl-universal';
import React, { import React, {
useCallback, useCallback,
useRef, useRef,
@@ -41,8 +41,9 @@ import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import Copy from '../../components/copy'; import Copy from '../../components/copy';
import { useVT } from 'virtualizedtableforantd4'; import { useVT } from 'virtualizedtableforantd4';
import dayjs from 'dayjs';
const { Text } = Typography; const { Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
enum Status { enum Status {
@@ -82,6 +83,16 @@ const Env = () => {
dataIndex: 'name', dataIndex: 'name',
key: 'name', key: 'name',
sorter: (a: any, b: any) => a.name.localeCompare(b.name), sorter: (a: any, b: any) => a.name.localeCompare(b.name),
render: (text: string, record: any) => {
return (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Tooltip title={text} placement="topLeft">
<div className="text-ellipsis">{text}</div>
</Tooltip>
<Copy text={text} />
</div>
);
},
}, },
{ {
title: intl.get('值'), title: intl.get('值'),
@@ -127,13 +138,9 @@ const Env = () => {
}, },
}, },
render: (text: string, record: any) => { render: (text: string, record: any) => {
const language = navigator.language || navigator.languages[0]; const date = dayjs(record.updatedAt || record.timestamp).format(
const time = record.updatedAt || record.timestamp; 'YYYY-MM-DD HH:mm:ss',
const date = new Date(time) );
.toLocaleString(language, {
hour12: false,
})
.replace(' 24:', ' 00:');
return ( return (
<Tooltip <Tooltip
placement="topLeft" placement="topLeft"
@@ -149,7 +156,7 @@ const Env = () => {
title: intl.get('状态'), title: intl.get('状态'),
key: 'status', key: 'status',
dataIndex: 'status', dataIndex: 'status',
width: 80, width: 100,
filters: [ filters: [
{ {
text: intl.get('已启用'), text: intl.get('已启用'),
@@ -186,7 +193,11 @@ const Env = () => {
</Tooltip> </Tooltip>
<Tooltip <Tooltip
title={ title={
isPc ? (record.status === Status. ? intl.get('启用') : intl.get('禁用')) : '' isPc
? record.status === Status.
? intl.get('启用')
: intl.get('禁用')
: ''
} }
> >
<a onClick={() => enabledOrDisabledEnv(record, index)}> <a onClick={() => enabledOrDisabledEnv(record, index)}>
@@ -232,14 +243,24 @@ const Env = () => {
const enabledOrDisabledEnv = (record: any, index: number) => { const enabledOrDisabledEnv = (record: any, index: number) => {
Modal.confirm({ Modal.confirm({
title: `确认${record.status === Status. ? intl.get('启用') : intl.get('禁用')}`, title: `确认${
record.status === Status. ? intl.get('启用') : intl.get('禁用')
}`,
content: ( content: (
<> <>
{intl.get('确认')}{record.status === Status. ? intl.get('启用') : intl.get('禁用')} {intl.get('确认')}
{record.status === Status.
? intl.get('启用')
: intl.get('禁用')}
Env{' '} Env{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Paragraph
style={{ wordBreak: 'break-all', display: 'inline' }}
ellipsis={{ rows: 6, expandable: true }}
type="warning"
copyable
>
{record.value} {record.value}
</Text>{' '} </Paragraph>{' '}
{intl.get('吗')} {intl.get('吗')}
</> </>
), ),
@@ -254,7 +275,11 @@ const Env = () => {
.then(({ code, data }) => { .then(({ code, data }) => {
if (code === 200) { if (code === 200) {
message.success( message.success(
`${record.status === Status. ? intl.get('启用') : intl.get('禁用')}${intl.get('成功')}`, `${
record.status === Status.
? intl.get('启用')
: intl.get('禁用')
}${intl.get('成功')}`,
); );
const newStatus = const newStatus =
record.status === Status. ? Status.已启用 : Status.已禁用; record.status === Status. ? Status.已启用 : Status.已禁用;
@@ -289,9 +314,14 @@ const Env = () => {
content: ( content: (
<> <>
{intl.get('确认删除变量')}{' '} {intl.get('确认删除变量')}{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning"> <Paragraph
style={{ wordBreak: 'break-all', display: 'inline' }}
ellipsis={{ rows: 6, expandable: true }}
type="warning"
copyable
>
{record.name}: {record.value} {record.name}: {record.value}
</Text>{' '} </Paragraph>{' '}
{intl.get('吗')} {intl.get('吗')}
</> </>
), ),
@@ -435,7 +465,13 @@ const Env = () => {
const operateEnvs = (operationStatus: number) => { const operateEnvs = (operationStatus: number) => {
Modal.confirm({ Modal.confirm({
title: `确认${OperationName[operationStatus]}`, title: `确认${OperationName[operationStatus]}`,
content: <>{intl.get('确认')}{OperationName[operationStatus]}{intl.get('选中的变量吗')}</>, content: (
<>
{intl.get('确认')}
{OperationName[operationStatus]}
{intl.get('选中的变量吗')}
</>
),
onOk() { onOk() {
request request
.put( .put(
@@ -567,7 +603,8 @@ const Env = () => {
</Button> </Button>
<span style={{ marginLeft: 8 }}> <span style={{ marginLeft: 8 }}>
{intl.get('已选择')} {intl.get('已选择')}
<a>{selectedRowIds?.length}</a>{intl.get('项')} <a>{selectedRowIds?.length}</a>
{intl.get('项')}
</span> </span>
</div> </div>
)} )}
@@ -579,7 +616,7 @@ const Env = () => {
dataSource={value} dataSource={value}
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 1000, y: tableScrollHeight }} scroll={{ x: 1200, y: tableScrollHeight }}
components={vt} components={vt}
loading={loading} loading={loading}
onRow={(record: any, index: number | undefined) => { onRow={(record: any, index: number | undefined) => {
+15 -6
View File
@@ -1,4 +1,4 @@
import intl from 'react-intl-universal' import intl from 'react-intl-universal';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, message, Input, Form, Radio } from 'antd'; import { Modal, message, Input, Form, Radio } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
@@ -44,7 +44,9 @@ const EnvModal = ({
); );
if (code === 200) { if (code === 200) {
message.success(env ? intl.get('更新变量成功') : intl.get('创建变量成功')); message.success(
env ? intl.get('更新变量成功') : intl.get('创建变量成功'),
);
handleCancel(data); handleCancel(data);
} }
setLoading(false); setLoading(false);
@@ -82,7 +84,11 @@ const EnvModal = ({
name="name" name="name"
label={intl.get('名称')} label={intl.get('名称')}
rules={[ rules={[
{ required: true, message: intl.get('请输入环境变量名称'), whitespace: true }, {
required: true,
message: intl.get('请输入环境变量名称'),
whitespace: true,
},
{ {
pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/, pattern: /^[a-zA-Z_][0-9a-zA-Z_]*$/,
message: intl.get('只能输入字母数字下划线,且不能以数字开头'), message: intl.get('只能输入字母数字下划线,且不能以数字开头'),
@@ -108,12 +114,15 @@ const EnvModal = ({
name="value" name="value"
label={intl.get('值')} label={intl.get('值')}
rules={[ rules={[
{ required: true, message: intl.get('请输入环境变量值'), whitespace: true }, {
required: true,
message: intl.get('请输入环境变量值'),
whitespace: true,
},
]} ]}
> >
<Input.TextArea <Input.TextArea
rows={4} autoSize={{ minRows: 1, maxRows: 8 }}
autoSize={true}
placeholder={intl.get('请输入环境变量值')} placeholder={intl.get('请输入环境变量值')}
/> />
</Form.Item> </Form.Item>
+1 -1
View File
@@ -185,7 +185,7 @@ const Initialization = () => {
style={{ maxWidth: 400 }} style={{ maxWidth: 400 }}
> >
<Input.TextArea <Input.TextArea
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={`请输入${x.label}`} placeholder={`请输入${x.label}`}
/> />
</Form.Item> </Form.Item>
+4 -10
View File
@@ -16,7 +16,7 @@ import { PageContainer } from '@ant-design/pro-layout';
import Editor from '@monaco-editor/react'; import Editor from '@monaco-editor/react';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import { Controlled as CodeMirror } from 'react-codemirror2'; import CodeMirror from '@uiw/react-codemirror';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
@@ -278,17 +278,11 @@ const Log = () => {
{isPhone && ( {isPhone && (
<CodeMirror <CodeMirror
value={value} value={value}
options={{ readOnly={true}
lineNumbers: true, theme={theme.includes('dark') ? 'dark' : 'light'}
lineWrapping: true, onChange={(value, viewUpdate) => {
styleActiveLine: true,
matchBrackets: true,
readOnly: true,
}}
onBeforeChange={(editor, data, value) => {
setValue(value); setValue(value);
}} }}
onChange={(editor, data, value) => {}}
/> />
)} )}
</div> </div>
+2 -1
View File
@@ -16,6 +16,7 @@ import { request } from '@/utils/http';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { MobileOutlined } from '@ant-design/icons'; import { MobileOutlined } from '@ant-design/icons';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import dayjs from 'dayjs';
const FormItem = Form.Item; const FormItem = Form.Item;
const { Countdown } = Statistic; const { Countdown } = Statistic;
@@ -86,7 +87,7 @@ const Login = () => {
<> <>
<div> <div>
{intl.get('上次登录时间:')} {intl.get('上次登录时间:')}
{lastlogon ? new Date(lastlogon).toLocaleString() : '-'} {lastlogon ? dayjs(lastlogon).format('YYYY-MM-DD HH:mm:ss') : '-'}
</div> </div>
<div> <div>
{intl.get('上次登录地点:')} {intl.get('上次登录地点:')}
+25 -23
View File
@@ -1,5 +1,11 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState, useRef } from 'react'; import React, {
useEffect,
useState,
useRef,
useCallback,
useReducer,
} from 'react';
import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd'; import { Drawer, Button, Tabs, Badge, Select, TreeSelect } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -9,6 +15,8 @@ import SaveModal from './saveModal';
import SettingModal from './setting'; import SettingModal from './setting';
import { useTheme } from '@/utils/hooks'; import { useTheme } from '@/utils/hooks';
import { getEditorMode, logEnded } from '@/utils'; import { getEditorMode, logEnded } from '@/utils';
import WebSocketManager from '@/utils/websocket';
import Ansi from 'ansi-to-react';
const { Option } = Select; const { Option } = Select;
@@ -18,12 +26,10 @@ const EditModal = ({
content, content,
handleCancel, handleCancel,
visible, visible,
socketMessage,
}: { }: {
treeData?: any; treeData?: any;
content?: string; content?: string;
visible: boolean; visible: boolean;
socketMessage: any;
currentNode: any; currentNode: any;
handleCancel: () => void; handleCancel: () => void;
}) => { }) => {
@@ -34,12 +40,11 @@ const EditModal = ({
const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false); const [saveModalVisible, setSaveModalVisible] = useState<boolean>(false);
const [settingModalVisible, setSettingModalVisible] = const [settingModalVisible, setSettingModalVisible] =
useState<boolean>(false); useState<boolean>(false);
const [log, setLog] = useState<string>(''); const [log, setLog] = useState('');
const { theme } = useTheme(); const { theme } = useTheme();
const editorRef = useRef<any>(null); const editorRef = useRef<any>(null);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [currentPid, setCurrentPid] = useState(null); const [currentPid, setCurrentPid] = useState(null);
const cancel = () => { const cancel = () => {
handleCancel(); handleCancel();
}; };
@@ -104,28 +109,25 @@ const EditModal = ({
}); });
}; };
useEffect(() => { const handleMessage = useCallback((payload: any) => {
if (!socketMessage) { let { message: _message } = payload;
return;
}
let { type, message: _message, references } = socketMessage;
if (type !== 'manuallyRunScript') {
return;
}
if (logEnded(_message)) { if (logEnded(_message)) {
setTimeout(() => { setTimeout(() => {
setIsRunning(false); setIsRunning(false);
}, 300); }, 300);
} }
if (log) { setLog((p) => `${p}${_message}`);
_message = `\n${_message}`; }, []);
}
setLog(`${log}${_message}`); useEffect(() => {
}, [socketMessage]); const ws = WebSocketManager.getInstance();
ws.subscribe('manuallyRunScript', handleMessage);
return () => {
ws.unsubscribe('manuallyRunScript', handleMessage);
};
}, []);
useEffect(() => { useEffect(() => {
setLog(''); setLog('');
@@ -224,6 +226,7 @@ const EditModal = ({
minSize={200} minSize={200}
defaultSize="50%" defaultSize="50%"
style={{ height: 'calc(100vh - 55px)' }} style={{ height: 'calc(100vh - 55px)' }}
pane2Style={{ overflowY: 'auto' }}
> >
<Editor <Editor
language={language} language={language}
@@ -241,11 +244,10 @@ const EditModal = ({
/> />
<pre <pre
style={{ style={{
height: '100%',
padding: '0 15px', padding: '0 15px',
}} }}
> >
{log} <Ansi>{log}</Ansi>
</pre> </pre>
</SplitPane> </SplitPane>
<SaveModal <SaveModal
+11 -15
View File
@@ -20,7 +20,7 @@ import Editor from '@monaco-editor/react';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import styles from './index.module.less'; import styles from './index.module.less';
import EditModal from './editModal'; import EditModal from './editModal';
import { Controlled as CodeMirror } from 'react-codemirror2'; import CodeMirror from '@uiw/react-codemirror';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { import {
DeleteOutlined, DeleteOutlined,
@@ -43,11 +43,12 @@ import useFilterTreeData from '@/hooks/useFilterTreeData';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import IconFont from '@/components/iconfont'; import IconFont from '@/components/iconfont';
import RenameModal from './renameModal'; import RenameModal from './renameModal';
import { langs } from '@uiw/codemirror-extensions-langs';
const { Text } = Typography; const { Text } = Typography;
const Script = () => { const Script = () => {
const { headerStyle, isPhone, theme, socketMessage } = const { headerStyle, isPhone, theme } =
useOutletContext<SharedContext>(); useOutletContext<SharedContext>();
const [value, setValue] = useState(intl.get('请选择脚本文件')); const [value, setValue] = useState(intl.get('请选择脚本文件'));
const [select, setSelect] = useState<string>(''); const [select, setSelect] = useState<string>('');
@@ -580,30 +581,25 @@ const Script = () => {
{isPhone && ( {isPhone && (
<CodeMirror <CodeMirror
value={value} value={value}
options={{ extensions={
lineNumbers: true, mode ? [langs[mode as keyof typeof langs]()] : undefined
lineWrapping: true, }
styleActiveLine: true, theme={theme.includes('dark') ? 'dark' : 'light'}
matchBrackets: true, readOnly={!isEditing}
mode, onChange={(value) => {
readOnly: !isEditing,
}}
onBeforeChange={(editor, data, value) => {
setValue(value); setValue(value);
}} }}
onChange={(editor, data, value) => {}}
/> />
)} )}
<EditModal {isLogModalVisible && <EditModal
visible={isLogModalVisible} visible={isLogModalVisible}
treeData={data} treeData={data}
currentNode={currentNode} currentNode={currentNode}
content={value} content={value}
socketMessage={socketMessage}
handleCancel={() => { handleCancel={() => {
setIsLogModalVisible(false); setIsLogModalVisible(false);
}} }}
/> />}
<EditScriptNameModal <EditScriptNameModal
visible={isAddFileModalVisible} visible={isAddFileModalVisible}
treeData={data} treeData={data}
+4 -6
View File
@@ -7,11 +7,6 @@ import dayjs from 'dayjs';
const { Link } = Typography; const { Link } = Typography;
enum TVersion {
'develop' = '开发版',
'master' = '正式版',
}
const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => { const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
@@ -29,7 +24,10 @@ const About = ({ systemInfo }: { systemInfo: SharedContext['systemInfo'] }) => {
</span> </span>
<Descriptions> <Descriptions>
<Descriptions.Item label={intl.get('版本')} span={3}> <Descriptions.Item label={intl.get('版本')} span={3}>
{intl.get(TVersion[systemInfo.branch])} v{systemInfo.version} {systemInfo?.branch === 'develop'
? intl.get('开发版')
: intl.get('正式版')}{' '}
v{systemInfo.version}
</Descriptions.Item> </Descriptions.Item>
<Descriptions.Item label={intl.get('更新时间')} span={3}> <Descriptions.Item label={intl.get('更新时间')} span={3}>
{dayjs(systemInfo.publishTime * 1000).format('YYYY-MM-DD HH:mm')} {dayjs(systemInfo.publishTime * 1000).format('YYYY-MM-DD HH:mm')}
+33 -48
View File
@@ -1,12 +1,14 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { Statistic, Modal, Tag, Button, Spin, message } from 'antd'; import { Statistic, Modal, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import WebSocketManager from '@/utils/websocket';
import Ansi from 'ansi-to-react';
const { Countdown } = Statistic; const { Countdown } = Statistic;
const CheckUpdate = ({ socketMessage, systemInfo }: any) => { const CheckUpdate = ({ systemInfo }: any) => {
const [updateLoading, setUpdateLoading] = useState(false); const [updateLoading, setUpdateLoading] = useState(false);
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const modalRef = useRef<any>(); const modalRef = useRef<any>();
@@ -75,16 +77,7 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
</div> </div>
</> </>
), ),
content: ( content: <pre><Ansi>{lastLog}</Ansi></pre>,
<pre
style={{
fontSize: 12,
fontWeight: 400,
}}
>
{lastLog}
</pre>
),
okText: intl.get('下载更新'), okText: intl.get('下载更新'),
cancelText: intl.get('以后再说'), cancelText: intl.get('以后再说'),
onOk() { onOk() {
@@ -109,16 +102,7 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
okButtonProps: { disabled: true }, okButtonProps: { disabled: true },
title: intl.get('下载更新中...'), title: intl.get('下载更新中...'),
centered: true, centered: true,
content: ( content: <pre><Ansi>{value}</Ansi></pre>,
<pre
style={{
fontSize: 12,
fontWeight: 400,
}}
>
{value}
</pre>
),
}); });
}; };
@@ -167,17 +151,8 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
}; };
useEffect(() => { useEffect(() => {
if (!modalRef.current || !socketMessage) { if (!value) return;
return; const updateFailed = value.includes('失败,请检查');
}
const { type, message: _message, references } = socketMessage;
if (type !== 'updateSystemVersion') {
return;
}
const newMessage = `${value}${_message}`;
const updateFailed = newMessage.includes(intl.get('失败'));
modalRef.current.update({ modalRef.current.update({
maskClosable: updateFailed, maskClosable: updateFailed,
@@ -185,36 +160,46 @@ const CheckUpdate = ({ socketMessage, systemInfo }: any) => {
okButtonProps: { disabled: !updateFailed }, okButtonProps: { disabled: !updateFailed },
content: ( content: (
<> <>
<pre <pre>
style={{ <Ansi>{value}</Ansi>
fontSize: 12,
fontWeight: 400,
}}
>
{newMessage}
</pre> </pre>
<div id="log-identifier" style={{ paddingBottom: 5 }}></div> <div id="log-identifier" style={{ paddingBottom: 5 }}></div>
</> </>
), ),
}); });
}, [value]);
if (updateFailed && !value.includes(intl.get('失败,请检查'))) { const handleMessage = useCallback((payload: any) => {
let { message: _message } = payload;
const updateFailed = _message.includes('失败,请检查');
if (updateFailed) {
message.error(intl.get('更新失败,请检查网络及日志或稍后再试')); message.error(intl.get('更新失败,请检查网络及日志或稍后再试'));
} }
setValue(newMessage); setTimeout(() => {
document.getElementById('log-identifier') &&
document document
.getElementById('log-identifier')! .querySelector('#log-identifier')!
.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); .scrollIntoView({ behavior: 'smooth' });
}, 600);
if (_message.includes(intl.get('更新包下载成功'))) { if (_message.includes('更新包下载成功')) {
setTimeout(() => { setTimeout(() => {
showReloadModal(); showReloadModal();
}, 1000); }, 1000);
} }
}, [socketMessage]);
setValue((p) => `${p}${_message}`);
}, []);
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('updateSystemVersion', handleMessage);
return () => {
ws.unsubscribe('updateSystemVersion', handleMessage);
};
}, []);
return ( return (
<> <>
+37 -4
View File
@@ -1,5 +1,5 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { import {
Button, Button,
InputNumber, InputNumber,
@@ -32,6 +32,8 @@ import About from './about';
import { useOutletContext } from '@umijs/max'; import { useOutletContext } from '@umijs/max';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import './index.less'; import './index.less';
import useResizeObserver from '@react-hook/resize-observer';
import SystemLog from './systemLog';
const { Text } = Typography; const { Text } = Typography;
const isDemoEnv = window.__ENV__DeployEnv === 'demo'; const isDemoEnv = window.__ENV__DeployEnv === 'demo';
@@ -41,9 +43,9 @@ const Setting = () => {
headerStyle, headerStyle,
isPhone, isPhone,
user, user,
theme,
reloadUser, reloadUser,
reloadTheme, reloadTheme,
socketMessage,
systemInfo, systemInfo,
} = useOutletContext<SharedContext>(); } = useOutletContext<SharedContext>();
const columns = [ const columns = [
@@ -113,7 +115,17 @@ const Setting = () => {
const [editedApp, setEditedApp] = useState<any>(); const [editedApp, setEditedApp] = useState<any>();
const [tabActiveKey, setTabActiveKey] = useState('security'); const [tabActiveKey, setTabActiveKey] = useState('security');
const [loginLogData, setLoginLogData] = useState<any[]>([]); const [loginLogData, setLoginLogData] = useState<any[]>([]);
const [systemLogData, setSystemLogData] = useState<string>('');
const [notificationInfo, setNotificationInfo] = useState<any>(); const [notificationInfo, setNotificationInfo] = useState<any>();
const containergRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<number>(0);
useResizeObserver(containergRef, (entry) => {
const _height = entry.target.parentElement?.parentElement?.offsetHeight;
if (_height && height !== _height - 66) {
setHeight(_height - 66);
}
});
const getApps = () => { const getApps = () => {
setLoading(true); setLoading(true);
@@ -232,6 +244,19 @@ const Setting = () => {
}); });
}; };
const getSystemLog = () => {
request
.get<Blob>(`${config.apiPrefix}system/log`, {
responseType: 'blob',
})
.then(async (res) => {
setSystemLogData(await res.text());
})
.catch((error: any) => {
console.log(error);
});
};
const tabChange = (activeKey: string) => { const tabChange = (activeKey: string) => {
setTabActiveKey(activeKey); setTabActiveKey(activeKey);
if (activeKey === 'app') { if (activeKey === 'app') {
@@ -240,6 +265,8 @@ const Setting = () => {
getLoginLog(); getLoginLog();
} else if (activeKey === 'notification') { } else if (activeKey === 'notification') {
getNotification(); getNotification();
} else if (activeKey === 'syslog') {
getSystemLog();
} }
}; };
@@ -279,6 +306,7 @@ const Setting = () => {
: [] : []
} }
> >
<div ref={containergRef}>
<Tabs <Tabs
defaultActiveKey="security" defaultActiveKey="security"
size="small" size="small"
@@ -306,7 +334,7 @@ const Setting = () => {
dataSource={dataSource} dataSource={dataSource}
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 1000 }}
loading={loading} loading={loading}
/> />
), ),
@@ -316,6 +344,11 @@ const Setting = () => {
label: intl.get('通知设置'), label: intl.get('通知设置'),
children: <NotificationSetting data={notificationInfo} />, children: <NotificationSetting data={notificationInfo} />,
}, },
{
key: 'syslog',
label: intl.get('系统日志'),
children: <SystemLog data={systemLogData} height={height} theme={theme}/>,
},
{ {
key: 'login', key: 'login',
label: intl.get('登录日志'), label: intl.get('登录日志'),
@@ -327,7 +360,6 @@ const Setting = () => {
children: ( children: (
<Other <Other
reloadTheme={reloadTheme} reloadTheme={reloadTheme}
socketMessage={socketMessage}
systemInfo={systemInfo} systemInfo={systemInfo}
/> />
), ),
@@ -339,6 +371,7 @@ const Setting = () => {
}, },
]} ]}
></Tabs> ></Tabs>
</div>
<AppModal <AppModal
visible={isModalVisible} visible={isModalVisible}
handleCancel={handleCancel} handleCancel={handleCancel}
+4 -3
View File
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import { Typography, Table, Tag, Button, Spin, message } from 'antd'; import { Typography, Table, Tag, Button, Spin, message } from 'antd';
import { request } from '@/utils/http'; import { request } from '@/utils/http';
import config from '@/utils/config'; import config from '@/utils/config';
import dayjs from 'dayjs';
const { Text, Link } = Typography; const { Text, Link } = Typography;
@@ -19,7 +20,7 @@ enum LoginStatusColor {
const columns = [ const columns = [
{ {
title: intl.get('序号'), title: intl.get('序号'),
width: 40, width: 50,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
return index + 1; return index + 1;
}, },
@@ -30,7 +31,7 @@ const columns = [
key: 'timestamp', key: 'timestamp',
width: 120, width: 120,
render: (text: string, record: any) => { render: (text: string, record: any) => {
return new Date(record.timestamp).toLocaleString(); return dayjs(record.timestamp).format('YYYY-MM-DD HH:mm:ss');
}, },
}, },
{ {
@@ -75,7 +76,7 @@ const LoginLog = ({ data }: any) => {
dataSource={data} dataSource={data}
rowKey="id" rowKey="id"
size="middle" size="middle"
scroll={{ x: 768 }} scroll={{ x: 1000 }}
sticky sticky
/> />
</> </>
+3 -3
View File
@@ -76,7 +76,7 @@ const NotificationSetting = ({ data }: any) => {
> >
{x.items ? ( {x.items ? (
<Select <Select
placeholder={x.placeholder || `请选择${x.label}`} placeholder={x.placeholder || `${intl.get('请选择')} ${x.label}`}
disabled={loading} disabled={loading}
> >
{x.items.map((y) => ( {x.items.map((y) => (
@@ -88,8 +88,8 @@ const NotificationSetting = ({ data }: any) => {
) : ( ) : (
<Input.TextArea <Input.TextArea
disabled={loading} disabled={loading}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={x.placeholder || `请输入${x.label}`} placeholder={x.placeholder || `${intl.get('请输入')} ${x.label}`}
/> />
)} )}
</Form.Item> </Form.Item>
+51 -16
View File
@@ -9,7 +9,7 @@ import {
Input, Input,
Upload, Upload,
Modal, Modal,
Progress, Select,
} from 'antd'; } from 'antd';
import * as DarkReader from '@umijs/ssr-darkreader'; import * as DarkReader from '@umijs/ssr-darkreader';
import config from '@/utils/config'; import config from '@/utils/config';
@@ -22,17 +22,10 @@ import { UploadOutlined } from '@ant-design/icons';
import Countdown from 'antd/lib/statistic/Countdown'; import Countdown from 'antd/lib/statistic/Countdown';
import useProgress from './progress'; import useProgress from './progress';
const optionsWithDisabled = [
{ label: intl.get('亮色'), value: 'light' },
{ label: intl.get('暗色'), value: 'dark' },
{ label: intl.get('跟随系统'), value: 'auto' },
];
const Other = ({ const Other = ({
systemInfo, systemInfo,
socketMessage,
reloadTheme, reloadTheme,
}: Pick<SharedContext, 'socketMessage' | 'reloadTheme' | 'systemInfo'>) => { }: Pick<SharedContext, 'reloadTheme' | 'systemInfo'>) => {
const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto'; const defaultTheme = localStorage.getItem('qinglong_dark_theme') || 'auto';
const [systemConfig, setSystemConfig] = useState<{ const [systemConfig, setSystemConfig] = useState<{
logRemoveFrequency?: number | null; logRemoveFrequency?: number | null;
@@ -67,6 +60,13 @@ const Other = ({
reloadTheme(); reloadTheme();
}; };
const handleLangChange = (v: string) => {
localStorage.setItem('lang', v);
setTimeout(() => {
window.location.reload();
}, 500);
};
const getSystemConfig = () => { const getSystemConfig = () => {
request request
.get(`${config.apiPrefix}system/config`) .get(`${config.apiPrefix}system/config`)
@@ -167,12 +167,27 @@ const Other = ({
initialValue={defaultTheme} initialValue={defaultTheme}
> >
<Radio.Group <Radio.Group
options={optionsWithDisabled}
onChange={themeChange} onChange={themeChange}
value={defaultTheme} value={defaultTheme}
optionType="button" optionType="button"
buttonStyle="solid" buttonStyle="solid"
/> >
<Radio.Button
value="light"
style={{ width: 70, textAlign: 'center' }}
>
{intl.get('亮色')}
</Radio.Button>
<Radio.Button value="dark" style={{ width: 66, textAlign: 'center' }}>
{intl.get('暗色')}
</Radio.Button>
<Radio.Button
value="auto"
style={{ width: 129, textAlign: 'center' }}
>
{intl.get('跟随系统')}
</Radio.Button>
</Radio.Group>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={intl.get('日志删除频率')} label={intl.get('日志删除频率')}
@@ -190,7 +205,11 @@ const Other = ({
setSystemConfig({ ...systemConfig, logRemoveFrequency: value }); setSystemConfig({ ...systemConfig, logRemoveFrequency: value });
}} }}
/> />
<Button type="primary" onClick={updateSystemConfig}> <Button
type="primary"
onClick={updateSystemConfig}
style={{ width: 84 }}
>
{intl.get('确认')} {intl.get('确认')}
</Button> </Button>
</Input.Group> </Input.Group>
@@ -198,18 +217,34 @@ const Other = ({
<Form.Item label={intl.get('定时任务并发数')} name="frequency"> <Form.Item label={intl.get('定时任务并发数')} name="frequency">
<Input.Group compact> <Input.Group compact>
<InputNumber <InputNumber
style={{ width: 150 }} style={{ width: 180 }}
min={1} min={1}
value={systemConfig?.cronConcurrency} value={systemConfig?.cronConcurrency}
onChange={(value) => { onChange={(value) => {
setSystemConfig({ ...systemConfig, cronConcurrency: value }); setSystemConfig({ ...systemConfig, cronConcurrency: value });
}} }}
/> />
<Button type="primary" onClick={updateSystemConfig}> <Button
type="primary"
onClick={updateSystemConfig}
style={{ width: 84 }}
>
{intl.get('确认')} {intl.get('确认')}
</Button> </Button>
</Input.Group> </Input.Group>
</Form.Item> </Form.Item>
<Form.Item label={intl.get('语言')} name="lang">
<Select
defaultValue={localStorage.getItem('lang') || ''}
style={{ width: 264 }}
onChange={handleLangChange}
options={[
{ value: '', label: intl.get('跟随系统') },
{ value: 'zh', label: '简体中文' },
{ value: 'en', label: 'English' },
]}
/>
</Form.Item>
<Form.Item label={intl.get('数据备份还原')} name="frequency"> <Form.Item label={intl.get('数据备份还原')} name="frequency">
<Button type="primary" onClick={exportData} loading={exportLoading}> <Button type="primary" onClick={exportData} loading={exportLoading}>
{exportLoading ? intl.get('生成数据中...') : intl.get('备份')} {exportLoading ? intl.get('生成数据中...') : intl.get('备份')}
@@ -218,7 +253,7 @@ const Other = ({
method="put" method="put"
showUploadList={false} showUploadList={false}
maxCount={1} maxCount={1}
action="/api/system/data/import" action={`${config.apiPrefix}system/data/import`}
onChange={(e) => { onChange={(e) => {
if (e.event?.percent) { if (e.event?.percent) {
showUploadProgress(parseFloat(e.event?.percent.toFixed(1))); showUploadProgress(parseFloat(e.event?.percent.toFixed(1)));
@@ -238,7 +273,7 @@ const Other = ({
</Upload> </Upload>
</Form.Item> </Form.Item>
<Form.Item label={intl.get('检查更新')} name="update"> <Form.Item label={intl.get('检查更新')} name="update">
<CheckUpdate systemInfo={systemInfo} socketMessage={socketMessage} /> <CheckUpdate systemInfo={systemInfo} />
</Form.Item> </Form.Item>
</Form> </Form>
); );
+3 -2
View File
@@ -1,3 +1,4 @@
import intl from 'react-intl-universal';
import { Modal, Progress } from 'antd'; import { Modal, Progress } from 'antd';
import { useRef } from 'react'; import { useRef } from 'react';
@@ -15,14 +16,14 @@ export default function useProgress(title: string) {
const showProgress = (percent: number) => { const showProgress = (percent: number) => {
if (modalRef.current) { if (modalRef.current) {
modalRef.current.update({ modalRef.current.update({
title: `${title}${percent >= 100 ? '成功' : '中...'}`, title: `${title}${percent >= 100 ? intl.get('成功') : intl.get('中...')}`,
content: <ProgressElement percent={percent} />, content: <ProgressElement percent={percent} />,
}); });
} else { } else {
modalRef.current = Modal.info({ modalRef.current = Modal.info({
width: 600, width: 600,
maskClosable: false, maskClosable: false,
title: `${title}${percent >= 100 ? '成功' : '中...'}`, title: `${title}${percent >= 100 ? intl.get('成功') : intl.get('中...')}`,
centered: true, centered: true,
content: <ProgressElement percent={percent} />, content: <ProgressElement percent={percent} />,
}); });
+11 -5
View File
@@ -97,14 +97,16 @@ const SecuritySettings = ({ user, userChange }: any) => {
const onChange = (e) => { const onChange = (e) => {
if (e.file && e.file.response) { if (e.file && e.file.response) {
setAvatar(`/api/static/${e.file.response.data}`); setAvatar(
`${config.apiPrefix}static/${e.file.response.data}`,
);
userChange(); userChange();
} }
}; };
useEffect(() => { useEffect(() => {
setTwoFactorActivated(user && user.twoFactorActivated); setTwoFactorActivated(user && user.twoFactorActivated);
setAvatar(user.avatar && `/api/static/${user.avatar}`); setAvatar(user.avatar && `${config.apiPrefix}static/${user.avatar}`);
}, [user]); }, [user]);
return twoFactoring ? ( return twoFactoring ? (
@@ -188,7 +190,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
hasFeedback hasFeedback
style={{ maxWidth: 300 }} style={{ maxWidth: 300 }}
> >
<Input placeholder={intl.get('用户名')} /> <Input autoComplete="username" placeholder={intl.get('用户名')} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label={intl.get('密码')} label={intl.get('密码')}
@@ -203,7 +205,11 @@ const SecuritySettings = ({ user, userChange }: any) => {
hasFeedback hasFeedback
style={{ maxWidth: 300 }} style={{ maxWidth: 300 }}
> >
<Input type="password" placeholder={intl.get('密码')} /> <Input
type="password"
autoComplete="current-password"
placeholder={intl.get('密码')}
/>
</Form.Item> </Form.Item>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
{intl.get('保存')} {intl.get('保存')}
@@ -246,7 +252,7 @@ const SecuritySettings = ({ user, userChange }: any) => {
method="put" method="put"
showUploadList={false} showUploadList={false}
maxCount={1} maxCount={1}
action="/api/user/avatar" action={`${config.apiPrefix}user/avatar`}
onChange={onChange} onChange={onChange}
name="avatar" name="avatar"
headers={{ headers={{
+58
View File
@@ -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;
+25 -23
View File
@@ -1,5 +1,5 @@
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef, useCallback } from 'react';
import { import {
Button, Button,
message, message,
@@ -36,6 +36,7 @@ import './index.less';
import SubscriptionLogModal from './logModal'; import SubscriptionLogModal from './logModal';
import { SharedContext } from '@/layouts'; import { SharedContext } from '@/layouts';
import useTableScrollHeight from '@/hooks/useTableScrollHeight'; import useTableScrollHeight from '@/hooks/useTableScrollHeight';
import WebSocketManager from '@/utils/websocket';
const { Text, Paragraph } = Typography; const { Text, Paragraph } = Typography;
const { Search } = Input; const { Search } = Input;
@@ -61,8 +62,7 @@ export enum SubscriptionType {
} }
const Subscription = () => { const Subscription = () => {
const { headerStyle, isPhone, socketMessage } = const { headerStyle, isPhone } = useOutletContext<SharedContext>();
useOutletContext<SharedContext>();
const columns: any = [ const columns: any = [
{ {
@@ -184,45 +184,39 @@ const Subscription = () => {
{ {
title: intl.get('操作'), title: intl.get('操作'),
key: 'action', key: 'action',
width: 130, width: 140,
render: (text: string, record: any, index: number) => { render: (text: string, record: any, index: number) => {
const isPc = !isPhone; const isPc = !isPhone;
return ( return (
<Space size="middle"> <Space size="middle">
{record.status === SubscriptionStatus.idle && ( {record.status === SubscriptionStatus.idle && (
<Tooltip title={isPc ? intl.get('运行') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
runSubscription(record, index); runSubscription(record, index);
}} }}
> >
<PlayCircleOutlined /> {intl.get('运行')}
</a> </a>
</Tooltip>
)} )}
{record.status !== SubscriptionStatus.idle && ( {record.status !== SubscriptionStatus.idle && (
<Tooltip title={isPc ? intl.get('停止') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
stopSubsciption(record, index); stopSubsciption(record, index);
}} }}
> >
<PauseCircleOutlined /> {intl.get('停止')}
</a> </a>
</Tooltip>
)} )}
<Tooltip title={isPc ? intl.get('日志') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setLogSubscription({ ...record, timestamp: Date.now() }); setLogSubscription({ ...record, timestamp: Date.now() });
}} }}
> >
<FileTextOutlined /> {intl.get('日志')}
</a> </a>
</Tooltip>
<MoreBtn key="more" record={record} index={index} /> <MoreBtn key="more" record={record} index={index} />
</Space> </Space>
); );
@@ -514,23 +508,31 @@ const Subscription = () => {
: 'subscription'; : 'subscription';
}; };
useEffect(() => { const handleMessage = useCallback((payload: any) => {
if (!socketMessage) return; const { message, references } = payload;
const { type, message, references } = socketMessage; setValue((p) => {
if (type === 'runSubscriptionEnd' && references.length > 0) { const result = [...p];
const result = [...value];
for (let i = 0; i < references.length; i++) { for (let i = 0; i < references.length; i++) {
const index = value.findIndex((x) => x.id === references[i]); const index = p.findIndex((x) => x.id === references[i]);
if (index !== -1) { if (index !== -1) {
result.splice(index, 1, { result.splice(index, 1, {
...value[index], ...p[index],
status: SubscriptionStatus.idle, status: SubscriptionStatus.idle,
}); });
} }
} }
setValue(result); return result;
} });
}, [socketMessage]); }, []);
useEffect(() => {
const ws = WebSocketManager.getInstance();
ws.subscribe('runSubscriptionEnd', handleMessage);
return () => {
ws.unsubscribe('runSubscriptionEnd', handleMessage);
};
}, []);
useEffect(() => { useEffect(() => {
if (logSubscription) { if (logSubscription) {
+4 -5
View File
@@ -9,6 +9,7 @@ import {
} from '@ant-design/icons'; } from '@ant-design/icons';
import { PageLoading } from '@ant-design/pro-layout'; import { PageLoading } from '@ant-design/pro-layout';
import { logEnded } from '@/utils'; import { logEnded } from '@/utils';
import Ansi from 'ansi-to-react';
const SubscriptionLogModal = ({ const SubscriptionLogModal = ({
subscription, subscription,
@@ -99,9 +100,6 @@ const SubscriptionLogModal = ({
open={visible} open={visible}
centered centered
className="log-modal" className="log-modal"
bodyStyle={{
minHeight: '300px',
}}
forceRender forceRender
onOk={() => cancel()} onOk={() => cancel()}
onCancel={() => cancel()} onCancel={() => cancel()}
@@ -111,6 +109,7 @@ const SubscriptionLogModal = ({
</Button>, </Button>,
]} ]}
> >
<div className="log-container">
{loading ? ( {loading ? (
<PageLoading /> <PageLoading />
) : ( ) : (
@@ -119,15 +118,15 @@ const SubscriptionLogModal = ({
isPhone isPhone
? { ? {
fontFamily: 'Source Code Pro', fontFamily: 'Source Code Pro',
width: 375,
zoom: 0.83, zoom: 0.83,
} }
: {} : {}
} }
> >
{value} <Ansi>{value}</Ansi>
</pre> </pre>
)} )}
</div>
</Modal> </Modal>
); );
}; };
+23 -13
View File
@@ -241,10 +241,9 @@ const SubscriptionModal = ({
const onNamePaste = useCallback((e) => { const onNamePaste = useCallback((e) => {
const text = e.clipboardData.getData('text') as string; const text = e.clipboardData.getData('text') as string;
if (text.startsWith('ql ')) { if (text.includes('ql repo') || text.includes('ql raw')) {
e.preventDefault(); e.preventDefault();
} }
onPaste(e);
}, []); }, []);
const formatParams = (sub) => { const formatParams = (sub) => {
@@ -327,10 +326,9 @@ const SubscriptionModal = ({
]} ]}
> >
<Input.TextArea <Input.TextArea
rows={4} autoSize={{ minRows: 1, maxRows: 5 }}
autoSize={true}
placeholder={intl.get('请输入订阅链接')} placeholder={intl.get('请输入订阅链接')}
onPaste={onUrlChange} onPaste={onNamePaste}
onChange={onUrlChange} onChange={onUrlChange}
/> />
</Form.Item> </Form.Item>
@@ -338,7 +336,7 @@ const SubscriptionModal = ({
<Form.Item name="branch" label={intl.get('分支')}> <Form.Item name="branch" label={intl.get('分支')}>
<Input <Input
placeholder={intl.get('请输入分支')} placeholder={intl.get('请输入分支')}
onPaste={onBranchChange} onPaste={onNamePaste}
onChange={onBranchChange} onChange={onBranchChange}
/> />
</Form.Item> </Form.Item>
@@ -401,7 +399,10 @@ const SubscriptionModal = ({
{scheduleType === 'interval' ? ( {scheduleType === 'interval' ? (
<IntervalSelect /> <IntervalSelect />
) : ( ) : (
<Input placeholder={intl.get('秒(可选) 分 时 天 月 周')} /> <Input
onPaste={onNamePaste}
placeholder={intl.get('秒(可选) 分 时 天 月 周')}
/>
)} )}
</Form.Item> </Form.Item>
{type !== 'file' && ( {type !== 'file' && (
@@ -413,10 +414,11 @@ const SubscriptionModal = ({
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get( placeholder={intl.get(
'请输入脚本筛选白名单关键词,多个关键词竖线分割', '请输入脚本筛选白名单关键词,多个关键词竖线分割',
)} )}
onPaste={onNamePaste}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -426,10 +428,11 @@ const SubscriptionModal = ({
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get( placeholder={intl.get(
'请输入脚本筛选黑名单关键词,多个关键词竖线分割', '请输入脚本筛选黑名单关键词,多个关键词竖线分割',
)} )}
onPaste={onNamePaste}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -439,10 +442,11 @@ const SubscriptionModal = ({
> >
<Input.TextArea <Input.TextArea
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get( placeholder={intl.get(
'请输入脚本依赖文件关键词,多个关键词竖线分割', '请输入脚本依赖文件关键词,多个关键词竖线分割',
)} )}
onPaste={onNamePaste}
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@@ -452,7 +456,10 @@ const SubscriptionModal = ({
'仓库需要拉取的文件后缀,多个后缀空格分隔,默认使用配置文件中的RepoFileExtensions', '仓库需要拉取的文件后缀,多个后缀空格分隔,默认使用配置文件中的RepoFileExtensions',
)} )}
> >
<Input placeholder={intl.get('请输入文件后缀')} /> <Input
onPaste={onNamePaste}
placeholder={intl.get('请输入文件后缀')}
/>
</Form.Item> </Form.Item>
<Form.Item <Form.Item
name="sub_before" name="sub_before"
@@ -462,8 +469,9 @@ const SubscriptionModal = ({
)} )}
> >
<Input.TextArea <Input.TextArea
onPaste={onNamePaste}
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get('请输入运行订阅前要执行的命令')} placeholder={intl.get('请输入运行订阅前要执行的命令')}
/> />
</Form.Item> </Form.Item>
@@ -475,8 +483,9 @@ const SubscriptionModal = ({
)} )}
> >
<Input.TextArea <Input.TextArea
onPaste={onNamePaste}
rows={4} rows={4}
autoSize={true} autoSize={{ minRows: 1, maxRows: 5 }}
placeholder={intl.get('请输入运行订阅后要执行的命令')} placeholder={intl.get('请输入运行订阅后要执行的命令')}
/> />
</Form.Item> </Form.Item>
@@ -490,6 +499,7 @@ const SubscriptionModal = ({
)} )}
> >
<Input <Input
onPaste={onNamePaste}
placeholder={ placeholder={
type === 'private-repo' type === 'private-repo'
? 'SOCK5代理,例如 IP:PORT' ? 'SOCK5代理,例如 IP:PORT'
+71 -20
View File
@@ -98,6 +98,7 @@ export default {
{ value: 'email', label: intl.get('邮箱') }, { value: 'email', label: intl.get('邮箱') },
{ value: 'lark', label: intl.get('飞书机器人') }, { value: 'lark', label: intl.get('飞书机器人') },
{ value: 'pushMe', label: 'PushMe' }, { value: 'pushMe', label: 'PushMe' },
{ value: 'chronocat', label: 'Chronocat' },
{ value: 'webhook', label: intl.get('自定义通知') }, { value: 'webhook', label: intl.get('自定义通知') },
{ value: 'closed', label: intl.get('已关闭') }, { value: 'closed', label: intl.get('已关闭') },
], ],
@@ -126,14 +127,16 @@ export default {
goCqHttpBot: [ goCqHttpBot: [
{ {
label: 'goCqHttpBotUrl', label: 'goCqHttpBotUrl',
tip: intl.get('推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg', tip: intl.get(
'推送到个人QQ: http://127.0.0.1/send_private_msg,群:http://127.0.0.1/send_group_msg',
), ),
required: true, required: true,
}, },
{ label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true }, { label: 'goCqHttpBotToken', tip: intl.get('访问密钥'), required: true },
{ {
label: 'goCqHttpBotQq', label: 'goCqHttpBotQq',
tip: intl.get('如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群', tip: intl.get(
'如果GOBOT_URL设置 /send_private_msg 则需要填入 user_id=个人QQ 相反如果是 /send_group_msg 则需要填入 group_id=QQ群',
), ),
required: true, required: true,
}, },
@@ -153,14 +156,16 @@ export default {
}, },
{ {
label: 'pushDeerUrl', label: 'pushDeerUrl',
tip: intl.get('PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push', tip: intl.get(
'PushDeer的自架API endpoint,默认是 https://api2.pushdeer.com/message/push',
), ),
}, },
], ],
bark: [ bark: [
{ {
label: 'barkPush', label: 'barkPush',
tip: intl.get('Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX', tip: intl.get(
'Bark的信息IP/设备码,例如:https://api.day.app/XXXXXXXX',
), ),
required: true, required: true,
}, },
@@ -174,13 +179,22 @@ export default {
}, },
{ {
label: 'barkGroup', label: 'barkGroup',
tip: intl.get('BARK推送消息的分组, 默认为qinglong'), tip: intl.get('BARK推送消息的分组默认为qinglong'),
},
{
label: 'barkLevel',
tip: intl.get('BARK推送消息的时效性,默认为active'),
},
{
label: 'barkUrl',
tip: intl.get('BARK推送消息的跳转URL'),
}, },
], ],
telegramBot: [ telegramBot: [
{ {
label: 'telegramBotToken', label: 'telegramBotToken',
tip: intl.get('telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw', tip: intl.get(
'telegram机器人的token,例如:1077xxx4424:AAFjv0FcqxxxxxxgEMGfi22B4yh15R5uw',
), ),
required: true, required: true,
}, },
@@ -193,7 +207,8 @@ export default {
{ label: 'telegramBotProxyPort', tip: intl.get('代理端口') }, { label: 'telegramBotProxyPort', tip: intl.get('代理端口') },
{ {
label: 'telegramBotProxyAuth', label: 'telegramBotProxyAuth',
tip: intl.get('telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password', tip: intl.get(
'telegram代理配置认证参数,用户名与密码用英文冒号连接 user:password',
), ),
}, },
{ {
@@ -204,20 +219,23 @@ export default {
dingtalkBot: [ dingtalkBot: [
{ {
label: 'dingtalkBotToken', label: 'dingtalkBotToken',
tip: intl.get('钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd', tip: intl.get(
'钉钉机器人webhook token,例如:5a544165465465645d0f31dca676e7bd07415asdasd',
), ),
required: true, required: true,
}, },
{ {
label: 'dingtalkBotSecret', label: 'dingtalkBotSecret',
tip: intl.get('密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串', tip: intl.get(
'密钥,机器人安全设置页面,加签一栏下面显示的SEC开头的字符串',
), ),
}, },
], ],
weWorkBot: [ weWorkBot: [
{ {
label: 'weWorkBotKey', label: 'weWorkBotKey',
tip: intl.get('企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa', tip: intl.get(
'企业微信机器人的webhook(详见文档 https://work.weixin.qq.com/api/doc/90000/90136/91770),例如:693a91f6-7xxx-4bc4-97a0-0ec2sifa5aaa',
), ),
required: true, required: true,
}, },
@@ -229,7 +247,8 @@ export default {
weWorkApp: [ weWorkApp: [
{ {
label: 'weWorkAppKey', label: 'weWorkAppKey',
tip: intl.get('corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat', tip: intl.get(
'corpid、corpsecret、touser(注:多个成员ID使用|隔开)、agentid、消息类型(选填,不填默认文本消息类型) 注意用,号隔开(英文输入法的逗号),例如:wwcfrs,B-76WERQ,qinglong,1000001,2COat',
), ),
required: true, required: true,
}, },
@@ -241,7 +260,8 @@ export default {
aibotk: [ aibotk: [
{ {
label: 'aibotkKey', label: 'aibotkKey',
tip: intl.get('密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql', tip: intl.get(
'密钥key,智能微秘书个人中心获取apikey,申请地址:https://wechat.aibotk.com/signup?from=ql',
), ),
required: true, required: true,
}, },
@@ -257,7 +277,8 @@ export default {
}, },
{ {
label: 'aibotkName', label: 'aibotkName',
tip: intl.get('要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称', tip: intl.get(
'要发送的用户昵称或群名,如果目标是群,需要填群名,如果目标是好友,需要填好友昵称',
), ),
required: true, required: true,
}, },
@@ -265,7 +286,8 @@ export default {
iGot: [ iGot: [
{ {
label: 'iGotPushKey', label: 'iGotPushKey',
tip: intl.get('iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX', tip: intl.get(
'iGot的信息推送key,例如:https://push.hellyw.com/XXXXXXXX',
), ),
required: true, required: true,
}, },
@@ -273,20 +295,23 @@ export default {
pushPlus: [ pushPlus: [
{ {
label: 'pushPlusToken', label: 'pushPlusToken',
tip: intl.get('微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/', tip: intl.get(
'微信扫码登录后一对一推送或一对多推送下面的token(您的Token),不提供PUSH_PLUS_USER则默认为一对一推送,参考 https://www.pushplus.plus/',
), ),
required: true, required: true,
}, },
{ {
label: 'pushPlusUser', label: 'pushPlusUser',
tip: intl.get('一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)', tip: intl.get(
'一对多推送的“群组编码”(一对多推送下面->您的群组(如无则创建)->群组编码,如果您是创建群组人。也需点击“查看二维码”扫描绑定,否则不能接受群组消息推送)',
), ),
}, },
], ],
lark: [ lark: [
{ {
label: 'larkKey', label: 'larkKey',
tip: intl.get('飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973', tip: intl.get(
'飞书群组机器人:https://www.feishu.cn/hc/zh-CN/articles/360024984973',
), ),
required: true, required: true,
}, },
@@ -294,7 +319,8 @@ export default {
email: [ email: [
{ {
label: 'emailService', label: 'emailService',
tip: intl.get('邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/', tip: intl.get(
'邮箱服务名称,比如126、163、Gmail、QQ等,支持列表https://nodemailer.com/smtp/well-known/',
), ),
required: true, required: true,
}, },
@@ -308,6 +334,29 @@ export default {
required: true, required: true,
}, },
], ],
chronocat: [
{
label: 'chronocatURL',
tip: intl.get(
'Chronocat Red 服务的连接地址 https://chronocat.vercel.app/install/docker/official/',
),
required: true,
},
{
label: 'chronocatQQ',
tip: intl.get(
'个人:user_id=个人QQ 群则填入group_id=QQ群 多个用英文;隔开同时支持个人和群 如:user_id=xxx;group_id=xxxx;group_id=xxxxx',
),
required: true,
},
{
label: 'chronocatToken',
tip: intl.get(
'docker安装在持久化config目录下的chronocat.yml文件可找到',
),
required: true,
},
],
webhook: [ webhook: [
{ {
label: 'webhookMethod', label: 'webhookMethod',
@@ -327,7 +376,8 @@ export default {
}, },
{ {
label: 'webhookUrl', label: 'webhookUrl',
tip: intl.get('请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置', tip: intl.get(
'请求链接以http或者https开头。url或者body中必须包含$title$content可选,对应api内容的位置',
), ),
required: true, required: true,
placeholder: 'https://xxx.cn/api?content=$title\n', placeholder: 'https://xxx.cn/api?content=$title\n',
@@ -339,7 +389,8 @@ export default {
}, },
{ {
label: 'webhookBody', label: 'webhookBody',
tip: intl.get('请求体格式key1: value1,多个换行分割。url或者body中必须包含$title$content可选,对应api内容的位置', tip: intl.get(
'请求体格式key1: value1,多个换行分割。url或者body中必须包含$title$content可选,对应api内容的位置',
), ),
placeholder: 'key1: $title\nkey2: $content', placeholder: 'key1: $title\nkey2: $content',
}, },
+15 -2
View File
@@ -329,7 +329,7 @@ export function getCommandScript(
return [s, p]; return [s, p];
} }
export function parseCrontab(schedule: string): Date { export function parseCrontab(schedule: string): Date | null {
try { try {
const time = cron_parser.parseExpression(schedule); const time = cron_parser.parseExpression(schedule);
if (time) { if (time) {
@@ -337,7 +337,20 @@ export function parseCrontab(schedule: string): Date {
} }
} catch (error) { } } catch (error) { }
return new Date('1970'); return null;
}
export function getCrontabsNextDate(schedule: string, extra_schedules: string[]): Date | null {
let date = parseCrontab(schedule)
if (extra_schedules?.length) {
extra_schedules.forEach(x => {
const _date = parseCrontab(x)
if (_date && (!date || _date < date)) {
date = _date;
}
})
}
return date;
} }
export function getExtension(filename: string) { export function getExtension(filename: string) {
+8
View File
@@ -0,0 +1,8 @@
export type SockMessageType =
| 'ping'
| 'installDependence'
| 'uninstallDependence'
| 'updateSystemVersion'
| 'manuallyRunScript'
| 'runSubscriptionEnd'
| 'reloadSystem';

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