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,
+73 -56
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
@@ -192,4 +209,4 @@ The Green Dragon, also known as the Canglong, is one of the four elephants and o
In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star) In the Book of the Later Han Dynasty (後漢書-律曆志下), it is written: "The sun is in the sky, a cold and a summer, the four seasons are ready, all things are changed, the regency moves, and the green dragon moves to the star, which is called the year. (The Year of the Star)
Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers. Among the [twenty-eight Chinese constellations](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%8D%81%E5%85%AB%E5%AE%BF), the Green Dragon is the generic name for the seven eastern constellations (Horn, Hyper, Diao, Fang, Heart, Tail and Minchi). It is known in Taoism as "Mengzhang" and in different Taoist scriptures as "Dijun", "Shengjian", "Shenjian" and He is also known in different Daoist scriptures as "Dijun", "Shengjun", "Shenjun" and "Ghost Catcher"[1], and is the guardian deity of Daoism, together with the White Tiger Supervisor of Soldiers.
+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';
+24 -14
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 {
Logger.info('✌️ Dependency Injector loaded'); depInjectorLoader();
Logger.info('✌️ Dependency Injector loaded');
await expressLoader({ app: expressApp }); console.log('✌️ Dependency Injector loaded');
Logger.info('✌️ Express loaded');
expressLoader({ app: expressApp });
await initData(); Logger.info('✌️ Express loaded');
Logger.info('✌️ init data loaded'); console.log('✌️ Express loaded');
await linkDeps(); await initData();
Logger.info('✌️ link deps loaded'); Logger.info('✌️ init data loaded');
console.log('✌️ init data loaded');
initTask();
Logger.info('✌️ init task loaded'); await linkDeps();
Logger.info('✌️ link deps loaded');
console.log('✌️ link deps loaded');
initTask();
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;
}
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) { 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);
}); });
+34 -18
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(); Logger.info(
if (!cmdStr.startsWith(TASK_PREFIX) && !cmdStr.startsWith(QL_PREFIX)) { '[schedule][创建定时任务], 任务ID: %s, 名称: %s, cron: %s, 执行命令: %s',
cmdStr = `${TASK_PREFIX}${cmdStr}`;
}
scheduleStacks.set(
id, id,
nodeSchedule.scheduleJob(id, schedule, async () => { name,
Logger.info( schedule,
`当前时间: ${dayjs().format( command,
'YYYY-MM-DD HH:mm:ss',
)},运行命令: ${cmdStr}`,
);
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');
}, },
); );
+67 -61
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,
+181 -3
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, # 控制台输出
@@ -99,7 +101,17 @@ push_config = {
'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定 'SMTP_PASSWORD': '', # SMTP 登录密码,也可能为特殊口令,视具体邮件服务商说明而定
'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"
} }
+23 -10
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
} }
@@ -69,11 +69,11 @@ handle_log_path() {
if [[ $real_log_path ]]; then if [[ $real_log_path ]]; then
log_path="$real_log_path" log_path="$real_log_path"
fi fi
cmd=">> $dir_log/$log_path 2>&1" cmd="2>&1 | tee -a $dir_log/$log_path"
make_dir "$dir_log/$log_dir" make_dir "$dir_log/$log_dir"
if [[ "$show_log" == "true" ]]; then if [[ "$no_tee" == "true" ]]; then
cmd="2>&1 | tee -a $dir_log/$log_path" 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;
}
+79 -91
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 ? ( {record.name || '-'}
<Popover </Link>
placement="right" </Paragraph>
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 || '-'}
</Popover>
) : (
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
onClick={(e) => {
e.stopPropagation();
runCron(record, index);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
)}
{record.status !== CrontabStatus.idle && (
<Tooltip title={isPc ? intl.get('停止') : ''}>
<a
onClick={(e) => {
e.stopPropagation();
stopCron(record, index);
}}
>
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title={isPc ? intl.get('日志') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() }); runCron(record, index);
}} }}
> >
<FileTextOutlined /> {intl.get('运行')}
</a> </a>
</Tooltip> )}
{record.status !== CrontabStatus.idle && (
<a
onClick={(e) => {
e.stopPropagation();
stopCron(record, index);
}}
>
{intl.get('停止')}
</a>
)}
<a
onClick={(e) => {
e.stopPropagation();
setLogCron({ ...record, timestamp: Date.now() });
}}
>
{intl.get('日志')}
</a>
<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('任务未运行'),
); );
if (!hasNext && !logEnded(value) && value !== intl.get('启动中...')) {
setTimeout(() => {
autoScroll();
});
}
setExecuting(hasNext); setExecuting(hasNext);
autoScroll();
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; }>;
} }
+27 -14
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.Option key={x.name} value={x.value}> <Select
{x.name} style={{ width: 120 }}
</Select.Option> placeholder={intl.get('请选择操作符')}
))} {...others}
</Select> >
); {OPERATIONS.filter((x) =>
STATUS_MAP[property as 'status' | 'sub_id'] ? x.type === 'select' : x,
).map((x) => (
<Select.Option key={x.name} value={x.value}>
{x.name}
</Select.Option>
))}
</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>
)} )}
+73 -72
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.删除中,
});
}
}
setValue(result);
} }
if ( if (message.includes('结束时间') && references.length > 0) {
type === 'installDependence' && if (message.includes('安装')) {
message.includes(intl.get('结束时间')) && status = message.includes('成功') ? Status.已安装 : Status.安装失败;
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) => {
for (let i = 0; i < references.length; i++) { const _result = [...p];
const index = value.findIndex((x) => x.id === references[i]); for (let i = 0; i < references.length; i++) {
if (index !== -1) { const index = p.findIndex((x) => x.id === references[i]);
_result.splice(index, 1); if (index !== -1) {
_result.splice(index, 1);
}
} }
} return _result;
setValue(_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,24 +548,25 @@ const Dependence = () => {
dependence={editedDependence} dependence={editedDependence}
defaultType={type} defaultType={type}
/> />
<DependenceLogModal {logDependence && (
visible={isLogModalVisible} <DependenceLogModal
handleCancel={(needRemove?: boolean) => { visible={isLogModalVisible}
setIsLogModalVisible(false); handleCancel={(needRemove?: boolean) => {
if (needRemove) { setIsLogModalVisible(false);
const index = value.findIndex((x) => x.id === logDependence.id); if (needRemove) {
const result = [...value]; const index = value.findIndex((x) => x.id === logDependence.id);
if (index !== -1) { const result = [...value];
result.splice(index, 1); if (index !== -1) {
setValue(result); result.splice(index, 1);
setValue(result);
}
} else if ([...value].map((x) => x.id).includes(logDependence.id)) {
getDependenceDetail(logDependence);
} }
} else if ([...value].map((x) => x.id).includes(logDependence.id)) { }}
getDependenceDetail(logDependence); dependence={logDependence}
} />
}} )}
socketMessage={socketMessage}
dependence={logDependence}
/>
</PageContainer> </PageContainer>
); );
}; };
+38 -39
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,23 +133,24 @@ const DependenceLogModal = ({
</Button>, </Button>,
]} ]}
> >
{loading ? ( <div className="log-container">
<PageLoading /> {loading ? (
) : ( <PageLoading />
<pre ) : (
style={ <pre
isPhone style={
? { isPhone
fontFamily: 'Source Code Pro', ? {
width: 375, fontFamily: 'Source Code Pro',
zoom: 0.83, zoom: 0.83,
} }
: {} : {}
} }
> >
<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 (
<> <>
+95 -62
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,66 +306,72 @@ const Setting = () => {
: [] : []
} }
> >
<Tabs <div ref={containergRef}>
defaultActiveKey="security" <Tabs
size="small" defaultActiveKey="security"
tabPosition="top" size="small"
onChange={tabChange} tabPosition="top"
items={[ onChange={tabChange}
...(!isDemoEnv items={[
? [ ...(!isDemoEnv
{ ? [
key: 'security', {
label: intl.get('安全设置'), key: 'security',
children: ( label: intl.get('安全设置'),
<SecuritySettings user={user} userChange={reloadUser} /> children: (
), <SecuritySettings user={user} userChange={reloadUser} />
}, ),
] },
: []), ]
{ : []),
key: 'app', {
label: intl.get('应用设置'), key: 'app',
children: ( label: intl.get('应用设置'),
<Table children: (
columns={columns} <Table
pagination={false} columns={columns}
dataSource={dataSource} pagination={false}
rowKey="id" dataSource={dataSource}
size="middle" rowKey="id"
scroll={{ x: 768 }} size="middle"
loading={loading} scroll={{ x: 1000 }}
/> loading={loading}
), />
}, ),
{ },
key: 'notification', {
label: intl.get('通知设置'), key: 'notification',
children: <NotificationSetting data={notificationInfo} />, label: intl.get('通知设置'),
}, children: <NotificationSetting data={notificationInfo} />,
{ },
key: 'login', {
label: intl.get('登录日志'), key: 'syslog',
children: <LoginLog data={loginLogData} />, label: intl.get('系统日志'),
}, children: <SystemLog data={systemLogData} height={height} theme={theme}/>,
{ },
key: 'other', {
label: intl.get('其他设置'), key: 'login',
children: ( label: intl.get('登录日志'),
<Other children: <LoginLog data={loginLogData} />,
reloadTheme={reloadTheme} },
socketMessage={socketMessage} {
systemInfo={systemInfo} key: 'other',
/> label: intl.get('其他设置'),
), children: (
}, <Other
{ reloadTheme={reloadTheme}
key: 'about', systemInfo={systemInfo}
label: intl.get('关于'), />
children: <About systemInfo={systemInfo} />, ),
}, },
]} {
></Tabs> key: 'about',
label: intl.get('关于'),
children: <About systemInfo={systemInfo} />,
},
]}
></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;
+43 -41
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
onClick={(e) => {
e.stopPropagation();
runSubscription(record, index);
}}
>
<PlayCircleOutlined />
</a>
</Tooltip>
)}
{record.status !== SubscriptionStatus.idle && (
<Tooltip title={isPc ? intl.get('停止') : ''}>
<a
onClick={(e) => {
e.stopPropagation();
stopSubsciption(record, index);
}}
>
<PauseCircleOutlined />
</a>
</Tooltip>
)}
<Tooltip title={isPc ? intl.get('日志') : ''}>
<a <a
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setLogSubscription({ ...record, timestamp: Date.now() }); runSubscription(record, index);
}} }}
> >
<FileTextOutlined /> {intl.get('运行')}
</a> </a>
</Tooltip> )}
{record.status !== SubscriptionStatus.idle && (
<a
onClick={(e) => {
e.stopPropagation();
stopSubsciption(record, index);
}}
>
{intl.get('停止')}
</a>
)}
<a
onClick={(e) => {
e.stopPropagation();
setLogSubscription({ ...record, timestamp: Date.now() });
}}
>
{intl.get('日志')}
</a>
<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) {
+19 -20
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,23 +109,24 @@ const SubscriptionLogModal = ({
</Button>, </Button>,
]} ]}
> >
{loading ? ( <div className="log-container">
<PageLoading /> {loading ? (
) : ( <PageLoading />
<pre ) : (
style={ <pre
isPhone style={
? { isPhone
fontFamily: 'Source Code Pro', ? {
width: 375, fontFamily: 'Source Code Pro',
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